1220 lines
41 KiB
HTML
1220 lines
41 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=IBM+Plex+Mono:wght@400;500;600;700&family=Space+Grotesk:wght@600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
:root {
|
|
--bg: #19150f;
|
|
--ink: #f4e7bc;
|
|
--muted: #a69262;
|
|
--dim: #6e613f;
|
|
--line: #51452c;
|
|
--line-strong: #8b7544;
|
|
--panel: #241e14;
|
|
--panel-hi: #302717;
|
|
--green: #5cff9b;
|
|
--green-dim: #245b34;
|
|
--amber: #ffbf47;
|
|
--red: #ff5d54;
|
|
--blue: #6ac8ff;
|
|
--violet: #c59cff;
|
|
--shadow: 0 18px 40px rgba(0, 0, 0, 0.35);
|
|
}
|
|
|
|
html { scroll-behavior: smooth; }
|
|
|
|
body {
|
|
min-height: 100vh;
|
|
background:
|
|
linear-gradient(180deg, rgba(255, 191, 71, 0.04), transparent 32rem),
|
|
radial-gradient(circle at top left, rgba(92, 255, 155, 0.08), transparent 26rem),
|
|
var(--bg);
|
|
color: var(--ink);
|
|
font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
body::before {
|
|
content: "";
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 0;
|
|
pointer-events: none;
|
|
background:
|
|
repeating-linear-gradient(0deg, rgba(255,255,255,0.035) 0 1px, transparent 1px 4px),
|
|
linear-gradient(90deg, rgba(255, 191, 71, 0.05), transparent 18%, transparent 82%, rgba(255, 191, 71, 0.04));
|
|
mix-blend-mode: screen;
|
|
opacity: 0.36;
|
|
}
|
|
|
|
body::after {
|
|
content: "";
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 0;
|
|
pointer-events: none;
|
|
background-image:
|
|
linear-gradient(rgba(255, 191, 71, 0.045) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(255, 191, 71, 0.035) 1px, transparent 1px);
|
|
background-size: 24px 24px;
|
|
mask-image: linear-gradient(to bottom, black, transparent 72%);
|
|
}
|
|
|
|
a { color: inherit; }
|
|
|
|
button {
|
|
font: inherit;
|
|
}
|
|
|
|
.page {
|
|
position: relative;
|
|
z-index: 1;
|
|
width: min(1180px, calc(100% - 32px));
|
|
margin: 0 auto;
|
|
padding: 24px 0 48px;
|
|
}
|
|
|
|
.topbar {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
gap: 18px;
|
|
align-items: end;
|
|
border: 2px solid var(--line-strong);
|
|
border-radius: 8px;
|
|
padding: 18px;
|
|
background: linear-gradient(135deg, rgba(48, 39, 23, 0.95), rgba(25, 21, 15, 0.95));
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.brand-kicker {
|
|
color: var(--green);
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.18em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.site-title {
|
|
margin-top: 6px;
|
|
font-family: "Space Grotesk", system-ui, sans-serif;
|
|
font-size: clamp(2rem, 5vw, 4rem);
|
|
line-height: 0.9;
|
|
letter-spacing: 0;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.header-meta {
|
|
min-width: 230px;
|
|
text-align: right;
|
|
color: var(--muted);
|
|
font-size: 0.78rem;
|
|
line-height: 1.7;
|
|
}
|
|
|
|
.clock-display {
|
|
color: var(--amber);
|
|
font-size: 1.8rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.06em;
|
|
}
|
|
|
|
#globalStatus {
|
|
margin: 16px 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
border: 2px solid;
|
|
border-radius: 8px;
|
|
padding: 12px 14px;
|
|
font-size: 0.78rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.global-ok {
|
|
color: var(--green);
|
|
background: rgba(36, 91, 52, 0.32);
|
|
border-color: var(--green-dim);
|
|
}
|
|
|
|
.global-error {
|
|
color: var(--red);
|
|
background: rgba(255, 93, 84, 0.12);
|
|
border-color: rgba(255, 93, 84, 0.5);
|
|
}
|
|
|
|
.status-pulse {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
background: currentColor;
|
|
box-shadow: 0 0 0 4px rgba(92, 255, 155, 0.12);
|
|
}
|
|
|
|
.dashboard-grid {
|
|
display: grid;
|
|
grid-template-columns: 280px minmax(0, 1fr);
|
|
gap: 16px;
|
|
align-items: start;
|
|
}
|
|
|
|
.rail {
|
|
position: sticky;
|
|
top: 16px;
|
|
display: grid;
|
|
gap: 16px;
|
|
}
|
|
|
|
.main-stack {
|
|
display: grid;
|
|
gap: 16px;
|
|
}
|
|
|
|
.panel {
|
|
border: 2px solid var(--line);
|
|
border-radius: 8px;
|
|
background: rgba(36, 30, 20, 0.94);
|
|
box-shadow: var(--shadow);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.panel-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 10px 12px;
|
|
background: var(--panel-hi);
|
|
border-bottom: 2px solid var(--line);
|
|
}
|
|
|
|
.panel-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: var(--amber);
|
|
font-size: 0.8rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.panel-title::before {
|
|
content: ">";
|
|
color: var(--green);
|
|
}
|
|
|
|
.panel-body {
|
|
padding: 14px;
|
|
}
|
|
|
|
.status-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 10px;
|
|
}
|
|
|
|
.stat-card {
|
|
min-height: 84px;
|
|
display: grid;
|
|
align-content: space-between;
|
|
gap: 10px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 6px;
|
|
padding: 10px;
|
|
background: rgba(25, 21, 15, 0.62);
|
|
}
|
|
|
|
.stat-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
color: var(--muted);
|
|
font-size: 0.68rem;
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
color: var(--ink);
|
|
}
|
|
|
|
.dot {
|
|
width: 11px;
|
|
height: 11px;
|
|
border-radius: 50%;
|
|
border: 1px solid rgba(244, 231, 188, 0.35);
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.dot.green { background: var(--green); box-shadow: 0 0 10px rgba(92, 255, 155, 0.55); }
|
|
.dot.red { background: var(--red); box-shadow: 0 0 10px rgba(255, 93, 84, 0.55); }
|
|
.dot.yellow { background: var(--amber); box-shadow: 0 0 10px rgba(255, 191, 71, 0.55); animation: blink 0.8s step-end infinite; }
|
|
@keyframes blink { 50% { opacity: 0.35; } }
|
|
|
|
.badge {
|
|
border: 1px solid var(--line);
|
|
border-radius: 999px;
|
|
padding: 3px 8px;
|
|
color: var(--muted);
|
|
font-size: 0.68rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.badge.ok { color: var(--green); border-color: var(--green-dim); background: rgba(36, 91, 52, 0.2); }
|
|
.badge.error { color: var(--red); border-color: rgba(255, 93, 84, 0.45); background: rgba(255, 93, 84, 0.12); }
|
|
.badge.neutral { color: var(--muted); }
|
|
.badge.warn { color: var(--amber); border-color: rgba(255, 191, 71, 0.45); background: rgba(255, 191, 71, 0.12); }
|
|
|
|
.quick-links {
|
|
display: grid;
|
|
gap: 8px;
|
|
}
|
|
|
|
.link-card {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
min-height: 42px;
|
|
padding: 9px 10px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 6px;
|
|
background: rgba(25, 21, 15, 0.62);
|
|
color: var(--ink);
|
|
font-size: 0.78rem;
|
|
font-weight: 700;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.link-card:hover,
|
|
.link-card:focus-visible {
|
|
outline: none;
|
|
border-color: var(--amber);
|
|
color: var(--amber);
|
|
}
|
|
|
|
.link-card .icon {
|
|
color: var(--green);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.utility-grid {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1.2fr) minmax(240px, 0.8fr);
|
|
gap: 16px;
|
|
}
|
|
|
|
.daily-message {
|
|
color: var(--ink);
|
|
font-size: 1.05rem;
|
|
line-height: 1.6;
|
|
border-left: 4px solid var(--violet);
|
|
padding-left: 12px;
|
|
}
|
|
|
|
.countdown-display {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
gap: 8px;
|
|
}
|
|
|
|
.time-unit {
|
|
min-width: 0;
|
|
border: 1px solid var(--line);
|
|
border-radius: 6px;
|
|
padding: 9px;
|
|
background: rgba(25, 21, 15, 0.62);
|
|
text-align: center;
|
|
}
|
|
|
|
.time-number {
|
|
display: block;
|
|
color: var(--green);
|
|
font-size: clamp(1.35rem, 3vw, 2rem);
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
}
|
|
|
|
.time-label {
|
|
margin-top: 5px;
|
|
color: var(--muted);
|
|
font-size: 0.62rem;
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.uptime-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
gap: 10px;
|
|
}
|
|
|
|
.uptime-item {
|
|
border: 1px solid var(--line);
|
|
border-radius: 6px;
|
|
padding: 10px;
|
|
background: rgba(25, 21, 15, 0.62);
|
|
}
|
|
|
|
.uptime-item-label {
|
|
color: var(--muted);
|
|
font-size: 0.65rem;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.uptime-item-value {
|
|
margin-top: 6px;
|
|
color: var(--blue);
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.uptime-bar-wrap { margin-top: 14px; }
|
|
|
|
.uptime-label {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
margin-bottom: 8px;
|
|
color: var(--muted);
|
|
font-size: 0.68rem;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.uptime-bar {
|
|
height: 8px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 999px;
|
|
background: #15110b;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.uptime-fill {
|
|
height: 100%;
|
|
width: 0%;
|
|
background: linear-gradient(90deg, var(--green), var(--blue));
|
|
transition: width 1s ease;
|
|
}
|
|
|
|
.ping-history {
|
|
display: flex;
|
|
align-items: end;
|
|
gap: 4px;
|
|
min-height: 36px;
|
|
margin-top: 10px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.ping-bar {
|
|
width: 8px;
|
|
flex: 0 0 8px;
|
|
border-radius: 2px 2px 0 0;
|
|
opacity: 0.82;
|
|
}
|
|
|
|
.run-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
.run-card .panel-body {
|
|
display: grid;
|
|
gap: 12px;
|
|
}
|
|
|
|
.run-summary {
|
|
display: grid;
|
|
gap: 8px;
|
|
padding: 12px;
|
|
border: 1px dashed var(--line);
|
|
border-radius: 6px;
|
|
background: rgba(25, 21, 15, 0.5);
|
|
}
|
|
|
|
.status-text {
|
|
min-height: 1.3em;
|
|
color: var(--muted);
|
|
font-size: 0.82rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.status-text.running { color: var(--amber); }
|
|
.status-text.success { color: var(--green); }
|
|
.status-text.failed { color: var(--red); }
|
|
|
|
.last-run-meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px 14px;
|
|
color: var(--muted);
|
|
font-size: 0.68rem;
|
|
}
|
|
|
|
.last-run-meta span {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.btn-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
min-height: 38px;
|
|
padding: 8px 11px;
|
|
border: 2px solid;
|
|
border-radius: 6px;
|
|
background: #15110b;
|
|
color: var(--ink);
|
|
cursor: pointer;
|
|
font-size: 0.76rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.btn:hover:not(:disabled),
|
|
.btn:focus-visible:not(:disabled) {
|
|
outline: none;
|
|
transform: translateY(-1px);
|
|
background: #20190e;
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.42;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-primary { border-color: var(--green-dim); color: var(--green); }
|
|
.btn-secondary { border-color: rgba(106, 200, 255, 0.5); color: var(--blue); }
|
|
.btn-danger { border-color: rgba(255, 93, 84, 0.55); color: var(--red); }
|
|
.btn-combined { border-color: rgba(255, 191, 71, 0.55); color: var(--amber); }
|
|
.btn-ghost { min-height: 32px; padding: 6px 9px; border-color: var(--line); color: var(--muted); }
|
|
|
|
.kill-btn { display: none; }
|
|
.kill-btn.visible { display: inline-flex; }
|
|
|
|
.log-tools {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
}
|
|
|
|
.log-label {
|
|
color: var(--dim);
|
|
font-size: 0.68rem;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.log-box {
|
|
height: 260px;
|
|
overflow-y: auto;
|
|
border: 2px solid var(--line);
|
|
border-radius: 6px;
|
|
padding: 12px;
|
|
background: #0b0f0a;
|
|
color: #c6f7b7;
|
|
font-size: 0.74rem;
|
|
line-height: 1.55;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
box-shadow: inset 0 0 22px rgba(92, 255, 155, 0.08);
|
|
}
|
|
|
|
.log-box::-webkit-scrollbar { width: 8px; }
|
|
.log-box::-webkit-scrollbar-track { background: #15110b; }
|
|
.log-box::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 8px; }
|
|
.log-line { display: block; }
|
|
|
|
.ansi-bold { font-weight: 700; }
|
|
.ansi-dim { opacity: 0.6; }
|
|
.ansi-red { color: #ff6b61; }
|
|
.ansi-green { color: #5cff9b; }
|
|
.ansi-yellow { color: #ffbf47; }
|
|
.ansi-blue { color: #6ac8ff; }
|
|
.ansi-magenta { color: #c59cff; }
|
|
.ansi-cyan { color: #77f0d4; }
|
|
.ansi-white { color: #f4e7bc; }
|
|
.ansi-reset { color: inherit; font-weight: inherit; opacity: inherit; }
|
|
|
|
.pipeline-steps {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
}
|
|
|
|
.pipeline-step {
|
|
border: 1px solid var(--line);
|
|
border-radius: 999px;
|
|
padding: 5px 10px;
|
|
color: var(--muted);
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.pipeline-step.active { color: var(--amber); border-color: rgba(255, 191, 71, 0.55); background: rgba(255, 191, 71, 0.1); }
|
|
.pipeline-step.done { color: var(--green); border-color: var(--green-dim); background: rgba(36, 91, 52, 0.2); }
|
|
.pipeline-step.failed { color: var(--red); border-color: rgba(255, 93, 84, 0.55); background: rgba(255, 93, 84, 0.1); }
|
|
.pipeline-arrow { color: var(--dim); }
|
|
|
|
#toast-container {
|
|
position: fixed;
|
|
right: 18px;
|
|
bottom: 18px;
|
|
z-index: 20;
|
|
display: grid;
|
|
gap: 8px;
|
|
}
|
|
|
|
.toast {
|
|
max-width: 320px;
|
|
border: 2px solid var(--line);
|
|
border-radius: 6px;
|
|
padding: 10px 12px;
|
|
background: var(--panel);
|
|
color: var(--ink);
|
|
font-size: 0.78rem;
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.toast.success { border-color: var(--green-dim); color: var(--green); }
|
|
.toast.error { border-color: rgba(255, 93, 84, 0.55); color: var(--red); }
|
|
.toast.info { border-color: rgba(106, 200, 255, 0.5); color: var(--blue); }
|
|
.toast.warn { border-color: rgba(255, 191, 71, 0.55); color: var(--amber); }
|
|
|
|
.modal-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 15;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 18px;
|
|
background: rgba(10, 8, 5, 0.82);
|
|
backdrop-filter: blur(3px);
|
|
}
|
|
|
|
.modal {
|
|
width: min(380px, 100%);
|
|
border: 2px solid var(--line-strong);
|
|
border-radius: 8px;
|
|
background: var(--panel);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.modal h3 {
|
|
padding: 12px 14px;
|
|
border-bottom: 2px solid var(--line);
|
|
color: var(--amber);
|
|
font-size: 0.95rem;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.modal p {
|
|
padding: 14px;
|
|
color: var(--muted);
|
|
font-size: 0.82rem;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.modal-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
padding: 0 14px 14px;
|
|
}
|
|
|
|
.hidden { display: none !important; }
|
|
|
|
@media (max-width: 940px) {
|
|
.dashboard-grid,
|
|
.utility-grid,
|
|
.run-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.rail { position: static; }
|
|
.quick-links { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.page { width: min(100% - 20px, 1180px); padding-top: 10px; }
|
|
.topbar { grid-template-columns: 1fr; align-items: start; }
|
|
.header-meta { min-width: 0; text-align: left; }
|
|
.status-grid,
|
|
.quick-links,
|
|
.uptime-grid,
|
|
.countdown-display {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
.btn { width: 100%; }
|
|
.log-tools { align-items: stretch; flex-direction: column; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="toast-container"></div>
|
|
|
|
<main class="page">
|
|
<header class="topbar">
|
|
<div>
|
|
<div class="brand-kicker">Local console / build deck</div>
|
|
<h1 class="site-title">Home Server Control</h1>
|
|
</div>
|
|
<div class="header-meta">
|
|
<div class="clock-display" id="clockDisplay">--:--:--</div>
|
|
<div id="dateDisplay"></div>
|
|
<div id="lastUpdated">Checking status...</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div id="globalStatus" class="global-ok">
|
|
<div class="status-pulse"></div>
|
|
<span>All systems running normally</span>
|
|
</div>
|
|
|
|
<div class="dashboard-grid">
|
|
<aside class="rail">
|
|
<section class="panel" aria-labelledby="statusTitle">
|
|
<div class="panel-header">
|
|
<h2 class="panel-title" id="statusTitle">System</h2>
|
|
<span id="healthBadge" class="badge neutral">Checking</span>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="status-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-head"><span>Server</span><span id="serverDot" class="dot green"></span></div>
|
|
<div class="stat-value" id="health">Online</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-head"><span>Website</span><span id="webDot" class="dot green"></span></div>
|
|
<div class="stat-value" id="webStatus">Idle</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-head"><span>Roam</span><span id="roamDot" class="dot green"></span></div>
|
|
<div class="stat-value" id="roamStatus">Idle</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-head"><span>Uptime</span><span class="dot green"></span></div>
|
|
<div class="stat-value" id="uptimeStat">--</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="panel" aria-labelledby="linksTitle">
|
|
<div class="panel-header">
|
|
<h2 class="panel-title" id="linksTitle">Links</h2>
|
|
</div>
|
|
<div class="panel-body">
|
|
<nav class="quick-links" aria-label="Services">
|
|
<a href="https://notes.zainezq.com" target="_blank" class="link-card" rel="noreferrer"><span>Notes</span><span class="icon">01</span></a>
|
|
<a href="https://zainezq.com" target="_blank" class="link-card" rel="noreferrer"><span>Website</span><span class="icon">02</span></a>
|
|
<a href="https://filebrowser.zainezq.com" target="_blank" class="link-card" rel="noreferrer"><span>Files</span><span class="icon">03</span></a>
|
|
<a href="https://calibre.zainezq.com" target="_blank" class="link-card" rel="noreferrer"><span>Library</span><span class="icon">04</span></a>
|
|
<a href="https://miniflux.zainezq.com" target="_blank" class="link-card" rel="noreferrer"><span>News</span><span class="icon">05</span></a>
|
|
<a href="https://guac.zainezq.com" target="_blank" class="link-card" rel="noreferrer"><span>RDP</span><span class="icon">06</span></a>
|
|
<a href="https://portainer.zainezq.com" target="_blank" class="link-card" rel="noreferrer"><span>Portainer</span><span class="icon">07</span></a>
|
|
<a href="https://nextcloud.zainezq.com" target="_blank" class="link-card" rel="noreferrer"><span>Nextcloud</span><span class="icon">08</span></a>
|
|
<a href="https://pgadmin.zainezq.com" target="_blank" class="link-card" rel="noreferrer"><span>Databases</span><span class="icon">09</span></a>
|
|
</nav>
|
|
</div>
|
|
</section>
|
|
</aside>
|
|
|
|
<div class="main-stack">
|
|
<section class="utility-grid" aria-label="Personal utilities">
|
|
<div class="panel">
|
|
<div class="panel-header">
|
|
<h2 class="panel-title">Daily Message</h2>
|
|
</div>
|
|
<div class="panel-body">
|
|
<p id="dailyMessage" class="daily-message"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<div class="panel-header">
|
|
<h2 class="panel-title">Countdown</h2>
|
|
<span id="countdownLabel" class="badge neutral"></span>
|
|
</div>
|
|
<div class="panel-body">
|
|
<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>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="panel" aria-labelledby="monitorTitle">
|
|
<div class="panel-header">
|
|
<h2 class="panel-title" id="monitorTitle">Uptime Monitor</h2>
|
|
<span class="badge neutral">30 checks</span>
|
|
</div>
|
|
<div class="panel-body">
|
|
<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">--</div></div>
|
|
</div>
|
|
<div class="uptime-bar-wrap">
|
|
<div class="uptime-label"><span>Ping success</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" aria-label="Recent ping history"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="run-grid" aria-label="Build runs">
|
|
<article class="panel run-card">
|
|
<div class="panel-header">
|
|
<h2 class="panel-title">Website Build</h2>
|
|
<span class="badge neutral">Web</span>
|
|
</div>
|
|
<div class="panel-body">
|
|
<div class="btn-row">
|
|
<button class="btn btn-primary" id="buildBtn">Run website build</button>
|
|
<button class="btn btn-danger kill-btn" id="killWebBtn">Cancel</button>
|
|
</div>
|
|
<div class="run-summary">
|
|
<p id="buildResult" class="status-text">Waiting for action</p>
|
|
<div class="last-run-meta" id="webLastRunMeta"></div>
|
|
</div>
|
|
<div class="log-tools">
|
|
<span class="log-label">Website run log</span>
|
|
<button class="btn btn-ghost toggleLogs" data-target="logOutput">Show logs</button>
|
|
</div>
|
|
<div id="logOutput" class="log-box hidden" aria-live="polite"></div>
|
|
</div>
|
|
</article>
|
|
|
|
<article class="panel run-card">
|
|
<div class="panel-header">
|
|
<h2 class="panel-title">Roam Build</h2>
|
|
<span class="badge neutral">Roam</span>
|
|
</div>
|
|
<div class="panel-body">
|
|
<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>
|
|
<div class="pipeline-steps hidden" id="pipelineSteps">
|
|
<div class="pipeline-step" id="pipeStep1">Emacs</div>
|
|
<div class="pipeline-arrow">to</div>
|
|
<div class="pipeline-step" id="pipeStep2">Notes</div>
|
|
</div>
|
|
<div class="run-summary">
|
|
<p id="orgRoamResult" class="status-text">Waiting for action</p>
|
|
<div class="last-run-meta" id="roamLastRunMeta"></div>
|
|
</div>
|
|
<div class="log-tools">
|
|
<span class="log-label">Roam run log</span>
|
|
<button class="btn btn-ghost toggleLogs" data-target="orgRoamLogs">Show logs</button>
|
|
</div>
|
|
<div id="orgRoamLogs" class="log-box hidden" aria-live="polite"></div>
|
|
</div>
|
|
</article>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<div id="modalOverlay" class="modal-overlay hidden">
|
|
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
|
|
<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>
|
|
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);
|
|
|
|
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);
|
|
}
|
|
|
|
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);
|
|
});
|
|
}
|
|
|
|
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?'28px':'10px'; b.style.background=r?'var(--green)':'var(--red)'; 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+'%';
|
|
}
|
|
|
|
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 {}
|
|
}
|
|
|
|
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(--green)':'var(--red)'}">exit ${code}</span>`
|
|
: '';
|
|
el.innerHTML = `<span>Last run: ${t}</span>${codeHtml}`;
|
|
}
|
|
|
|
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);
|
|
|
|
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';
|
|
});
|
|
});
|
|
|
|
let emacsES=null, webES=null, roamES=null, combinedES=null;
|
|
|
|
function startLogStream(url, container, setter) {
|
|
container.innerHTML = '';
|
|
const es = new EventSource(url);
|
|
es.addEventListener('log', e => {
|
|
const line = document.createElement('span');
|
|
line.className = 'log-line';
|
|
line.innerHTML = e.data;
|
|
container.appendChild(line);
|
|
container.scrollTop = container.scrollHeight;
|
|
});
|
|
es.addEventListener('done', () => {
|
|
es.close();
|
|
setter(null);
|
|
});
|
|
es.onerror = () => {
|
|
es.close();
|
|
setter(null);
|
|
const line = document.createElement('span');
|
|
line.className = 'log-line ansi-dim';
|
|
line.textContent = '-- stream closed --';
|
|
container.appendChild(line);
|
|
};
|
|
setter(es);
|
|
}
|
|
|
|
function closeStream(s){ if(s) s.close(); }
|
|
|
|
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'); }
|
|
}
|
|
|
|
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';
|
|
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');
|
|
}
|
|
}
|
|
|
|
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';
|
|
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');
|
|
}
|
|
}
|
|
|
|
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';
|
|
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');
|
|
}
|
|
}
|
|
|
|
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;
|
|
['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 to Notes...'; res.className='status-text running';
|
|
document.getElementById('roamDot').className='dot yellow';
|
|
document.getElementById('roamStatus').textContent='Pipeline';
|
|
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(){
|
|
const logEl = document.getElementById('orgRoamLogs');
|
|
const interval = setInterval(async()=>{
|
|
const r = await fetch('/api/run-combined/status');
|
|
const s = await r.json();
|
|
if(logEl.textContent.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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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();
|
|
|
|
function startCountdown(){
|
|
const target = new Date('2026-05-27T00: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(--green);font-weight:700">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();
|
|
|
|
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');
|
|
});
|
|
|
|
document.getElementById('buildBtn').addEventListener('click', runWebBuild);
|
|
document.getElementById('orgRoamBtn').addEventListener('click', runOrgRoamBuild);
|
|
document.getElementById('orgRoamBtn2').addEventListener('click', rerunEmacs);
|
|
document.getElementById('combinedBtn').addEventListener('click', runCombined);
|
|
|
|
fetchHealth();
|
|
</script>
|
|
</body>
|
|
</html>
|