main changes
This commit is contained in:
943
index.html
943
index.html
@@ -2,126 +2,869 @@
|
||||
<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="stylesheet" href="style.css" />
|
||||
<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>
|
||||
|
||||
<main class="container">
|
||||
<div id="grid-overlay"></div>
|
||||
<canvas id="particles"></canvas>
|
||||
<div id="toast-container"></div>
|
||||
|
||||
<h1>🏠 Home Server Control</h1>
|
||||
<section class="card">
|
||||
<section class="card">
|
||||
<h2>💌 Daily Message</h2>
|
||||
<p id="dailyMessage" class="daily-message"></p>
|
||||
</section>
|
||||
<section class="card">
|
||||
<h2>⏳ Countdown</h2>
|
||||
<p id="countdownText"></p>
|
||||
</section>
|
||||
<div class="container">
|
||||
|
||||
<h2>🚀 Quick Links</h2>
|
||||
|
||||
<div class="quick-links">
|
||||
<a href="https://notes.zainezq.com" target="_blank" class="link-card">
|
||||
📚 Notes
|
||||
</a>
|
||||
|
||||
<a href="https://zainezq.com" target="_blank" class="link-card">
|
||||
🌍 Website
|
||||
</a>
|
||||
|
||||
<a href="https://filebrowser.zainezq.com" target="_blank" class="link-card">
|
||||
📂 Files
|
||||
</a>
|
||||
|
||||
<a href="https://calibre.zainezq.com" target="_blank" class="link-card">
|
||||
📖 Library
|
||||
</a>
|
||||
|
||||
<a href="https://miniflux.zainezq.com" target="_blank" class="link-card">
|
||||
📰 News
|
||||
</a>
|
||||
|
||||
<a href="https://guac.zainezq.com" target="_blank" class="link-card">
|
||||
🖥️ RDP
|
||||
</a>
|
||||
|
||||
<a href="https://portainer.zainezq.com" target="_blank" class="link-card">
|
||||
🐳 Portainer
|
||||
</a>
|
||||
|
||||
<a href="https://nextcloud.zainezq.com" target="_blank" class="link-card">
|
||||
☁️ Nextcloud
|
||||
</a>
|
||||
|
||||
<a href="https://pgadmin.zainezq.com" target="_blank" class="link-card">
|
||||
🗄️ Databases
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<!-- Global Status -->
|
||||
<div id="globalStatus" class="global-ok">
|
||||
🟢 All Systems Running Normally
|
||||
<!-- 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>
|
||||
|
||||
<!-- Overview -->
|
||||
<div class="overview">
|
||||
<div class="stat">
|
||||
<span>Server</span>
|
||||
<span id="serverDot" class="dot green"></span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>Website</span>
|
||||
<span id="webDot" class="dot green"></span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>Org Notes</span>
|
||||
<span id="roamDot" class="dot green"></span>
|
||||
<!-- 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>
|
||||
|
||||
<!-- Health -->
|
||||
<section class="card">
|
||||
<h2>📡 Server Status</h2>
|
||||
<p>
|
||||
Health:
|
||||
<span id="health" class="badge neutral">Checking...</span>
|
||||
</p>
|
||||
<p id="lastUpdated" class="muted"></p>
|
||||
</section>
|
||||
<!-- PERSONAL -->
|
||||
<div class="section-label">Personal</div>
|
||||
|
||||
<!-- Website -->
|
||||
<section class="card">
|
||||
<h2>🌍 Website</h2>
|
||||
<div class="card fade-up">
|
||||
<div class="card-header">
|
||||
<div class="card-title">💌 <span>Daily</span> Message</div>
|
||||
</div>
|
||||
<p id="dailyMessage" class="daily-message"></p>
|
||||
</div>
|
||||
|
||||
<button id="buildBtn">🔄 Update Website</button>
|
||||
<p id="buildResult" class="muted">💤 Waiting for action</p>
|
||||
<div class="card fade-up">
|
||||
<div class="card-header">
|
||||
<div class="card-title">⏳ <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>
|
||||
|
||||
<button class="toggleLogs" data-target="logOutput">
|
||||
Show technical details
|
||||
</button>
|
||||
<!-- SERVICES -->
|
||||
<div class="section-label">Services</div>
|
||||
|
||||
<pre id="logOutput" class="log-box hidden"></pre>
|
||||
</section>
|
||||
<div class="card fade-up">
|
||||
<div class="card-header">
|
||||
<div class="card-title">🚀 <span>Quick</span> 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>
|
||||
|
||||
<!-- Org Roam -->
|
||||
<section class="card">
|
||||
<h2>📚 Knowledge Base</h2>
|
||||
<!-- MONITORING -->
|
||||
<div class="section-label">Monitoring</div>
|
||||
|
||||
<button id="orgRoamBtn">🔄 Refresh Notes</button>
|
||||
<p id="orgRoamResult" class="muted">💤 Waiting for action</p>
|
||||
<div class="card fade-up">
|
||||
<div class="card-header">
|
||||
<div class="card-title">📡 <span>Server</span> Status</div>
|
||||
<span id="healthBadge" class="badge neutral">Checking</span>
|
||||
</div>
|
||||
|
||||
<button class="toggleLogs" data-target="orgRoamLogs">
|
||||
Show technical details
|
||||
</button>
|
||||
<!-- 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>
|
||||
|
||||
<pre id="orgRoamLogs" class="log-box hidden"></pre>
|
||||
</section>
|
||||
<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>
|
||||
|
||||
</main>
|
||||
<!-- ACTIONS -->
|
||||
<div class="section-label">Actions</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
<!-- WEBSITE -->
|
||||
<div class="card fade-up">
|
||||
<div class="card-header">
|
||||
<div class="card-title">🌍 <span>Website</span> 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">📚 <span>Knowledge</span> 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>
|
||||
|
||||
Reference in New Issue
Block a user