mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-06-03 20:12:58 +00:00
overhaul
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 223 KiB |
@@ -1,631 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Railway Dashboard</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
railway: {
|
||||
bg: '#0d0d12',
|
||||
sidebar: '#0a0a0f',
|
||||
card: '#15151d',
|
||||
cardHover: '#1a1a24',
|
||||
border: '#232330',
|
||||
accent: '#9d4edd',
|
||||
accentHover: '#7b2cbf',
|
||||
success: '#22c55e',
|
||||
muted: '#6b7280',
|
||||
text: '#f3f4f6',
|
||||
textMuted: '#9ca3af'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.connection-line {
|
||||
position: absolute;
|
||||
border: 2px dashed #3d3d55;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.connection-line.horizontal {
|
||||
height: 2px;
|
||||
border-top: 2px dashed #3d3d55;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.service-card {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.group-container {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
animation: dropdownIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
@keyframes dropdownIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-icon {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.sidebar-icon:hover {
|
||||
background-color: rgba(157, 78, 221, 0.1);
|
||||
color: #9d4edd;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.add-btn:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 15px rgba(157, 78, 221, 0.3);
|
||||
}
|
||||
|
||||
/* Modal styles */
|
||||
.modal-backdrop {
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #0a0a0f;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #3d3d55;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #4d4d66;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-railway-bg text-railway-text h-screen overflow-hidden">
|
||||
<div class="flex h-full">
|
||||
<!-- Sidebar -->
|
||||
<aside class="w-16 bg-railway-sidebar border-r border-railway-border flex flex-col items-center py-4 flex-shrink-0">
|
||||
<!-- Logo -->
|
||||
<div class="mb-6">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-railway-accent to-railway-accentHover flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nav Icons -->
|
||||
<nav class="flex flex-col gap-2 flex-1">
|
||||
<button class="sidebar-icon w-10 h-10 rounded-lg flex items-center justify-center text-railway-accent bg-railway-card">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="sidebar-icon w-10 h-10 rounded-lg flex items-center justify-center text-railway-textMuted">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="sidebar-icon w-10 h-10 rounded-lg flex items-center justify-center text-railway-textMuted">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="sidebar-icon w-10 h-10 rounded-lg flex items-center justify-center text-railway-textMuted">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Bottom Icons -->
|
||||
<div class="flex flex-col gap-2 mt-auto">
|
||||
<button class="sidebar-icon w-10 h-10 rounded-lg flex items-center justify-center text-railway-textMuted">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="sidebar-icon w-10 h-10 rounded-lg flex items-center justify-center text-railway-textMuted">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 flex flex-col min-w-0">
|
||||
<!-- Header -->
|
||||
<header class="h-14 border-b border-railway-border flex items-center justify-between px-6 bg-railway-bg/50 backdrop-blur-sm">
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="flex items-center gap-2 text-railway-text hover:text-white transition-colors">
|
||||
<span class="font-semibold">dazzling-curiosity</span>
|
||||
<svg class="w-4 h-4 text-railway-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="text-railway-muted">/</span>
|
||||
<button class="flex items-center gap-2 text-railway-textMuted hover:text-white transition-colors text-sm">
|
||||
<span>production</span>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="add-btn flex items-center gap-2 bg-railway-card hover:bg-railway-cardHover border border-railway-border px-3 py-1.5 rounded-lg text-sm font-medium">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
Add
|
||||
</button>
|
||||
<div class="w-px h-6 bg-railway-border mx-1"></div>
|
||||
<button class="w-8 h-8 rounded-lg flex items-center justify-center text-railway-textMuted hover:text-white hover:bg-railway-card transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="w-8 h-8 rounded-lg flex items-center justify-center text-railway-textMuted hover:text-white hover:bg-railway-card transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Dashboard Content -->
|
||||
<div class="flex-1 overflow-auto p-6">
|
||||
<div id="groups-container" class="flex flex-col gap-6 max-w-7xl mx-auto">
|
||||
<!-- Groups will be rendered here -->
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Add Service Modal -->
|
||||
<div id="add-service-modal" class="fixed inset-0 z-50 hidden">
|
||||
<div class="modal-backdrop absolute inset-0 bg-black/60" onclick="closeModal()"></div>
|
||||
<div class="absolute inset-0 flex items-center justify-center p-4">
|
||||
<div class="bg-railway-card border border-railway-border rounded-xl w-full max-w-md p-6 shadow-2xl">
|
||||
<h3 class="text-lg font-semibold mb-4">Add New Service</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm text-railway-textMuted mb-1">Service Name</label>
|
||||
<input type="text" id="service-name" class="w-full bg-railway-bg border border-railway-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-railway-accent" placeholder="e.g., MyApp">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-railway-textMuted mb-1">Domain</label>
|
||||
<input type="text" id="service-domain" class="w-full bg-railway-bg border border-railway-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-railway-accent" placeholder="e.g., myapp.example.com">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-railway-textMuted mb-1">Group</label>
|
||||
<select id="service-group" class="w-full bg-railway-bg border border-railway-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-railway-accent">
|
||||
<option value="homelab">Homelab</option>
|
||||
<option value="competition">Competition</option>
|
||||
<option value="apis">API's</option>
|
||||
<option value="new">+ New Group</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="new-group-input" class="hidden">
|
||||
<label class="block text-sm text-railway-textMuted mb-1">New Group Name</label>
|
||||
<input type="text" id="new-group-name" class="w-full bg-railway-bg border border-railway-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-railway-accent" placeholder="Group name">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-railway-textMuted mb-1">Icon</label>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button onclick="selectIcon('server')" class="icon-select w-10 h-10 rounded-lg bg-railway-bg border border-railway-border flex items-center justify-center hover:border-railway-accent" data-icon="server">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 01-2 2v4a2 2 0 012 2h14a2 2 0 012-2v-4a2 2 0 01-2-2m-2-4h.01M17 16h.01"></path></svg>
|
||||
</button>
|
||||
<button onclick="selectIcon('database')" class="icon-select w-10 h-10 rounded-lg bg-railway-bg border border-railway-border flex items-center justify-center hover:border-railway-accent" data-icon="database">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg>
|
||||
</button>
|
||||
<button onclick="selectIcon('cloud')" class="icon-select w-10 h-10 rounded-lg bg-railway-bg border border-railway-border flex items-center justify-center hover:border-railway-accent" data-icon="cloud">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"></path></svg>
|
||||
</button>
|
||||
<button onclick="selectIcon('code')" class="icon-select w-10 h-10 rounded-lg bg-railway-bg border border-railway-border flex items-center justify-center hover:border-railway-accent" data-icon="code">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>
|
||||
</button>
|
||||
<button onclick="selectIcon('mail')" class="icon-select w-10 h-10 rounded-lg bg-railway-bg border border-railway-border flex items-center justify-center hover:border-railway-accent" data-icon="mail">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>
|
||||
</button>
|
||||
<button onclick="selectIcon('globe')" class="icon-select w-10 h-10 rounded-lg bg-railway-bg border border-railway-border flex items-center justify-center hover:border-railway-accent" data-icon="globe">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 mt-6">
|
||||
<button onclick="closeModal()" class="px-4 py-2 text-sm text-railway-textMuted hover:text-white transition-colors">Cancel</button>
|
||||
<button onclick="addService()" class="px-4 py-2 bg-railway-accent hover:bg-railway-accentHover text-white rounded-lg text-sm font-medium transition-colors">Add Service</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initial data
|
||||
let groups = [
|
||||
{
|
||||
id: 'homelab',
|
||||
name: 'Homelab',
|
||||
icon: 'home',
|
||||
services: [
|
||||
{
|
||||
id: 'koffan',
|
||||
name: 'Koffan',
|
||||
domain: 'shopping.tdvorak.dev',
|
||||
status: 'online',
|
||||
volume: 'koffan-volume',
|
||||
icon: 'server'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'competition',
|
||||
name: 'Competition',
|
||||
icon: 'trophy',
|
||||
services: [
|
||||
{
|
||||
id: 'insightful-optimism',
|
||||
name: 'insightful-optimism',
|
||||
domain: 'co-back.tdvorak.dev',
|
||||
status: 'online',
|
||||
connections: ['postgres'],
|
||||
icon: 'github'
|
||||
},
|
||||
{
|
||||
id: 'postgres',
|
||||
name: 'Postgres',
|
||||
domain: '',
|
||||
status: 'online',
|
||||
volume: 'postgres-volume-89XR',
|
||||
icon: 'database'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'apis',
|
||||
name: "API's",
|
||||
icon: 'code',
|
||||
services: [
|
||||
{
|
||||
id: 'sendmail',
|
||||
name: 'SendMail',
|
||||
domain: 'sendmail.tdvorak.dev',
|
||||
status: 'online',
|
||||
icon: 'mail'
|
||||
},
|
||||
{
|
||||
id: 'youtubescraper',
|
||||
name: 'YoutubeScraper',
|
||||
domain: 'youtube.tdvorak.dev',
|
||||
status: 'online',
|
||||
icon: 'youtube'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
let selectedIcon = 'server';
|
||||
let dropdownOpen = null;
|
||||
|
||||
// Icon SVGs
|
||||
const icons = {
|
||||
server: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 01-2 2v4a2 2 0 012 2h14a2 2 0 012-2v-4a2 2 0 01-2-2m-2-4h.01M17 16h.01"></path></svg>',
|
||||
database: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg>',
|
||||
cloud: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"></path></svg>',
|
||||
code: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>',
|
||||
mail: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>',
|
||||
globe: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"></path></svg>',
|
||||
home: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>',
|
||||
trophy: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>',
|
||||
github: '<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>',
|
||||
youtube: '<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>'
|
||||
};
|
||||
|
||||
function renderIcon(name) {
|
||||
return icons[name] || icons.server;
|
||||
}
|
||||
|
||||
function getGroupIcon(icon) {
|
||||
const groupIcons = {
|
||||
home: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>',
|
||||
trophy: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 21h8m-4-4v4m-5-16l2.5 9h7L19 5H4zM4 5v6a2 2 0 002 2h2V5H4zm14 0v8h2a2 2 0 002-2V5h-4z"></path></svg>',
|
||||
code: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>'
|
||||
};
|
||||
return groupIcons[icon] || groupIcons.home;
|
||||
}
|
||||
|
||||
function toggleDropdown(groupId, event) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (dropdownOpen === groupId) {
|
||||
closeDropdown();
|
||||
return;
|
||||
}
|
||||
|
||||
closeDropdown();
|
||||
dropdownOpen = groupId;
|
||||
|
||||
const dropdown = document.getElementById(`dropdown-${groupId}`);
|
||||
if (dropdown) {
|
||||
dropdown.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function closeDropdown() {
|
||||
if (dropdownOpen) {
|
||||
const dropdown = document.getElementById(`dropdown-${dropdownOpen}`);
|
||||
if (dropdown) {
|
||||
dropdown.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
dropdownOpen = null;
|
||||
}
|
||||
|
||||
function deleteGroup(groupId) {
|
||||
groups = groups.filter(g => g.id !== groupId);
|
||||
renderGroups();
|
||||
closeDropdown();
|
||||
}
|
||||
|
||||
function deleteService(groupId, serviceId) {
|
||||
const group = groups.find(g => g.id === groupId);
|
||||
if (group) {
|
||||
group.services = group.services.filter(s => s.id !== serviceId);
|
||||
renderGroups();
|
||||
}
|
||||
}
|
||||
|
||||
function renderConnection() {
|
||||
// Connection line between insightful-optimism and postgres
|
||||
return `
|
||||
<div class="hidden md:flex items-center justify-center w-16 relative">
|
||||
<div class="absolute h-0.5 border-t-2 border-dashed border-railway-border w-full"></div>
|
||||
<div class="absolute -left-1 w-2 h-2 rounded-full bg-railway-border"></div>
|
||||
<div class="absolute -right-1 w-2 h-2 rounded-full bg-railway-border"></div>
|
||||
<svg class="w-4 h-4 text-railway-muted absolute right-0 transform translate-x-full" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderServiceCard(service, groupId) {
|
||||
const hasVolume = service.volume ? `
|
||||
<div class="flex items-center gap-2 text-xs text-railway-textMuted mt-3 pt-3 border-t border-railway-border/50">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
|
||||
</svg>
|
||||
${service.volume}
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
return `
|
||||
<div class="service-card bg-railway-card border border-railway-border rounded-lg p-4 min-w-[220px] max-w-[280px] relative group">
|
||||
<button onclick="deleteService('${groupId}', '${service.id}')" class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity text-railway-textMuted hover:text-red-400 p-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="flex items-start gap-3 mb-2">
|
||||
<div class="w-10 h-10 rounded-lg bg-railway-bg flex items-center justify-center text-railway-text shrink-0">
|
||||
${renderIcon(service.icon)}
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h4 class="font-semibold text-sm truncate">${service.name}</h4>
|
||||
<p class="text-xs text-railway-textMuted truncate">${service.domain || 'Internal Service'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full bg-railway-success status-dot"></span>
|
||||
<span class="text-xs text-railway-success font-medium">Online</span>
|
||||
</div>
|
||||
${hasVolume}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderGroup(group) {
|
||||
const servicesHtml = group.services.map((service, index) => {
|
||||
const card = renderServiceCard(service, group.id);
|
||||
// Add connection line if this is the first service and there are 2 services
|
||||
if (group.id === 'competition' && index === 0 && group.services.length > 1) {
|
||||
return card + renderConnection();
|
||||
}
|
||||
return card;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="group-container">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-railway-accent">
|
||||
${getGroupIcon(group.icon)}
|
||||
</div>
|
||||
<h3 class="font-semibold text-railway-text">${group.name}</h3>
|
||||
</div>
|
||||
<button onclick="toggleDropdown('${group.id}', event)" class="relative p-1 text-railway-textMuted hover:text-white transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"></path>
|
||||
</svg>
|
||||
<div id="dropdown-${group.id}" class="dropdown-menu hidden absolute right-0 top-full mt-1 w-32 bg-railway-card border border-railway-border rounded-lg shadow-xl z-10">
|
||||
<button onclick="deleteGroup('${group.id}')" class="w-full text-left px-3 py-2 text-sm text-red-400 hover:bg-railway-bg rounded-lg">Delete Group</button>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
${servicesHtml}
|
||||
<button onclick="openModal('${group.id}')" class="border-2 border-dashed border-railway-border rounded-lg p-4 min-w-[220px] max-w-[280px] flex items-center justify-center text-railway-textMuted hover:text-railway-accent hover:border-railway-accent/50 transition-all group">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<svg class="w-6 h-6 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
<span class="text-sm font-medium">Add Service</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderGroups() {
|
||||
const container = document.getElementById('groups-container');
|
||||
container.innerHTML = groups.map(renderGroup).join('');
|
||||
}
|
||||
|
||||
function openModal(preselectedGroup = null) {
|
||||
const modal = document.getElementById('add-service-modal');
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
if (preselectedGroup) {
|
||||
document.getElementById('service-group').value = preselectedGroup;
|
||||
}
|
||||
|
||||
// Reset form
|
||||
document.getElementById('service-name').value = '';
|
||||
document.getElementById('service-domain').value = '';
|
||||
document.getElementById('new-group-name').value = '';
|
||||
document.getElementById('new-group-input').classList.add('hidden');
|
||||
selectIcon('server');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const modal = document.getElementById('add-service-modal');
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
function selectIcon(icon) {
|
||||
selectedIcon = icon;
|
||||
document.querySelectorAll('.icon-select').forEach(btn => {
|
||||
if (btn.dataset.icon === icon) {
|
||||
btn.classList.add('border-railway-accent', 'text-railway-accent');
|
||||
btn.classList.remove('border-railway-border');
|
||||
} else {
|
||||
btn.classList.remove('border-railway-accent', 'text-railway-accent');
|
||||
btn.classList.add('border-railway-border');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addService() {
|
||||
const name = document.getElementById('service-name').value.trim();
|
||||
const domain = document.getElementById('service-domain').value.trim();
|
||||
const groupSelect = document.getElementById('service-group').value;
|
||||
const newGroupName = document.getElementById('new-group-name').value.trim();
|
||||
|
||||
if (!name) {
|
||||
alert('Please enter a service name');
|
||||
return;
|
||||
}
|
||||
|
||||
let groupId = groupSelect;
|
||||
|
||||
if (groupSelect === 'new') {
|
||||
if (!newGroupName) {
|
||||
alert('Please enter a group name');
|
||||
return;
|
||||
}
|
||||
groupId = 'group-' + Date.now();
|
||||
groups.push({
|
||||
id: groupId,
|
||||
name: newGroupName,
|
||||
icon: 'home',
|
||||
services: []
|
||||
});
|
||||
}
|
||||
|
||||
const service = {
|
||||
id: 'service-' + Date.now(),
|
||||
name: name,
|
||||
domain: domain,
|
||||
status: 'online',
|
||||
icon: selectedIcon
|
||||
};
|
||||
|
||||
const group = groups.find(g => g.id === groupId);
|
||||
if (group) {
|
||||
group.services.push(service);
|
||||
}
|
||||
|
||||
renderGroups();
|
||||
closeModal();
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.addEventListener('click', closeDropdown);
|
||||
|
||||
document.getElementById('service-group').addEventListener('change', function() {
|
||||
const newGroupInput = document.getElementById('new-group-input');
|
||||
if (this.value === 'new') {
|
||||
newGroupInput.classList.remove('hidden');
|
||||
} else {
|
||||
newGroupInput.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector('.add-btn').addEventListener('click', () => openModal());
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeModal();
|
||||
closeDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
// Initial render
|
||||
renderGroups();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,612 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<title>[NuFest] – App Project · Clounest (1:1 Match)</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet"/>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: { sans: ['Inter','system-ui','sans-serif'] },
|
||||
colors: {
|
||||
base:'#16171c', sidebar:'#111217', card:'#1c1d24',
|
||||
pink:'#e8316a', green:'#3dd68c', orange:'#ff7043',
|
||||
purple:'#9c7ef0', muted:'#6b6e7d', dim:'#9295a4', primary:'#e8e9f0',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html,body{height:100%;margin:0;background:#16171c;color:#e8e9f0;font-family:'Inter',sans-serif;overflow:hidden}
|
||||
::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:rgba(255,255,255,.1);border-radius:99px}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
|
||||
@keyframes pulse-dot{0%{box-shadow:0 0 0 0 rgba(61, 214, 140, 0.7)}70%{box-shadow:0 0 0 6px rgba(61, 214, 140, 0)}100%{box-shadow:0 0 0 0 rgba(61, 214, 140, 0)}}
|
||||
@keyframes slideInRight{from{opacity:0;transform:translateX(20px)}to{opacity:1;transform:translateX(0)}}
|
||||
@keyframes slideOutRight{from{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(20px)}}
|
||||
|
||||
/* Components */
|
||||
.nav-item{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#6b6e7d;transition:background .15s,color .15s}
|
||||
.nav-item:hover{background:rgba(255,255,255,.06);color:#9295a4}
|
||||
.nav-item.active{background:rgba(255,255,255,.09);color:#e8e9f0}
|
||||
.nav-item svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
|
||||
|
||||
.tab{display:flex;align-items:center;gap:6px;padding:10px 14px;font-size:13.5px;font-weight:500;color:#6b6e7d;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;transition:all .15s;white-space:nowrap;user-select:none}
|
||||
.tab:hover{color:#9295a4}
|
||||
.tab.active{color:#e8e9f0;border-bottom-color:#e8316a}
|
||||
.tab svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round}
|
||||
|
||||
.card{background:#1c1d24;border:1px solid rgba(255,255,255,.08);border-radius:20px;padding:20px;display:flex;flex-direction:column;transition:border-color .2s;animation:fadeUp .4s ease both}
|
||||
.card:hover{border-color:rgba(255,255,255,.14)}
|
||||
.card:nth-child(1){animation-delay:.04s}.card:nth-child(2){animation-delay:.09s}.card:nth-child(3){animation-delay:.14s}.card:nth-child(4){animation-delay:.19s}.card:nth-child(5){animation-delay:.24s}
|
||||
|
||||
.card-icon{width:34px;height:34px;border-radius:10px;background:rgba(255,255,255,.07);display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||
.card-icon svg{width:16px;height:16px;stroke:#9295a4;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
|
||||
|
||||
.arrow-btn{width:30px;height:30px;border-radius:9px;background:#e8316a;display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0;transition:box-shadow .15s,transform .15s}
|
||||
.arrow-btn:hover{transform:scale(1.07)}
|
||||
.arrow-btn svg{width:13px;height:13px;stroke:white;fill:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round}
|
||||
|
||||
.search-box{display:flex;align-items:center;gap:8px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:0 12px;height:34px;width:220px}
|
||||
.search-box svg{width:14px;height:14px;stroke:#6b6e7d;fill:none;stroke-width:2;stroke-linecap:round;flex-shrink:0}
|
||||
.search-box input{background:none;border:none;outline:none;color:#9295a4;font-size:13px;width:100%;font-family:inherit}
|
||||
.search-box input::placeholder{color:#6b6e7d}
|
||||
|
||||
.pill-group{display:flex;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);border-radius:10px;overflow:hidden}
|
||||
.pill{padding:5px 14px;font-size:12.5px;font-weight:500;color:#6b6e7d;cursor:pointer;transition:all .15s;user-select:none}
|
||||
.pill.active{background:rgba(255,255,255,.1);color:#e8e9f0}
|
||||
.pill:hover:not(.active){color:#9295a4}
|
||||
|
||||
.btn-stop{height:38px;padding:0 20px;border-radius:11px;border:1px solid rgba(255,255,255,.15);background:rgba(255,255,255,.06);color:#e8e9f0;font-size:13.5px;font-weight:700;font-family:inherit;cursor:pointer;display:flex;align-items:center;gap:8px;transition:background .15s}
|
||||
.btn-stop:hover{background:rgba(255,255,255,.1)}
|
||||
.btn-stop.disabled{opacity:0.5;cursor:not-allowed;}
|
||||
|
||||
.btn-restart{height:38px;padding:0 20px;border-radius:11px;border:none;background:#e8316a;color:white;font-size:13.5px;font-weight:700;font-family:inherit;cursor:pointer;display:flex;align-items:center;gap:8px;transition:all .15s}
|
||||
.btn-restart:hover{background:#d12960}
|
||||
.btn-restart.disabled{background:#444;cursor:not-allowed;}
|
||||
|
||||
.btn-restart svg,.btn-stop svg{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:2.2;stroke-linecap:round}
|
||||
|
||||
.chart-wrap{position:relative;width:100%}
|
||||
.chart-wrap canvas{width:100%!important;height:100%!important}
|
||||
|
||||
.flag{width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:15px;background:rgba(255,255,255,.05)}
|
||||
.stat-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
||||
.badge-active{padding:2px 8px;border-radius:6px;background:rgba(61, 214, 140, 0.15);font-size:10px;font-weight:700;letter-spacing:.7px;color:#3dd68c;text-transform:uppercase;transition: all 0.3s;}
|
||||
.badge-stopped{padding:2px 8px;border-radius:6px;background:rgba(255, 255, 255, 0.08);font-size:10px;font-weight:700;letter-spacing:.7px;color:#6b6e7d;text-transform:uppercase;}
|
||||
|
||||
.cache-seg{height:32px;transition:opacity .2s,filter .2s;cursor:pointer}
|
||||
.cache-seg:hover{filter:brightness(1.15)}
|
||||
.speed-row{display:flex;align-items:center;gap:5px;font-size:12.5px;font-weight:600}
|
||||
.speed-row svg{width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:2.5;stroke-linecap:round}
|
||||
|
||||
/* Live Indicator */
|
||||
.live-dot{width:8px;height:8px;background:#3dd68c;border-radius:50%;display:inline-block;margin-right:6px;animation:pulse-dot 2s infinite}
|
||||
.live-dot.stopped{background:#6b6e7d;animation:none}
|
||||
|
||||
/* Toast Notifications */
|
||||
.toast-container{position:fixed;bottom:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:10px;pointer-events:none}
|
||||
.toast{pointer-events:auto;background:#1c1d24;border:1px solid rgba(255,255,255,.1);color:#e8e9f0;padding:12px 16px;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,.4);font-size:13px;font-weight:500;display:flex;align-items:center;gap:10px;animation:slideInRight .3s ease forwards}
|
||||
.toast.hiding{animation:slideOutRight .3s ease forwards}
|
||||
.toast svg{width:18px;height:18px;flex-shrink:0}
|
||||
.toast.success svg{stroke:#3dd68c}
|
||||
.toast.info svg{stroke:#6c8ef0}
|
||||
.toast.warning svg{stroke:#ff7043}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div style="display:flex;height:100vh;overflow:hidden">
|
||||
|
||||
<!-- ════ SIDEBAR ════ -->
|
||||
<aside style="width:64px;background:#111217;border-right:1px solid rgba(255,255,255,.07);display:flex;flex-direction:column;align-items:center;padding:16px 0;gap:5px;flex-shrink:0">
|
||||
<div style="width:38px;height:38px;border-radius:50%;background:#e8316a;display:flex;align-items:center;justify-content:center;margin-bottom:14px">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="white"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14H9V8h2v8zm4 0h-2V8h2v8z"/></svg>
|
||||
</div>
|
||||
<div class="nav-item active"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg></div>
|
||||
<div class="nav-item"><svg viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg></div>
|
||||
<div class="nav-item"><svg viewBox="0 0 24 24"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v6c0 1.66 4.03 3 9 3s9-1.34 9-3V5"/><path d="M3 11v6c0 1.66 4.03 3 9 3s9-1.34 9-3v-6"/></svg></div>
|
||||
<div class="nav-item"><svg viewBox="0 0 24 24"><circle cx="18" cy="8" r="3"/><circle cx="6" cy="15" r="3"/><path d="M18 11a9 9 0 0 1-9 9M6 12a9 9 0 0 1 9-9"/></svg></div>
|
||||
<div class="nav-item"><svg viewBox="0 0 24 24"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></div>
|
||||
<div style="flex:1"></div>
|
||||
<div class="nav-item"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"/></svg></div>
|
||||
<div class="nav-item"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg></div>
|
||||
<div style="width:34px;height:34px;border-radius:50%;background:#22233a;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:#9295a4;cursor:pointer;margin-top:4px;letter-spacing:-.3px">w.</div>
|
||||
</aside>
|
||||
|
||||
<!-- ════ MAIN ════ -->
|
||||
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0">
|
||||
|
||||
<!-- ── TOPBAR ── -->
|
||||
<header style="height:52px;background:#111217;border-bottom:1px solid rgba(255,255,255,.07);display:flex;align-items:center;padding:0 22px;gap:14px;flex-shrink:0">
|
||||
<div class="search-box">
|
||||
<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" placeholder="Search logs..."/>
|
||||
</div>
|
||||
<div style="margin-left:auto;display:flex;align-items:center;gap:8px">
|
||||
<button style="height:32px;padding:0 14px;border-radius:9px;border:1px solid rgba(255,255,255,.1);background:transparent;color:#9295a4;font-size:13px;font-weight:500;cursor:pointer;font-family:inherit">Support</button>
|
||||
<button style="height:32px;padding:0 14px;border-radius:9px;border:none;background:rgba(255,255,255,.08);color:#e8e9f0;font-size:13px;font-weight:500;cursor:pointer;font-family:inherit;display:flex;align-items:center;gap:6px">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><polyline points="17 11 12 6 7 11"/><polyline points="17 18 12 13 7 18"/></svg>
|
||||
Upgrade
|
||||
</button>
|
||||
<button style="width:32px;height:32px;border-radius:9px;border:1px solid rgba(255,255,255,.1);background:transparent;display:flex;align-items:center;justify-content:center;cursor:pointer">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#9295a4" stroke-width="2" stroke-linecap="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ── SCROLL CONTENT ── -->
|
||||
<div style="flex:1;overflow-y:auto;padding:0 24px 28px">
|
||||
|
||||
<!-- breadcrumb -->
|
||||
<div style="display:flex;align-items:center;gap:6px;padding:14px 0 10px;color:#6b6e7d;font-size:13px">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#6b6e7d" stroke-width="2" stroke-linecap:round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
<a href="#" style="color:#6b6e7d;text-decoration:none">Servers</a>
|
||||
<span style="opacity:.4">/</span>
|
||||
<span style="color:#9295a4">[NuFest] - App Project</span>
|
||||
</div>
|
||||
|
||||
<!-- project header -->
|
||||
<div style="display:flex;align-items:center;padding-bottom:18px">
|
||||
<div style="width:46px;height:46px;border-radius:13px;background:#e8316a;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-right:14px">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="white"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14H9V8h2v8zm4 0h-2V8h2v8z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<span style="font-size:20px;font-weight:800;letter-spacing:-.5px">[NuFest] - App Project</span>
|
||||
<span class="badge-active" id="statusBadge"><span class="live-dot" id="liveDot"></span>Active</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:16px;margin-top:4px">
|
||||
<a href="#" style="display:flex;align-items:center;gap:4px;color:#6b6e7d;font-size:12.5px;text-decoration:none">https://nufest-dth.app <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg></a>
|
||||
<a href="#" style="display:flex;align-items:center;gap:4px;color:#6b6e7d;font-size:12.5px;text-decoration:none">Project Information <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="9 18 15 12 9 6"/></svg></a>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-left:auto;display:flex;gap:10px">
|
||||
<button class="btn-stop" id="btnStop">
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3" fill="currentColor" stroke="none"/></svg>
|
||||
STOP
|
||||
</button>
|
||||
<button class="btn-restart disabled" id="btnRestart" disabled>
|
||||
<svg viewBox="0 0 24 24"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-4.5"/></svg>
|
||||
RESTART
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- tabs -->
|
||||
<div style="display:flex;border-bottom:1px solid rgba(255,255,255,.07);margin-bottom:18px">
|
||||
<div class="tab active" onclick="setTab(this)"><svg viewBox="0 0 24 24"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>Metrics</div>
|
||||
<div class="tab" onclick="setTab(this)"><svg viewBox="0 0 24 24"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>Requests</div>
|
||||
<div class="tab" onclick="setTab(this)"><svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>APIs</div>
|
||||
<div class="tab" onclick="setTab(this)"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"/></svg>Config</div>
|
||||
</div>
|
||||
|
||||
<!-- metrics header -->
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
|
||||
<span style="font-size:15px;font-weight:700">Metrics</span>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<button style="height:32px;padding:0 12px;border-radius:9px;border:1px solid rgba(255,255,255,.09);background:rgba(255,255,255,.04);color:#9295a4;font-size:12.5px;font-weight:500;font-family:inherit;cursor:pointer;display:flex;align-items:center;gap:6px">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>
|
||||
Filter
|
||||
</button>
|
||||
<div class="pill-group" id="timePills">
|
||||
<div class="pill active" onclick="setPill(this)">Day</div>
|
||||
<div class="pill" onclick="setPill(this)">Month</div>
|
||||
<div class="pill" onclick="setPill(this)">Year</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ████ ROW 1 ████ -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1.95fr;gap:13px;margin-bottom:13px">
|
||||
|
||||
<!-- CPU -->
|
||||
<div class="card">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
|
||||
<div class="card-icon"><svg viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/></svg></div>
|
||||
<span style="font-size:14px;font-weight:600">CPU Usage</span>
|
||||
</div>
|
||||
<div style="font-size:38px;font-weight:900;letter-spacing:-1.5px;line-height:1" id="cpuText">12%</div>
|
||||
<div style="font-size:12px;color:#6b6e7d;margin-top:4px"><span style="color:#3dd68c;font-weight:700" id="cpuStatus">Good</span> Daily usage</div>
|
||||
<div class="chart-wrap" style="height:76px;margin:12px 0 6px"><canvas id="cpuChart"></canvas></div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding-top:4px">
|
||||
<span style="font-size:13px;color:#6b6e7d;font-weight:500;cursor:pointer">Details</span>
|
||||
<div class="arrow-btn"><svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RAM -->
|
||||
<div class="card">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
|
||||
<div class="card-icon"><svg viewBox="0 0 24 24"><rect x="2" y="8" width="20" height="8" rx="2"/><path d="M6 8V6M10 8V6M14 8V6M18 8V6M6 16v2M18 16v2"/></svg></div>
|
||||
<span style="font-size:14px;font-weight:600">RAM Usage</span>
|
||||
</div>
|
||||
<div style="font-size:38px;font-weight:900;letter-spacing:-1.5px;line-height:1" id="ramText">65%</div>
|
||||
<div style="font-size:12px;color:#6b6e7d;margin-top:4px"><span style="color:#f0a040;font-weight:700">Average</span> Daily usage</div>
|
||||
<div style="display:flex;justify-content:center;align-items:center;margin:10px 0 4px;position:relative">
|
||||
<canvas id="ramCanvas" width="160" height="94"></canvas>
|
||||
<div style="position:absolute;bottom:14px;text-align:center">
|
||||
<div style="font-size:10.5px;color:#6b6e7d;margin-bottom:1px">Used</div>
|
||||
<div style="font-size:12.5px;font-weight:700" id="ramDetail">5.4 GB / 8GB</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding-top:4px">
|
||||
<span style="font-size:13px;color:#6b6e7d;font-weight:500;cursor:pointer">Details</span>
|
||||
<div class="arrow-btn"><svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CACHE -->
|
||||
<div class="card">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
|
||||
<div class="card-icon"><svg viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></div>
|
||||
<span style="font-size:14px;font-weight:600">Cache</span>
|
||||
</div>
|
||||
<div style="font-size:38px;font-weight:900;letter-spacing:-1.5px;line-height:1">352 MB</div>
|
||||
<div style="font-size:12px;color:#6b6e7d;margin-top:4px"><span style="color:#f0a040;font-weight:700">220MB Average</span> cached images and files</div>
|
||||
|
||||
<!-- SEGMENTED BAR -->
|
||||
<div style="display:flex;align-items:center;gap:5px;margin:16px 0 15px;height:32px">
|
||||
<div class="cache-seg" style="width:43%;background:#ff6b5b;border-radius:10px 4px 4px 10px"></div>
|
||||
<div class="cache-seg" style="width:13%;background:#8c6ef0;border-radius:5px"></div>
|
||||
<div class="cache-seg" style="flex:1;background:rgba(255,255,255,.07);border-radius:4px 10px 10px 4px"></div>
|
||||
</div>
|
||||
|
||||
<!-- stats row -->
|
||||
<div style="display:grid;grid-template-columns:1fr auto 1fr auto 1fr;gap:0;align-items:stretch">
|
||||
<div>
|
||||
<div style="display:flex;align-items:center;gap:5px;font-size:11px;color:#6b6e7d;margin-bottom:5px">
|
||||
<div class="stat-dot" style="background:#ff6b5b"></div> Cache
|
||||
</div>
|
||||
<div style="display:flex;align-items:baseline;gap:4px">
|
||||
<span style="font-size:15px;font-weight:800">212 MB</span>
|
||||
<span style="font-size:11px;color:#6b6e7d">12%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width:1px;background:rgba(255,255,255,.08);margin:0 16px"></div>
|
||||
<div>
|
||||
<div style="display:flex;align-items:center;gap:5px;font-size:11px;color:#6b6e7d;margin-bottom:5px">
|
||||
<div class="stat-dot" style="background:#8c6ef0"></div> Non-Cache
|
||||
</div>
|
||||
<div style="display:flex;align-items:baseline;gap:4px">
|
||||
<span style="font-size:15px;font-weight:800">85.5 MB</span>
|
||||
<span style="font-size:11px;color:#6b6e7d">4%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width:1px;background:rgba(255,255,255,.08);margin:0 16px"></div>
|
||||
<div>
|
||||
<div style="font-size:11px;color:#6b6e7d;margin-bottom:5px">Total</div>
|
||||
<div style="font-size:15px;font-weight:800">1.75 GB</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding-top:16px">
|
||||
<span style="font-size:13px;color:#6b6e7d;font-weight:500;cursor:pointer">Details</span>
|
||||
<div class="arrow-btn"><svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /row 1 -->
|
||||
|
||||
<!-- ████ ROW 2 ████ -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:13px">
|
||||
|
||||
<!-- ACTIVE USER (Changed to Line Area Chart) -->
|
||||
<div class="card" style="flex-direction:row;padding:0;overflow:hidden">
|
||||
<div style="flex:1;padding:20px 18px 18px 20px;display:flex;flex-direction:column">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
|
||||
<div class="card-icon"><svg viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></svg></div>
|
||||
<span style="font-size:14px;font-weight:600">Active User</span>
|
||||
</div>
|
||||
<div style="font-size:36px;font-weight:900;letter-spacing:-1.5px;line-height:1" id="userText">475 K</div>
|
||||
<div style="font-size:12px;color:#6b6e7d;margin-top:4px">User active right now</div>
|
||||
<div style="display:flex;align-items:center;gap:5px;margin-top:12px;flex-wrap:wrap">
|
||||
<span class="flag">🇨🇳</span><span class="flag">🇮🇩</span><span class="flag">🇲🇲</span><span class="flag">🇲🇾</span><span class="flag">🇯🇵</span><span class="flag">🇮🇳</span><span class="flag">🇰🇷</span><span class="flag">🇵🇭</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:auto;padding-top:14px">
|
||||
<span style="font-size:13px;color:#6b6e7d;font-weight:500;cursor:pointer">Details</span>
|
||||
<div class="arrow-btn"><svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Line Area Chart -->
|
||||
<div style="width:50%;padding:16px 14px 46px 0;display:flex;align-items:flex-end">
|
||||
<div class="chart-wrap" style="height:130px"><canvas id="userChart"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PERFORMANCE -->
|
||||
<div class="card">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
|
||||
<div class="card-icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></div>
|
||||
<span style="font-size:14px;font-weight:600">Performance</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:flex-start;gap:16px;flex:1">
|
||||
<div style="flex:1">
|
||||
<div style="font-size:36px;font-weight:900;letter-spacing:-1.5px;line-height:1" id="perfText">89%</div>
|
||||
<div style="font-size:12px;color:#6b6e7d;margin-top:4px"><span style="color:#3dd68c;font-weight:700">Good</span> Last scan on Jun 12, 2024</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:8px">
|
||||
<div class="chart-wrap" style="width:134px;height:58px"><canvas id="perfChart"></canvas></div>
|
||||
<div style="display:flex;flex-direction:column;gap:4px;align-items:flex-end">
|
||||
<div class="speed-row" style="color:#6c8ef0">
|
||||
<svg viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 19 19 12"/></svg>
|
||||
<span id="upSpeed">10.4</span> Mbps
|
||||
</div>
|
||||
<div class="speed-row" style="color:#e8316a">
|
||||
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 5 5 12"/></svg>
|
||||
<span id="downSpeed">5.2</span> Mbps
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding-top:14px">
|
||||
<span style="font-size:13px;color:#6b6e7d;font-weight:500;cursor:pointer">Check Speed</span>
|
||||
<div class="arrow-btn"><svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /row 2 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Container -->
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
|
||||
<!-- ══════════════════════ SCRIPTS ══════════════════════ -->
|
||||
<script>
|
||||
Chart.defaults.font.family = "'Inter',sans-serif";
|
||||
Chart.defaults.color = '#6b6e7d';
|
||||
Chart.defaults.animation.duration = 800;
|
||||
Chart.defaults.animation.easing = 'easeInOutQuart';
|
||||
|
||||
// System State
|
||||
let isSystemRunning = true;
|
||||
let simulationInterval;
|
||||
const MAX_DATA_POINTS = 24;
|
||||
|
||||
/* ── TOAST SYSTEM ── */
|
||||
function showToast(message, type = 'info') {
|
||||
const container = document.getElementById('toastContainer');
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
|
||||
let icon = '';
|
||||
if(type === 'success') icon = '<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>';
|
||||
else if(type === 'warning') icon = '<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>';
|
||||
else icon = '<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>';
|
||||
|
||||
toast.innerHTML = `${icon}<span>${message}</span>`;
|
||||
container.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.add('hiding');
|
||||
toast.addEventListener('animationend', () => toast.remove());
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/* ── DATA & CHART SETUP ── */
|
||||
const initialData = {
|
||||
cpu: Array.from({length: MAX_DATA_POINTS}, () => Math.floor(Math.random() * 30) + 10),
|
||||
user: Array.from({length: 15}, () => Math.floor(Math.random() * 50) + 40),
|
||||
perf: Array.from({length: 12}, () => Math.floor(Math.random() * 20) + 70),
|
||||
perf2: Array.from({length: 12}, () => Math.floor(Math.random() * 20) + 60),
|
||||
};
|
||||
|
||||
/* ── CPU CHART (Line with Dots) ── */
|
||||
const cpuCtx = document.getElementById('cpuChart').getContext('2d');
|
||||
const cpuChart = new Chart(cpuCtx,{
|
||||
type:'line',
|
||||
data:{
|
||||
labels:Array.from({length: MAX_DATA_POINTS}, (_,i) => i),
|
||||
datasets:[{
|
||||
data: initialData.cpu,
|
||||
borderColor:'#ff7043',
|
||||
borderWidth: 2,
|
||||
backgroundColor: 'transparent', // No fill
|
||||
tension: 0.3, // Slight curve, not too wavy
|
||||
pointRadius: 2, // Visible small dots
|
||||
pointBackgroundColor: '#ff7043',
|
||||
pointHoverRadius: 5,
|
||||
fill: false
|
||||
}]
|
||||
},
|
||||
options:{
|
||||
responsive:true,maintainAspectRatio:false,
|
||||
plugins:{legend:{display:false},tooltip:{enabled:false}},
|
||||
scales:{
|
||||
x:{display:false},
|
||||
y:{display:false, min:0, max:100}
|
||||
},
|
||||
interaction:{mode:'index',intersect:false},
|
||||
}
|
||||
});
|
||||
|
||||
/* ── RAM DONUT (Flat Solid Color) ── */
|
||||
let currentRamPercent = 65;
|
||||
function drawRam(percent){
|
||||
const c=document.getElementById('ramCanvas'), ctx=c.getContext('2d');
|
||||
const W=c.width, H=c.height, cx=W/2, cy=H-8, R=68, r=52;
|
||||
ctx.clearRect(0,0,W,H);
|
||||
const segs=28, gap=.048, filled=Math.round(segs * (percent/100));
|
||||
for(let i=0;i<segs;i++){
|
||||
const a0=Math.PI+(Math.PI/segs)*i+gap/2;
|
||||
const a1=Math.PI+(Math.PI/segs)*(i+1)-gap/2;
|
||||
const f=i<filled;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx,cy,R,a0,a1);
|
||||
ctx.arc(cx,cy,r,a1,a0,true);
|
||||
ctx.closePath();
|
||||
if(f){
|
||||
ctx.fillStyle = '#9c7ef0';
|
||||
} else {
|
||||
ctx.fillStyle='rgba(255,255,255,.07)';
|
||||
}
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
drawRam(currentRamPercent);
|
||||
|
||||
/* ── USER CHART (Line Area Chart - 1:1 Match) ── */
|
||||
const userCtx=document.getElementById('userChart').getContext('2d');
|
||||
const userChart=new Chart(userCtx,{
|
||||
type:'line',
|
||||
data:{
|
||||
labels:Array.from({length:15},(_,i)=>i),
|
||||
datasets:[{
|
||||
data:initialData.user,
|
||||
borderColor:'#e8316a', // Pink Line
|
||||
borderWidth: 2,
|
||||
backgroundColor: 'rgba(232, 49, 106, 0.15)', // Subtle Pink Fill
|
||||
tension: 0.4, // Smooth curve
|
||||
pointRadius: 0, // No dots, just the wave
|
||||
fill: true, // This creates the area chart
|
||||
borderCapStyle: 'round'
|
||||
}]
|
||||
},
|
||||
options:{
|
||||
responsive:true,maintainAspectRatio:false,
|
||||
plugins:{legend:{display:false},tooltip:{enabled:false}},
|
||||
scales:{
|
||||
x:{display:false},
|
||||
y:{display:false, min:0, max:120}
|
||||
},
|
||||
interaction:{mode:'index',intersect:false},
|
||||
}
|
||||
});
|
||||
|
||||
/* ── PERF CHART (Solid Area Fill) ── */
|
||||
const perfCtx=document.getElementById('perfChart').getContext('2d');
|
||||
const perfChart=new Chart(perfCtx,{
|
||||
type:'line',
|
||||
data:{
|
||||
labels:Array.from({length:12},(_,i)=>i),
|
||||
datasets:[
|
||||
{
|
||||
data:initialData.perf,
|
||||
borderColor:'#6c8ef0',
|
||||
borderWidth: 2,
|
||||
backgroundColor: 'rgba(108, 142, 240, 0.15)',
|
||||
tension:.4,
|
||||
pointRadius: 0,
|
||||
fill: true
|
||||
},
|
||||
{
|
||||
data:initialData.perf2,
|
||||
borderColor:'#9c7ef0',
|
||||
borderWidth: 2,
|
||||
backgroundColor: 'rgba(156, 126, 240, 0.15)',
|
||||
tension:.4,
|
||||
pointRadius: 0,
|
||||
fill: true
|
||||
}
|
||||
]
|
||||
},
|
||||
options:{
|
||||
responsive:true,maintainAspectRatio:false,
|
||||
plugins:{legend:{display:false},tooltip:{enabled:false}},
|
||||
scales:{x:{display:false},y:{display:false}},
|
||||
}
|
||||
});
|
||||
|
||||
/* ── SIMULATION LOGIC ── */
|
||||
function getRandom(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function updateDashboard() {
|
||||
if(!isSystemRunning) return;
|
||||
|
||||
// 1. Update CPU
|
||||
const newCpu = getRandom(10, 45);
|
||||
document.getElementById('cpuText').innerText = `${newCpu}%`;
|
||||
|
||||
cpuChart.data.datasets[0].data.shift();
|
||||
cpuChart.data.datasets[0].data.push(newCpu);
|
||||
cpuChart.update('none');
|
||||
|
||||
// 2. Update RAM
|
||||
currentRamPercent = getRandom(50, 85);
|
||||
document.getElementById('ramText').innerText = `${currentRamPercent}%`;
|
||||
const gbUsed = (8 * (currentRamPercent/100)).toFixed(1);
|
||||
document.getElementById('ramDetail').innerText = `${gbUsed} GB / 8GB`;
|
||||
drawRam(currentRamPercent);
|
||||
|
||||
// 3. Update Users
|
||||
const newUserVal = getRandom(60, 110);
|
||||
document.getElementById('userText').innerText = `${newUserVal} K`;
|
||||
userChart.data.datasets[0].data.shift();
|
||||
userChart.data.datasets[0].data.push(newUserVal);
|
||||
userChart.update('none');
|
||||
|
||||
// 4. Update Perf & Speed
|
||||
const perfVal = getRandom(82, 99);
|
||||
document.getElementById('perfText').innerText = `${perfVal}%`;
|
||||
|
||||
const up = (Math.random() * 5 + 8).toFixed(1);
|
||||
const down = (Math.random() * 3 + 4).toFixed(1);
|
||||
document.getElementById('upSpeed').innerText = up;
|
||||
document.getElementById('downSpeed').innerText = down;
|
||||
|
||||
perfChart.data.datasets[0].data.shift();
|
||||
perfChart.data.datasets[0].data.push(perfVal);
|
||||
perfChart.data.datasets[1].data.shift();
|
||||
perfChart.data.datasets[1].data.push(perfVal - 5);
|
||||
perfChart.update('none');
|
||||
}
|
||||
|
||||
simulationInterval = setInterval(updateDashboard, 2000);
|
||||
|
||||
/* ── CONTROLS ── */
|
||||
const btnStop = document.getElementById('btnStop');
|
||||
const btnRestart = document.getElementById('btnRestart');
|
||||
const statusBadge = document.getElementById('statusBadge');
|
||||
const liveDot = document.getElementById('liveDot');
|
||||
|
||||
btnStop.addEventListener('click', () => {
|
||||
if(!isSystemRunning) return;
|
||||
isSystemRunning = false;
|
||||
statusBadge.className = 'badge-stopped';
|
||||
statusBadge.innerHTML = 'Stopped';
|
||||
liveDot.classList.add('stopped');
|
||||
btnStop.classList.add('disabled');
|
||||
btnStop.disabled = true;
|
||||
btnRestart.classList.remove('disabled');
|
||||
btnRestart.disabled = false;
|
||||
showToast('System monitoring paused', 'warning');
|
||||
});
|
||||
|
||||
btnRestart.addEventListener('click', () => {
|
||||
if(isSystemRunning) return;
|
||||
isSystemRunning = true;
|
||||
statusBadge.className = 'badge-active';
|
||||
statusBadge.innerHTML = '<span class="live-dot" id="liveDot"></span>Active';
|
||||
btnRestart.classList.add('disabled');
|
||||
btnRestart.disabled = true;
|
||||
btnStop.classList.remove('disabled');
|
||||
btnStop.disabled = false;
|
||||
showToast('System restarted successfully', 'success');
|
||||
});
|
||||
|
||||
/* ── TAB/PILL SWITCH ── */
|
||||
function setPill(el){
|
||||
document.querySelectorAll('#timePills .pill').forEach(p=>p.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
}
|
||||
|
||||
function setTab(el){
|
||||
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user