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

+17
View File
@@ -53,6 +53,23 @@ describe('mergeStats counters', () => {
expect(Object.keys(merged.stats)).toEqual(Object.keys(stats()))
})
it('sums what each server holds: zones, cache, allowed, blocked and the two lists', () => {
const a = response({ stats: stats({ zones: 3, cachedEntries: 400, allowedZones: 1, blockedZones: 2, allowListZones: 1, blockListZones: 1000 }) })
const b = response({ stats: stats({ zones: 5, cachedEntries: 50, allowedZones: 4, blockedZones: 6, allowListZones: 2, blockListZones: 10 }) })
const merged = mergeStats([a, b])
expect(merged.stats).toMatchObject({ zones: 8, cachedEntries: 450, allowedZones: 5, blockedZones: 8, allowListZones: 3, blockListZones: 1010 })
})
it('counts a numeric string, and a server that left a field out as none', () => {
const a = response({ stats: { ...stats(), zones: '4' as unknown as number } })
const b = response({ stats: { totalQueries: 1 } })
const merged = mergeStats([a, b])
expect(merged.stats.zones).toBe(4)
expect(merged.stats.allowedZones).toBe(1)
expect(merged.stats.totalQueries).toBe(101)
expect(mergeStats([b]).stats.blockListZones).toBe(0)
})
it('returns a single response with the same numbers', () => {
const merged = mergeStats([response()])
expect(merged.stats).toEqual(stats())