clupilot/templates/CluPilot Login.html

414 lines
23 KiB
HTML

<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Anmelden · CluPilot</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;
--bg-2:#0c0c12;
--card:#16161e;
--card-hover:#1a1a24;
--surface:#101019;
--surface-2:#1d1d28;
--border:rgba(255,255,255,0.06);
--border-strong:rgba(255,255,255,0.10);
--border-focus:rgba(79,142,247,0.5);
--fg:#f0f0f5;
--muted:#8888a0;
--muted-2:#5c5c70;
--accent:#4f8ef7;
--accent-2:#7c5cff;
--accent-soft:rgba(79,142,247,0.12);
--success:#3ecf8e;
--warning:#f5a623;
--danger:#f05252;
--success-soft:rgba(62,207,142,0.12);
--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:13.5px;line-height:1.5;-webkit-font-smoothing:antialiased;min-height:100vh}
a{color:var(--accent);text-decoration:none}
a:hover{text-decoration:underline}
button{font-family:inherit;color:inherit;background:none;border:0;cursor:pointer}
/* ===== ambient backdrop ===== */
body::before{
content:"";
position:fixed;inset:0;z-index:0;pointer-events:none;
background:
radial-gradient(60% 50% at 12% 10%, rgba(79,142,247,0.10), transparent 65%),
radial-gradient(50% 50% at 88% 100%, rgba(124,92,255,0.08), transparent 65%);
}
body::after{
content:"";
position:fixed;inset:0;z-index:0;pointer-events:none;
background-image:
linear-gradient(rgba(255,255,255,0.018) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.018) 1px, transparent 1px);
background-size:32px 32px;
mask-image:radial-gradient(ellipse at center, #000 30%, transparent 75%);
}
/* ===== layout ===== */
.page{position:relative;z-index:1;min-height:100vh;display:grid;grid-template-columns:minmax(420px,1fr) 1fr;gap:0}
/* ===== left: form ===== */
.form-side{display:flex;flex-direction:column;padding:32px 48px;min-height:100vh}
.top-row{display:flex;align-items:center;justify-content:space-between;gap:16px}
.brand{display:flex;align-items:center;gap:10px}
.brand-mark{width:30px;height:30px;border-radius:8px;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 6px 16px rgba(79,142,247,0.30)}
.brand-mark::before{content:"";width:11px;height:11px;border-radius:50%;background:#0a0a0f;box-shadow:inset 0 0 0 2px #fff}
.brand-name{font-weight:600;font-size:15px;letter-spacing:-0.01em}
.brand-name span{color:var(--muted);font-weight:500;margin-left:5px;font-size:11.5px}
.status-chip{display:inline-flex;align-items:center;gap:6px;padding:4px 9px 4px 8px;border:1px solid rgba(62,207,142,0.22);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.4);opacity:0}}
.form-wrap{margin:auto 0;display:flex;flex-direction:column;gap:24px;max-width:380px;width:100%;padding:48px 0}
h1{margin:0;font-size:24px;font-weight:600;letter-spacing:-0.02em;line-height:1.2}
.sub{margin:6px 0 0;font-size:13.5px;color:var(--muted)}
.sub a{font-weight:500}
/* SSO row */
.sso-row{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.sso-btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:9px 12px;background:var(--surface);border:1px solid var(--border);border-radius:8px;color:var(--fg);font-size:12.5px;font-weight:500;transition:all .15s}
.sso-btn:hover{background:var(--surface-2);border-color:var(--border-strong)}
.sso-btn svg{flex:none}
.divider{display:flex;align-items:center;gap:12px;color:var(--muted-2);font-size:11px;font-family:var(--mono);text-transform:uppercase;letter-spacing:0.08em}
.divider::before,.divider::after{content:"";flex:1;height:1px;background:var(--border)}
/* Inputs */
form{display:flex;flex-direction:column;gap:14px}
.field{display:flex;flex-direction:column;gap:6px}
.field-label{display:flex;align-items:center;justify-content:space-between;font-size:11.5px;color:var(--muted);font-weight:500}
.field-label a{font-size:11.5px;color:var(--muted);font-weight:500}
.field-label a:hover{color:var(--fg);text-decoration:none}
.input{display:flex;align-items:center;gap:10px;padding:0 12px;background:var(--surface);border:1px solid var(--border);border-radius:8px;transition:border-color .15s, background .15s, box-shadow .15s;height:40px}
.input:hover{border-color:var(--border-strong)}
.input:focus-within{border-color:var(--border-focus);background:#12121c;box-shadow:0 0 0 3px rgba(79,142,247,0.10)}
.input svg{color:var(--muted-2);flex:none}
.input input{flex:1;background:none;border:0;outline:0;color:var(--fg);font-size:13.5px;font-family:inherit;min-width:0;height:100%}
.input input::placeholder{color:var(--muted-2)}
.input .eye{color:var(--muted);padding:4px;border-radius:4px;display:grid;place-items:center;transition:color .15s,background .15s}
.input .eye:hover{color:var(--fg);background:var(--surface-2)}
/* Remember + submit */
.row-between{display:flex;align-items:center;justify-content:space-between;margin-top:2px}
.checkbox{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--muted);cursor:pointer;user-select:none}
.checkbox input{display:none}
.checkbox .box{width:15px;height:15px;border-radius:4px;border:1px solid var(--border-strong);background:var(--surface);display:grid;place-items:center;transition:all .15s}
.checkbox input:checked + .box{background:var(--accent);border-color:var(--accent)}
.checkbox input:checked + .box svg{opacity:1;transform:scale(1)}
.checkbox .box svg{opacity:0;transform:scale(0.6);transition:all .15s;color:#fff}
.checkbox:hover{color:var(--fg)}
.btn-primary{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:10px 16px;background:var(--accent);color:#fff;border-radius:8px;font-weight:500;font-size:13.5px;height:40px;transition:background .15s,transform .05s;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}
.btn-primary:active{transform:translateY(1px)}
.btn-primary .arrow{transition:transform .15s}
.btn-primary:hover .arrow{transform:translateX(2px)}
/* Magic link / passkey link */
.alt-options{display:flex;flex-direction:column;gap:8px;margin-top:4px}
.alt-btn{display:flex;align-items:center;gap:10px;padding:9px 12px;background:transparent;border:1px solid var(--border);border-radius:8px;font-size:12.5px;color:var(--fg);font-weight:500;transition:all .15s}
.alt-btn:hover{border-color:var(--border-strong);background:var(--surface)}
.alt-btn .badge{margin-left:auto;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);font-weight:500}
.alt-btn .ico{color:var(--muted)}
/* footer */
.form-footer{display:flex;align-items:center;justify-content:space-between;gap:12px;color:var(--muted);font-size:11.5px;font-family:var(--mono);padding-top:12px;border-top:1px solid var(--border);flex-wrap:wrap}
.form-footer .links{display:flex;gap:14px}
.form-footer .links a{color:var(--muted)}
.form-footer .links a:hover{color:var(--fg);text-decoration:none}
.form-footer .lock{display:flex;align-items:center;gap:6px}
/* ===== right: live panel ===== */
.live-side{position:relative;border-left:1px solid var(--border);background:linear-gradient(180deg,#0c0c12 0%, #0a0a0f 100%);padding:32px 36px;overflow:hidden;display:flex;flex-direction:column;gap:18px;min-height:100vh}
.live-side::before{
content:"";position:absolute;inset:0;pointer-events:none;
background:radial-gradient(80% 50% at 100% 0%, rgba(79,142,247,0.12), transparent 65%);
}
.live-side > *{position:relative}
.live-head{display:flex;align-items:center;justify-content:space-between}
.live-head .label{font-size:11px;color:var(--muted);font-family:var(--mono);text-transform:uppercase;letter-spacing:0.10em}
.live-head .meta{font-size:11px;color:var(--muted);font-family:var(--mono)}
.hero-quote{margin:8px 0 0;font-size:22px;font-weight:600;letter-spacing:-0.015em;line-height:1.3;max-width:480px}
.hero-quote em{font-style:normal;color:var(--accent);font-weight:600}
.hero-sub{color:var(--muted);font-size:13px;max-width:460px;margin-top:6px}
/* Stat strip */
.stat-strip{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:6px}
.stat{background:var(--card);border:1px solid var(--border);border-radius:9px;padding:12px}
.stat-label{font-size:10.5px;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em;font-weight:500}
.stat-value{font-family:var(--mono);font-size:18px;font-weight:600;letter-spacing:-0.01em;margin-top:4px}
.stat-value .unit{font-size:12px;color:var(--muted);font-weight:500;margin-left:2px}
.stat-trend{display:inline-flex;align-items:center;gap:4px;font-family:var(--mono);font-size:10.5px;color:var(--success);margin-top:4px}
.stat-trend.warn{color:var(--warning)}
/* terminal card */
.term-card{background:var(--card);border:1px solid var(--border);border-radius:10px;overflow:hidden;flex:1;display:flex;flex-direction:column;min-height:280px}
.term-head{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--border);background:#101019}
.term-dots{display:flex;gap:5px}
.term-dots span{width:9px;height:9px;border-radius:50%;background:#2a2a36}
.term-dots span:nth-child(1){background:#f05252}
.term-dots span:nth-child(2){background:#f5a623}
.term-dots span:nth-child(3){background:#3ecf8e}
.term-title{font-family:var(--mono);font-size:11.5px;color:var(--muted);flex:1;text-align:center}
.term-meta{font-family:var(--mono);font-size:10.5px;color:var(--muted-2)}
.term-body{padding:14px 16px;font-family:var(--mono);font-size:11.5px;line-height:1.6;flex:1;overflow:hidden;position:relative}
.term-line{display:flex;gap:10px;align-items:flex-start;opacity:0;transform:translateY(4px);animation:typeIn .35s ease forwards}
.term-time{color:var(--muted-2);flex:none;width:60px;font-size:10.5px;padding-top:1px}
.term-text{color:#cccddd;flex:1;min-width:0;word-break:break-word}
.term-text .ok{color:var(--success)}
.term-text .warn{color:var(--warning)}
.term-text .err{color:var(--danger)}
.term-text .acc{color:var(--accent)}
.term-text .dim{color:var(--muted)}
.term-text .pkg{color:#c084fc}
.term-prompt{display:inline-flex;align-items:center;gap:7px;color:var(--success)}
.term-prompt::before{content:"$";color:var(--accent);font-weight:600}
@keyframes typeIn{to{opacity:1;transform:translateY(0)}}
.cursor{display:inline-block;width:7px;height:13px;background:var(--accent);vertical-align:-2px;animation:blink 1s steps(1) infinite;margin-left:2px}
@keyframes blink{50%{opacity:0}}
/* mini activity row */
.live-tags{display:flex;flex-wrap:wrap;gap:6px}
.live-tag{display:inline-flex;align-items:center;gap:5px;padding:3px 7px;font-size:10.5px;font-family:var(--mono);color:var(--muted);background:var(--surface);border:1px solid var(--border);border-radius:4px}
.live-tag .dot{width:5px;height:5px;border-radius:50%;background:var(--success)}
.live-tag.warn .dot{background:var(--warning)}
/* responsive */
@media (max-width:1080px){
.page{grid-template-columns:1fr}
.live-side{display:none}
.form-side{padding:28px}
}
@media (max-width:520px){
.form-side{padding:22px}
.sso-row{grid-template-columns:1fr}
.form-footer{justify-content:flex-start}
}
</style>
</head>
<body>
<div class="page">
<!-- ============ LEFT: FORM ============ -->
<section class="form-side">
<div class="top-row">
<div class="brand">
<div class="brand-mark" aria-hidden="true"></div>
<div class="brand-name">CluPilot<span>v2.4</span></div>
</div>
<span class="status-chip"><span class="pulse-dot"></span> All systems operational</span>
</div>
<div class="form-wrap">
<header>
<h1>Willkommen zurück</h1>
<p class="sub">Bei CluPilot anmelden oder <a href="#">neues Konto erstellen</a></p>
</header>
<div class="sso-row">
<button class="sso-btn" type="button">
<svg width="15" height="15" viewBox="0 0 24 24"><path fill="#4285F4" d="M22.5 12.27c0-.79-.07-1.54-.2-2.27H12v4.51h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.32z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.99.66-2.25 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84A11 11 0 0 0 12 23z"/><path fill="#FBBC05" d="M5.84 14.10A6.6 6.6 0 0 1 5.5 12c0-.73.13-1.44.34-2.10V7.06H2.18A11 11 0 0 0 1 12c0 1.77.42 3.45 1.18 4.94l3.66-2.84z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.07.56 4.21 1.64l3.15-3.15C17.45 2.10 14.96 1 12 1A11 11 0 0 0 2.18 7.06l3.66 2.84C6.71 7.30 9.14 5.38 12 5.38z"/></svg>
Google
</button>
<button class="sso-btn" type="button">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.55v-2.02c-3.2.7-3.87-1.36-3.87-1.36-.52-1.33-1.27-1.68-1.27-1.68-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.02 1.75 2.69 1.24 3.35.95.1-.74.4-1.24.73-1.53-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.28 1.18-3.09-.12-.29-.51-1.46.11-3.05 0 0 .97-.31 3.18 1.18.92-.26 1.91-.39 2.89-.39.98 0 1.97.13 2.89.39 2.20-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.12 3.05.74.81 1.18 1.83 1.18 3.09 0 4.42-2.69 5.40-5.25 5.68.41.36.78 1.06.78 2.14v3.18c0 .31.21.67.79.55C20.21 21.39 23.5 17.08 23.5 12c0-6.35-5.15-11.5-11.5-11.5z"/></svg>
GitHub
</button>
</div>
<div class="divider">oder mit E-Mail</div>
<form id="login-form" autocomplete="on">
<div class="field">
<label class="field-label" for="email">E-Mail-Adresse</label>
<div class="input">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 6 9-6"/></svg>
<input id="email" name="email" type="email" placeholder="du@firma.de" autocomplete="email" required value="m.weber@acme-cluster.de" />
</div>
</div>
<div class="field">
<label class="field-label" for="password">
Passwort
<a href="#">Vergessen?</a>
</label>
<div class="input">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
<input id="password" name="password" type="password" placeholder="••••••••••••" autocomplete="current-password" required value="hunter2-very-secure" />
<button type="button" class="eye" id="toggle-eye" aria-label="Passwort anzeigen">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
</div>
</div>
<div class="row-between">
<label class="checkbox">
<input type="checkbox" checked />
<span class="box">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l5 5L20 7"/></svg>
</span>
<span>30 Tage angemeldet bleiben</span>
</label>
</div>
<button class="btn-primary" type="submit">
Anmelden
<svg class="arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
</button>
<div class="alt-options">
<button class="alt-btn" type="button">
<svg class="ico" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 2L11 13"/><path d="M22 2l-7 20-4-9-9-4 20-7z"/></svg>
Magic-Link per E-Mail senden
</button>
<button class="alt-btn" type="button">
<svg class="ico" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12c0-4.97-4.03-9-9-9-2.5 0-4.76 1.02-6.39 2.67M3 12c0 4.97 4.03 9 9 9 2.5 0 4.76-1.02 6.39-2.67"/><path d="M9 12a3 3 0 1 0 6 0 3 3 0 1 0-6 0M12 1v4M12 19v4"/></svg>
Mit Passkey anmelden
<span class="badge">empfohlen</span>
</button>
</div>
</form>
</div>
<footer class="form-footer">
<div class="lock">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
TLS 1.3 · AES-256 · region eu-central-1
</div>
<div class="links">
<a href="#">Datenschutz</a>
<a href="#">AGB</a>
<a href="#">Status</a>
<a href="#">Hilfe</a>
</div>
</footer>
</section>
<!-- ============ RIGHT: LIVE PANEL ============ -->
<aside class="live-side" aria-hidden="true">
<div class="live-head">
<div class="label">— live cluster</div>
<div class="meta">eu-central-1 · build #4f8a32c</div>
</div>
<div>
<p class="hero-quote">Verwalte <em>87 WordPress-Sites</em> auf 6 Servern. Ein Login. Volle Kontrolle.</p>
<p class="hero-sub">SSH-Tunnel, Backups, Updates und Security in einer Konsole — von Push-to-Deploy bis Brute-Force-Defense.</p>
</div>
<div class="stat-strip">
<div class="stat">
<div class="stat-label">Sites Online</div>
<div class="stat-value">87<span class="unit">/87</span></div>
<div class="stat-trend">● 100% Uptime · 24h</div>
</div>
<div class="stat">
<div class="stat-label">Ø Response</div>
<div class="stat-value">142<span class="unit">ms</span></div>
<div class="stat-trend">↓ 8.4% vs. 7d</div>
</div>
<div class="stat">
<div class="stat-label">Blocked / 24h</div>
<div class="stat-value">128</div>
<div class="stat-trend warn">↑ 23% Brute-force</div>
</div>
</div>
<div class="term-card">
<div class="term-head">
<div class="term-dots"><span></span><span></span><span></span></div>
<div class="term-title">ssh <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="e28d9291a28797cf92908d86cfd2d3cc818e97928b8e8d96cc8b8d">[email&#160;protected]</a></div>
<div class="term-meta">pts/0</div>
</div>
<div class="term-body" id="term-body">
<!-- lines injected by JS -->
</div>
</div>
<div class="live-tags">
<span class="live-tag"><span class="dot"></span>eu-prod-01 · 23 sites</span>
<span class="live-tag"><span class="dot"></span>eu-prod-02 · 19 sites</span>
<span class="live-tag"><span class="dot"></span>edge-ams · 14 sites</span>
<span class="live-tag warn"><span class="dot"></span>us-east-01 · CPU 78%</span>
<span class="live-tag"><span class="dot"></span>staging · 6 sites</span>
</div>
</aside>
</div>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script>
/* ----- password eye toggle ----- */
const eyeBtn = document.getElementById('toggle-eye');
const pwd = document.getElementById('password');
eyeBtn.addEventListener('click', () => {
pwd.type = pwd.type === 'password' ? 'text' : 'password';
});
/* ----- prevent submit (demo) ----- */
document.getElementById('login-form').addEventListener('submit', (e) => {
e.preventDefault();
const btn = e.target.querySelector('.btn-primary');
const orig = btn.innerHTML;
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="animation:spin 0.8s linear infinite"><path d="M21 12a9 9 0 1 1-6.2-8.55"/></svg> Authentifiziere…';
btn.disabled = true;
setTimeout(() => { btn.innerHTML = orig; btn.disabled = false; }, 1800);
});
/* ----- fake terminal stream ----- */
const lines = [
['09:42:18', '<span class="term-prompt"></span> <span class="dim">clupilot</span> status --cluster=acme'],
['09:42:18', '<span class="ok">✓</span> 87/87 sites <span class="dim">healthy</span> · <span class="acc">142ms</span> ø response'],
['09:42:19', '<span class="ok">✓</span> backup eu-prod-02 → <span class="pkg">s3://clupilot-backups</span> <span class="dim">248 MB</span>'],
['09:42:21', '<span class="acc">→</span> deploying <span class="pkg">Yoast-SEO@22.5</span> to <span class="dim">3 sites…</span>'],
['09:42:23', '<span class="ok">✓</span> agentur-helio.com <span class="dim">(312ms)</span>'],
['09:42:24', '<span class="ok">✓</span> blog.fintech-radar.io <span class="dim">(287ms)</span>'],
['09:42:24', '<span class="ok">✓</span> docs.opencrate.dev <span class="dim">(298ms)</span>'],
['09:42:26', '<span class="warn">!</span> classic.tonhoff-bau.de <span class="dim">PHP 7.4 EOL — migration queued</span>'],
['09:42:28', '<span class="err">✗</span> fw <span class="dim">blocked</span> 17 login attempts from <span class="acc">185.220.101.18</span> <span class="dim">(TOR)</span>'],
['09:42:30', '<span class="ok">✓</span> SSL renewed: <span class="pkg">*.kunde-mueller.de</span> <span class="dim">until 2026-08-25</span>'],
['09:42:32', '<span class="term-prompt"></span> <span class="dim">tail -f /var/log/clupilot/audit.log</span><span class="cursor"></span>'],
];
const body = document.getElementById('term-body');
let i = 0;
function addLine(){
if(i >= lines.length){
// restart cycle after a pause
setTimeout(() => { body.innerHTML = ''; i = 0; addLine(); }, 5000);
return;
}
const [t, html] = lines[i++];
const el = document.createElement('div');
el.className = 'term-line';
el.innerHTML = '<div class="term-time">'+t+'</div><div class="term-text">'+html+'</div>';
body.appendChild(el);
// auto-scroll
body.scrollTop = body.scrollHeight;
// remove top lines if too many
while(body.children.length > 11) body.removeChild(body.firstChild);
setTimeout(addLine, 600 + Math.random()*450);
}
addLine();
/* spinner keyframes injected */
const style = document.createElement('style');
style.textContent = '@keyframes spin{to{transform:rotate(360deg)}}';
document.head.appendChild(style);
</script>
</body>
</html>