Skip to content

fix(ui): audit batch — auth, sessions, settings, admin pages, mocks#270

Closed
JoshuaAFerguson wants to merge 5 commits into
mainfrom
fix/ui-audit-batch-1
Closed

fix(ui): audit batch — auth, sessions, settings, admin pages, mocks#270
JoshuaAFerguson wants to merge 5 commits into
mainfrom
fix/ui-audit-batch-1

Conversation

@JoshuaAFerguson

Copy link
Copy Markdown
Member

Summary

Live-walk audit of every UI route through MSW + Playwright surfaced 5 distinct bugs ranging from a P0 page-killer to mock-data fidelity gaps. All fixed in this batch:

Commit What broke
`2f5174b` Login → blank page. `LoginResponse.expiresAt` was required in TS but the mock omitted it; `isTokenExpired()` returned true on undefined and the route guard immediately bounced back to /login. Defensive 24h fallback in `userStore.setAuth` so stack/mock contract drift can't silently break auth.
`351c4b8` Sessions cards: 3 stacked status pills (state Chip + phase Chip + ActivityIndicator) all showing the same conceptual state, plus title/URL overflow with no ellipsis, plus inconsistent action-row wrapping. Consolidated to single phase pill, proper flex ellipsis (`minWidth: 0` + `textOverflow: ellipsis`), `flexWrap: wrap` on actions.
`26cd2b8` Settings page 100% dead. `QuotaCard.tsx` did `import type { UserQuota as _UserQuota }` then `void(_UserQuota)` to silence an unused-import lint. Type-only imports get erased at runtime, so `_UserQuota` was undefined and the `void` expression threw `ReferenceError` — caught by ErrorBoundary, killing the whole route. Removed the dead import.
`c743a26` Admin Dashboard, admin Users, and Recent Sessions table all broken via three separate mock contract gaps: missing `/api/v1/metrics` handler (Dashboard "Failed to load metrics"), bare-array `/users` response vs `{users,total}` envelope (Users "No users found"), `created_at` snake_case vs `createdAt` camelCase in MOCK_SESSIONS (Recent Sessions "Invalid Date"). Plus added missing `/admin/agents` handler.
`ac2e9fe` SessionViewer dead with "Failed to connect to session". Mock was POST for `/sessions/:id/connect` but the backend route (api/cmd/main.go:637) is GET. Switched mock to GET.

Verification

Walked all major routes in Playwright after each fix:

  • ✅ /, /sessions, /shared-sessions, /settings, /sessions/:id/viewer
  • ✅ /admin/{dashboard,nodes,users,agents,audit,monitoring,compliance,plugins,scaling,license,recordings,integrations}

All routes render without ErrorBoundary catches.

Out of scope

Three known cosmetic issues noted but not fixed (low priority):

  • Admin Dashboard `formatBytes('14.5 GiB')` shows "14B / 48B" — needs raw-byte strings from backend
  • Agents table mixes `lastHeartbeat` and `last_heartbeat` references
  • License page feature labels render "Saml Sso", "Mfa" — need acronym casing

Test plan

  • CI passes
  • Manual: login form works with admin/admin123 (or whatever seed credentials)
  • Manual: navigate to /settings — no ErrorBoundary
  • Manual (with MSW `?msw=true`): every admin/* page renders the data, not error toast

Four real bugs surfaced + fixed during a Playwright-driven audit pass
through Login → Dashboard → My Sessions → Session Viewer.

ui/src/store/userStore.ts — defensive expiresAt fallback
  TypeScript LoginResponse.expiresAt is required, but TS contracts
  aren't enforced at the network boundary. A backend (or MSW mock)
  returning {token, user} without expiresAt left the field undefined,
  isTokenExpired() returned true on the next render, and the route
  guard silently bounced the user back to /login with no error shown.
  Default to 24h ahead when missing so the session is at least usable
  until the next /auth/me call refreshes state.

ui/src/pages/Sessions.tsx — initial REST fetch + correct empty-state
  Page relied entirely on useSessionsWebSocket() for data with no
  initial REST seed, so a failing WS (mock mode, network glitch, or
  any race) left the page silently empty forever. Wired up the
  existing useSessions() hook to seed `sessions` on mount; WS pushes
  still win the race when faster.
  Also fixed empty-state copy that pointed at a non-existent
  "Template Catalog" — there's no Catalog link in the sidebar.
  Replaced with a "Browse Applications" inline action that routes to
  the My Applications page (the actual launch surface).

ui/src/lib/toast.ts — dedupe identical toasts within 2s window
  A single page navigation that triggers N parallel API requests all
  returning 5xx (e.g. Sessions page hitting /sessions/:id/connect plus
  /users/me/quota during a brief backend hiccup) stacked the same red
  "Server error. Please try again later." 3-4× simultaneously, which
  looked like the app was dying. 2s is long enough to absorb a request
  burst but short enough that a real second occurrence still shows up.

ui/src/mocks/handlers.ts — mock infra completeness pass
  - /auth/login: include expiresAt (paired with userStore fix above)
  - /sessions: wrap in {sessions, total} envelope to match real
    backend response shape (api/internal/api/handlers.go ListSessions);
    bare-array response made api.listSessions() return undefined
  - Removed undefined MOCK_SESSIONS.vnc reference that produced a
    trailing null in the array
  - Added stub handlers for ~25 endpoints called by the UI but never
    mocked, returning [] / {} so pages render their empty states
    instead of stacking 500s from MSW's default unhandled-request
    behavior. Real backend returns real data; mocks just need to not
    crash the audit.

Long-term follow-up: generate the MSW handlers + the API client from
a single OpenAPI spec exposed by the Go backend so they can never
drift. Three of the four bugs above are the same class — TS contract
not enforced at the network boundary because mock and backend shapes
were hand-maintained separately.
Card layout had three accumulated bugs:

1. Three stacked status pills per card (state Chip + phase Chip +
   ActivityIndicator) all conveying the same conceptual state.
   Consolidated to a single phase Chip; ActivityIndicator now renders
   inline below only when isIdle && state === 'running'.

2. Long template names, session names, and URLs overflowed the card.
   Added flex ellipsis (minWidth: 0 on parent + overflow/textOverflow/
   whiteSpace on the text node) with title= attributes for full value
   on hover.

3. CardActions wrapped inconsistently — Connect button on its own line
   on narrow cards. flexWrap: 'wrap' + gap keeps the row coherent and
   wraps cleanly when needed. Added title= to icon-only buttons for a11y.
QuotaCard imported `UserQuota as _UserQuota` (type-only) and tried to
suppress the unused-import lint with `void (_UserQuota)`. TS erases
type-only imports entirely, but Vite/esbuild kept the void expression,
so the runtime threw `ReferenceError: _UserQuota is not defined` and
ErrorBoundary caught the entire Settings route — Settings was 100%
broken.

The type wasn't actually used in the file. Removing both lines unblocks
the page. The correct way to silence the lint for genuinely needed
type-only imports is an eslint-disable comment, not a runtime void.
UI audit batch 2 — fixes uncovered while walking admin routes in MSW mode.

- Add /api/v1/metrics handler returning the ClusterMetrics shape the
  admin Dashboard requires (cluster.{nodes,sessions,resources,users}).
  Without this, useMetrics() resolves undefined and Dashboard renders
  "Failed to load metrics".
- Add /api/v1/admin/agents handler with the {agents,total,page,limit}
  envelope the admin Agents page expects (the /agents handler was on a
  different path; admin/agents was 500-ing).
- Wrap /api/v1/users response in {users,total} envelope to match the
  api.listUsers contract — bare array deserialized as [] and the User
  Management page showed "No users found" with two seeded users.
- Rename MOCK_SESSIONS timestamp fields from snake_case (created_at,
  last_activity) to camelCase (createdAt, lastActivity) to match the
  Session interface in api.ts. Was rendering "Invalid Date" in the
  Recent Sessions table on the admin Dashboard.
The backend route is GET /sessions/:id/connect (api/cmd/main.go:637) and
the API client uses GET (api.connectSession). The mock used POST, so the
SessionViewer flow 500'd on every load with "Failed to connect to
session". Switching the mock to GET unblocks the viewer chrome.
Comment thread ui/src/pages/Sessions.tsx
<Chip
label={session.status.phase || session.state}
size="small"
color={getPhaseColor(session.status.phase) || getStateColor(session.state)}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ac2e9fe761

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ui/src/pages/Sessions.tsx
Comment on lines +153 to +157
if (initialSessions && sessions.length === 0) {
const filtered = username
? initialSessions.filter((s) => s.user === username)
: initialSessions;
setSessions(filtered);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep REST session fallback synchronized after refetches

The new seeding effect only copies initialSessions when sessions.length === 0, so once local state is populated it stops tracking React Query updates. That breaks the explicit WebSocket-fallback path: useUpdateSessionState/useDeleteSession invalidate ['sessions'], but when the socket is down the refetched REST data is ignored and the UI can keep showing stale or deleted sessions. This regression is introduced here because the page now relies on local state that no longer updates from query results after the first seed.

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale because it has not had recent activity.

Action Required:

  • If this PR is still being worked on, please add a comment
  • If this is blocked, add the status:blocked label
  • If this is no longer needed, it will be closed in 7 days

@github-actions github-actions Bot added the stale No recent activity - will be closed if no response label May 29, 2026
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

This pull request was automatically closed due to inactivity.

If you believe this was closed in error, please reopen it.

@github-actions github-actions Bot closed this Jun 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component:ui Frontend UI (React) stale No recent activity - will be closed if no response

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants