Phase 3: Svelte 5 UI skeleton

Svelte 5 + Vite + TypeScript dashboard that consumes kamado-api over
REST for the first paint and then subscribes to /api/ws for live
updates. Zero runtime deps beyond svelte itself; plain CSS, no
component library.

Layout:

- Header with brand, WebSocket status badge, chain + height
- PoolOverview: hashrate (1m/5m/1h/24h), miner count, network
  hashrate + diff, pool share (ppb), expected time to block, uptime
- BlocksTable: recent solves from the in-memory ring (will be
  SQLite-backed in Phase 2b.5)
- BestShares: top-10 workers by bestever, falling back to bestdiff
  when the ckpool patch isn't present
- MinersTable: joined view of stratum clients and workers with
  user-agent-based hardware detection (Bitaxe, NerdQAxe, Antminer,
  ...), hashrate, best-round, best-ever, last share

State is a single $state() snapshot store in svelte-runes form;
components read from it via $derived. The store does one initial
REST snapshot fetch, then owns the WebSocket with exponential
backoff reconnects.

Vite dev server on :5173 proxies /api and /api/ws to localhost:8080
so you can run `make ui-dev` alongside `make up` in development.
Production serving (bundled into the Go binary via embed, behind /
on :8080) lands in Phase 4.
This commit is contained in:
satoshi
2026-04-13 03:07:59 +03:00
parent 36c08647e2
commit d37a23bc57
20 changed files with 966 additions and 2 deletions
+151
View File
@@ -0,0 +1,151 @@
:root {
color-scheme: dark light;
--bg: #0b0e14;
--bg-alt: #11151d;
--bg-card: #151a24;
--bg-hover: #1c2230;
--border: #232a3a;
--fg: #e6e9ef;
--fg-dim: #8a92a6;
--accent: #ff7a3a;
--accent-dim: #b8501f;
--good: #5ce0a8;
--bad: #ff6b6b;
--warn: #f5c447;
font-family: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;
font-size: 14px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
background: var(--bg);
color: var(--fg);
min-height: 100vh;
}
body {
font-variant-numeric: tabular-nums;
}
a {
color: var(--accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
button {
font: inherit;
color: inherit;
background: var(--bg-card);
border: 1px solid var(--border);
padding: 0.5em 1em;
border-radius: 6px;
cursor: pointer;
}
button:hover {
background: var(--bg-hover);
}
code,
.mono {
font-family: "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular,
Menlo, monospace;
font-size: 0.92em;
}
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 1.25rem 1.5rem;
}
.grid {
display: grid;
gap: 1rem;
}
.grid-4 {
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.grid-2 {
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
}
.stat-label {
color: var(--fg-dim);
font-size: 0.85em;
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 0.35em;
}
.stat-value {
font-size: 1.7rem;
font-weight: 600;
letter-spacing: -0.01em;
}
.stat-sub {
color: var(--fg-dim);
font-size: 0.85em;
margin-top: 0.25em;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
text-align: left;
padding: 0.55rem 0.85rem;
border-bottom: 1px solid var(--border);
}
th {
font-weight: 500;
color: var(--fg-dim);
font-size: 0.82em;
text-transform: uppercase;
letter-spacing: 0.04em;
}
tbody tr:hover {
background: var(--bg-hover);
}
td.num,
th.num {
text-align: right;
}
.badge {
display: inline-block;
padding: 0.15em 0.55em;
border-radius: 999px;
font-size: 0.75em;
border: 1px solid var(--border);
background: var(--bg-alt);
}
.badge.good {
color: var(--good);
border-color: #2f5c48;
background: #0f2419;
}
.badge.bad {
color: var(--bad);
border-color: #5c2f2f;
background: #241010;
}
.badge.warn {
color: var(--warn);
border-color: #5c502f;
background: #241f10;
}