UI polish across sections: one server selector, skeletons, inline errors
CI / check (push) Successful in 2m39s
Hub / check (push) Successful in 50s
CI / image (push) Successful in 1m30s
Hub / image (push) Successful in 1m37s

- Every section puts the hub's server selector in the chrome slot, the
  same place and gap as the Dashboard.
- Data regions load with a Skeleton and fail inline with Retry; KPIs show
  the no-data dash instead of 0 when a server did not answer. Settings
  keeps its header and selector while loading or failing.
- Cache, Apps and Logs headers group secondary or hub-refused actions in
  menus; app descriptions clamp to two lines; Unknown cells say why.
- DNS Client: Protocol label, Resolve and Import together. DHCP and Query
  Logs empty states with actions. View Logs list capped with scroll.
- Blocking: Lists KPI tiles like the Overview, app list table fits its
  panel with one-line URLs. Status re-reads on tab return at most once
  a minute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-11-02 21:42:28 +07:00
1 parent 172e06f1ce
commit ba3cdd08d2
43 files changed
+671 -261

No files matched your search

+16 -1
View File
@@ -162,6 +162,16 @@ export function mergeTopResponses(
return { topDomains: [] }
}
/* A counter as a number: a number, or a numeric string some servers send. Anything else is not a counter. */
function counter(value: unknown): number | null {
if (typeof value === 'number') return Number.isFinite(value) ? value : null
if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) return Number(value)
return null
}
/* What a server holds, as the "Server" panel of the Dashboard counts it. */
const HELD = ['zones', 'cachedEntries', 'allowedZones', 'blockedZones', 'allowListZones', 'blockListZones']
/*
`dashboard/stats/get`. Every numeric counter in `stats` is summed. That includes
`totalClients` (distinct clients), which over-counts a client seen by both servers:
@@ -174,9 +184,14 @@ export function mergeStats(responses: DashboardStats[]): DashboardStats {
const stats: Record<string, number> = {}
for (const r of responses) {
for (const [key, value] of Object.entries(r.stats ?? {})) {
if (typeof value === 'number') stats[key] = (stats[key] ?? 0) + value
const n = counter(value)
if (n !== null) stats[key] = (stats[key] ?? 0) + n
}
}
/* The Dashboard's "Server" panel counts what each server holds (zones, cache, allow and block
lists). Those are sums too, and a server that left one out counts as none, so the field is
never missing from the total. */
for (const key of HELD) stats[key] ??= 0
const merged: DashboardStats = { ...first, stats }
const main = mergeChart(responses.map((r) => r.mainChartData), true)