Files
zone/index.html
2026-03-19 17:58:56 +00:00

871 lines
42 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Home Server Control</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400&family=Syne:wght@400;700;800&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #03050d;
--surface: #080e1e;
--border: #0f1f3d;
--border-hi: #1a3560;
--accent: #00d4ff;
--accent2: #7c3aed;
--accent3: #10b981;
--warn: #f59e0b;
--danger: #ef4444;
--text: #c8d8f0;
--muted: #4a6080;
--glow: 0 0 20px rgba(0,212,255,0.3);
--glow-sm: 0 0 8px rgba(0,212,255,0.2);
}
html { scroll-behavior: smooth; }
body {
font-family: 'Space Mono', monospace;
background: var(--bg);
color: var(--text);
min-height: 100vh;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed; inset: 0;
background:
radial-gradient(ellipse 80% 50% at 20% 20%, rgba(0,212,255,0.04) 0%, transparent 60%),
radial-gradient(ellipse 60% 60% at 80% 80%, rgba(124,58,237,0.05) 0%, transparent 60%);
pointer-events: none; z-index: 0;
}
#grid-overlay {
position: fixed; inset: 0;
background-image:
linear-gradient(rgba(0,212,255,0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,212,255,0.025) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none; z-index: 0;
}
body::after {
content: '';
position: fixed; inset: 0;
background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.07) 2px, rgba(0,0,0,0.07) 4px);
pointer-events: none; z-index: 9999;
}
.container {
position: relative; z-index: 1;
max-width: 960px; margin: 0 auto;
padding: 2rem 1.5rem 4rem;
}
/* ── HEADER ── */
header {
display: flex; align-items: flex-start;
justify-content: space-between;
margin-bottom: 2.5rem; gap: 1rem; flex-wrap: wrap;
}
.site-title {
font-family: 'Syne', sans-serif; font-weight: 800;
font-size: clamp(1.6rem, 4vw, 2.4rem);
letter-spacing: -0.02em; line-height: 1;
}
.site-title .line1 { color: var(--text); }
.site-title .line2 { color: var(--accent); display: block; text-shadow: var(--glow); animation: flicker 6s infinite; }
@keyframes flicker { 0%,98%,100%{opacity:1} 99%{opacity:0.7} }
.header-meta { text-align: right; font-size: 0.72rem; color: var(--muted); line-height: 1.8; }
.clock-display { font-size: 1.5rem; font-weight: 700; color: var(--accent); text-shadow: var(--glow-sm); letter-spacing: 0.05em; }
/* ── GLOBAL STATUS ── */
#globalStatus {
display: flex; align-items: center; gap: 0.75rem;
padding: 0.7rem 1.2rem; border-radius: 6px;
font-size: 0.8rem; font-weight: 700;
letter-spacing: 0.08em; text-transform: uppercase;
margin-bottom: 1.5rem; border: 1px solid; transition: all 0.4s ease;
}
.global-ok { background: rgba(16,185,129,0.08); border-color: rgba(16,185,129,0.3); color: var(--accent3); box-shadow: 0 0 30px rgba(16,185,129,0.08); }
.global-error { background: rgba(239,68,68,0.08); border-color: rgba(239,68,68,0.3); color: var(--danger); box-shadow: 0 0 30px rgba(239,68,68,0.08); }
.status-pulse { width: 8px; height: 8px; border-radius: 50%; background: currentColor; flex-shrink: 0; animation: pulse-ring 2s ease infinite; }
@keyframes pulse-ring { 0%{box-shadow:0 0 0 0 currentColor;opacity:1} 70%{box-shadow:0 0 0 6px transparent} 100%{box-shadow:0 0 0 0 transparent;opacity:1} }
/* ── OVERVIEW ── */
.overview { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1.5rem; }
.stat-card {
background: var(--surface); border: 1px solid var(--border);
border-radius: 8px; padding: 1rem 1.2rem;
display: flex; align-items: center; gap: 0.8rem;
transition: border-color 0.3s, box-shadow 0.3s;
position: relative; overflow: hidden;
}
.stat-card::before {
content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
opacity: 0; transition: opacity 0.3s;
}
.stat-card:hover { border-color: var(--border-hi); }
.stat-card:hover::before { opacity: 0.6; }
.stat-label { font-size: 0.7rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.1em; }
.stat-value { font-size: 0.85rem; font-weight: 700; margin-top: 0.15rem; }
/* ── DOTS ── */
.dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; transition: background 0.4s; }
.dot.green { background: var(--accent3); box-shadow: 0 0 8px var(--accent3); }
.dot.red { background: var(--danger); box-shadow: 0 0 8px var(--danger); }
.dot.yellow { background: var(--warn); box-shadow: 0 0 8px var(--warn); animation: blink 0.8s step-end infinite; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.3} }
/* ── CARDS ── */
.card {
background: var(--surface); border: 1px solid var(--border);
border-radius: 10px; padding: 1.5rem; margin-bottom: 1.2rem;
transition: border-color 0.3s; position: relative; overflow: hidden;
}
.card:hover { border-color: var(--border-hi); }
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.2rem; }
.card-title {
font-family: 'Syne', sans-serif; font-size: 0.95rem; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.12em; color: var(--text);
display: flex; align-items: center; gap: 0.5rem;
}
.card-title span { color: var(--accent); }
/* ── BADGES ── */
.badge { font-size: 0.7rem; padding: 0.25rem 0.6rem; border-radius: 4px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; border: 1px solid; }
.badge.ok { color: var(--accent3); border-color: rgba(16,185,129,0.4); background: rgba(16,185,129,0.1); }
.badge.error { color: var(--danger); border-color: rgba(239,68,68,0.4); background: rgba(239,68,68,0.1); }
.badge.neutral { color: var(--muted); border-color: var(--border); background: transparent; }
.badge.warn { color: var(--warn); border-color: rgba(245,158,11,0.4); background: rgba(245,158,11,0.1); }
/* ── QUICK LINKS ── */
.quick-links { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 0.75rem; }
.link-card {
background: rgba(255,255,255,0.02); border: 1px solid var(--border);
padding: 0.9rem 0.6rem; border-radius: 8px; text-align: center;
text-decoration: none; color: var(--text); font-size: 0.78rem; font-weight: 700;
letter-spacing: 0.04em; transition: all 0.2s;
display: flex; flex-direction: column; gap: 0.4rem;
position: relative; overflow: hidden;
}
.link-card .icon { font-size: 1.4rem; }
.link-card::after { content: ''; position: absolute; inset: 0; background: linear-gradient(135deg, rgba(0,212,255,0.06), transparent); opacity: 0; transition: opacity 0.2s; }
.link-card:hover { border-color: rgba(0,212,255,0.4); color: var(--accent); transform: translateY(-3px); box-shadow: 0 8px 24px rgba(0,212,255,0.08); }
.link-card:hover::after { opacity: 1; }
/* ── COUNTDOWN ── */
.countdown-display { display: flex; gap: 1rem; flex-wrap: wrap; }
.time-unit { text-align: center; background: rgba(0,212,255,0.05); border: 1px solid rgba(0,212,255,0.15); border-radius: 8px; padding: 0.6rem 1rem; min-width: 64px; transition: all 0.3s; }
.time-unit:hover { border-color: rgba(0,212,255,0.4); background: rgba(0,212,255,0.1); }
.time-number { font-family: 'Syne', sans-serif; font-size: 1.8rem; font-weight: 800; color: var(--accent); text-shadow: var(--glow-sm); line-height: 1; display: block; }
.time-label { font-size: 0.6rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.1em; margin-top: 0.3rem; }
/* ── DAILY MESSAGE ── */
.daily-message { font-style: italic; font-size: 1rem; color: var(--text); border-left: 2px solid var(--accent2); padding-left: 1rem; line-height: 1.6; }
/* ── BUTTONS ── */
.btn {
display: inline-flex; align-items: center; gap: 0.5rem;
padding: 0.55rem 1.1rem; border-radius: 6px; border: 1px solid;
cursor: pointer; font-family: 'Space Mono', monospace;
font-size: 0.78rem; font-weight: 700; letter-spacing: 0.06em;
text-transform: uppercase; transition: all 0.2s; background: transparent;
}
.btn-primary { color: var(--accent); border-color: rgba(0,212,255,0.4); background: rgba(0,212,255,0.05); }
.btn-primary:hover:not(:disabled) { background: rgba(0,212,255,0.12); border-color: var(--accent); box-shadow: 0 0 16px rgba(0,212,255,0.2); }
.btn-secondary { color: var(--accent2); border-color: rgba(124,58,237,0.4); background: rgba(124,58,237,0.05); }
.btn-secondary:hover:not(:disabled) { background: rgba(124,58,237,0.12); border-color: var(--accent2); box-shadow: 0 0 16px rgba(124,58,237,0.2); }
.btn-danger { color: var(--danger); border-color: rgba(239,68,68,0.4); background: rgba(239,68,68,0.05); }
.btn-danger:hover:not(:disabled) { background: rgba(239,68,68,0.12); border-color: var(--danger); box-shadow: 0 0 16px rgba(239,68,68,0.2); }
.btn-combined { color: var(--warn); border-color: rgba(245,158,11,0.4); background: rgba(245,158,11,0.05); }
.btn-combined:hover:not(:disabled) { background: rgba(245,158,11,0.12); border-color: var(--warn); box-shadow: 0 0 16px rgba(245,158,11,0.2); }
.btn-ghost { color: var(--muted); border-color: transparent; font-size: 0.72rem; padding: 0.4rem 0.8rem; }
.btn-ghost:hover:not(:disabled) { color: var(--text); border-color: var(--border); }
button:disabled, .btn:disabled { opacity: 0.35; cursor: not-allowed; }
.btn-row { display: flex; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 0.8rem; }
/* kill button — shown only when running */
.kill-btn { display: none; }
.kill-btn.visible { display: inline-flex; }
/* ── STATUS TEXT ── */
.status-text { font-size: 0.8rem; color: var(--muted); margin: 0.5rem 0; min-height: 1.2em; transition: color 0.3s; }
.status-text.running { color: var(--warn); }
.status-text.success { color: var(--accent3); }
.status-text.failed { color: var(--danger); }
/* ── LAST RUN META ── */
.last-run-meta { font-size: 0.68rem; color: var(--muted); margin-top: 0.3rem; display: flex; gap: 1rem; flex-wrap: wrap; }
.last-run-meta span { display: flex; align-items: center; gap: 0.3rem; }
/* ── LOG BOX ── */
.log-box {
background: #030810; border: 1px solid var(--border); color: #4ade80;
padding: 1rem; height: 200px; overflow-y: auto; border-radius: 6px;
font-size: 0.73rem; line-height: 1.6; font-family: 'Space Mono', monospace; margin-top: 0.5rem;
}
.log-box::-webkit-scrollbar { width: 4px; }
.log-box::-webkit-scrollbar-thumb { background: var(--border-hi); border-radius: 2px; }
.hidden { display: none !important; }
/* ── UPTIME DISPLAY ── */
.uptime-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 0.8rem; margin-bottom: 1rem; }
.uptime-item { background: rgba(0,212,255,0.04); border: 1px solid var(--border); border-radius: 6px; padding: 0.6rem 0.8rem; }
.uptime-item-label { font-size: 0.65rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.1em; }
.uptime-item-value { font-size: 0.95rem; font-weight: 700; color: var(--accent); margin-top: 0.2rem; font-family: 'Syne', sans-serif; }
/* ── PING HISTORY ── */
.uptime-bar-wrap { margin-top: 1rem; }
.uptime-label { font-size: 0.7rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 0.4rem; display: flex; justify-content: space-between; }
.uptime-bar { height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; }
.uptime-fill { height: 100%; width: 0%; background: linear-gradient(90deg, var(--accent3), var(--accent)); border-radius: 2px; transition: width 1.5s ease; }
.ping-history { display: flex; gap: 3px; margin-top: 0.8rem; align-items: flex-end; height: 32px; }
.ping-bar { width: 6px; background: var(--accent3); border-radius: 1px; opacity: 0.7; transition: height 0.4s ease, background 0.3s; flex-shrink: 0; }
/* ── COMBINED PROGRESS STEPS ── */
.pipeline-steps { display: flex; align-items: center; gap: 0; margin: 1rem 0 0.5rem; }
.pipeline-step { display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: var(--muted); padding: 0.4rem 0.8rem; border: 1px solid var(--border); border-radius: 4px; transition: all 0.3s; }
.pipeline-step.active { color: var(--warn); border-color: rgba(245,158,11,0.4); background: rgba(245,158,11,0.07); }
.pipeline-step.done { color: var(--accent3); border-color: rgba(16,185,129,0.4); background: rgba(16,185,129,0.07); }
.pipeline-step.failed { color: var(--danger); border-color: rgba(239,68,68,0.4); background: rgba(239,68,68,0.07); }
.pipeline-arrow { color: var(--muted); font-size: 0.7rem; padding: 0 0.3rem; }
/* ── SECTION LABELS ── */
.section-label {
font-size: 0.65rem; color: var(--muted); text-transform: uppercase;
letter-spacing: 0.15em; margin: 1.8rem 0 0.8rem;
display: flex; align-items: center; gap: 0.8rem;
}
.section-label::before, .section-label::after { content: ''; flex: 1; height: 1px; background: var(--border); }
/* ── PARTICLES ── */
#particles { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 0; opacity: 0.4; }
/* ── TOAST ── */
#toast-container { position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 10000; display: flex; flex-direction: column; gap: 0.5rem; }
.toast { padding: 0.7rem 1.1rem; border-radius: 6px; font-size: 0.78rem; border: 1px solid; animation: toast-in 0.3s ease; max-width: 280px; }
@keyframes toast-in { from{transform:translateX(120%);opacity:0} to{transform:translateX(0);opacity:1} }
.toast.success { background: rgba(16,185,129,0.12); border-color: rgba(16,185,129,0.4); color: var(--accent3); }
.toast.error { background: rgba(239,68,68,0.12); border-color: rgba(239,68,68,0.4); color: var(--danger); }
.toast.info { background: rgba(0,212,255,0.08); border-color: rgba(0,212,255,0.3); color: var(--accent); }
.toast.warn { background: rgba(245,158,11,0.08); border-color: rgba(245,158,11,0.3); color: var(--warn); }
/* ── MODAL ── */
.modal-overlay { position: fixed; inset: 0; background: rgba(3,5,13,0.85); backdrop-filter: blur(4px); z-index: 9000; display: flex; align-items: center; justify-content: center; animation: fade-in 0.15s ease; }
@keyframes fade-in { from{opacity:0} to{opacity:1} }
.modal { background: var(--surface); border: 1px solid var(--border-hi); border-radius: 10px; padding: 1.8rem; max-width: 360px; width: 90%; box-shadow: 0 24px 80px rgba(0,0,0,0.6), var(--glow); animation: modal-up 0.2s ease; }
@keyframes modal-up { from{transform:translateY(12px);opacity:0} to{transform:none;opacity:1} }
.modal h3 { font-family: 'Syne', sans-serif; font-size: 1rem; color: var(--text); margin-bottom: 0.5rem; }
.modal p { font-size: 0.8rem; color: var(--muted); line-height: 1.6; margin-bottom: 1.2rem; }
.modal-actions { display: flex; gap: 0.6rem; justify-content: flex-end; }
/* ── ENTRY ANIMATIONS ── */
.fade-up { opacity: 0; transform: translateY(16px); animation: fade-up 0.5s ease forwards; }
@keyframes fade-up { to{opacity:1;transform:none} }
/* ── MOBILE ── */
@media (max-width: 700px) {
.overview { grid-template-columns: 1fr 1fr; }
.countdown-display { gap: 0.6rem; }
.time-number { font-size: 1.4rem; }
.btn-row { flex-direction: column; }
.btn { width: 100%; justify-content: center; }
header { flex-direction: column; }
.header-meta { text-align: left; }
.pipeline-steps { flex-wrap: wrap; }
}
</style>
</head>
<body>
<div id="grid-overlay"></div>
<canvas id="particles"></canvas>
<div id="toast-container"></div>
<div class="container">
<!-- HEADER -->
<header class="fade-up">
<div class="site-title">
<div class="line1">Home</div>
<span class="line2">Server Control_</span>
</div>
<div class="header-meta">
<div class="clock-display" id="clockDisplay">--:--:--</div>
<div id="dateDisplay" style="margin-top:0.2rem"></div>
<div style="margin-top:0.3rem" id="lastUpdated">Checking status...</div>
</div>
</header>
<!-- GLOBAL STATUS -->
<div id="globalStatus" class="global-ok fade-up" style="animation-delay:0.1s">
<div class="status-pulse"></div>
<span>All Systems Running Normally</span>
</div>
<!-- OVERVIEW STATS (4 cols — added uptime) -->
<div class="overview fade-up" style="animation-delay:0.15s">
<div class="stat-card">
<span id="serverDot" class="dot green"></span>
<div>
<div class="stat-label">Server</div>
<div class="stat-value" id="health">Online</div>
</div>
</div>
<div class="stat-card">
<span id="webDot" class="dot green"></span>
<div>
<div class="stat-label">Website</div>
<div class="stat-value" id="webStatus">Idle</div>
</div>
</div>
<div class="stat-card">
<span id="roamDot" class="dot green"></span>
<div>
<div class="stat-label">Org Notes</div>
<div class="stat-value" id="roamStatus">Idle</div>
</div>
</div>
<div class="stat-card">
<span class="dot green" style="background:var(--accent2);box-shadow:0 0 8px var(--accent2)"></span>
<div>
<div class="stat-label">Uptime</div>
<div class="stat-value" id="uptimeStat"></div>
</div>
</div>
</div>
<!-- PERSONAL -->
<div class="section-label">Personal</div>
<div class="card fade-up">
<div class="card-header">
<div class="card-title">💌 &nbsp;<span>Daily</span>&nbsp;Message</div>
</div>
<p id="dailyMessage" class="daily-message"></p>
</div>
<div class="card fade-up">
<div class="card-header">
<div class="card-title">&nbsp;<span>Countdown</span></div>
<span id="countdownLabel" class="badge neutral"></span>
</div>
<div class="countdown-display" id="countdownDisplay">
<div class="time-unit"><span class="time-number" id="cd-days">--</span><div class="time-label">Days</div></div>
<div class="time-unit"><span class="time-number" id="cd-hours">--</span><div class="time-label">Hours</div></div>
<div class="time-unit"><span class="time-number" id="cd-mins">--</span><div class="time-label">Mins</div></div>
<div class="time-unit"><span class="time-number" id="cd-secs">--</span><div class="time-label">Secs</div></div>
</div>
</div>
<!-- SERVICES -->
<div class="section-label">Services</div>
<div class="card fade-up">
<div class="card-header">
<div class="card-title">🚀 &nbsp;<span>Quick</span>&nbsp;Links</div>
</div>
<div class="quick-links">
<a href="https://notes.zainezq.com" target="_blank" class="link-card"><span class="icon">📚</span>Notes</a>
<a href="https://zainezq.com" target="_blank" class="link-card"><span class="icon">🌍</span>Website</a>
<a href="https://filebrowser.zainezq.com" target="_blank" class="link-card"><span class="icon">📂</span>Files</a>
<a href="https://calibre.zainezq.com" target="_blank" class="link-card"><span class="icon">📖</span>Library</a>
<a href="https://miniflux.zainezq.com" target="_blank" class="link-card"><span class="icon">📰</span>News</a>
<a href="https://guac.zainezq.com" target="_blank" class="link-card"><span class="icon">🖥️</span>RDP</a>
<a href="https://portainer.zainezq.com" target="_blank" class="link-card"><span class="icon">🐳</span>Portainer</a>
<a href="https://nextcloud.zainezq.com" target="_blank" class="link-card"><span class="icon">☁️</span>Nextcloud</a>
<a href="https://pgadmin.zainezq.com" target="_blank" class="link-card"><span class="icon">🗄️</span>Databases</a>
</div>
</div>
<!-- MONITORING -->
<div class="section-label">Monitoring</div>
<div class="card fade-up">
<div class="card-header">
<div class="card-title">📡 &nbsp;<span>Server</span>&nbsp;Status</div>
<span id="healthBadge" class="badge neutral">Checking</span>
</div>
<!-- Uptime breakdown -->
<div class="uptime-grid" id="uptimeGrid">
<div class="uptime-item"><div class="uptime-item-label">Days up</div><div class="uptime-item-value" id="uptimeDays"></div></div>
<div class="uptime-item"><div class="uptime-item-label">Hours</div><div class="uptime-item-value" id="uptimeHours"></div></div>
<div class="uptime-item"><div class="uptime-item-label">Minutes</div><div class="uptime-item-value" id="uptimeMins"></div></div>
<div class="uptime-item"><div class="uptime-item-label">Started at</div><div class="uptime-item-value" id="uptimeStart" style="font-size:0.72rem"></div></div>
</div>
<div class="uptime-bar-wrap">
<div class="uptime-label"><span>Ping History (30 checks)</span><span id="uptimePct"></span></div>
<div class="uptime-bar"><div class="uptime-fill" id="uptimeFill"></div></div>
</div>
<div class="ping-history" id="pingHistory"></div>
</div>
<!-- ACTIONS -->
<div class="section-label">Actions</div>
<!-- WEBSITE -->
<div class="card fade-up">
<div class="card-header">
<div class="card-title">🌍 &nbsp;<span>Website</span>&nbsp;Deploy</div>
</div>
<div class="btn-row">
<button class="btn btn-primary" id="buildBtn">▶ Update Website</button>
<button class="btn btn-danger kill-btn" id="killWebBtn">✕ Cancel</button>
</div>
<p id="buildResult" class="status-text">💤 Waiting for action</p>
<div class="last-run-meta" id="webLastRunMeta"></div>
<button class="btn btn-ghost toggleLogs" data-target="logOutput">Show logs</button>
<pre id="logOutput" class="log-box hidden"></pre>
</div>
<!-- KNOWLEDGE BASE -->
<div class="card fade-up">
<div class="card-header">
<div class="card-title">📚 &nbsp;<span>Knowledge</span>&nbsp;Base</div>
</div>
<div class="btn-row">
<button class="btn btn-primary" id="orgRoamBtn">▶ Refresh Notes</button>
<button class="btn btn-secondary" id="orgRoamBtn2">⚡ Rerun Emacs</button>
<button class="btn btn-combined" id="combinedBtn">⚡▶ Emacs + Notes</button>
<button class="btn btn-danger kill-btn" id="killRoamBtn">✕ Cancel</button>
</div>
<!-- Pipeline progress (shown during combined run) -->
<div class="pipeline-steps hidden" id="pipelineSteps">
<div class="pipeline-step" id="pipeStep1">⚡ Emacs</div>
<div class="pipeline-arrow"></div>
<div class="pipeline-step" id="pipeStep2">▶ Notes</div>
</div>
<p id="orgRoamResult" class="status-text">💤 Waiting for action</p>
<div class="last-run-meta" id="roamLastRunMeta"></div>
<button class="btn btn-ghost toggleLogs" data-target="orgRoamLogs">Show logs</button>
<pre id="orgRoamLogs" class="log-box hidden"></pre>
</div>
</div>
<!-- MODAL -->
<div id="modalOverlay" class="modal-overlay hidden">
<div class="modal">
<h3 id="modalTitle">Confirm Action</h3>
<p id="modalBody">Are you sure?</p>
<div class="modal-actions">
<button class="btn btn-ghost" id="modalCancel">Cancel</button>
<button class="btn btn-primary" id="modalConfirm">Confirm</button>
</div>
</div>
</div>
<script>
/* ── PARTICLES ── */
(function(){
const canvas = document.getElementById('particles');
const ctx = canvas.getContext('2d');
let W, H, dots = [];
function resize(){ W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; }
function init(){ dots = Array.from({length:60},()=>({ x:Math.random()*W, y:Math.random()*H, r:Math.random()*1.2+0.3, vx:(Math.random()-0.5)*0.2, vy:(Math.random()-0.5)*0.2, a:Math.random()*0.4+0.1 })); }
function draw(){
ctx.clearRect(0,0,W,H);
dots.forEach(d=>{
d.x+=d.vx; d.y+=d.vy;
if(d.x<0)d.x=W; if(d.x>W)d.x=0;
if(d.y<0)d.y=H; if(d.y>H)d.y=0;
ctx.beginPath(); ctx.arc(d.x,d.y,d.r,0,Math.PI*2);
ctx.fillStyle=`rgba(0,212,255,${d.a})`; ctx.fill();
});
for(let i=0;i<dots.length;i++) for(let j=i+1;j<dots.length;j++){
const dx=dots[i].x-dots[j].x, dy=dots[i].y-dots[j].y, dist=Math.sqrt(dx*dx+dy*dy);
if(dist<100){ ctx.beginPath(); ctx.moveTo(dots[i].x,dots[i].y); ctx.lineTo(dots[j].x,dots[j].y); ctx.strokeStyle=`rgba(0,212,255,${0.06*(1-dist/100)})`; ctx.lineWidth=0.5; ctx.stroke(); }
}
requestAnimationFrame(draw);
}
window.addEventListener('resize',()=>{resize();init();});
resize(); init(); draw();
})();
/* ── CLOCK ── */
function updateClock(){
const now = new Date();
document.getElementById('clockDisplay').textContent = now.toTimeString().slice(0,8);
document.getElementById('dateDisplay').textContent = now.toLocaleDateString('en-GB',{weekday:'short',day:'numeric',month:'short',year:'numeric'});
}
updateClock(); setInterval(updateClock, 1000);
/* ── TOAST ── */
function showToast(msg, type='info', duration=3500){
const el = document.createElement('div');
el.className = `toast ${type}`; el.textContent = msg;
document.getElementById('toast-container').appendChild(el);
setTimeout(()=>{ el.style.transition='opacity 0.3s,transform 0.3s'; el.style.opacity='0'; el.style.transform='translateX(120%)'; setTimeout(()=>el.remove(),300); }, duration);
}
/* ── MODAL ── */
function showConfirm(title, body){
return new Promise(resolve=>{
const overlay = document.getElementById('modalOverlay');
document.getElementById('modalTitle').textContent = title;
document.getElementById('modalBody').textContent = body;
overlay.classList.remove('hidden');
function cleanup(r){ overlay.classList.add('hidden'); document.getElementById('modalConfirm').removeEventListener('click',onY); document.getElementById('modalCancel').removeEventListener('click',onN); resolve(r); }
const onY=()=>cleanup(true), onN=()=>cleanup(false);
document.getElementById('modalConfirm').addEventListener('click',onY);
document.getElementById('modalCancel').addEventListener('click',onN);
});
}
/* ── PING HISTORY ── */
const PING_MAX = 30;
let pingResults = [];
function addPingResult(ok){
pingResults.push(ok);
if(pingResults.length > PING_MAX) pingResults.shift();
const container = document.getElementById('pingHistory');
container.innerHTML = '';
pingResults.forEach(r=>{ const b=document.createElement('div'); b.className='ping-bar'; b.style.height=r?'24px':'8px'; b.style.background=r?'var(--accent3)':'var(--danger)'; container.appendChild(b); });
const pct = Math.round((pingResults.filter(Boolean).length/pingResults.length)*100);
document.getElementById('uptimePct').textContent = pct+'%';
document.getElementById('uptimeFill').style.width = pct+'%';
}
/* ── UPTIME ── */
async function fetchUptime(){
try {
const res = await fetch('/api/uptime');
const data = await res.json();
const ms = data.uptimeMs;
const d = Math.floor(ms/86400000);
const h = Math.floor((ms%86400000)/3600000);
const m = Math.floor((ms%3600000)/60000);
document.getElementById('uptimeDays').textContent = d;
document.getElementById('uptimeHours').textContent = h;
document.getElementById('uptimeMins').textContent = m;
document.getElementById('uptimeStart').textContent = new Date(data.serverStart).toLocaleTimeString('en-GB',{hour:'2-digit',minute:'2-digit'});
document.getElementById('uptimeStat').textContent = `${d}d ${h}h ${m}m`;
} catch { /* server offline, health check handles UI */ }
}
/* ── LAST RUN META ── */
async function fetchLastRuns(){
try {
const res = await fetch('/api/last-runs');
const data = await res.json();
renderLastRun('webLastRunMeta', data.web);
renderLastRun('roamLastRunMeta', data.roam);
} catch {}
}
function renderLastRun(elId, info){
const el = document.getElementById(elId);
if(!el) return;
const t = info.lastRun && info.lastRun !== 'never'
? new Date(info.lastRun).toLocaleString('en-GB',{day:'numeric',month:'short',hour:'2-digit',minute:'2-digit'})
: 'Never';
const code = info.exitCode !== undefined && info.exitCode !== 'never' ? info.exitCode : null;
const codeHtml = code !== null
? `<span style="color:${code===0?'var(--accent3)':'var(--danger)'}">exit ${code}</span>`
: '';
el.innerHTML = `<span>Last run: ${t}</span>${codeHtml}`;
}
/* ── HEALTH ── */
async function fetchHealth(){
try {
await fetch('/api/health');
document.getElementById('health').textContent = 'Online';
document.getElementById('healthBadge').textContent = 'Online';
document.getElementById('healthBadge').className = 'badge ok';
document.getElementById('serverDot').className = 'dot green';
document.getElementById('globalStatus').className = 'global-ok';
document.getElementById('globalStatus').innerHTML = '<div class="status-pulse"></div><span>All Systems Running Normally</span>';
addPingResult(true);
fetchUptime();
fetchLastRuns();
} catch {
document.getElementById('health').textContent = 'Offline';
document.getElementById('healthBadge').textContent = 'Offline';
document.getElementById('healthBadge').className = 'badge error';
document.getElementById('serverDot').className = 'dot red';
document.getElementById('globalStatus').className = 'global-error';
document.getElementById('globalStatus').innerHTML = '<div class="status-pulse"></div><span>⚠ Attention Required</span>';
addPingResult(false);
}
document.getElementById('lastUpdated').textContent = 'Last checked: '+new Date().toLocaleTimeString();
}
setInterval(fetchHealth, 10000);
/* ── LOG TOGGLE ── */
document.querySelectorAll('.toggleLogs').forEach(btn=>{
btn.addEventListener('click',()=>{
const t = document.getElementById(btn.dataset.target);
t.classList.toggle('hidden');
btn.textContent = t.classList.contains('hidden') ? 'Show logs' : 'Hide logs';
});
});
/* ── STREAM HELPERS ── */
let emacsES=null, webES=null, roamES=null, combinedES=null;
function startLogStream(url,el,setter){ const s=new EventSource(url); s.onmessage=e=>{el.textContent+=e.data+'\n';el.scrollTop=el.scrollHeight;}; s.onerror=()=>s.close(); setter(s); }
function closeStream(s){ if(s) s.close(); }
/* ── KILL HELPERS ── */
async function killEndpoint(url, label){
try {
const res = await fetch(url, {method:'DELETE'});
if(res.ok) showToast(`${label} cancelled`, 'warn');
else showToast(`Nothing to cancel`, 'info');
} catch { showToast('Kill request failed','error'); }
}
/* ── WEBSITE BUILD ── */
async function runWebBuild(){
const ok = await showConfirm('Update Website','This will rebuild and deploy the website. Continue?');
if(!ok) return;
document.getElementById('buildBtn').disabled = true;
document.getElementById('killWebBtn').classList.add('visible');
const res = document.getElementById('buildResult');
res.textContent='🔄 Updating website...'; res.className='status-text running';
document.getElementById('webDot').className='dot yellow';
document.getElementById('webStatus').textContent='Deploying';
document.getElementById('logOutput').textContent='';
showToast('Website build started…','info');
try {
const r = await fetch('/api/build-web',{method:'POST'});
if(!r.ok) throw new Error();
closeStream(webES);
startLogStream('/api/build-web/logs', document.getElementById('logOutput'), s=>webES=s);
pollStatus('/api/build-web/status', 'buildResult', 'buildBtn', 'killWebBtn', 'webDot', 'webStatus', ()=>{closeStream(webES); fetchLastRuns();}, 'Website', null);
} catch {
res.textContent='❌ Could not start update'; res.className='status-text failed';
document.getElementById('buildBtn').disabled=false;
document.getElementById('killWebBtn').classList.remove('visible');
document.getElementById('webDot').className='dot red';
showToast('Failed to start website build','error');
}
}
/* ── ROAM BUILD ── */
async function runOrgRoamBuild(){
const ok = await showConfirm('Refresh Knowledge Base','This will refresh your Org Roam notes. Continue?');
if(!ok) return;
document.getElementById('orgRoamBtn').disabled=true;
document.getElementById('killRoamBtn').classList.add('visible');
const res = document.getElementById('orgRoamResult');
res.textContent='🔄 Refreshing notes...'; res.className='status-text running';
document.getElementById('roamDot').className='dot yellow';
document.getElementById('roamStatus').textContent='Syncing';
document.getElementById('orgRoamLogs').textContent='';
showToast('Knowledge base refresh started…','info');
try {
const r = await fetch('/api/build-roam',{method:'POST'});
if(!r.ok) throw new Error();
closeStream(roamES);
startLogStream('/api/build-roam/logs', document.getElementById('orgRoamLogs'), s=>roamES=s);
pollStatus('/api/build-roam/status','orgRoamResult','orgRoamBtn','killRoamBtn','roamDot','roamStatus',()=>{closeStream(roamES); fetchLastRuns();},'Notes',null);
} catch {
res.textContent='❌ Could not start refresh'; res.className='status-text failed';
document.getElementById('orgRoamBtn').disabled=false;
document.getElementById('killRoamBtn').classList.remove('visible');
document.getElementById('roamDot').className='dot red';
showToast('Failed to start notes refresh','error');
}
}
/* ── EMACS ── */
async function rerunEmacs(){
const ok = await showConfirm('Rerun Emacs','This will rerun Emacs. Continue?');
if(!ok) return;
document.getElementById('orgRoamBtn2').disabled=true;
document.getElementById('killRoamBtn').classList.add('visible');
const res = document.getElementById('orgRoamResult');
res.textContent='🔄 Running Emacs...'; res.className='status-text running';
document.getElementById('roamDot').className='dot yellow';
document.getElementById('roamStatus').textContent='Running';
document.getElementById('orgRoamLogs').textContent='';
showToast('Emacs starting…','info');
try {
const r = await fetch('/api/rerun-emacs',{method:'POST'});
if(!r.ok) throw new Error();
closeStream(emacsES);
startLogStream('/api/rerun-emacs/logs', document.getElementById('orgRoamLogs'), s=>emacsES=s);
pollStatus('/api/rerun-emacs/status','orgRoamResult','orgRoamBtn2','killRoamBtn','roamDot','roamStatus',()=>{closeStream(emacsES);},'Emacs',[0,124]);
} catch {
res.textContent='❌ Could not start Emacs'; res.className='status-text failed';
document.getElementById('orgRoamBtn2').disabled=false;
document.getElementById('killRoamBtn').classList.remove('visible');
document.getElementById('roamDot').className='dot red';
showToast('Failed to start Emacs','error');
}
}
/* ── COMBINED RUN ── */
async function runCombined(){
const ok = await showConfirm('Emacs + Notes Refresh','This will run Emacs then rebuild your Org Roam notes sequentially. Continue?');
if(!ok) return;
// Disable all kb buttons, show kill
['orgRoamBtn','orgRoamBtn2','combinedBtn'].forEach(id=>document.getElementById(id).disabled=true);
document.getElementById('killRoamBtn').classList.add('visible');
const res = document.getElementById('orgRoamResult');
res.textContent='⚡▶ Running Emacs → Notes...'; res.className='status-text running';
document.getElementById('roamDot').className='dot yellow';
document.getElementById('roamStatus').textContent='Pipeline';
document.getElementById('orgRoamLogs').textContent='';
// Show pipeline steps
const steps = document.getElementById('pipelineSteps');
steps.classList.remove('hidden');
document.getElementById('pipeStep1').className='pipeline-step active';
document.getElementById('pipeStep2').className='pipeline-step';
showToast('Emacs + Notes pipeline started…','warn');
try {
const r = await fetch('/api/run-combined',{method:'POST'});
if(!r.ok) throw new Error();
closeStream(combinedES);
startLogStream('/api/run-combined/logs', document.getElementById('orgRoamLogs'), s=>combinedES=s);
pollCombinedStatus();
} catch {
res.textContent='❌ Could not start pipeline'; res.className='status-text failed';
['orgRoamBtn','orgRoamBtn2','combinedBtn'].forEach(id=>document.getElementById(id).disabled=false);
document.getElementById('killRoamBtn').classList.remove('visible');
document.getElementById('roamDot').className='dot red';
steps.classList.add('hidden');
showToast('Failed to start pipeline','error');
}
}
function pollCombinedStatus(){
// We watch the log to detect which step we're on
const logEl = document.getElementById('orgRoamLogs');
const interval = setInterval(async()=>{
const r = await fetch('/api/run-combined/status');
const s = await r.json();
// Update step indicators from log text
const log = logEl.textContent;
if(log.includes('[2/2]')){
document.getElementById('pipeStep1').className='pipeline-step done';
document.getElementById('pipeStep2').className='pipeline-step active';
}
if(!s.running){
clearInterval(interval);
closeStream(combinedES);
['orgRoamBtn','orgRoamBtn2','combinedBtn'].forEach(id=>document.getElementById(id).disabled=false);
document.getElementById('killRoamBtn').classList.remove('visible');
const res = document.getElementById('orgRoamResult');
if(s.lastExitCode===0){
res.textContent='✅ Emacs + Notes completed'; res.className='status-text success';
document.getElementById('roamDot').className='dot green';
document.getElementById('roamStatus').textContent='Done';
document.getElementById('pipeStep2').className='pipeline-step done';
showToast('Pipeline complete!','success');
} else {
res.textContent=`⚠ Pipeline finished with issue (code ${s.lastExitCode})`; res.className='status-text failed';
document.getElementById('roamDot').className='dot red';
document.getElementById('roamStatus').textContent='Error';
showToast('Pipeline finished with errors','error');
}
fetchLastRuns();
setTimeout(()=>document.getElementById('pipelineSteps').classList.add('hidden'), 4000);
}
}, 2000);
}
/* ── GENERIC POLL ── */
function pollStatus(statusUrl, resultId, btnId, killBtnId, dotId, statusId, onDone, label, successCodes){
const interval = setInterval(async()=>{
const r = await fetch(statusUrl);
const s = await r.json();
if(!s.running){
clearInterval(interval);
document.getElementById(btnId).disabled=false;
document.getElementById(killBtnId).classList.remove('visible');
const res = document.getElementById(resultId);
const ok = successCodes ? successCodes.includes(s.lastExitCode) : s.lastExitCode===0;
if(ok){
res.textContent=`${label} completed`; res.className='status-text success';
document.getElementById(dotId).className='dot green';
document.getElementById(statusId).textContent='Done';
showToast(`${label} complete!`,'success');
} else {
res.textContent=`${label} finished with issue (code ${s.lastExitCode})`; res.className='status-text failed';
document.getElementById(dotId).className='dot red';
document.getElementById(statusId).textContent='Error';
showToast(`${label} finished with errors`,'error');
}
if(onDone) onDone();
}
}, 2000);
}
/* ── DAILY MESSAGE ── */
const messages = ["You are my favourite person."];
function setDailyMessage(){
const today = new Date().toDateString();
const index = today.split('').reduce((a,c)=>a+c.charCodeAt(0),0)%messages.length;
document.getElementById('dailyMessage').textContent = messages[index];
}
setDailyMessage();
/* ── COUNTDOWN ── */
function startCountdown(){
const target = new Date('2026-03-20T00:00:00');
document.getElementById('countdownLabel').textContent = 'Eid';
function update(){
const diff = target-new Date();
if(diff<=0){ document.getElementById('countdownDisplay').innerHTML=`<p style="color:var(--accent);font-family:'Syne',sans-serif;font-size:1.2rem;font-weight:800">Eid is now! 🎉</p>`; return; }
document.getElementById('cd-days').textContent = String(Math.floor(diff/86400000)).padStart(3,'0');
document.getElementById('cd-hours').textContent = String(Math.floor((diff/3600000)%24)).padStart(2,'0');
document.getElementById('cd-mins').textContent = String(Math.floor((diff/60000)%60)).padStart(2,'0');
document.getElementById('cd-secs').textContent = String(Math.floor((diff/1000)%60)).padStart(2,'0');
}
update(); setInterval(update,1000);
}
startCountdown();
/* ── KILL BUTTONS ── */
document.getElementById('killWebBtn').addEventListener('click', async()=>{
const ok = await showConfirm('Cancel Build','Force-kill the running website build?');
if(ok) killEndpoint('/api/build-web','Web build');
});
document.getElementById('killRoamBtn').addEventListener('click', async()=>{
const ok = await showConfirm('Cancel Process','Force-kill the running process?');
if(ok) killEndpoint('/api/run-combined','Process');
});
/* ── WIRE UP ── */
document.getElementById('buildBtn').addEventListener('click', runWebBuild);
document.getElementById('orgRoamBtn').addEventListener('click', runOrgRoamBuild);
document.getElementById('orgRoamBtn2').addEventListener('click', rerunEmacs);
document.getElementById('combinedBtn').addEventListener('click', runCombined);
/* ── INIT ── */
fetchHealth();
</script>
</body>
</html>