912 lines
53 KiB
HTML
912 lines
53 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||
<title>CluPilot — Dashboard</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{
|
||
--bg:#0a0a0f;
|
||
--card:#16161e;
|
||
--card-hover:#1a1a24;
|
||
--border:rgba(255,255,255,0.06);
|
||
--border-strong:rgba(255,255,255,0.10);
|
||
--fg:#f0f0f5;
|
||
--muted:#8888a0;
|
||
--muted-2:#5c5c70;
|
||
--accent:#4f8ef7;
|
||
--accent-soft:rgba(79,142,247,0.12);
|
||
--success:#3ecf8e;
|
||
--warning:#f5a623;
|
||
--danger:#f05252;
|
||
--success-soft:rgba(62,207,142,0.12);
|
||
--warning-soft:rgba(245,166,35,0.12);
|
||
--danger-soft:rgba(240,82,82,0.12);
|
||
--radius:10px;
|
||
--radius-sm:6px;
|
||
--mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
}
|
||
*{box-sizing:border-box}
|
||
html,body{margin:0;padding:0;background:var(--bg);color:var(--fg);font-family:'Inter',system-ui,-apple-system,sans-serif;font-size:13px;line-height:1.45;-webkit-font-smoothing:antialiased}
|
||
a{color:inherit;text-decoration:none}
|
||
button{font-family:inherit;color:inherit;background:none;border:0;cursor:pointer}
|
||
|
||
/* ---------- App layout ---------- */
|
||
.app{display:grid;grid-template-columns:220px 1fr;min-height:100vh}
|
||
|
||
/* ---------- Sidebar ---------- */
|
||
.sidebar{position:sticky;top:0;height:100vh;background:#0c0c12;border-right:1px solid var(--border);display:flex;flex-direction:column;padding:16px 12px;gap:14px}
|
||
.brand{display:flex;align-items:center;gap:9px;padding:4px 6px 8px;border-bottom:1px solid var(--border)}
|
||
.brand-mark{width:26px;height:26px;border-radius:7px;background:linear-gradient(135deg,#4f8ef7 0%,#7c5cff 100%);position:relative;display:grid;place-items:center;box-shadow:0 0 0 1px rgba(255,255,255,0.06) inset, 0 4px 12px rgba(79,142,247,0.25)}
|
||
.brand-mark::before{content:"";width:10px;height:10px;border-radius:50%;background:#0c0c12;box-shadow:inset 0 0 0 2px #fff}
|
||
.brand-name{font-weight:600;font-size:14px;letter-spacing:-0.01em}
|
||
.brand-name span{color:var(--muted);font-weight:500;margin-left:4px;font-size:11px}
|
||
|
||
.ws-switch{display:flex;align-items:center;gap:9px;padding:8px 9px;border:1px solid var(--border);border-radius:var(--radius-sm);background:#101019;cursor:pointer;transition:background .15s}
|
||
.ws-switch:hover{background:#14141e}
|
||
.ws-avatar{width:22px;height:22px;border-radius:5px;background:linear-gradient(135deg,#3ecf8e,#2a9d6f);display:grid;place-items:center;color:#0a0a0f;font-weight:700;font-size:11px;flex:none}
|
||
.ws-meta{flex:1;min-width:0}
|
||
.ws-name{font-size:12.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.ws-plan{font-size:10.5px;color:var(--muted);font-family:var(--mono)}
|
||
.ws-chev{color:var(--muted-2)}
|
||
|
||
.nav{display:flex;flex-direction:column;gap:14px;flex:1;overflow-y:auto;margin:0 -4px;padding:0 4px}
|
||
.nav-section{display:flex;flex-direction:column;gap:1px}
|
||
.nav-title{font-size:10.5px;text-transform:uppercase;letter-spacing:0.08em;color:var(--muted-2);padding:6px 9px 4px;font-weight:600}
|
||
.nav-item{display:flex;align-items:center;gap:9px;padding:6.5px 9px;border-radius:6px;color:#c8c8d8;cursor:pointer;font-size:12.5px;transition:background .12s,color .12s;position:relative}
|
||
.nav-item:hover{background:#14141e;color:var(--fg)}
|
||
.nav-item.active{background:#181822;color:var(--fg)}
|
||
.nav-item.active::before{content:"";position:absolute;left:-12px;top:6px;bottom:6px;width:2px;border-radius:2px;background:var(--accent)}
|
||
.nav-item .ico{width:15px;height:15px;color:var(--muted);flex:none}
|
||
.nav-item.active .ico{color:var(--accent)}
|
||
.nav-item .count{margin-left:auto;font-size:10.5px;font-family:var(--mono);color:var(--muted);background:#14141e;padding:1px 6px;border-radius:10px;border:1px solid var(--border)}
|
||
.nav-item.active .count{color:var(--fg);background:#1f1f2c}
|
||
|
||
.plan-box{margin-top:auto;padding:12px;border:1px solid var(--border);border-radius:var(--radius);background:linear-gradient(180deg,#11111a 0%,#0e0e15 100%);position:relative;overflow:hidden}
|
||
.plan-box::before{content:"";position:absolute;inset:0;background:radial-gradient(120% 80% at 0% 0%,rgba(79,142,247,0.10),transparent 60%);pointer-events:none}
|
||
.plan-row{display:flex;align-items:center;justify-content:space-between;font-size:11.5px;position:relative}
|
||
.plan-name{font-weight:600}
|
||
.plan-tag{font-family:var(--mono);font-size:10px;color:var(--accent);background:var(--accent-soft);padding:2px 6px;border-radius:4px;border:1px solid rgba(79,142,247,0.25)}
|
||
.plan-meter{margin-top:9px;height:5px;border-radius:3px;background:#191923;overflow:hidden;position:relative}
|
||
.plan-meter span{display:block;height:100%;width:62%;background:linear-gradient(90deg,#4f8ef7,#7c5cff);border-radius:3px}
|
||
.plan-stats{display:flex;justify-content:space-between;margin-top:6px;font-size:10.5px;color:var(--muted);font-family:var(--mono)}
|
||
.plan-cta{margin-top:10px;width:100%;padding:7px;border:1px solid var(--border-strong);border-radius:6px;font-size:11.5px;font-weight:500;color:var(--fg);background:#16161e;transition:background .15s}
|
||
.plan-cta:hover{background:#1d1d28}
|
||
|
||
/* ---------- Main ---------- */
|
||
.main{display:flex;flex-direction:column;min-width:0}
|
||
|
||
/* Topbar */
|
||
.topbar{position:sticky;top:0;z-index:5;display:flex;align-items:center;gap:14px;padding:14px 22px;background:rgba(10,10,15,0.85);backdrop-filter:blur(12px);border-bottom:1px solid var(--border)}
|
||
.crumbs{display:flex;align-items:center;gap:8px;min-width:0}
|
||
.crumb{color:var(--muted);font-size:12.5px}
|
||
.crumb.sep{color:var(--muted-2)}
|
||
.page-title{font-size:14.5px;font-weight:600;letter-spacing:-0.01em}
|
||
.status-chip{display:inline-flex;align-items:center;gap:6px;padding:3px 8px 3px 7px;border:1px solid rgba(62,207,142,0.25);background:var(--success-soft);color:var(--success);border-radius:20px;font-size:11px;font-weight:500;font-family:var(--mono)}
|
||
.pulse-dot{width:6px;height:6px;border-radius:50%;background:currentColor;position:relative;flex:none}
|
||
.pulse-dot::after{content:"";position:absolute;inset:-3px;border-radius:50%;background:currentColor;opacity:0.35;animation:pulse 1.8s ease-out infinite}
|
||
@keyframes pulse{0%{transform:scale(0.6);opacity:.5}100%{transform:scale(2.2);opacity:0}}
|
||
|
||
.top-spacer{flex:1}
|
||
.search{display:flex;align-items:center;gap:8px;padding:6px 10px;background:#101019;border:1px solid var(--border);border-radius:7px;min-width:280px;color:var(--muted);transition:border-color .15s}
|
||
.search:hover{border-color:var(--border-strong)}
|
||
.search:focus-within{border-color:rgba(79,142,247,0.5);background:#12121c}
|
||
.search input{flex:1;background:none;border:0;outline:0;color:var(--fg);font-size:12.5px;font-family:inherit;min-width:0}
|
||
.search input::placeholder{color:var(--muted)}
|
||
.kbd{font-family:var(--mono);font-size:10.5px;color:var(--muted);padding:1px 5px;border:1px solid var(--border);border-radius:4px;background:#0c0c14}
|
||
|
||
.icon-btn{width:32px;height:32px;border:1px solid var(--border);border-radius:7px;display:grid;place-items:center;color:var(--muted);background:#101019;transition:all .15s;position:relative}
|
||
.icon-btn:hover{color:var(--fg);border-color:var(--border-strong);background:#15151f}
|
||
.icon-btn .badge{position:absolute;top:-3px;right:-3px;background:var(--danger);color:#fff;font-size:9.5px;font-weight:600;min-width:16px;height:16px;border-radius:8px;display:grid;place-items:center;padding:0 4px;border:2px solid var(--bg);font-family:var(--mono)}
|
||
|
||
.btn-primary{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;background:var(--accent);color:#fff;border-radius:7px;font-weight:500;font-size:12.5px;transition:background .15s;box-shadow:0 1px 0 rgba(255,255,255,0.08) inset, 0 1px 3px rgba(79,142,247,0.35)}
|
||
.btn-primary:hover{background:#5e98f7}
|
||
|
||
/* Content */
|
||
.content{padding:22px;display:flex;flex-direction:column;gap:22px;max-width:1600px;width:100%;margin:0 auto}
|
||
|
||
.section-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:-6px}
|
||
.section-title{font-size:13.5px;font-weight:600;letter-spacing:-0.005em;display:flex;align-items:center;gap:9px}
|
||
.section-title .live{font-size:10.5px;color:var(--success);font-family:var(--mono);display:inline-flex;align-items:center;gap:5px;font-weight:500}
|
||
.section-sub{font-size:12px;color:var(--muted);margin-top:2px}
|
||
.section-actions{display:flex;gap:8px;align-items:center}
|
||
.ghost-btn{padding:5px 9px;font-size:11.5px;color:var(--muted);border:1px solid var(--border);border-radius:6px;background:#101019;transition:all .15s}
|
||
.ghost-btn:hover{color:var(--fg);border-color:var(--border-strong)}
|
||
.ghost-btn.active{color:var(--fg);background:#1a1a24;border-color:var(--border-strong)}
|
||
|
||
/* ---------- Metric cards ---------- */
|
||
.metrics{display:grid;grid-template-columns:repeat(6,1fr);gap:12px}
|
||
.metric{position:relative;background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:14px 14px 13px;overflow:hidden;transition:border-color .15s,transform .15s}
|
||
.metric:hover{border-color:var(--border-strong)}
|
||
.metric::before{content:"";position:absolute;top:0;left:0;right:0;height:2px;background:var(--accent)}
|
||
.metric.success::before{background:var(--success)}
|
||
.metric.warning::before{background:var(--warning)}
|
||
.metric.danger::before{background:var(--danger)}
|
||
.metric.neutral::before{background:var(--muted-2)}
|
||
.metric-label{display:flex;align-items:center;gap:7px;color:var(--muted);font-size:11.5px;font-weight:500}
|
||
.metric-label .ico{width:13px;height:13px}
|
||
.metric-value{margin-top:8px;font-family:var(--mono);font-size:22px;font-weight:600;letter-spacing:-0.02em;color:var(--fg);line-height:1.1}
|
||
.metric-value .sub{font-size:13px;color:var(--muted);font-weight:500;margin-left:2px}
|
||
.metric-delta{margin-top:6px;font-size:11px;display:flex;align-items:center;gap:6px;color:var(--muted)}
|
||
.metric-delta .chg{font-family:var(--mono);font-weight:500}
|
||
.metric-delta .chg.up{color:var(--success)}
|
||
.metric-delta .chg.down{color:var(--danger)}
|
||
.metric-delta .chg.warn{color:var(--warning)}
|
||
.metric .spark{position:absolute;right:10px;bottom:10px;opacity:0.7}
|
||
|
||
/* ---------- Card primitive ---------- */
|
||
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
|
||
.card-head{display:flex;align-items:center;justify-content:space-between;padding:13px 16px 12px;border-bottom:1px solid var(--border)}
|
||
.card-head h3{margin:0;font-size:13px;font-weight:600;letter-spacing:-0.005em;display:flex;align-items:center;gap:8px}
|
||
.card-head .meta{font-size:11.5px;color:var(--muted);font-family:var(--mono)}
|
||
.card-tabs{display:flex;gap:2px}
|
||
.card-tabs button{padding:4px 9px;font-size:11.5px;color:var(--muted);border-radius:5px;transition:all .15s}
|
||
.card-tabs button:hover{color:var(--fg)}
|
||
.card-tabs button.active{color:var(--fg);background:#1d1d28}
|
||
|
||
/* ---------- Tables ---------- */
|
||
.table{width:100%;border-collapse:collapse;font-size:12.5px}
|
||
.table th{text-align:left;font-weight:500;color:var(--muted);font-size:11px;letter-spacing:0.04em;text-transform:uppercase;padding:9px 14px;border-bottom:1px solid var(--border);background:#101019;position:sticky;top:0}
|
||
.table th.num{text-align:right}
|
||
.table td{padding:11px 14px;border-bottom:1px solid var(--border);vertical-align:middle}
|
||
.table tbody tr{transition:background .12s}
|
||
.table tbody tr:hover{background:#1a1a24}
|
||
.table tbody tr:last-child td{border-bottom:0}
|
||
.table td.num{text-align:right;font-family:var(--mono)}
|
||
.table td.mono{font-family:var(--mono);color:var(--muted);font-size:11.5px}
|
||
|
||
/* Server cells */
|
||
.server-cell{display:flex;align-items:center;gap:10px}
|
||
.server-icon{width:30px;height:30px;border-radius:7px;background:#1d1d28;border:1px solid var(--border);display:grid;place-items:center;color:var(--muted);flex:none}
|
||
.server-icon.up{color:var(--success);background:rgba(62,207,142,0.06);border-color:rgba(62,207,142,0.18)}
|
||
.server-icon.warn{color:var(--warning);background:rgba(245,166,35,0.06);border-color:rgba(245,166,35,0.18)}
|
||
.server-meta{display:flex;flex-direction:column;gap:1px;min-width:0}
|
||
.server-name{font-weight:500;color:var(--fg);font-size:12.5px}
|
||
.server-ip{font-family:var(--mono);font-size:10.5px;color:var(--muted)}
|
||
|
||
/* Resource bars */
|
||
.res{display:flex;flex-direction:column;gap:3px;min-width:90px}
|
||
.res-top{display:flex;align-items:center;justify-content:space-between;font-size:10.5px;color:var(--muted);font-family:var(--mono)}
|
||
.res-top .val{color:var(--fg)}
|
||
.res-bar{height:4px;background:#1d1d28;border-radius:2px;overflow:hidden;position:relative}
|
||
.res-fill{height:100%;border-radius:2px;background:var(--success);transition:width .3s}
|
||
.res-fill.ok{background:var(--success)}
|
||
.res-fill.warn{background:var(--warning)}
|
||
.res-fill.bad{background:var(--danger)}
|
||
|
||
/* Resource grid (multi-stat cells) */
|
||
.res-grid{display:grid;grid-template-columns:repeat(3,minmax(80px,1fr));gap:10px;min-width:300px}
|
||
|
||
/* Pills */
|
||
.pill{display:inline-flex;align-items:center;gap:5px;padding:2px 7px;font-size:10.5px;font-weight:500;border-radius:4px;font-family:var(--mono);border:1px solid var(--border);background:#1a1a24;color:var(--fg);line-height:1.4}
|
||
.pill.muted{color:var(--muted)}
|
||
.pill.success{color:var(--success);background:var(--success-soft);border-color:rgba(62,207,142,0.22)}
|
||
.pill.warning{color:var(--warning);background:var(--warning-soft);border-color:rgba(245,166,35,0.22)}
|
||
.pill.danger{color:var(--danger);background:var(--danger-soft);border-color:rgba(240,82,82,0.22)}
|
||
.pill.accent{color:var(--accent);background:var(--accent-soft);border-color:rgba(79,142,247,0.22)}
|
||
|
||
/* Grade badge */
|
||
.grade{display:inline-grid;place-items:center;width:24px;height:24px;border-radius:6px;font-family:var(--mono);font-weight:600;font-size:12px;line-height:1}
|
||
.grade.a{background:rgba(62,207,142,0.15);color:var(--success);box-shadow:0 0 0 1px rgba(62,207,142,0.25) inset}
|
||
.grade.b{background:rgba(79,142,247,0.15);color:var(--accent);box-shadow:0 0 0 1px rgba(79,142,247,0.25) inset}
|
||
.grade.c{background:rgba(245,166,35,0.15);color:var(--warning);box-shadow:0 0 0 1px rgba(245,166,35,0.25) inset}
|
||
.grade.d{background:rgba(245,166,35,0.18);color:var(--warning);box-shadow:0 0 0 1px rgba(245,166,35,0.32) inset}
|
||
.grade.f{background:rgba(240,82,82,0.15);color:var(--danger);box-shadow:0 0 0 1px rgba(240,82,82,0.28) inset}
|
||
|
||
/* Site link */
|
||
.site-cell{display:flex;align-items:center;gap:10px;min-width:0}
|
||
.favicon{width:24px;height:24px;border-radius:5px;display:grid;place-items:center;font-weight:600;font-size:11px;color:#0a0a0f;flex:none;font-family:var(--mono)}
|
||
.site-name{font-weight:500;color:var(--fg);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px}
|
||
.site-meta{font-family:var(--mono);font-size:10.5px;color:var(--muted)}
|
||
|
||
/* Action icons */
|
||
.row-actions{display:flex;gap:4px;justify-content:flex-end;opacity:0;transition:opacity .15s}
|
||
tr:hover .row-actions{opacity:1}
|
||
.row-actions button{width:26px;height:26px;border-radius:5px;color:var(--muted);display:grid;place-items:center;transition:all .15s}
|
||
.row-actions button:hover{color:var(--fg);background:#22222e}
|
||
|
||
/* ---------- Bottom row ---------- */
|
||
.bottom-row{display:grid;grid-template-columns:1.4fr 1fr 1fr;gap:14px}
|
||
|
||
/* Chart */
|
||
.chart-wrap{padding:14px 16px 8px}
|
||
.chart-head{display:flex;align-items:flex-end;justify-content:space-between;margin-bottom:6px}
|
||
.chart-figure{font-family:var(--mono);font-size:22px;font-weight:600;letter-spacing:-0.02em}
|
||
.chart-figure .unit{font-size:13px;color:var(--muted);margin-left:4px;font-weight:500}
|
||
.chart-sub{font-size:11px;color:var(--muted);margin-top:2px}
|
||
.legend{display:flex;gap:14px;font-size:11px;color:var(--muted);font-family:var(--mono)}
|
||
.legend i{display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:5px;vertical-align:middle}
|
||
.svg-chart{width:100%;height:160px;display:block}
|
||
.chart-axis{font-family:var(--mono);font-size:9.5px;fill:var(--muted-2)}
|
||
|
||
/* Update pipeline list */
|
||
.pipe{display:flex;flex-direction:column}
|
||
.pipe-row{display:flex;align-items:center;gap:11px;padding:10px 16px;border-bottom:1px solid var(--border);transition:background .12s}
|
||
.pipe-row:last-child{border-bottom:0}
|
||
.pipe-row:hover{background:#1a1a24}
|
||
.pipe-ico{width:26px;height:26px;border-radius:6px;display:grid;place-items:center;flex:none;border:1px solid var(--border);background:#101019;font-size:10.5px;font-family:var(--mono);font-weight:600;color:var(--muted)}
|
||
.pipe-ico.wp{color:#21759b;border-color:rgba(33,117,155,0.3);background:rgba(33,117,155,0.08)}
|
||
.pipe-ico.pl{color:var(--accent);border-color:rgba(79,142,247,0.25);background:var(--accent-soft)}
|
||
.pipe-ico.php{color:#7c5cff;border-color:rgba(124,92,255,0.28);background:rgba(124,92,255,0.10)}
|
||
.pipe-info{flex:1;min-width:0}
|
||
.pipe-title{font-size:12.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.pipe-sub{font-size:11px;color:var(--muted);font-family:var(--mono)}
|
||
.pipe-status{font-size:10.5px;font-family:var(--mono)}
|
||
|
||
/* Mini progress (pipeline) */
|
||
.mini-bar{width:60px;height:4px;background:#1d1d28;border-radius:2px;overflow:hidden}
|
||
.mini-bar span{display:block;height:100%;background:var(--accent);border-radius:2px}
|
||
|
||
/* Activity feed */
|
||
.feed{padding:6px 0}
|
||
.feed-row{display:flex;gap:11px;padding:10px 16px;align-items:flex-start;position:relative;transition:background .12s}
|
||
.feed-row:hover{background:#1a1a24}
|
||
.feed-ico{width:24px;height:24px;border-radius:6px;display:grid;place-items:center;flex:none;color:var(--muted);background:#101019;border:1px solid var(--border)}
|
||
.feed-ico.success{color:var(--success);background:var(--success-soft);border-color:rgba(62,207,142,0.20)}
|
||
.feed-ico.warning{color:var(--warning);background:var(--warning-soft);border-color:rgba(245,166,35,0.20)}
|
||
.feed-ico.danger{color:var(--danger);background:var(--danger-soft);border-color:rgba(240,82,82,0.20)}
|
||
.feed-ico.accent{color:var(--accent);background:var(--accent-soft);border-color:rgba(79,142,247,0.22)}
|
||
.feed-body{flex:1;min-width:0;font-size:12px}
|
||
.feed-body b{font-weight:500;color:var(--fg)}
|
||
.feed-body .target{font-family:var(--mono);color:var(--fg);background:#1d1d28;padding:1px 5px;border-radius:3px;font-size:11px;border:1px solid var(--border)}
|
||
.feed-meta{color:var(--muted);font-size:10.5px;margin-top:2px;font-family:var(--mono)}
|
||
|
||
/* Misc */
|
||
.scroll-x{overflow-x:auto}
|
||
.scroll-x::-webkit-scrollbar{height:8px}
|
||
.scroll-x::-webkit-scrollbar-thumb{background:#1d1d28;border-radius:4px}
|
||
.nav::-webkit-scrollbar{width:6px}
|
||
.nav::-webkit-scrollbar-thumb{background:#1d1d28;border-radius:3px}
|
||
|
||
/* ---------- Responsive ---------- */
|
||
@media (max-width:1380px){
|
||
.metrics{grid-template-columns:repeat(3,1fr)}
|
||
}
|
||
@media (max-width:1180px){
|
||
.bottom-row{grid-template-columns:1fr 1fr}
|
||
.bottom-row > :first-child{grid-column:1/-1}
|
||
}
|
||
@media (max-width:980px){
|
||
.app{grid-template-columns:64px 1fr}
|
||
.sidebar{padding:14px 8px}
|
||
.brand-name,.ws-meta,.ws-chev,.nav-title,.nav-item .count,.plan-box,.nav-item span:not(.ico-w){display:none}
|
||
.nav-item{justify-content:center;padding:8px}
|
||
.ws-switch{justify-content:center;padding:6px}
|
||
.search{min-width:160px}
|
||
}
|
||
@media (max-width:760px){
|
||
.metrics{grid-template-columns:repeat(2,1fr)}
|
||
.bottom-row{grid-template-columns:1fr}
|
||
.content{padding:16px}
|
||
.search{display:none}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app">
|
||
<!-- ============ SIDEBAR ============ -->
|
||
<aside class="sidebar">
|
||
<div class="brand">
|
||
<div class="brand-mark" aria-hidden="true"></div>
|
||
<div class="brand-name">CluPilot<span>v2.4</span></div>
|
||
</div>
|
||
|
||
<button class="ws-switch" aria-label="Workspace wechseln">
|
||
<div class="ws-avatar">AC</div>
|
||
<div class="ws-meta">
|
||
<div class="ws-name">Acme Cluster</div>
|
||
<div class="ws-plan">team · 14 user</div>
|
||
</div>
|
||
<svg class="ws-chev" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 9l4-4 4 4M8 15l4 4 4-4"/></svg>
|
||
</button>
|
||
|
||
<nav class="nav">
|
||
<div class="nav-section">
|
||
<div class="nav-title">Übersicht</div>
|
||
<div class="nav-item active">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/></svg>
|
||
<span>Dashboard</span>
|
||
</div>
|
||
<div class="nav-item">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M3 12h4l3-7 4 14 3-7h4"/></svg>
|
||
<span>Metriken</span>
|
||
<span class="count">live</span>
|
||
</div>
|
||
<div class="nav-item">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>
|
||
<span>Aktivität</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="nav-section">
|
||
<div class="nav-title">Infrastruktur</div>
|
||
<div class="nav-item">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/><circle cx="7" cy="7.5" r="0.7" fill="currentColor"/><circle cx="7" cy="16.5" r="0.7" fill="currentColor"/></svg>
|
||
<span>Server</span>
|
||
<span class="count">6</span>
|
||
</div>
|
||
<div class="nav-item">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M5 12V7a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v5M3 12h18l-2 8H5l-2-8z"/></svg>
|
||
<span>Cluster</span>
|
||
</div>
|
||
<div class="nav-item">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M9 17l-5 3v-7l5 3zM15 17l5 3v-7l-5 3zM12 4l5 3v6l-5 3-5-3V7l5-3z"/></svg>
|
||
<span>SSH Keys</span>
|
||
</div>
|
||
<div class="nav-item">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></svg>
|
||
<span>DNS & Domains</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="nav-section">
|
||
<div class="nav-title">WordPress</div>
|
||
<div class="nav-item">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3v18"/></svg>
|
||
<span>Sites</span>
|
||
<span class="count">87</span>
|
||
</div>
|
||
<div class="nav-item">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 4v16M4 12h16"/><circle cx="12" cy="12" r="9"/></svg>
|
||
<span>Plugins</span>
|
||
</div>
|
||
<div class="nav-item">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
|
||
<span>Themes</span>
|
||
</div>
|
||
<div class="nav-item">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M21 12c0 5-9 9-9 9s-9-4-9-9V5l9-3 9 3v7z"/><path d="M9 11l2 2 4-4"/></svg>
|
||
<span>Backups</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="nav-section">
|
||
<div class="nav-title">Sicherheit</div>
|
||
<div class="nav-item">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7l8-4z"/></svg>
|
||
<span>Firewall</span>
|
||
<span class="count" style="color:var(--warning);background:var(--warning-soft);border-color:rgba(245,166,35,0.25)">3</span>
|
||
</div>
|
||
<div class="nav-item">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M12 8v4l3 2"/></svg>
|
||
<span>Audit-Log</span>
|
||
</div>
|
||
<div class="nav-item">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="5" y="11" width="14" height="9" rx="1.5"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
|
||
<span>Zertifikate</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="nav-section">
|
||
<div class="nav-title">Einstellungen</div>
|
||
<div class="nav-item">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>
|
||
<span>Allgemein</span>
|
||
</div>
|
||
<div class="nav-item">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8"/></svg>
|
||
<span>Team</span>
|
||
</div>
|
||
<div class="nav-item">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18"/></svg>
|
||
<span>Abrechnung</span>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<div class="plan-box">
|
||
<div class="plan-row">
|
||
<div class="plan-name">Team Plan</div>
|
||
<div class="plan-tag">PRO</div>
|
||
</div>
|
||
<div class="plan-meter"><span></span></div>
|
||
<div class="plan-stats">
|
||
<span>87 / 140 sites</span>
|
||
<span>62%</span>
|
||
</div>
|
||
<button class="plan-cta">Upgrade auf Scale →</button>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- ============ MAIN ============ -->
|
||
<main class="main">
|
||
<!-- Topbar -->
|
||
<div class="topbar">
|
||
<div class="crumbs">
|
||
<span class="crumb">Acme Cluster</span>
|
||
<span class="crumb sep">/</span>
|
||
<span class="page-title">Dashboard</span>
|
||
</div>
|
||
<span class="status-chip"><span class="pulse-dot"></span> All systems operational</span>
|
||
<div class="top-spacer"></div>
|
||
<div class="search">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
|
||
<input placeholder="Sites, Server, Domains suchen…" />
|
||
<span class="kbd">⌘K</span>
|
||
</div>
|
||
<button class="icon-btn" aria-label="Dokumentation">
|
||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01"/></svg>
|
||
</button>
|
||
<button class="icon-btn" aria-label="Benachrichtigungen">
|
||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0"/></svg>
|
||
<span class="badge">3</span>
|
||
</button>
|
||
<button class="btn-primary">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg>
|
||
Server
|
||
</button>
|
||
</div>
|
||
|
||
<div class="content">
|
||
|
||
<!-- ============ METRICS ============ -->
|
||
<div class="metrics">
|
||
<div class="metric success">
|
||
<div class="metric-label">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M9 12l2 2 4-4"/></svg>
|
||
Sites Online
|
||
</div>
|
||
<div class="metric-value">87<span class="sub">/87</span></div>
|
||
<div class="metric-delta"><span class="chg up">100%</span> Verfügbarkeit · 24h</div>
|
||
<svg class="spark" width="60" height="22" viewBox="0 0 60 22"><polyline fill="none" stroke="#3ecf8e" stroke-width="1.5" points="0,16 8,14 16,15 24,12 32,13 40,10 48,11 60,8"/></svg>
|
||
</div>
|
||
<div class="metric">
|
||
<div class="metric-label">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>
|
||
Ø Response
|
||
</div>
|
||
<div class="metric-value">142<span class="sub">ms</span></div>
|
||
<div class="metric-delta"><span class="chg up">↓ 8.4%</span> vs. letzte Woche</div>
|
||
<svg class="spark" width="60" height="22" viewBox="0 0 60 22"><polyline fill="none" stroke="#4f8ef7" stroke-width="1.5" points="0,8 8,12 16,9 24,14 32,11 40,10 48,7 60,9"/></svg>
|
||
</div>
|
||
<div class="metric warning">
|
||
<div class="metric-label">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/></svg>
|
||
Updates Pending
|
||
</div>
|
||
<div class="metric-value">24</div>
|
||
<div class="metric-delta"><span class="chg warn">7 kritisch</span> · 17 minor</div>
|
||
<svg class="spark" width="60" height="22" viewBox="0 0 60 22"><polyline fill="none" stroke="#f5a623" stroke-width="1.5" points="0,12 8,14 16,11 24,9 32,10 40,8 48,6 60,5"/></svg>
|
||
</div>
|
||
<div class="metric success">
|
||
<div class="metric-label">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12c0 5-9 9-9 9s-9-4-9-9V5l9-3 9 3v7z"/><path d="M9 11l2 2 4-4"/></svg>
|
||
Backups 24h
|
||
</div>
|
||
<div class="metric-value">86<span class="sub">/87</span></div>
|
||
<div class="metric-delta"><span class="chg warn">1 fehlgeschlagen</span> · 02:14 UTC</div>
|
||
<svg class="spark" width="60" height="22" viewBox="0 0 60 22"><polyline fill="none" stroke="#3ecf8e" stroke-width="1.5" points="0,6 8,5 16,6 24,5 32,6 40,5 48,6 60,5"/></svg>
|
||
</div>
|
||
<div class="metric danger">
|
||
<div class="metric-label">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7l8-4z"/><path d="M12 8v5M12 16h.01"/></svg>
|
||
Security Events
|
||
</div>
|
||
<div class="metric-value">128</div>
|
||
<div class="metric-delta"><span class="chg down">↑ 23%</span> Brute-force Attempts blockiert</div>
|
||
<svg class="spark" width="60" height="22" viewBox="0 0 60 22"><polyline fill="none" stroke="#f05252" stroke-width="1.5" points="0,16 8,12 16,14 24,9 32,11 40,6 48,8 60,4"/></svg>
|
||
</div>
|
||
<div class="metric">
|
||
<div class="metric-label">
|
||
<svg class="ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="6" rx="9" ry="3"/><path d="M3 6v12c0 1.7 4 3 9 3s9-1.3 9-3V6M3 12c0 1.7 4 3 9 3s9-1.3 9-3"/></svg>
|
||
Storage
|
||
</div>
|
||
<div class="metric-value">1.84<span class="sub">TB</span></div>
|
||
<div class="metric-delta"><span class="chg warn">73%</span> von 2.5 TB belegt</div>
|
||
<svg class="spark" width="60" height="22" viewBox="0 0 60 22"><polyline fill="none" stroke="#8888a0" stroke-width="1.5" points="0,14 8,13 16,12 24,11 32,10 40,9 48,8 60,7"/></svg>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ============ SERVER TABLE ============ -->
|
||
<section class="card">
|
||
<div class="card-head">
|
||
<h3>
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/><circle cx="7" cy="7.5" r="0.7" fill="currentColor"/><circle cx="7" cy="16.5" r="0.7" fill="currentColor"/></svg>
|
||
Server-Flotte
|
||
<span class="pill muted" style="margin-left:6px">6 nodes</span>
|
||
</h3>
|
||
<div class="card-tabs">
|
||
<button class="active">Alle</button>
|
||
<button>Produktion</button>
|
||
<button>Staging</button>
|
||
<button>Edge</button>
|
||
</div>
|
||
</div>
|
||
<div class="scroll-x">
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th>Server</th>
|
||
<th>Sites</th>
|
||
<th>CPU · RAM · Disk</th>
|
||
<th>Standort</th>
|
||
<th class="num">Uptime</th>
|
||
<th class="num" style="width:120px"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="server-body"></tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ SITES TABLE ============ -->
|
||
<section class="card">
|
||
<div class="card-head">
|
||
<h3>
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></svg>
|
||
WordPress-Sites
|
||
<span class="pill muted" style="margin-left:6px">8 sichtbar · 87 gesamt</span>
|
||
</h3>
|
||
<div class="section-actions">
|
||
<button class="ghost-btn">Filter</button>
|
||
<button class="ghost-btn">Sortieren</button>
|
||
<button class="ghost-btn active">Alle anzeigen →</button>
|
||
</div>
|
||
</div>
|
||
<div class="scroll-x">
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th>Site</th>
|
||
<th>Server</th>
|
||
<th>WP</th>
|
||
<th>PHP</th>
|
||
<th class="num">Visitors / 24h</th>
|
||
<th>Updates</th>
|
||
<th>Backup</th>
|
||
<th>Security</th>
|
||
<th class="num"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="sites-body"></tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============ BOTTOM ROW ============ -->
|
||
<div class="bottom-row">
|
||
<!-- Traffic chart -->
|
||
<section class="card">
|
||
<div class="card-head">
|
||
<h3>
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 17l5-5 4 4 7-8M14 8h6v6"/></svg>
|
||
Traffic
|
||
<span style="font-size:10.5px;color:var(--success);font-family:var(--mono);display:inline-flex;align-items:center;gap:5px;font-weight:500;margin-left:2px"><span class="pulse-dot" style="color:var(--success)"></span>live</span>
|
||
</h3>
|
||
<div class="card-tabs">
|
||
<button>15m</button>
|
||
<button class="active">60m</button>
|
||
<button>24h</button>
|
||
<button>7d</button>
|
||
</div>
|
||
</div>
|
||
<div class="chart-wrap">
|
||
<div class="chart-head">
|
||
<div>
|
||
<div class="chart-figure">14.2k<span class="unit">req/min</span></div>
|
||
<div class="chart-sub">Peak vor 12 min · 16.8k req/min</div>
|
||
</div>
|
||
<div class="legend">
|
||
<span><i style="background:#4f8ef7"></i>Requests</span>
|
||
<span><i style="background:#7c5cff"></i>Cache hit</span>
|
||
</div>
|
||
</div>
|
||
<svg class="svg-chart" viewBox="0 0 600 160" preserveAspectRatio="none">
|
||
<defs>
|
||
<linearGradient id="grad-req" x1="0" x2="0" y1="0" y2="1">
|
||
<stop offset="0%" stop-color="#4f8ef7" stop-opacity="0.35"/>
|
||
<stop offset="100%" stop-color="#4f8ef7" stop-opacity="0"/>
|
||
</linearGradient>
|
||
<linearGradient id="grad-cache" x1="0" x2="0" y1="0" y2="1">
|
||
<stop offset="0%" stop-color="#7c5cff" stop-opacity="0.20"/>
|
||
<stop offset="100%" stop-color="#7c5cff" stop-opacity="0"/>
|
||
</linearGradient>
|
||
</defs>
|
||
<!-- grid -->
|
||
<g stroke="rgba(255,255,255,0.04)" stroke-width="1">
|
||
<line x1="0" y1="30" x2="600" y2="30"/>
|
||
<line x1="0" y1="65" x2="600" y2="65"/>
|
||
<line x1="0" y1="100" x2="600" y2="100"/>
|
||
<line x1="0" y1="135" x2="600" y2="135"/>
|
||
</g>
|
||
<!-- y axis labels -->
|
||
<text class="chart-axis" x="4" y="34">20k</text>
|
||
<text class="chart-axis" x="4" y="69">15k</text>
|
||
<text class="chart-axis" x="4" y="104">10k</text>
|
||
<text class="chart-axis" x="4" y="139">5k</text>
|
||
<!-- cache area -->
|
||
<path id="cache-area" d="" fill="url(#grad-cache)" />
|
||
<path id="cache-line" d="" stroke="#7c5cff" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||
<!-- req area -->
|
||
<path id="req-area" d="" fill="url(#grad-req)" />
|
||
<path id="req-line" d="" stroke="#4f8ef7" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||
<!-- live dot -->
|
||
<circle id="live-dot" cx="0" cy="0" r="3.5" fill="#4f8ef7" stroke="#0a0a0f" stroke-width="2"/>
|
||
<!-- x axis labels -->
|
||
<text class="chart-axis" x="40" y="156">-60m</text>
|
||
<text class="chart-axis" x="180" y="156">-45m</text>
|
||
<text class="chart-axis" x="320" y="156">-30m</text>
|
||
<text class="chart-axis" x="460" y="156">-15m</text>
|
||
<text class="chart-axis" x="568" y="156">jetzt</text>
|
||
</svg>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Update pipeline -->
|
||
<section class="card">
|
||
<div class="card-head">
|
||
<h3>
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/></svg>
|
||
Update-Pipeline
|
||
</h3>
|
||
<span class="meta">5 aktiv · 19 wartend</span>
|
||
</div>
|
||
<div class="pipe" id="pipe-body"></div>
|
||
</section>
|
||
|
||
<!-- Activity feed -->
|
||
<section class="card">
|
||
<div class="card-head">
|
||
<h3>
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
|
||
Live Activity
|
||
<span style="font-size:10.5px;color:var(--success);font-family:var(--mono);display:inline-flex;align-items:center;gap:5px;font-weight:500;margin-left:2px"><span class="pulse-dot" style="color:var(--success)"></span>live</span>
|
||
</h3>
|
||
<span class="meta">letzte 30 min</span>
|
||
</div>
|
||
<div class="feed" id="feed-body"></div>
|
||
</section>
|
||
</div>
|
||
|
||
<div style="text-align:center;color:var(--muted-2);font-size:11px;font-family:var(--mono);padding:6px 0 20px;letter-spacing:0.04em">
|
||
CluPilot · region: eu-central-1 · build #4f8a32c · uptime 99.984%
|
||
</div>
|
||
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<script>
|
||
/* ============ ICONS (inline) ============ */
|
||
const Icon = {
|
||
terminal:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17l5-5-5-5M13 19h7"/></svg>',
|
||
restart:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/></svg>',
|
||
more:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/></svg>',
|
||
ext:'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 3h7v7M21 3l-9 9M19 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h5"/></svg>',
|
||
server:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="7" rx="1.5"/><rect x="3" y="13" width="18" height="7" rx="1.5"/><circle cx="7" cy="7.5" r="0.7" fill="currentColor"/><circle cx="7" cy="16.5" r="0.7" fill="currentColor"/></svg>',
|
||
};
|
||
|
||
/* ============ HELPERS ============ */
|
||
const statusClass = v => v < 60 ? 'ok' : v < 80 ? 'warn' : 'bad';
|
||
const statusPillClass = v => v < 60 ? 'success' : v < 80 ? 'warning' : 'danger';
|
||
|
||
/* ============ SERVERS ============ */
|
||
const servers = [
|
||
{name:'eu-prod-01', ip:'185.243.117.42', sites:23, cpu:42, ram:58, disk:71, location:'Frankfurt 🇩🇪', flag:'FRA', uptime:'127d 14h', state:'up', host:'Hetzner · CX42'},
|
||
{name:'eu-prod-02', ip:'185.243.117.43', sites:19, cpu:67, ram:74, disk:55, location:'Frankfurt 🇩🇪', flag:'FRA', uptime:'127d 14h', state:'up', host:'Hetzner · CX42'},
|
||
{name:'eu-edge-amsterdam', ip:'78.46.201.18', sites:14, cpu:31, ram:44, disk:38, location:'Amsterdam 🇳🇱', flag:'AMS', uptime:'89d 02h', state:'up', host:'Hetzner · CX32'},
|
||
{name:'us-east-prod-01', ip:'5.161.42.198', sites:21, cpu:78, ram:81, disk:62, location:'Ashburn 🇺🇸', flag:'IAD', uptime:'214d 06h', state:'warn', host:'Hetzner · CCX23'},
|
||
{name:'eu-staging', ip:'168.119.84.221', sites:6, cpu:18, ram:29, disk:22, location:'Nürnberg 🇩🇪', flag:'NBG', uptime:'43d 11h', state:'up', host:'Hetzner · CX22'},
|
||
{name:'apac-sydney-01', ip:'209.97.165.88', sites:4, cpu:54, ram:51, disk:84, location:'Sydney 🇦🇺', flag:'SYD', uptime:'62d 19h', state:'warn', host:'DigitalOcean'},
|
||
];
|
||
|
||
const serverBody = document.getElementById('server-body');
|
||
servers.forEach(s => {
|
||
const cpuC = statusClass(s.cpu), ramC = statusClass(s.ram), diskC = statusClass(s.disk);
|
||
const stateIcon = s.state==='up'
|
||
? '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12l5 5L20 7"/></svg>'
|
||
: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 9v4M12 17h.01"/><circle cx="12" cy="12" r="9"/></svg>';
|
||
serverBody.insertAdjacentHTML('beforeend',`
|
||
<tr>
|
||
<td>
|
||
<div class="server-cell">
|
||
<div class="server-icon ${s.state==='up'?'up':'warn'}">${stateIcon}</div>
|
||
<div class="server-meta">
|
||
<div class="server-name">${s.name}</div>
|
||
<div class="server-ip">${s.ip} · ${s.host}</div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td><span class="pill">${s.sites} sites</span></td>
|
||
<td>
|
||
<div class="res-grid">
|
||
<div class="res">
|
||
<div class="res-top"><span>CPU</span><span class="val">${s.cpu}%</span></div>
|
||
<div class="res-bar"><div class="res-fill ${cpuC}" style="width:${s.cpu}%"></div></div>
|
||
</div>
|
||
<div class="res">
|
||
<div class="res-top"><span>RAM</span><span class="val">${s.ram}%</span></div>
|
||
<div class="res-bar"><div class="res-fill ${ramC}" style="width:${s.ram}%"></div></div>
|
||
</div>
|
||
<div class="res">
|
||
<div class="res-top"><span>Disk</span><span class="val">${s.disk}%</span></div>
|
||
<div class="res-bar"><div class="res-fill ${diskC}" style="width:${s.disk}%"></div></div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td><span style="color:var(--fg)">${s.location.replace(/(.+) (.+)/,'$1')}</span> <span class="pill muted" style="margin-left:6px">${s.flag}</span></td>
|
||
<td class="num"><span style="color:var(--fg)">${s.uptime}</span></td>
|
||
<td class="num">
|
||
<div class="row-actions">
|
||
<button title="SSH-Terminal">${Icon.terminal}</button>
|
||
<button title="Neustart">${Icon.restart}</button>
|
||
<button title="Öffnen">${Icon.ext}</button>
|
||
<button title="Mehr">${Icon.more}</button>
|
||
</div>
|
||
</td>
|
||
</tr>`);
|
||
});
|
||
|
||
/* ============ SITES ============ */
|
||
const palette = ['#4f8ef7','#3ecf8e','#f5a623','#7c5cff','#f05252','#22c1d6','#e879f9','#facc15'];
|
||
const sites = [
|
||
{url:'kunde-mueller.de', server:'eu-prod-01', wp:'6.5.2', wpOk:true, php:'8.2', visitors:'12.4k', updates:0, backup:'ok', grade:'A'},
|
||
{url:'shop.bergmann-coffee.com', server:'eu-prod-01', wp:'6.4.3', wpOk:false, php:'8.1', visitors:'48.7k', updates:3, backup:'ok', grade:'A'},
|
||
{url:'blog.fintech-radar.io', server:'eu-prod-02', wp:'6.5.2', wpOk:true, php:'8.3', visitors:'9.1k', updates:1, backup:'ok', grade:'B'},
|
||
{url:'praxis-dr-schwarz.de', server:'eu-prod-02', wp:'6.2.1', wpOk:false, php:'7.4', visitors:'1.2k', updates:7, backup:'warn', grade:'D'},
|
||
{url:'agentur-helio.com', server:'eu-edge-amsterdam', wp:'6.5.2', wpOk:true, php:'8.2', visitors:'18.2k', updates:0, backup:'ok', grade:'A'},
|
||
{url:'docs.opencrate.dev', server:'us-east-prod-01', wp:'6.5.1', wpOk:true, php:'8.3', visitors:'7.8k', updates:2, backup:'ok', grade:'B'},
|
||
{url:'staging.opencrate.dev', server:'eu-staging', wp:'6.6-RC1', wpOk:true, php:'8.3', visitors:'82', updates:0, backup:'ok', grade:'B'},
|
||
{url:'classic.tonhoff-bau.de', server:'us-east-prod-01', wp:'5.9.7', wpOk:false, php:'7.4', visitors:'3.4k', updates:11,backup:'err', grade:'F'},
|
||
];
|
||
const sitesBody = document.getElementById('sites-body');
|
||
sites.forEach((s,i)=>{
|
||
const initial = s.url[0].toUpperCase();
|
||
const color = palette[i % palette.length];
|
||
const wpPill = s.wpOk ? 'success' : 'warning';
|
||
const phpOld = parseFloat(s.php) < 8.0;
|
||
const phpPill = phpOld ? 'warning' : 'muted';
|
||
const updPill = s.updates===0 ? 'success' : s.updates>=5 ? 'danger' : 'warning';
|
||
const updLabel = s.updates===0 ? 'aktuell' : `${s.updates} verfügbar`;
|
||
const backupMap = {
|
||
ok: {cls:'success', dot:'✓', label:'vor 2 h'},
|
||
warn: {cls:'warning', dot:'!', label:'vor 18 h'},
|
||
err: {cls:'danger', dot:'×', label:'fehlgeschl.'},
|
||
}[s.backup];
|
||
const grade = s.grade.toLowerCase();
|
||
sitesBody.insertAdjacentHTML('beforeend',`
|
||
<tr>
|
||
<td>
|
||
<div class="site-cell">
|
||
<div class="favicon" style="background:${color}">${initial}</div>
|
||
<div style="min-width:0">
|
||
<div class="site-name">${s.url}</div>
|
||
<div class="site-meta">id_${(7300+i).toString(16)} · ssl ✓</div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td><span class="pill muted">${s.server}</span></td>
|
||
<td><span class="pill ${wpPill}">WP ${s.wp}</span></td>
|
||
<td><span class="pill ${phpPill}">PHP ${s.php}</span></td>
|
||
<td class="num"><span style="color:var(--fg)">${s.visitors}</span></td>
|
||
<td><span class="pill ${updPill}">${updLabel}</span></td>
|
||
<td>
|
||
<span class="pill ${backupMap.cls}">
|
||
<span style="display:inline-block;width:5px;height:5px;border-radius:50%;background:currentColor"></span>
|
||
${backupMap.label}
|
||
</span>
|
||
</td>
|
||
<td><span class="grade ${grade}">${s.grade}</span></td>
|
||
<td class="num">
|
||
<div class="row-actions">
|
||
<button title="Site öffnen">${Icon.ext}</button>
|
||
<button title="WP-Admin">${Icon.terminal}</button>
|
||
<button title="Mehr">${Icon.more}</button>
|
||
</div>
|
||
</td>
|
||
</tr>`);
|
||
});
|
||
|
||
/* ============ UPDATE PIPELINE ============ */
|
||
const pipe = [
|
||
{type:'wp', title:'WordPress 6.5.2 → 6.5.3', sub:'agentur-helio.com', pct:78, status:'läuft', statusCls:'accent'},
|
||
{type:'pl', title:'WooCommerce 8.7 → 8.8.1', sub:'shop.bergmann-coffee.com', pct:42, status:'läuft', statusCls:'accent'},
|
||
{type:'pl', title:'Yoast SEO 22.4 → 22.5', sub:'blog.fintech-radar.io', pct:100,status:'erledigt', statusCls:'success'},
|
||
{type:'php', title:'PHP 7.4 → 8.2 Migration', sub:'praxis-dr-schwarz.de', pct:12, status:'review', statusCls:'warning'},
|
||
{type:'wp', title:'WordPress 5.9 → 6.5.2', sub:'classic.tonhoff-bau.de', pct:0, status:'fehler', statusCls:'danger'},
|
||
{type:'pl', title:'Wordfence 7.11 → 7.12.0', sub:'+ 14 sites batch', pct:0, status:'wartet', statusCls:'muted'},
|
||
];
|
||
const pipeBody = document.getElementById('pipe-body');
|
||
pipe.forEach(p=>{
|
||
pipeBody.insertAdjacentHTML('beforeend',`
|
||
<div class="pipe-row">
|
||
<div class="pipe-ico ${p.type}">${p.type.toUpperCase()}</div>
|
||
<div class="pipe-info">
|
||
<div class="pipe-title">${p.title}</div>
|
||
<div class="pipe-sub">${p.sub}</div>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px">
|
||
<span class="pill ${p.statusCls}">${p.pct>0&&p.pct<100?p.pct+'%':''} ${p.status}</span>
|
||
${p.pct>0 ? `<div class="mini-bar"><span style="width:${p.pct}%;background:${p.statusCls==='success'?'var(--success)':p.statusCls==='warning'?'var(--warning)':p.statusCls==='danger'?'var(--danger)':'var(--accent)'}"></span></div>` : ''}
|
||
</div>
|
||
</div>`);
|
||
});
|
||
|
||
/* ============ ACTIVITY FEED ============ */
|
||
const feed = [
|
||
{ic:'success', icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M5 12l5 5L20 7"/></svg>',
|
||
html:'Backup erstellt für <span class="target">agentur-helio.com</span>', meta:'vor 12 sek · 248 MB · eu-prod-02'},
|
||
{ic:'danger', icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7l8-4z"/></svg>',
|
||
html:'<b>17 Login-Versuche</b> blockiert auf <span class="target">classic.tonhoff-bau.de</span>', meta:'vor 1 min · IP 185.220.101.18 · TOR exit'},
|
||
{ic:'accent', icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/></svg>',
|
||
html:'Plugin-Update <span class="target">Yoast SEO 22.5</span> erfolgreich auf 3 Sites', meta:'vor 4 min · ausgeführt von <b>m.weber</b>'},
|
||
{ic:'warning', icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M12 9v4M12 17h.01"/><circle cx="12" cy="12" r="9"/></svg>',
|
||
html:'CPU > 80% auf <span class="target">us-east-prod-01</span>', meta:'vor 7 min · sustained 4m 12s'},
|
||
{ic:'success', icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><rect x="5" y="11" width="14" height="9" rx="1.5"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>',
|
||
html:'SSL-Zertifikat erneuert · <span class="target">*.kunde-mueller.de</span>', meta:'vor 14 min · gültig bis 25.08.2026'},
|
||
{ic:'accent', icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M4 17l5-5-5-5M13 19h7"/></svg>',
|
||
html:'SSH-Session geöffnet auf <span class="target">eu-prod-01</span>', meta:'vor 19 min · user <b>j.koch</b> · key id_ed25519_jk'},
|
||
{ic:'success', icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M5 12l5 5L20 7"/></svg>',
|
||
html:'Health-Check OK · <b>87/87 Sites</b>', meta:'vor 23 min · run #4421'},
|
||
{ic:'warning', icon:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M21 12c0 5-9 9-9 9s-9-4-9-9V7l9-3 9 3v7z"/></svg>',
|
||
html:'Backup verspätet · <span class="target">staging.opencrate.dev</span>', meta:'vor 27 min · retry geplant'},
|
||
];
|
||
const feedBody = document.getElementById('feed-body');
|
||
feed.forEach(f=>{
|
||
feedBody.insertAdjacentHTML('beforeend',`
|
||
<div class="feed-row">
|
||
<div class="feed-ico ${f.ic}">${f.icon}</div>
|
||
<div class="feed-body">
|
||
<div>${f.html}</div>
|
||
<div class="feed-meta">${f.meta}</div>
|
||
</div>
|
||
</div>`);
|
||
});
|
||
|
||
/* ============ TRAFFIC CHART (smooth procedural) ============ */
|
||
(function(){
|
||
const W = 600, H = 160, padL = 30, padR = 8, padT = 10, padB = 28;
|
||
const N = 60;
|
||
const reqData = [], cacheData = [];
|
||
// generate plausible req-per-min around 14k with peaks
|
||
let v = 11; // in k
|
||
for(let i=0;i<N;i++){
|
||
v += (Math.random()-0.45)*1.6;
|
||
// peak around index 48 (12 min ago)
|
||
if(i>40 && i<52) v += Math.sin((i-40)/12*Math.PI)*3.5;
|
||
v = Math.max(5, Math.min(19, v));
|
||
reqData.push(v);
|
||
cacheData.push(v * (0.55 + Math.random()*0.15));
|
||
}
|
||
const x = i => padL + (W-padL-padR) * (i/(N-1));
|
||
const y = vv => {
|
||
// scale 0-20 → padT..H-padB
|
||
const t = vv/20;
|
||
return H - padB - t*(H-padT-padB);
|
||
};
|
||
function pathFor(arr){
|
||
let d = `M ${x(0)} ${y(arr[0])}`;
|
||
for(let i=1;i<arr.length;i++){
|
||
const xc = (x(i-1)+x(i))/2;
|
||
d += ` Q ${x(i-1)} ${y(arr[i-1])} ${xc} ${(y(arr[i-1])+y(arr[i]))/2}`;
|
||
}
|
||
d += ` T ${x(arr.length-1)} ${y(arr[arr.length-1])}`;
|
||
return d;
|
||
}
|
||
function areaFor(arr){
|
||
return pathFor(arr) + ` L ${x(arr.length-1)} ${H-padB} L ${x(0)} ${H-padB} Z`;
|
||
}
|
||
document.getElementById('req-line').setAttribute('d', pathFor(reqData));
|
||
document.getElementById('req-area').setAttribute('d', areaFor(reqData));
|
||
document.getElementById('cache-line').setAttribute('d', pathFor(cacheData));
|
||
document.getElementById('cache-area').setAttribute('d', areaFor(cacheData));
|
||
const dot = document.getElementById('live-dot');
|
||
dot.setAttribute('cx', x(N-1));
|
||
dot.setAttribute('cy', y(reqData[N-1]));
|
||
// gentle pulse
|
||
let t0 = 0;
|
||
function tick(){
|
||
t0 += 0.04;
|
||
const r = 3.5 + Math.sin(t0)*0.8;
|
||
dot.setAttribute('r', r);
|
||
requestAnimationFrame(tick);
|
||
}
|
||
tick();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|