clupilot/templates/CluPilot Dashboard.html

912 lines
53 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!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 &amp; 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>