Add a dashboard and backup download to the web UI

Metric history (5s to 1h tiers, persisted), uplink status, ping targets,
DHCP leases, and a tar.gz backup of the state directory.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
2026-10-29 13:34:21 +01:00
co-authored by Claude Sonnet 5.5
parent e684ddb06c
commit 2a111d012b
13 changed files with 1835 additions and 37 deletions
+33
View File
@@ -35,3 +35,36 @@ button.link { background: none; border: none; color: var(--accent); padding: 0;
.narrow label { margin: 0; }
pre.log { background: color-mix(in srgb, var(--line) 35%, transparent); padding: .75rem; border-radius: 6px; overflow-x: auto; font-size: 12.5px; max-height: 30rem; }
code { font-size: .9em; overflow-wrap: anywhere; }
.muted { color: var(--muted); }
.bad { color: var(--bad); }
.ok { color: var(--good); }
:root { --good: #1e7b34; --s0: #2457c5; --s1: #d9730d; --s2: #2f9e44; --s3: #9c36b5; --s4: #c92a2a; }
@media (prefers-color-scheme: dark) { :root { --good: #69db7c; --s0: #7aa2ff; --s1: #ffa94d; --s2: #69db7c; --s3: #da77f2; --s4: #ff8787; } }
.ranges { display: flex; gap: .25rem; margin: 0 0 1rem; }
.ranges a { padding: .2rem .7rem; border: 1px solid var(--line); border-radius: 4px; text-decoration: none; }
.ranges a.active { background: var(--accent); color: var(--bg); border-color: var(--accent); }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); gap: 1rem; margin: 0 0 1.25rem; }
.card { border: 1px solid var(--line); border-left-width: 4px; border-radius: 6px; padding: .75rem 1rem; margin: 0; }
.card.up { border-left-color: var(--good); }
.card.degraded { border-left-color: #e8a600; }
.card.down { border-left-color: var(--bad); }
.card .state { font-weight: 600; margin: 0 0 .5rem; }
.card dl { margin: 0; font-size: .92em; }
.events { list-style: none; margin: 0; padding: 0; font-size: .9em; }
.events li span:first-child { display: inline-block; width: 2.5rem; font-weight: 600; }
.scroll { overflow-x: auto; }
.leases td, .leases th { padding: .2rem .75rem .2rem 0; white-space: nowrap; }
.charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 30rem), 1fr)); gap: 1rem; margin: 0 0 1.5rem; }
.chart { margin: 0; border: 1px solid var(--line); border-radius: 6px; padding: .5rem .75rem; min-width: 0; }
.chart figcaption { display: flex; flex-wrap: wrap; gap: .25rem 1rem; justify-content: space-between; font-size: .9em; }
.legend { display: flex; flex-wrap: wrap; gap: .1rem .9rem; }
.key::before { content: ""; display: inline-block; width: .7rem; height: .2rem; margin: 0 .3rem .2rem 0; background: currentColor; vertical-align: middle; }
.key b { font-weight: 500; color: var(--fg); }
.chart svg { width: 100%; height: auto; display: block; overflow: visible; }
.chart .grid { stroke: var(--line); stroke-width: 1; }
.chart .lab { fill: var(--muted); font-size: 11px; }
.chart .line { fill: none; stroke-width: 1.6; stroke-linejoin: round; stroke-linecap: round; }
.chart .hover { fill: transparent; }
.chart .hover:hover { fill: color-mix(in srgb, var(--fg) 8%, transparent); }
.s0 { color: var(--s0); stroke: var(--s0); } .s1 { color: var(--s1); stroke: var(--s1); } .s2 { color: var(--s2); stroke: var(--s2); }
.s3 { color: var(--s3); stroke: var(--s3); } .s4 { color: var(--s4); stroke: var(--s4); }