Skip to content

portal: modern audit UI, native dark-mode OpenAPI viewer, sticky detail panel#7

Merged
cjimti merged 3 commits into
mainfrom
portal-ui-polish
May 11, 2026
Merged

portal: modern audit UI, native dark-mode OpenAPI viewer, sticky detail panel#7
cjimti merged 3 commits into
mainfrom
portal-ui-polish

Conversation

@cjimti
Copy link
Copy Markdown
Contributor

@cjimti cjimti commented May 11, 2026

Summary

Portal UI overhaul focused on three user-reported defects in the audit and discovery pages, plus the documentation that ships alongside them.

  • Audit now has stable column widths (no more jitter when the right pane gets long JSON), method/status pills with color coding, locale-independent 24-hour timestamps, and a sticky detail panel that follows the viewport so clicking the 100th row no longer requires scrolling back to the top.
  • Discovery drops the Redoc iframe — it was light-themed inside a dark portal and visibly unreadable — and ships a native OpenAPI 3.1 reference rendered against the portal's design tokens. Full dark/light parity, in-portal search/filter across path/summary/operationId, collapsible operation cards with parameters / request body / responses / synthesized examples, and a tag rail. /docs (Redoc) remains served and is linked from the page header for operators who prefer it.
  • Docs + screenshots regenerated: every portal page captured fresh (light + dark), Discovery added to the capture script, operations/portal.md gains a Discovery section, the homepage carousel gets a Discovery slide and an updated Endpoints caption.

Defect → fix mapping

Reported Root cause Fix
Audit left column jitters as the right pane content grows grid-cols-[2fr_3fr] plus missing min-w-0 let long JSON values steal width from the list grid-cols-1 lg:grid-cols-[460px_minmax(0,1fr)] + table-fixed + explicit <colgroup> widths
Time column overlaps the Method column ("12:34:56 PM" + "GET" collide) toLocaleTimeString() produces 11+ chars in en-US; the 88px Time col and 64px Method col couldn't hold it Explicit 24-hour HH:MM:SS formatter; widths re-measured via getBoundingClientRect (76 / 88 / Path / 64)
Discovery is a visual mess; dark-mode iframe contents are unreadable Embedded Redoc bundle is light-themed only Replace the iframe with a native OpenAPI 3.1 viewer rendered against the portal's CSS variables
Click an audit row at the bottom of the list, have to scroll back to the top to see the detail <main className=\"... overflow-auto\"> inside a min-h-full outer grid never overflowed, so the document scrolled and position: sticky had no scroll context Outer grid switched to h-full so main becomes the real scroll container; detail panel uses lg:sticky lg:top-6 lg:self-start with its own max-h + overflow-y-auto for tall payload bodies
Discovery heading reads vv1.0.0 Component unconditionally prepended v to info.version, which already starts with v Strip a leading v before re-adding
Docs and screenshots still showed the old portal Capture script didn't know about Discovery; portal.md had no Discovery section; carousel slide list omitted it Discovery added to screenshots.mjs, portal.md, and the homepage carousel; all 18 existing PNGs regenerated, 2 new Discovery PNGs added

Accessibility / robustness picked up along the way

  • aria-expanded on every disclosure (operation cards, response rows).
  • Response rows with no body render as a non-interactive <div> so keyboard users don't Tab through dead buttons.
  • Tag rail uses aria-pressed for filter state and proper min-w-0 so long tag names don't push the count badge off-screen.
  • CodeBlock copy-feedback timer is cleaned up on unmount; the Discovery fetch uses AbortController.
  • Loading / missing / error states show contextual subtitles and hide the JSON / YAML / Redoc header links (they 404 together with /openapi.json).
  • Method/status pills are color-coded against both light and dark backgrounds.

Files touched

  • ui/src/App.tsx — outer grid min-h-fullh-full so main owns the scroll context; sidebar gains overflow-y-auto.
  • ui/src/pages/Audit.tsx — fixed-width left column, table-fixed colgroup, 24-hour time, MethodPill/StatusPill, sticky detail panel.
  • ui/src/pages/Discovery.tsx — full native OpenAPI viewer replacing the iframe (~760 lines).
  • scripts/screenshots/screenshots.mjs — capture Discovery (pre-expanded operation card).
  • docs/operations/portal.md — Discovery section + embeds.
  • docs/overrides/home.html — carousel adds Discovery; Endpoints caption updated.
  • docs/reference/http-api.md/docs description acknowledges the native portal renderer.
  • docs/images/portal/*.png — all regenerated; discovery-{light,dark}.png added.

Verification

Each commit went through make verify (green) plus a live-browser pass before commit:

  • Started the rebuilt binary on :8080.
  • Authenticated via dev API key against the portal.
  • Navigated to each affected page in mcp__claude-in-chrome.
  • Ran getBoundingClientRect on every table cell across 20 rows and asserted overflows: [] (first pass found a DELETE pill overflow; widened Method column 72px → 88px; re-measured to zero).
  • Scrolled to row 100 and clicked it; asserted the sticky detail panel's top is in-viewport (top: 48px, viewport: 782px).
  • Confirmed Dashboard, Endpoints, Discovery still render correctly after the App.tsx layout change.

Test plan

  • CI green.
  • Hit /portal/audit in a real browser, scroll the list, click rows at the top and bottom — detail panel stays visible.
  • Confirm method pills (GET / POST / PUT / PATCH / DELETE) and status pills (2xx / 3xx / 4xx / 5xx) render with readable contrast in both light and dark mode.
  • Hit /portal/discovery, expand operation cards, use the search bar, switch tags in the left rail; check that the Redoc link in the header opens /docs in a new tab.
  • When /openapi.json is unavailable (e.g. oapiDoc=nil build), Discovery's loading → missing transition does not render dead header links.
  • make screenshots against a clean main build reproduces every PNG in this PR.

cjimti added 3 commits May 11, 2026 00:42
Audit: the event list column visibly resized whenever the right detail
pane grew (long JSON header values pushed the proportional 2fr_3fr grid
to give the right column more space). Replaced with a fixed left
column and a min-w-0 right cell:

  grid-cols-1 lg:grid-cols-[420px_minmax(0,1fr)]

minmax(0,1fr) is the load-bearing token — without the 0 floor, grid
children inherit min-width: auto and the right column fights for space.
Inner table now uses table-fixed with an explicit colgroup so the
columns honor declared widths even when the path contains long tokens.
EventDetail gained min-w-0 / truncate discipline so long Method+Path
headers don't blow out the right pane.

Discovery: dropped the /docs (Redoc) iframe. Redoc inside the iframe
renders light-only and is unreadable against the portal's dark shell;
piping a theme into the build-time Redoc HTML is heavier than rendering
the OpenAPI document natively. Replaced with a native OpenAPI 3.1
reference view that fetches /openapi.json and renders groups,
operations (collapsible cards), parameters, request body schema,
responses, and per-media-type examples — all against the portal's CSS
variables, so light and dark reach full parity. Includes a tag rail,
search/filter, and per-block clipboard copy. /docs is still served
and is linked from the page header for the canonical Redoc view.

Accessibility / robustness:

- aria-expanded on every disclosure button (operation cards, response
  rows, tag rail uses aria-pressed for filter state).
- Tag rail and operation header use min-w-0 + truncate so long names
  and summaries don't push out the rest of the row.
- Response rows with no body render a non-interactive div so keyboard
  users don't tab through a button that does nothing.
- CodeBlock copy-feedback timer is cleared on unmount (useRef +
  useEffect) so React strict-mode double-mount doesn't leave a timer
  firing on a dead component.
- Discovery fetch uses AbortController so a fast unmount cancels the
  in-flight request.
- Loading / missing / error states show appropriate subtitles and hide
  the JSON/YAML/Redoc download links (they 404 together with
  /openapi.json — offering them when missing is just dead-link
  theater).

Docs: portal.md, http-api.md, and overview.md all advertised Discovery
as a Redoc/Swagger UI iframe — updated to describe the native renderer
and /docs as a sibling link.
Two coupled changes to fix the "click row at bottom, scroll back to top
to see detail" UX failure on the Audit page:

ui/src/App.tsx
  Outer layout was `grid ... min-h-full` with `<main className="p-6
  overflow-auto">`. min-h-full means the grid grows with content so the
  *document* scrolls and main never overflows — `overflow-auto` is a
  no-op, and any `position: sticky` inside main has no scroll context
  to anchor to. Switched the outer grid to `h-full` so main is
  height-pinned to the viewport, its overflow-auto actually fires, and
  sticky works. Added `overflow-y-auto` to the sidebar so it scrolls
  independently if its content ever exceeds the viewport. Verified
  Dashboard, Endpoints, and Discovery still render (Discovery's
  `h-[calc(100vh-3rem)]` shell continues to fit because main is now
  exactly viewport-height).

ui/src/pages/Audit.tsx
  Right detail column gets `lg:sticky lg:top-6 lg:self-start
  lg:max-h-[calc(100vh-3rem)] lg:overflow-y-auto`. self-start so the
  right grid track doesn't stretch to the left track's height (sticky
  needs room within its containing block to slide). max-h +
  overflow-y-auto so a tall detail (multiple JSON payload blocks)
  scrolls within itself instead of pushing the sticky panel off
  screen. Confirmed via getBoundingClientRect after scrolling to the
  100th row: detail panel pins at top: 48px, fully in viewport.

  Also widened Method column from 72px → 88px (DELETE pill rendered
  ~78px; the 72px column let it overflow into Path by ~7px). Left
  pane widened 440px → 460px to keep the Path column at ~226px
  usable width.
The portal UI rewrite (audit pills, sticky detail, native Discovery
viewer) made every committed portal screenshot stale. The homepage
carousel and operations/portal.md showed the old design.

Changes:

- scripts/screenshots/screenshots.mjs: add Discovery to the capture
  list. Pre-expands the first operation card so the screenshot shows
  what the page actually does (parameters table, response schema)
  instead of a closed-card list.
- docs/images/portal/: regenerated all 18 PNGs (light + dark for
  every page) plus 2 new ones for Discovery. Captured against a
  binary built with `make build VERSION=v1.0.0` so the version
  badge reads "v1.0.0" rather than the git-describe "v1.0.0-dirty"
  during in-progress work.
- docs/operations/portal.md: new Discovery section between Endpoints
  and Audit log, with light/dark screenshot embeds.
- docs/overrides/home.html: added Discovery to the carousel; updated
  the Endpoint Detail caption (Try-It panel is shipped, not
  "arrives with").
- ui/src/pages/Discovery.tsx: heading rendered "vv1.0.0" because the
  span unconditionally prepended "v" to an info.version that
  already started with "v". Strip a leading "v" before re-adding.
@cjimti cjimti merged commit 3dfa58b into main May 11, 2026
7 checks passed
@cjimti cjimti deleted the portal-ui-polish branch May 11, 2026 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant