Skip to content

feat(v1.1/W3): read-paths wired — 8 page surfaces on live hooks#7

Merged
lopadova merged 3 commits into
mainfrom
feature/v1.1/W3-read-paths
May 18, 2026
Merged

feat(v1.1/W3): read-paths wired — 8 page surfaces on live hooks#7
lopadova merged 3 commits into
mainfrom
feature/v1.1/W3-read-paths

Conversation

@lopadova
Copy link
Copy Markdown
Contributor

Summary

Wires the 8 read-path page surfaces in resources/js/pages/ to the W2 TanStack Query hooks. Every page surface enforces R14 (surface failures loudly: visible loading / error / empty + retry), R11 (testid contract: <page>-loading|error|empty|ready), and R15 (a11y: aria-busy / role=alert / role=status).

Page surfaces migrated

Page Hook(s)
DashboardPage useServers() + useAudit({per_page:24}) + useBreakers()
ServersListPage useServers({q, status, transport, page, per_page})
ServerDetailPage useServer(id) + per-tab useServerTools / useResources / usePrompts / useAudit({server_id})
ToolsPage useTools() + useServers() for grouping; per-tool useAudit({tool_name})
AuditPage useAudit({...filters}); drawer → useAuditDetail(auditId)
BreakersPage useBreakers() (normalises wire half_open → UI half)
ResourcesPage useServers() + useResources(serverId) + useResource(serverId, uri)
PromptsPage useServers() + usePrompts(serverId) + usePrompt(serverId, name)

New helper

resources/js/lib/data-state.tsx<DataState> wrapper that translates a UseQueryResult into the four R14-mandated visible states with the R11 testid contract + R15 a11y attrs baked in. Reused across every page tab.

Excluded by design (stay on fixtures until W4)

  • PlaygroundPage — OpenAPI explorer, no live-data dependency.
  • SettingsPage — wires in W4 alongside useCreateApiKey / useRevokeApiKey.
  • HelpPage — static content.

resources/js/lib/data.ts is RETAINED in the bundle because the excluded surfaces still import from it.

Test coverage

  • Vitest: 66 → 131 tests (+65 new across 11 files).
  • Files added: tests/js/lib/data-state.test.tsx, tests/js/pages/*.test.tsx (10 specs).
  • Smoke tests updated to provide a QueryClient + MSW default handlers so they pass after the data-layer swap.
  • npm test + npm run typecheck + npm run build all green locally.

Test plan

  • CI: Vitest 131 tests green.
  • CI: tsc + Vite build green.
  • Manual: open the SPA against a live host, navigate Dashboard → Servers → Server Detail → Tools → Audit → Resources → Prompts → Breakers; verify every page renders loading → ready or loading → empty → retry-from-error correctly.
  • Playwright E2E: EXPECTED to fail locally — specs still run on fixture data; updating them is W5 scope.

Hard constraints honoured

  • ✅ No write-path swap (W4 scope).
  • ✅ Standalone-agnostic — zero refs to AskMyDocs host.
  • ✅ SPA never sets X-Tenant-Id (R30).
  • ✅ Every fixture-only DOM string (openai-mcp, github-mcp, …) verified absent from the live-data rendered tree in integration.test.tsx.

🤖 Generated with Claude Code

… / Resources / Prompts / Breakers

Wires the 8 read-path page surfaces in resources/js/pages/ to the W2
TanStack Query hooks. Every page surface enforces R14 (surface failures
loudly: visible loading / error / empty + retry), R11 (testid contract:
<page>-loading|error|empty|ready), and R15 (a11y: aria-busy / role=alert
/ role=status).

Page surfaces migrated:
- DashboardPage — useServers + useAudit + useBreakers
- ServersListPage — useServers({filters})
- ServerDetailPage — useServer + per-tab useServerTools / useResources /
  usePrompts / useAudit({server_id})
- ToolsPage — useTools + useServers; per-tool ToolRecentCalls →
  useAudit({tool_name})
- AuditPage — useAudit({filters}); AuditDrilldown → useAuditDetail
- BreakersPage — useBreakers (normalises wire half_open → UI half)
- ResourcesPage — useServers + useResources + useResource
- PromptsPage — useServers + usePrompts + usePrompt

New helper: resources/js/lib/data-state.tsx — <DataState> wrapper that
translates a UseQueryResult into the four R14 states with R11 testids +
R15 a11y attrs baked in. Reused across every page tab.

Excluded by design (stay on fixtures until W4): PlaygroundPage,
SettingsPage, HelpPage. lib/data.ts retained for those surfaces.

Tests: 66 → 131 (+65 new). 11 new files under tests/js/pages/ +
tests/js/lib/data-state.test.tsx. Smoke tests updated to provide
QueryClient + MSW handlers so they still pass post-swap. npm test +
npm run typecheck + npm run build all green.

Playwright E2E specs still run on fixture data; updating them to the
live-data layer is W5 scope — local Playwright failures during W3 are
expected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

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

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: 23c14779ea

ℹ️ 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".

</div>
)}
{!contentQ.isLoading && !contentQ.isError && previewTab === 'rendered' && content && selected.mime === 'text/markdown' && (
<MarkdownRender source={content}/>
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 Sanitize live Markdown before rendering

When a live MCP server returns a text/markdown resource, this branch passes the server-controlled contentQ.data.content into MarkdownRender, which ultimately uses dangerouslySetInnerHTML without sanitizing raw HTML. A malicious or compromised server can advertise a Markdown file containing HTML such as an image with an event handler, causing script execution in the admin UI when an operator opens the rendered preview; escape or sanitize Markdown output before injecting it.

Useful? React with 👍 / 👎.

// fixture so the drawer still renders a useful narrative — flagged in the
// banner when the auditId doesn't match the fixture id.
const live = detailQ.data;
const detail = live ? { ...FALLBACK_AUDIT_DETAIL, ...live } : FALLBACK_AUDIT_DETAIL;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Map wire audit fields before rendering the drawer

For a successful GET /audit/:id response using the wire shape (mcp_server_name, tool_name, duration_ms, created_at), this spread does not populate the legacy fields that the drawer renders below (detail.server, detail.tool, detail.dur, detail.ts). In that scenario the drilldown shows the seeded fixture server/tool/timestamp/duration for the selected live audit row, so operators inspect misleading audit metadata; normalize the live detail into the keys used by the UI before merging with fixture-only fields.

Useful? React with 👍 / 👎.

Comment thread resources/js/pages/servers.tsx Outdated
const filters = React.useMemo(() => {
const f = { per_page: 100 };
if (q) f.q = q;
if (statusFilter !== 'all') f.status = statusFilter;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't send UI-only server filters as status

Selecting the Active or Disabled chips now sends /servers?...&status=active or status=disabled before the client-side filter runs, but the server status values in this codebase are ok/warn/err while disabled is represented by enabled. If the backend applies the query filter, active/disabled views will come back empty even when matching servers exist; only send real wire status values here, and use enabled=false or client-side filtering for disabled/active.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR migrates multiple SPA “read-path” pages from fixture-backed data to TanStack Query hook-backed live data, standardizing visible loading/error/empty behaviors and adding significant Vitest coverage to lock in the new state contracts and retry semantics.

Changes:

  • Wire major page surfaces (Dashboard/Servers/Tools/Audit/Breakers/Resources/Prompts) to W2 TanStack Query read hooks.
  • Introduce a shared <DataState> wrapper to normalize loading/error/empty UI (and retry) across query-driven sections.
  • Expand and update JS test coverage (page-level specs + smoke/integration updates with MSW + QueryClient provider).

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/js/smoke.test.tsx Updates smoke tests to mount QueryClient and provide MSW handlers for hook-backed routes.
tests/js/pages/tools.test.tsx Adds page-level ToolsPage state/behavior tests using MSW.
tests/js/pages/tools-recent-calls.test.tsx Adds coverage for per-tool audit query inside ToolsPage.
tests/js/pages/servers.test.tsx Adds ServersListPage + ServerDetailPage state/behavior tests.
tests/js/pages/server-detail-tabs.test.tsx Adds coverage for ServerDetailPage tab queries (tools/prompts/audit).
tests/js/pages/resources.test.tsx Adds ResourcesPage state tests including per-server resource tree states.
tests/js/pages/resources-content.test.tsx Adds coverage for per-resource content preview states.
tests/js/pages/prompts.test.tsx Adds PromptsPage state tests including per-server prompt list states.
tests/js/pages/integration.test.tsx Adds integration sanity checks ensuring fixture-only strings don’t bleed into live-hook UI.
tests/js/pages/helpers.tsx Adds shared render helpers (QueryClient + MemoryRouter + ToastProvider).
tests/js/pages/dashboard.test.tsx Adds DashboardPage state tests and retry behavior verification.
tests/js/pages/breakers.test.tsx Adds BreakersPage state tests and half_open normalization assertion.
tests/js/pages/audit.test.tsx Adds AuditPage + AuditDrilldown state tests including retry coverage.
tests/js/lib/data-state.test.tsx Adds unit tests for <DataState> loading/error/empty/ready + retry semantics.
resources/js/pages/tools.tsx Migrates ToolsPage to useTools/useServers and wires per-tool recent calls via useAudit.
resources/js/pages/servers.tsx Migrates Servers list/detail and detail tabs to hook-backed queries with fixture telemetry fallbacks.
resources/js/pages/resources.tsx Migrates ResourcesPage + PromptsPage to hook-backed queries with per-pane state handling.
resources/js/pages/dashboard.tsx Migrates Dashboard KPIs/failures/health/breakers to hooks with fixture telemetry fallbacks.
resources/js/pages/audit.tsx Migrates AuditPage, AuditDrilldown, and BreakersPage to hook-backed queries and adds state handling.
resources/js/lib/data-state.tsx Adds <DataState> wrapper for consistent query state rendering and retry.
CHANGELOG.md Documents the W3 read-path wiring scope, helper introduction, and test coverage expansion.
Comments suppressed due to low confidence (1)

resources/js/pages/resources.tsx:245

  • The “No preview” empty-state renders whenever !content, even while the content query is loading or in an error state. This can result in loading/error UI being shown together with the empty-state. Gate this branch on !contentQ.isLoading && !contentQ.isPending && !contentQ.isError (or contentQ.isSuccess) so only one state renders at a time.
                {previewTab === 'rendered' && !content && (
                  <EmptyState icon={<I.File size={20}/>}
                              title="No preview"
                              body={`This is a ${selected.mime} resource. Switch to Raw or Hex to inspect contents.`}/>
                )}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +238 to 240
{!contentQ.isLoading && !contentQ.isError && previewTab === 'rendered' && content && selected.mime === 'application/json' && (
<pre className="code-block" dangerouslySetInnerHTML={{ __html: jsonHighlight(JSON.parse(content || '{}')) }}/>
)}
Comment thread resources/js/pages/servers.tsx Outdated
Comment on lines +253 to +256
<tr><td colSpan={10} data-testid="servers-empty">
<EmptyState
icon={<I.Server size={26}/>}
title="No servers match your filters"
body="Try clearing some filters or registering a new server."
action={<button className="btn" onClick={() => { setStatusFilter('all'); setTransportFilter('all'); setQ(''); }}>Clear filters</button>}
secondary={<button className="btn primary" onClick={() => onNav('servers-new')}><I.Plus size={12}/>New server</button>}
title={rawServers.length === 0 ? 'No servers yet' : 'No servers match your filters'}
Comment thread resources/js/pages/audit.tsx Outdated
Comment on lines +209 to +219
<tr><td colSpan={9} data-testid="audit-empty">
<EmptyState
icon={<I.Scroll size={26}/>}
title={rawRows.length === 0 ? 'No audit rows yet' : 'No rows match your filters'}
body={rawRows.length === 0
? 'Invoke a tool to generate audit history.'
: 'Try widening the filters or selecting a different time window.'}
action={rawRows.length > 0
? <button className="btn" onClick={clearFilters}>Clear filters</button>
: <button className="btn primary" onClick={() => onNav('tools')}><I.Play size={12}/>Open tools</button>}
/>
Comment thread resources/js/lib/data-state.tsx Outdated
Comment on lines +7 to +21
// Small, reusable wrappers that translate a TanStack `UseQueryResult` into the
// three R14-mandated visible states (loading / error / empty / ready) with the
// R11 testid contract baked in. Every page that consumes a live hook routes
// through one of these so behaviour stays uniform.
//
// <DataState query={q} testIdBase="dashboard"
// isEmpty={(d) => d.length === 0}
// ready={(d) => <Grid items={d} />} />
//
// R14: never silently drop a non-success state on the floor — render either a
// visible loading affordance, an error message + retry, or an empty
// state. Loading shows `aria-busy`; error uses `role="alert"`; empty
// uses `role="status"`.
// R11: the wrapper attaches `data-testid="<base>-loading|error|empty"` so
// Playwright + Vitest can wait on them without sniffing internal markup.
…re field mapping, filter shape, a11y, ready-testid

Six review findings addressed:

- P1 XSS: `MarkdownRender` now HTML-escapes the server-controlled
  source string before running the markdown regex chain. A malicious
  or compromised MCP server can no longer execute scripts in the
  admin UI via a `text/markdown` resource carrying raw HTML.
- JSON preview no longer crashes the whole Resources page when the
  wire payload isn't valid JSON — wrapped in a `<JsonPreview>` with
  try/catch that surfaces an inline error banner instead.
- `AuditDrilldown` now projects the wire `AuditDetail` shape
  (mcp_server_name / tool_name / duration_ms / created_at / tenant_id)
  onto the legacy fixture-shaped UI keys BEFORE the fixture spread
  merge, so the drawer no longer displays misleading fixture metadata
  for real audit rows.
- `ServersListPage` no longer forwards UI-only chip values (`active` /
  `disabled`) as `?status=` to the BE — translates `disabled` to
  `enabled=false` and lets `active` fall through to client-side
  filtering. `err` / `warn` still forward.
- R15 a11y: in-table empty-state cells in `servers.tsx` + `audit.tsx`
  now wrap their `EmptyState` in `<div role="status" aria-live="polite">`
  so screen readers announce them.
- R11 ready-testid: `<DataState>` now wraps the ready branch in a
  `<div role="presentation" data-testid="<base>-ready">` sentinel,
  honouring the contract advertised in the header comment. New
  spec pins it.

Vitest: 132/132 green locally.
Playwright: still drives fixtures — W5 scope (unchanged from iter-1).
@lopadova
Copy link
Copy Markdown
Contributor Author

@copilot review

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 6 comments.

/>
</div>
);
}
Comment on lines +397 to +405
function DashboardPage({ liveEvents, livePaused, onTogglePaused, onClearFeed, onNav, onSelectAudit }) {
const serversQ = useServers();
const auditQ = useAudit({ per_page: 24 });
const breakersQ = useBreakers();

// Compose three queries into a single ready-gate: render loading until the
// primary `servers` query lands; surface error if any of the three failed
// (servers is load-bearing; audit + breakers degrade to []).
if (serversQ.isLoading || serversQ.isPending) {
Comment thread resources/js/pages/dashboard.tsx Outdated
} from '../lib/ui';
import { Breadcrumbs, ROUTES, SECONDARY } from '../components/shell';
import { useServers, useAudit, useBreakers } from '../lib/queries/hooks';
import { DataState } from '../lib/data-state';
Comment thread resources/js/pages/audit.tsx Outdated
import {
useAudit, useAuditDetail, useBreakers, useServers,
} from '../lib/queries/hooks';
import { DataState } from '../lib/data-state';
Comment thread resources/js/pages/resources.tsx Outdated
Comment on lines 10 to 12
TENANTS,
} from '../lib/data';
import {
Comment on lines 25 to +42
function ResourcesPage({ onNav }) {
const [serverId, setServerId] = React.useState('srv_01');
const [selectedUri, setSelectedUri] = React.useState('mcp://openai/docs/readme.md');
const serversQ = useServers();
const liveServers = (serversQ.data?.data ?? []).filter(s => s.enabled !== false);
const [serverId, setServerId] = React.useState(null);
const [selectedUri, setSelectedUri] = React.useState(null);
const [previewTab, setPreviewTab] = React.useState('rendered');
const [openDirs, setOpenDirs] = React.useState({ 'mcp://openai/docs/': true, 'mcp://openai/schemas/': false });
const [openDirs, setOpenDirs] = React.useState({});

// Auto-pick first server when the list lands.
React.useEffect(() => {
if (!serverId && liveServers.length > 0) {
setServerId(liveServers[0].id);
}
}, [serverId, liveServers.length]);

const items = RESOURCES[serverId] || [];
const resourcesQ = useResources(serverId);
const contentQ = useResource(serverId, selectedUri);
const items = resourcesQ.data ?? [];
Copy link
Copy Markdown

Copilot AI commented May 18, 2026

@copilot review

Reviewed. The requested review fixes are already present in commit d52e797 (JSON preview parse guard, R15 status semantics on servers/audit empty states, and *-ready testid in DataState). I re-ran targeted Vitest coverage for those surfaces (30/30 passing).

…rs + drop dead imports + fix serverId flicker

Six findings on iter-2:

- tools.tsx: surface serversQ.isError as a page-level error
  with retry; previously fell through to fixture-derived
  grouping that misattributed seed names to live tool ids.
- dashboard.tsx: secondary auditQ + breakersQ failures now
  surface via a page-level alert banner + a card-scoped
  error state inside RecentFailures. Previously silent
  degradation to [] read as "0 failures / all closed".
- dashboard.tsx + audit.tsx: removed dead `DataState` imports
  (neither module routes through the wrapper).
- resources.tsx: removed dead `TENANTS` import.
- resources.tsx: derive `serverId` synchronously from
  liveServers in render instead of useState(null) +
  useEffect auto-pick. Eliminates the one-render flicker
  where the tree showed "No resources advertised" empty
  state for a frame after serversQ resolved.

Vitest: 132/132 green locally.
@lopadova
Copy link
Copy Markdown
Contributor Author

@copilot review

Copy link
Copy Markdown

Copilot AI commented May 18, 2026

@copilot review

Reviewed. The six follow-up review findings are already addressed in commit 4161c29 (servers-query error surfaced on Tools, secondary-query error surfacing on Dashboard, unused imports removed, and Resources serverId flicker fix). I also re-ran local gates: npm test (132/132), npm run typecheck, and npm run build — all passing.

@lopadova lopadova merged commit 6fa0269 into main May 18, 2026
9 of 10 checks passed
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.

3 participants