Repository navigation
light theme, command palette, structured help, Servers page - #247
Merged
Merged
Conversation
added 23 commits
May 12, 2026 18:11
Pure #000 + JetBrains Mono everywhere + max-w-6xl read as a wasted-space banner on 4K monitors; pool names in the heatmap also truncated while the right half of each row sat empty. Surfaces shift to zinc-tinted dark per the 2026 dev-tools baseline (Linear/Vercel/shadcn), Geist Sans takes chrome and headings while JetBrains Mono keeps numbers and identifiers. Overview stretches to 1680px, PageHero collapses to a single line with an (i) popover for the description, and the heatmap pool name flexes so long ids fit when there is room.
The first redesign pass fixed colour tokens and the page hero but operators still saw a wasted sidebar with no live data, a war-room page filled with empty 7xl tiles, and a sign-in surface that looked like a 90s terminal. Charts also lost their history on tab open and showed mock 2026-2029 axes while samples were collecting; the wait-queue chart kept the 1024 px hard-coded uPlot width so a dead strip stretched on the right. Sidebar grows an operational status panel: brand on one line, global health pill (pulse on critical), live QPS / err-s / saturation / alert breakdown, hostname / pid / uptime. The hottest-database block is shown compact under the signals. /wall route turns into a kiosk: sidebar is hidden, a 16x22 px heatmap spans the page for at-a-glance pool saturation, six KPI tiles run beneath with a 90 s MiniSparkline under each number, recent admin events sit at the bottom, and a fixed red pulse outlines the page when any signal is critical. Sample history persists in localStorage so a wall display survives a reload. AuthGate drops the CLI cookies: no leading `>`, no oversized eyebrow, sentence-case button labels, a softer divider between SSO and local admin, and the transport chip swaps the half-block glyph for a status dot. Chart.tsx now installs a ResizeObserver on the canvas wrapper so every uPlot chart tracks its container width. Sparkline, AreaChart, and DualAxisChart render a "collecting samples · N/120" placeholder until at least two points exist, hiding the uPlot mock axis. HelpTip positions under the trigger with max-h-60vh overflow-y-auto so long descriptions no longer clip above the viewport.
Hot orange-amber #ff9000 on pure black gives the operator the punch they asked for; the zinc revision read clinically next to the amber accent. Logo trims to a mono "pg_doorman" with the second word in amber so the brand is recognisable without sitting on coloured plates. Active sidebar nav items wear an amber left-border with a faint amber wash. On the Pools table the OK state goes green instead of dim grey so the eye does not have to translate a muted chip into "everything is fine". The chip-strip on Overview duplicated the sidebar's live health and rate read; removed it. The page hero copy shrank to fit the (i) popover without overflowing. CPU% and per-thread deltas on ProcessBar previously lived in a useRef-only prev snapshot — every page navigation reset them to "sampling…" until two snapshots accumulated again. The previous ProcessDto and the previous QPS/errors totals now persist in localStorage with a 60 s sanity guard, so reopening a tab or jumping between pages keeps the live rate visible. The war-room kiosk grew a heavy amber "back · esc" CTA and an Esc-key handler — OS-level notifications often drift across the page header on operator desktops, and the previous outline button was easy to miss. KPI tiles also picked up a 4 px left-border accent (amber when ok, danger when alerting) and the number weight went up to extrabold.
Sparkline, AreaChart, and DualAxisChart kept the admin events array in the useMemo deps for uPlot Options. The /api/events poll handed every chart a fresh array reference every 3 s, the memoised options changed, and useEffect tore the plot down and rebuilt it. From the operator's side this read as the dashboard flickering on its own and the "collecting samples" placeholder briefly returning every event tick. Events now live in a useRef the draw hook pulls from on each frame, so the options memo no longer depends on the events array. The Sparkline also kept `width` in the same deps, so every ResizeObserver tick — including the initial 0 → real width transition on mount — destroyed and recreated the plot. The size now applies through a separate setSize() call against the live plotRef, options builds with the current width once and stays stable for the rest of the chart's lifetime.
The sidebar carried 8 top-level pins (Overview/Pools/Clients/Apps/Caches/ Logs/Config/War room), which crosses the 7±2 boundary and put a kiosk view next to operational pages. War room is now reachable only from the Overview hero — its presence in the sidebar duplicated the operator's mental model. The route stays live, the sidebar entry is gone. Per-pool startup_parameters overrides used to live only on Pool detail. An operator coming from Config to verify "what overrides are configured in this deployment" had no answer without clicking through every pool. Config grows a "startup parameters" collapsible that flattens the cascade across every pool — pool id, parameter, value, source, state. Reuses /api/pools so no new endpoint is needed.
Three changes shipped together because they all read on the operator who reported Chrome ballooning when sitting on the Clients tab. The Clients table polled /api/clients every 1.5 s and rendered 50 rows × 13 cells unconditionally — every poll allocated a fresh row tree even when nothing changed. Two fixes: poll cadence dropped to 3 s (the per-client view is not incident-grade, the dashboards still tick at 1.5 s), and the row is now a memoised component with a custom comparator on the actual fields driving the render. useClientRates also returns early when the snapshot ts has not advanced — Strict Mode or a parent re-render was previously rebuilding the ClientRates record on every pass. Sidebar nav items grow a 16 px lucide icon next to each label so the top of the screen reads as a modern admin console instead of a flat text list. The status pill in the same sidebar gets an animate-ping ring around the verdict dot so it is obvious that data is still flowing — no separate "live" badge, just a single dot that breathes. cmdk and sonner are installed but not wired yet; the next round will add a Cmd+K palette and admin-action toasts on top of these primitives.
The SPA polls /api/overview, /api/pools, /api/clients and /api/process every 1.5–3 s. The previous rule treated every authenticated 2xx as info, so an operator sitting on the Logs tab watched their own polls echo back at them and the info stream stopped being useful as a "events that matter" feed. Authentication state no longer drives the level. info now means: a non-success response, a personal-data path, an admin action, or a hit on the auth/SSO surface. Anything else — anonymous or authenticated GET — rides on debug. The login surface itself stays on info so we keep a record of who signed in and through which provider.
Round 2 of the modernisation pass. The pieces here are interlinked but ship together because each one closes a usability gap the operator noted on the previous build. Cmd+K command palette (cmdk). Opens with ⌘K / Ctrl-K from anywhere, narrows across pages and pools by substring, jumps to /pools/:id on Enter. Pool list refetches on each open so the palette never holds a stale snapshot. Replaces the click-through-sidebar-to-Pools-to-row sequence with one shortcut. Toast feedback (sonner). PAUSE / RESUME / RECONNECT / RELOAD previously applied silently — the only sign of success was the row tone shifting on the next poll. The handler now fires toast.success or toast.error on top of the existing inline feedback so an operator sees the action landed without watching the table for two seconds. Severity filter on Pools. The four-option select dropdown became a row of pill buttons (all / ok / degraded / critical). Each chip tints to its own semantic colour when active; tonal contrast carries the intent instead of a generic blue highlight. Route fade-in. A keyed wrapper around <Routes> replays a 150 ms opacity+translateY ease-in on every pathname change, so jumping between pages reads as a transition instead of a snap. Respects prefers-reduced-motion.
Wholesale shift away from the Bloomberg-dark identity. The console opens light by default — Linear/Vercel/Stripe palette — and operators who prefer the kiosk look toggle dark from the sidebar footer. The system option follows OS preference. Token system. Light tokens are zinc-50 canvas, white surfaces, zinc-200 borders, zinc-900 text. .dark overrides re-skin every surface in one place so page-level styles never branch on mode. The accent moved off amber to a classic SaaS blue (#2563eb in light, #60a5fa in dark) — the amber that came with the previous direction kept reading as Bloomberg or worse, so it retired into the warning tone where it belongs. Card geometry restored. Radius scale goes back to 4-8 px (Bloomberg-zero was retired), and a .shadow-card class lifts cards off the canvas with a soft elevation on light and a near-invisible top hairline on dark. ThemeProvider + ThemeToggle. The provider reads the persisted preference on first paint to avoid the light-then-dark flash, listens for OS preference changes while in "system" mode, and applies .dark on <html>. The toggle is a three-segment radio (Sun / Monitor / Moon) in the sidebar footer; the sonner Toaster reads the same resolved theme so notifications match the page. Charts. uPlot palette swapped to blue/cyan/green/red — readable on both canvases without re-skinning per-mode.
The light-mode pass landed but operator reported "everything is tiny and I have plenty of free space". Three changes bring the console up to a modern density without losing data: Base typography goes from 14 px to 15 px (xs/sm/base/md/lg/xl shift proportionally). Sidebar nav widens from 240 px to 256 px, items get gap-3 + py-2.5 + 18 px icons, the host/uptime block reads at xs (was 11 px text), the footer row at sm. Card geometry restored. Overview Card now wears shadow-card by default and lifts on hover (translate-y-px + hover:shadow-md + 150 ms transition). The static section variant also picked up the soft elevation so a card never disappears flat into the canvas. `?` help modal. Listens for the literal `?` key outside form fields, opens a modal listing the global shortcuts. Esc closes. Wires next to the command palette in App.tsx so the keymap stays in one place. Uppercase tracking sweep. tracking-[0.2em] (Bloomberg eyebrow style) demoted to tracking-wide and tracking-[0.3em] to tracking-wider across Wall, PoolDetail, ConfigState, Caches, Overview, MemoryPanel, PanelView, and HelpTip — the wider letter-spacing was a retro artefact that did not survive the light skin.
The sidebar carried three usePoll instances for /api/overview, /api/pools, /api/process. Every page navigation tore them down with the rest of the sidebar (which never unmounts in practice, but the fetchers were re-issued on every authHeader change) and the operator saw "0.00" on the live ticker until the next 3 s tick landed. Three useQuery calls replace them. The shared QueryClient lives at the top of App and keeps the last response in cache for five minutes after the last observer detaches; staleTime is 1 s so a deliberate refresh still hits the network. Version is a one-shot query (5 min staleTime, no refetchInterval). The persisted prevRef + rate derivation still runs in a useEffect against the latest overview object. This is the first step of the Sidebar-first migration plan. Overview / Pools / Clients pages still use usePoll; they will follow in a later round once the cache primitives are proven on the sidebar.
Three operator-driven fixes in one round. Sidebar grows a "Documentation" link in the footer with an external- link icon — clicks open ozontech.github.io/pg_doorman in a new tab. The footer reflows into two rows so the SSO username no longer truncates to "sso: dmi…" behind the theme toggle. Stale-tab guard on Overview and Wall used to drop the rolling window on any gap larger than 5 × 1.5 s = 7.5 s. Opening the war room kiosk and returning to Overview took longer than that, so every sparkline reset to "collecting samples · 1/120". The threshold lifts to 90 s — brief navigation is invisible, while a multi-hour sleep still wipes the buffer instead of bridging it with a flat line. PanelView legend rows are now interactive. Click isolates the series (hides every other); click the isolated row again restores all. ⌘/Ctrl-click toggles a single series without affecting the rest. The caption above the table tells the operator the shortcut. Hidden rows fade to 40 % opacity. Visibility applies before stacking, so the stacked-area chart keeps a sensible cumulative shape when one band is zeroed out.
The (i) popovers were three free-form paragraphs (What / How / Healthy). Operators read them once and could not cross-check the number from psql because the help never named the admin SQL that backed it. The 2026 pattern from Datadog / Grafana / Vercel is a structured card: short definition, source-of-truth, formula, thresholds with concrete numbers, related metrics, and a docs link. HelpTip now renders the structured form when given a `help: HelpContent` prop and falls back to the legacy free-form children when given them. Each section drives the popover through `SectionHeader.help` / `Card.helpStructured`. The legacy What/How/Normal triad still works unchanged on call-sites that have not migrated yet. Overview migrates four cards as the first wave: Golden signals, Connection breakdown, Pool fill heatmap, Wait queue vs oldest active. Source names the PgBouncer-compatible admin command an operator runs through psql against pg_doorman's admin port (`SHOW POOLS`, `SHOW STATS`), not the SPA's `/api/*` endpoint — so the popover doubles as a quick console reference.
Two operator-facing fixes shipping together. The structured help cards on Overview linked to two non-existent doc pages: concepts/admin-monitoring.html and concepts/connection-pooling. html — pg_doorman's mdBook never had those routes. Every "Open in docs" click landed on a 404. Routes are remapped to the real pages the docs ship: tutorials/overview.html, tutorials/pool-pressure.html, tutorials/troubleshooting.html. Pools and Apps table columns "Pool" / "Mode" / "application_name" carried (i) icons explaining what the column was. The column label is already self-explanatory, the icon read as a duplicate of the PageHero (i) and visually crowded the header. Removed those three; the (i) icons stay on metric columns where the value needs a threshold or computation note (saturation, p95, errors, q/s, t/s).
Round-out of the help redesign. Pools / Clients / Apps / Caches / Logs / Config now drive PageHero through the structured HelpContent — the same shape as Overview, the format the operator already approved. Each page's source field names the admin SHOW command an operator can re-run from psql against pg_doorman's admin protocol (SHOW POOLS for Pools, SHOW CLIENTS for Clients, SHOW PREPARED_STATEMENTS / SHOW INTERNER for Caches, the lot via SHOW CONFIG / SHOW DATABASES etc on Config). Apps notes that its rows are aggregated browser-side from SHOW CLIENTS. Logs explains that the LogTap is a side-channel — not an admin SQL — and points at SET log_level for runtime verbosity changes. PageHero gained an optional `help: HelpContent` prop so the structured form is opt-in; legacy `description` still works for any page that has not migrated yet. All docs URLs verified against the live mdBook build at ozontech.github.io/pg_doorman.
Batch 1 — small UX and durability fixes. HelpTip dismisses on mouseleave with a 200 ms grace timer so a hover that travelled off the trigger does not leave a stale popover floating over the page. The popover panel itself reacts to the same enter/leave pair so dropping out of the panel also closes. Theme application hoisted into useLayoutEffect plus an inline <script> in index.html that reads `pgdoorman.theme` and applies `dark` before React parses. Operators with a persisted dark preference no longer see a single-frame flash of the light palette on reload. Sidebar / Overview / useHistory storage keys grow a host suffix (`pgdoorman.history.<host>.<key>`, same for prev totals / prev process) so two tabs pointing at different poolers no longer share or overwrite each other's rolling buffers. Sidebar rate calc detects counter rollback. When pg_doorman restarts, query_count_total resets to 0 and the next delta is negative — the previous Math.max(0, ...) silently rendered "0.00 qps". Now we toast "pg_doorman restarted — rate baseline reset" and keep the prior rate visible until the new baseline gives a real delta. Batch 2 — the gaps the DevOps + DBA reviews flagged. Global RELOAD lives on the Config page hero as an admin-only button with a typed RELOAD confirmation, hooks into POST /api/admin/reload, and feeds back via toast. Previously the only entry point sat behind Pools → drill-down — four clicks during an incident. Patroni-assisted fallback now surfaces on Overview as a banner when any pool reports fallback_active=true. The DTO has carried the flag for releases but only PoolDetail rendered it as a KV pair; a DBA on call had no way to tell "are we on the fallback host right now" from the dashboard. The banner lists affected pool ids and links to the patroni-assisted-fallback tutorial. Overview grows a "Top SQLSTATE codes" card that aggregates errors_by_sqlstate across every pool with describeSqlstate() context. Closes the gap that the "Errors / s ↗" tile tooltip promised — the operator no longer has to drill into every pool to find the dominant error class.
Batch 3 — URL state on Logs and Clients. Logs filters (level, q, paused, scroll) now live in the URL, so an operator narrowing /logs?level=ERROR&q=53300 can paste the link into chat and the teammate lands on the exact narrowed view. Previous useState was reset on every refresh. Clients filters / sort / dir / offset migrate to useSearchParams with the same writeParams helper used elsewhere. A `filters` useMemo stabilises the object reference so downstream useMemo / useEffect hooks do not re-fire on every parent render. Batch 4 — Servers page, wake lock, palette focus and dialog labelling. New Servers route (`/servers`, plumbed into Sidebar + Routes) renders SHOW SERVERS: backend connections with database / user / app / state / active_age / queries / errors / bytes_sent / bytes_received / tls. Filters by database / user / state / application_name in the URL, plus 50-row pagination. Active-age cell tints amber > 30 s, red > 5 min so a stuck backend is visible in the column scan. War room requests a screen wake lock on mount so an office TV does not blank after the OS screensaver timeout; permission failures are silent. Lock is re-requested on visibilitychange because navigation releases it. CommandPalette closes on Esc (was bound only to ⌘K toggle), grew role="dialog" + aria-modal + aria-label on the backdrop, and the inner container picked up rounded-lg for visual consistency with HelpModal.
LogLevelPanel previously offered an Apply button to dispatch SET log_level over the web admin surface — the backend does not yet expose that mutation (web/routes/admin.rs only handles reload/pause/resume/ reconnect), so the button would have surfaced "unknown_action" errors. The panel now says so explicitly: runtime change available only via psql's admin protocol, with the SQL the operator would run pre-printed. Sidebar footer grows a persistent transport indicator. Previously the http/https warning was rendered exclusively inside the sign-in modal; once authenticated, the chip vanished and an operator could go a whole session pushing admin actions over plain HTTP without a visible cue. The hairline row sits above the documentation link and never moves.
Formatters extracted into `lib/format`. Sidebar, Wall, and Servers each carried their own fmtRate / fmtMs / fmtBytes / fmtUptime / fmtAge / fmtClock with subtle drift in punctuation and rounding — 1.2 MiB on one page sometimes read as "1.20 MiB" on another. Now a single module owns the canonical shape. Sidebar collapses below md. The 256-px nav consumed most of a 360-px mobile viewport with no escape hatch; `hidden md:flex` removes it on phones. Operators on mobile still reach every page through the Cmd+K palette and bookmarkable URLs. A proper slide-in hamburger is the follow-up; this is the minimal-viable mobile fallback. No backend changes — `Бакенд не трогаем` per operator. RESET INTERNER and SET log_level remain documented as psql-only mutations until the admin route grows.
PageHero on Caches had migrated to the structured help format but the three inner SectionHeaders (Prepared statements, Query interner, Top entries by bytes) still carried the legacy What / How / Healthy paragraphs. Operators reported the inconsistency — clicking (i) on the page title showed the right format, clicking (i) on the section below gave the old prose. All three converted: definition + source (SHOW PREPARED_STATEMENTS, SHOW INTERNER, SHOW INTERNER <N>) + formula (hit rate = hits / total where applicable) + thresholds with concrete numbers + related metrics + docs links to prepared-statements.html and monitoring-interner.html.
EN + RU guides/web-ui.md catch up to the actual SPA. The previous revision listed pages as a flat 10-bullet enumeration and predated most of the structured-help / theme / palette work. The new revision gives each route its own `###` section with the admin SHOW command it reads from, the URL filters, and the pagination shape — Pools, Clients, Servers (new page), Apps, Caches, Logs, Pool detail, Config, War room. Operator-facing surfaces that did not exist in the old text: - Patroni fallback banner on Overview when fallback_active=true, - Top SQLSTATE codes card aggregating errors_by_sqlstate, - Startup parameters block both on Pool detail and as a section in Config / state. Five new top-level sections: - Admin actions — table of RELOAD/PAUSE/RESUME/RECONNECT with scope, typed confirmation, toast notification, and the access-log info entry the action produces. - Keyboard shortcuts — ⌘K/Ctrl-K for the global command palette, ? for the keyboard sheet, Esc to dismiss palette/popovers and to leave the war room. - Theme — three-state Light / System / Dark toggle in the sidebar footer, persisted in localStorage; accent hex for both palettes. - In-app help — structure of the (i) popover (definition / source / formula / thresholds / related / Open in docs). - Toasts — Sonner top-right, 4 s, follows the resolved theme. Access-log section corrected: web/access_log.rs now sends every 2xx read (anonymous or authenticated) to debug; info covers admin actions, personal-data paths, /api/auth/, /api/sso/, and any non-2xx. Earlier text claimed authenticated 2xx stayed on info, which was true only before commit e225971. All cross-links use relative anchors (../tutorials/…, ../concepts/…, ../observability/…). mdbook build passes on both languages; Cyrillic anchor for "#admin-действия" survives URL-encoding.
Tighten comments and help-popover copy across the SPA and the EN/RU mdBook web-ui guides. No behaviour change.
added 4 commits
May 13, 2026 07:28
The admin shell only renders three weights of Geist Sans (regular / medium / semibold) and two of JetBrains Mono (regular / medium); the 700/800 imports added ~80 KB of woff2 to the binary without a single call-site. War-room KPI tiles used font-extrabold explicitly and the back-button used font-bold — both lowered to font-semibold, which the existing 600 weight covers. JetBrains Mono cyrillic dropped too; pool ids, application_name and the rest of the mono surface are ASCII in practice, and the system mono fallback covers the rare cyrillic glyph. Bundle: 7 woff2 down to 5, ~157 KB → ~146 KB.
The Overview page hero and the four golden-signals tooltips drifted
into multi-sentence prose. Shortened each to a single line stating
the metric and the threshold; removed AI-style filler ("pulse",
"event overlays", "the heatmap below identifies the pool under
pressure"). Tooltip rendering also explicitly resets text-transform
and letter-spacing so the popover text does not inherit the uppercase
+ wide tracking of its parent label.
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
22 commits on top of master rework the admin Web UI.
What changed
?opens a keyboard-shortcut sheet. Esc closes either.(i)popovers everywhere — definition, admin SHOW source, formula, thresholds, related metrics, link to docs. Operators can cross-check any number from psql against the admin port./serverspage reads SHOW SERVERS. Filters (database / user / state / application_name) and 50-row pagination, all URL-persisted. Adds the matching DTO tofrontend/src/types.ts./wallkiosk: hides the sidebar, requests a screen wake lock so a TV does not blank, Esc returns to Overview. Large heatmap on top, six KPI tiles with 90 s sparklines, recent admin events feed.fallback_active=true. Top SQLSTATE codes card aggregateserrors_by_sqlstateacross pools.useSearchParamsso a narrowed view can be shared as a URL.prefers-reduced-motion).query_count_totalrolls back, the sidebar toasts "pg_doorman restarted" instead of silently rendering 0 qps.mdso the 256-px nav does not eat a phone viewport.window.location.hostsuffix so two pooler tabs do not overwrite each other.Backend
One change in
src/web/access_log.rs: every 2xx GET — anonymous or authenticated — now rides ondebug.infocovers admin actions, personal-data paths (/api/logs, prepared text, interner top),/api/auth/,/api/sso/, and any non-2xx. Operators on the Logs tab no longer see their own SPA polls echo back at them.Docs
documentation/{en,ru}/src/guides/web-ui.mdupdated to match. Each page gets its own section with the SHOW source it reads from, URL filters, and pagination shape. New sections for admin actions, keyboard shortcuts, theme, in-app help, toasts. The access-log paragraph is corrected for the rule change above.