Skip to content

Open UI gaps (verified 2026-10-15)

Source: the 178-gap console audit, re-verified against the code after the standardisation merge (main 977b4681f). Each item was re-checked by reading the current files. OPEN = still there; PARTIAL = part fixed, the evidence line says what remains. Billing/top-up items are listed last and are deferred (not a priority for the open-source codebase). Shared pager, nav module and the new screens already exist: reuse them.

Counts: 118 open or partial (101 excluding billing).

Active (non-billing)

[blocker] /admin/data-privacy (no tenant-privacy UI exists; page only shows legacy export/delete) (PARTIAL, features)

  • Where: web/app/admin/data-privacy/page.tsx (33 lines); web/app/components/DataPrivacySection.tsx; backend core/app/src/main/java/com/agenticai/core/common/privacy/PrivacyController.java (@RequestMapping /api/tenant/privacy, 13 mappings, lines 70-153)
  • Problem: None of the DPDP endpoints has a web caller (grep of web/app for tenant/privacy, consent-polic, retention-polic, contacts/erase, audit-export finds nothing). The /admin/data-privacy page renders only DataPrivacySection, which calls the older POST /api/tenant/data-export and /api/tenant/data-deletion. So there is no UI to: set the per-agent consent notice (GET/PUT /consent-policies/{agentId}, per-language text, YES/NO/STOP); browse consent records (GET /consent-records); set retention periods per tenant/channel for transcripts, recordings and memory (GET/PUT /retention-policies); tag knowledge collections or agents with a purpose (GET/PUT /purposes/knowledge-collections/{id} and /purposes/agents/{id}); export or erase one contact (POST /contacts/export, /contacts/erase); or run the time-range audit export (GET /audit-export). The nav entry exists (components/AppShell.tsx:1098-1110, perm MANAGE_DATA_GOVERNANCE) but leads to a page with none of this. Because every control is opt-in and nothing is deleted until a tenant enables a rule, the whole feature is unreachable for a UI-only tenant.
  • Remaining now: Largely built. web/app/admin/data-privacy/page.tsx:36-49 is a Tabs page with Consent, Retention, Purposes, Contacts, Audit export and Workspace data. web/app/lib/api/privacy.ts is the client. ContactsTab has an AlertDialog erase confirm (line 214) and RetentionTab uses PaginationControls (line 133). Remaining: consent records are only a per-contact lookup by channel+externalId (privacy.ts:122-125, ContactsTab.tsx:137), with no browsable paginated list filtered by channel and date. PurposesTab is a standalone tab, not inline on the knowledge-collection and agent edit screens.
  • Fix: Rebuild /admin/data-privacy as a tabbed page with Tabs for Consent, Retention, Purposes, Contact requests (export/erase) and Audit export, plus the existing tenant-wide export/delete as an 'Workspace data' tab. Add a lib/api/privacy.ts client for the 13 endpoints. Consent tab: an agent picker plus a per-language notice editor with a version number and an on/off switch. Consent records: a paginated table using components/PaginationControls.tsx with channel and date filters. Retention tab: a rule table (channel x data type) with days input, an enabled toggle and a warning when under 365 days. Purposes: an inline purpose select on the knowledge collection and agent edit screens. Contact request: a phone/contact search plus export and a confirm-dialog erase that shows the 'not reached' list returned by the API. Audit tab: a date range plus a CSV/JSON download. Reuse PageHeader, page-kit Section/EmptyState, and add keys to web/messages/en.json; keep the useRequireScope('MANAGE_DATA_GOVERNANCE') guard.

[blocker] /agents (PARTIAL, layout)

  • Where: web/app/agents/page.tsx; web/app/components/agents/AgentWorkbench.tsx (switcher at ~2008-2130)
  • Problem: /agents has no list, table or search of agents. The only way to see or switch agents is a dropdown
      capped at max-h-80 w-[19rem] (AgentWorkbench.tsx ~2043-2086) that renders agents.map() with no search, no filter by status/category, no pagination and no bulk actions. The workbench is one 5,137-line component (violates the 1000-line ratchet). Sub-features are three header buttons (Quality, Learning, Import) at agents/page.tsx:93-110, plus /agents/templates with no rail entry. Header subtitle and button labels at agents/page.tsx:98,104,108,114,116 are raw English, and the error path is silent (comment at line 53: no banner if providers/agents fail to load).
    • Remaining now: The index gap is fixed. agents/page.tsx:15 renders AgentsIndex, which has a search Input (line 150), a Select filter (159) and usePagination(filtered, 10) with PaginationControls (240). Subtitle and buttons use t(). The load error is passed as error={loadError} at line 183. A Templates link is at 143. Remaining: AgentWorkbench.tsx is still 4894 lines (baselined at 5139), only partly split into the AgentWorkbench/ folder. I did not confirm bulk actions or row actions.
    • Fix: Add an agents index (Table via components/ui/table, search Input, status/category filter, usePagination + PaginationControls, row actions Open/Duplicate/Delete) as the /agents landing, and open the existing workbench at /agents/[id] or ?agentId=. Put Quality, Learning, Templates and Import in a Tabs strip or sub-nav under the PageHeader. Split AgentWorkbench into section components. Move literals to i18n (agentsPage namespace). Show an Alert with Try again when listAgents/listProviders reject.

    [blocker] /jobs, /research, /files, /data-tables, /scheduled, /templates, /build (PARTIAL, layout)

    • Where: web/app/components/jobs/JobsList.tsx:162-177; web/app/components/research/ResearchHistoryList.tsx:263+; web/app/files/page.tsx:803-819; web/app/components/dataTables/DataTableGrid.tsx:878; web/app/components/PaginationControls.tsx; web/app/components/LoadMoreBar.tsx; web/app/components/templates/TemplatesGallery.tsx:218-226
    • Problem: The user's complaint is confirmed. The shared PaginationControls (client-side slice, page sizes only 10/50, no first/last, no jump, inline style={{minHeight:'36px'}} objects) is used by some panels, but others hand-roll their own: JobsList has Previous/Next where Next is never disabled and the label reads 'Showing N from #offset'; ResearchHistoryList and DataTableGrid use their own offset pagers; files/page.tsx renders its own ChevronLeft/Right buttons at 803-819 even though it already calls usePagination; TemplatesGallery has its own page buttons; LoadMoreBar is a fifth style. Lists with no pagination: ScheduledTasksPanel.tsx:378 (visibleTasks.map) and its per-task runs list, SchedulePanel.tsx:252/366/515 (schedules and runs), build/gallery/GalleryPanel.tsx:207 (projects), DirectoryBrowser.tsx:141 (entries, public).
    • Remaining now: Now on the shared PaginationControls (first/prev/next/last, jump, hasMore or totalItems, class-based buttons): JobsList.tsx:189, ResearchHistoryList.tsx:256, files/page.tsx:416, DataTableGrid.tsx (import at :49), TemplatesGallery.tsx:211, ScheduledTasksPanel.tsx:391, ScheduledTaskRuns.tsx:166, DirectoryBrowser.tsx:258. Remaining: files/page.tsx:400 still stacks a LoadMoreBar above the pager. build/gallery/GalleryPanel.tsx:207 (projects.map) and jobs/SchedulePanel.tsx:515 (runs.map) have no pagination. LeadsPanel.tsx:429-430 hand-rolls Prev/Next.
    • Fix: Create one components/ui/pagination (or extend PaginationControls) supporting both client slices and server offset/total, with page-size options 10/25/50/100, first/prev/next/last, 'x-y of N', aria-labels and no inline styles. Have the backend list endpoints return total so Next can disable correctly. Replace the hand-rolled pagers in the files above and add it to ScheduledTasksPanel, SchedulePanel runs, GalleryPanel and DirectoryBrowser.

    [major] /channels (WhatsApp panel) (PARTIAL, features)

    • Where: web/app/lib/api/channels.ts:393-397 (recordWhatsAppOptIn, no caller); backend core/omnichannel/src/main/java/com/agenticai/core/omnichannel/whatsapp/policy/WhatsAppPolicyController.java lines 37 (POST /opt-in), 45 (DELETE /opt-in), 63 (GET /tasks)
    • Problem: The guard blocks template sends unless the recipient has an active opt-in record, but the console has no way to create, list or revoke one. recordWhatsAppOptIn is defined in channels.ts and referenced nowhere else in web/app, there is no client function for DELETE /opt-in, and there is no endpoint to list records, so there is nothing to render. Only the task picker (WhatsAppBindingCard) and the quality badge (WhatsAppQualityCard) are wired. The result: a tenant that follows the UI gets template sends refused with no in-app way to satisfy the rule. Also GET /tasks is unused; the task list is hard-coded in components/channels/whatsappPolicy.ts WHATSAPP_TASKS and can drift from the WhatsAppTask enum.
    • Remaining now: Opt-in is fixed. web/app/components/channels/WhatsAppOptInCard.tsx exists and is mounted at TwilioRoutingCards.tsx:86. It uses recordWhatsAppOptIn and revokeWhatsAppOptIn (lib/api/channels.ts:393-400). It lists records, and a backend test WhatsAppPolicyControllerOptInListTest exists. Remaining: WHATSAPP_TASKS is still hard-coded in components/channels/whatsappPolicy.ts:3-10. whatsappPolicy.test.ts only checks the count of 6 and the labels, not equality with the core WhatsAppTask enum, and GET /tasks is unused. I did not check whether the opt-in list uses PaginationControls.
    • Fix: Add a WhatsAppOptInCard to components/channels/WhatsAppPolicyCards.tsx, mounted in TwilioRoutingCards under the binding card. It should have a form (phone, source select, evidence text), a revoke action (add a revokeWhatsAppOptIn client for DELETE) and, with a small new list endpoint if one does not exist, a paginated table of records with PaginationControls. Fetch the task options from GET /tasks, or add a test that keeps WHATSAPP_TASKS equal to the enum.

    [major] Agent playground Share panel and workflow Share modal (not a nav route) (OPEN, features)

    • Where: web/app/components/agents/ShareAgentPanel.tsx:150-160; web/app/components/workflow/ShareWorkflowPanel.tsx:139-143; backend ShareStatusResponse in core/agents-turn/src/main/java/com/agenticai/core/agents/share/AgentShareOwnerController.java:61-62
    • Problem: The owner sees only '{n} viewer(s) so far' and '{n} report(s)'. The backend revokes the link automatically on the third report (CHANGELOG [Unreleased], 'Share abuse reports...'), but the panel then shows an ordinary 'Not shared' state with no explanation that abuse reports killed it, because the status response for an inactive link is just inactive(). expiresAt is returned by the API and never shown. Report reasons (max 500 chars each, ShareReportRequest) are not exposed to the owner at all. There is no page listing all of a tenant's public share links with their viewer/report counts, so an admin cannot find a reported share without opening each agent. The strings are hard-coded English rather than useTranslations keys.
    • Remaining now: ShareAgentPanel.tsx and ShareWorkflowPanel.tsx still have no useTranslations, no revoked-after-reports notice, no expiresAt display (only type field at :29/:28), no report reasons; no shared-links admin page exists under web/app/admin (no shared-links dir, no references).
    • Fix: Show the expiry date, and a 'Revoked after N abuse reports' notice with a re-share confirmation when the status says so (add a revokedReason field to ShareStatusResponse). Add a Shared links admin page (for example /admin/shared-links) with a paginated table (agent/workflow, created, viewers, reports, expires, Revoke) backed by a list endpoint, and move the panel strings to web/messages/en.json.

    [major] none; expected under /admin (for example /admin/sso) (PARTIAL, features)

    • Where: core/admin/src/main/java/com/agenticai/core/admin/sso/TenantSsoController.java (/api/tenant/sso GET/PUT/DELETE, /scim-tokens GET/POST/DELETE, lines 60-124); web/app/lib/api-reference-data.ts:396 is the only web reference
    • Problem: Per-tenant SSO connection management and SCIM token minting have no console page. web/app has no caller for /api/tenant/sso and no entry in the AppShell nav. This is the identity admin surface that sits next to the Auth0 adapter and the claim mapping; the Auth0 adapter and claim mapping themselves are operator config, but a Keycloak tenant still has no UI to connect an IdP or mint SCIM tokens. Under IDENTITY_PROVIDER=auth0 this API 404s by design, so any new page must hide itself in that case (the UI currently has no way to learn which provider is active).
    • Remaining now: The nav row exists (navSections.ts:941 /admin/sso) and web/app/admin/sso/page.tsx exists with lib/api/sso.ts. I did not read the page to confirm it covers SCIM token minting or hides itself under IDENTITY_PROVIDER=auth0.
    • Fix: Add /admin/sso with an SSO connection form (GET/PUT/DELETE) and a SCIM tokens table (list, create with one-time reveal, revoke) and an AppShell nav entry with a MANAGE_TENANT_SETTINGS-style perm. Treat a 404 from GET /api/tenant/sso as 'not available with this identity provider' and show an EmptyState that links to docs/extending.md section 7 instead of an error toast.

    [major] /admin/api-keys, /admin/access, /admin/issues, /admin/channels, /admin/contacts, /admin/co (PARTIAL, layout)

    • Where: web/app/admin/api-keys/page.tsx:~377 (div.ax-kb-error, no retry); web/app/admin/access/page.tsx:67 (div.ax-kb-error); web/app/admin/channels/page.tsx:34-42 and web/app/admin/contacts/page.tsx:35-39 (catch -> empty list silently); web/app/admin/issues/page.tsx (loadError, no retry)
    • Problem: Load failures either render a bare red div with no action, or are swallowed (.catch(() => setAgents([])), so the operator sees an empty page that looks like 'no data'. Retry affordance exists only in ApprovalsPanel and InboxPanel (retry count 7/3).
    • Remaining now: Fixed: api-keys/page.tsx:239 uses ErrorRetry (from commerce/listKit) with a refresh retry. agent-trust/page.tsx:611 uses ErrorRetry. mcp-servers/page.tsx:792 uses LoadErrorRetry. admin/channels/page.tsx sets loadFailed and has a retry at :92. issues/page.tsx:261-267 has an error state with a retry label. commerce and payments contain retry. Still open: admin/access/page.tsx:63 is still a bare
      with no retry. admin/contacts/page.tsx:31 still does .catch(() => setAgents([])), swallowing the failure. There is no single shared ErrorState in page-kit; the pages use several different components.
    • Fix: Add a shared ErrorState (Alert + 'Try again' Button) to page-kit and use it in every page. Do not turn a failed fetch into an empty list; show the error.

    [major] /admin/api-keys, /admin/payments, /admin/commerce, /admin/access (PARTIAL, layout)

    • Where: web/app/admin/api-keys/page.tsx:393-452 (inline style padding/borderCollapse, var(--hud-line)); web/app/admin/payments/page.tsx:495-560; web/app/admin/commerce/page.tsx:214-236; web/app/components/AdminAccessMatrix.tsx:93 (inline sticky column, background fallback #fff)
    • Problem: Four hand-rolled tables use raw , per-cell inline padding, and three different styling approaches (inline CSS vars, Tailwind text-xs, inline px). Headers, hover, row density and sticky behaviour differ across pages. AdminAccessMatrix has a hard-coded #fff colour fallback and a sticky column that depends on an inline background.
    • Remaining now: api-keys (ApiKeysList.tsx), payments (TransactionsTable.tsx) and commerce (WalletLedger.tsx) now import components/ui/table and have no raw
    • . Remaining: web/app/components/AdminAccessMatrix.tsx:93 is still a raw
      with inline styles, and the sticky column keeps the hard-coded #fff fallback in var(--surface-card, #fff) at lines 96 and 141.
    • Fix: Replace with Table, TableHeader, TableRow, TableHead, TableCell from components/ui/table inside a responsive overflow wrapper. Remove the inline styles and the #fff fallback; use the --card token.
    • [major] /admin/approvals, /admin/audit, /admin/input-requests, /admin/inbox (panel), /admin/users (PARTIAL, layout)

      • Where: web/app/admin/approvals/page.tsx:22-24; web/app/admin/input-requests/page.tsx:19-21; web/app/admin/audit/page.tsx:22-26; web/app/admin/inbox/InboxPanel.tsx (PH:0); web/app/admin/users/page.tsx
      • Problem: These pages pass title/eyebrow to AppShell only and render the panel directly. The page-level heading, subtitle and actions slot depend on each panel, so title placement and spacing differ from pages such as leads or contacts that use PageHeader. The audit.tsx comment confirms the heading lives inside AuditLogPanel. approvals and input-requests also hard-code eyebrow="Administration".
      • Remaining now: Fixed: admin/inbox/page.tsx:49 now uses PageHeader. admin/users/page.tsx is only a redirect to /admin/access (not applicable). Open: admin/approvals/page.tsx:22 and admin/input-requests/page.tsx:19 still pass title with a hard-coded eyebrow='Administration' to AppShell and render the panel directly, with no PageHeader or Section. admin/audit/page.tsx:22 still passes title/eyebrow to AppShell and renders AuditLogPanel directly, with a comment that the heading lives in the panel. The eyebrow there is translated.
      • Fix: Give each page a PageHeader with a translated eyebrow/title/subtitle and move panel headings into it. Wrap the body in the page-kit Section so spacing and max-width match the console-preview pages.

      [major] /admin/leads (PARTIAL, layout) (done)

      • Where: web/app/components/leads/LeadsPanel.tsx (446 lines; no Pagination import); web/app/admin/leads/page.tsx
      • Problem: The leads CRM table and pipeline board render all leads. Only the CSV import error list is sliced (slice(0,10) at line 313). Large lead sets will be slow and hard to scan.
      • Remaining now: components/leads/LeadsPanel.tsx now pages the table view on the server: TABLE_PAGE_SIZE=25 (:38), page state (:68), api.listLeads({page,size}) (:105-108), total and pageCount (:196). The board is capped at BOARD_LIMIT=200 (:37). The pager is two hand-rolled Prev/Next buttons (:429-430), not the shared PaginationControls, and has no page-size choice.
      • Fix: Add server-side or client paging for the table view with PaginationControls (page size 25/50/100), keep the board view virtualised or column-capped.

      [major] /agents/quality, /agents/learning, /agents/import, /agents/templates, /workflows/import, / (PARTIAL, layout)

      • Where: web/app/components/AppShell.tsx (NAV_SECTIONS ~240-1140); web/app/agents/page.tsx:91-111; web/app/workflows/page.tsx:55-78; web/app/tools/page.tsx:206-222
      • Problem: None of these routes has a rail or command-palette entry (no matching href in NAV_SECTIONS). They are discoverable only through outline buttons crammed into PageHeader actions, so Agent Quality, Learning, Import, Workflow Templates, Enabled servers, Credentials, Code tools and Quick Task history are effectively hidden. tools/page.tsx stacks Search, Enabled servers, Coach, Code tools, OpenAPI import and Add server into one action row (about 6 controls), which wraps unpredictably. /agents/templates is linked only from the dashboard and the workbench.
      • Remaining now: navSections.ts:585 now has a /agents/quality rail row. CommandPalette.tsx:15-21 PALETTE_EXTRAS registers quick-task/history, workflows/import, workflows/templates, agents/learning, agents/import and agents/templates. Remaining: no palette or rail entry for tools/servers, tools/credentials, tools/code or templates/mine. There is no sub-nav or tab strip. app/tools/page.tsx:206-235 still stacks Search, Enabled servers, Coach, Code tools and OpenAPI import buttons into the PageHeader actions, with no overflow DropdownMenu.
      • Fix: Add a child/sub-nav level (rail flyout children or a shared section Tabs strip under PageHeader) listing every sub-route with an active state, and register them in CommandPalette. Limit PageHeader actions to one primary plus one overflow DropdownMenu. Add a 'Quick Task > History' tab.

      [major] /build (PARTIAL, layout)

      • Where: web/app/build/page.tsx:348,362,384,407; web/app/build/studio/StudioSurface.tsx (3,350 lines); web/app/build/CreateSurface.tsx (1,113)
      • Problem: /build passes title= to AppShell with wide and never renders PageHeader or Section, unlike every other console route. It is 2,289 lines (plus a 3,350-line StudioSurface), far over the 1000-line limit, mixes 5+ states in one render, uses a grid of 3 surface tabs in a card (lines 183-211), a centered max-w-3xl empty state at 567 and inline style={{...}} objects for state pills (475-481, 1374, 1431, 1985, 2051-2080). The project list is only reachable through a Select dropdown (449) and a gallery grid with no pagination.
      • Remaining now: Fixed: build/page.tsx is now 824 lines (was 2,289). It uses PageHeader (:33,:124). ProjectsList.tsx has search (Input) and usePagination(rows,10) (:43) with PaginationControls. Remaining: StudioSurface.tsx is still 2,264 lines and CreateSurface.tsx 1,113, both over the 1000-line limit. page.tsx has no Section component. Inline style var-injection for state pills is still at page.tsx:400 and :406.
      • Fix: Add PageHeader + Section structure, split page.tsx and StudioSurface into feature components, give the project list a real grid/table with search + pagination, and replace inline style var-injection with className-based state-pill variants.

      [major] /capabilities/decision, /code-intel, /coding, /edge, /learn, /changelog (PARTIAL, layout)

      • Where: web/app/components/AppShell.tsx
      • Problem: Each of these has only one nav reference (edge/coding two). /changelog and /compare have none in the console shell, so shipped features and release notes are not discoverable from inside the app. /jev and /devices are redirect-only legacy URLs and /capabilities redirects to /capabilities/decision, so nothing links to the real destinations by a human label like 'Decision console'.
      • Remaining now: navSections.ts now references /coding (:243), /capabilities/decision (:463), /code-intel (:473), /edge (:489), /learn (:767). Still missing: no '/changelog' or '/compare' entry in navSections.ts or other console nav components (changelog only appears in the public oss-landing chrome, ossSite.ts:87 and OssChrome.tsx:52), so no 'What's new' in the console.
      • Fix: Audit the nav tree against ls web/app (82 dirs); add 'What's new' (changelog) to Help/Plans group, and make sure the redirect targets (/admin/channels?channel=mobile-sdk, /capabilities/decision) are named in nav. Consider a command-palette/search entry for every route.

      [major] /code-intel (PARTIAL, layout)

      • Where: web/app/code-intel/page.tsx (1,402 lines; AppShell at 174; buttons at 537)
      • Problem: No PageHeader and no Section; tabs, repo cards, search hits, impact, sandbox, agent-loop and RAG panels are all in one 1.4k-line untyped file (useState(null) everywhere). Raw
      • Remaining now: code-intel/page.tsx now uses Tabs (lines 49 and 196), and SearchTab.tsx and lib.ts were extracted, with useTranslations in the sub-components. Still open: the file is 1354 lines (baselined at 1402). There is no PageHeader or Section. The repo selector is still a raw
      • Fix: Split per tab into components, use PageHeader + Section/Card/Tabs from ui, replace raw buttons with components/ui/button (or role=radio list semantics), move strings to i18n, and add types.

      [major] /edge, /coding, /capabilities/decision, /help, /code-intel (PARTIAL, layout)

      • Where: web/app/edge/page.tsx, coding/page.tsx, capabilities/decision/page.tsx, code-intel/page.tsx
      • Problem: Zero Skeleton and zero EmptyState usage in these files (code-intel uses inline

        'noReposText'); errors are plain text without a Retry button. Only insights, usage and roi have skeleton + EmptyState + error alert.

      • Remaining now: edge/page.tsx and coding/page.tsx delegate to EdgeConsole and CodingTasksPanel, which have some Skeleton/EmptyState/Retry usage (CodingTasksPanel 6 matches, EdgeConsole 2). capabilities/decision/page.tsx has zero Skeleton/EmptyState/Retry (plain text at :373 'policyLoading'). code-intel/page.tsx:522 still uses an inline

        noReposText, and has no Skeleton or EmptyState. It has a Retry button only for the policy error (:1177). help/page.tsx has none. No shared AsyncBoundary helper.

      • Fix: Standard trio: Skeleton rows while loading, EmptyState (icon, title, hint, CTA) when empty, Alert with Retry on failure; share a small helper.

      [major] /files (PARTIAL, layout)

      • Where: web/app/files/page.tsx (useTranslations count: 0; 851 lines; 'Test & Monitor' eyebrow literal at 482; pager 803-819; raw <input> at 653,685)
      • Problem: Whole page is raw English (headers, 'Loading files…', dialog copy). It uses components/ui/table correctly but then builds its own prev/next buttons instead of PaginationControls (page size fixed at PAGE_SIZE=20, no size choice), has raw selection controls, and mixes Select, LoadMoreBar and usePagination on one screen. At 851 lines it is close to the file-size ratchet.
      • Remaining now: Fixed: the page is now 436 lines and split into FilesTable, FilePreviewDialog and FilesAlerts. It uses useTranslations('filesPage') (files/page.tsx:49) and an i18n eyebrow (line 301). It uses the shared PaginationControls (line 416). Still open: FilesTable.tsx:38 and :68 still use raw , with a comment saying no Checkbox primitive exists. page.tsx still mixes usePagination (line 221) and LoadMoreBar (lines 16 and 400).
      • Fix: Extract FilesTable, FilesToolbar and PreviewDialog components, adopt the shared pagination control and Checkbox primitive, and move strings to a files namespace.

      [major] /files, /data-tables, /scheduled, /agents, /knowledge, /research, /studio, /tools, /jobs (PARTIAL, layout)

      • Where: web/app/files/page.tsx:563-565; web/app/components/dataTables/DataTablesPanel.tsx:156; web/app/components/scheduledTasks/ScheduledTasksPanel.tsx:244; web/app/components/jobs/JobsList.tsx (no loading UI beyond refresh icon); web/app/studio/page.tsx:200-205; web/app/jobs/page.tsx:70-76 and agents/page.tsx:143-147 (Suspense fallback={null})
      • Problem: Only skills/page.tsx, shared-with-me/page.tsx and workflows/[id]/runs/page.tsx use the Skeleton primitive. files shows ' Loading files…' (raw English), DataTablesPanel and ScheduledTasksPanel show a bare

        Loading

        , JobsList renders an empty Card until data arrives, and studio/page.tsx hand-rolls 'animate-pulse bg-muted/40' divs instead of Skeleton. Jobs and Agents use Suspense fallback={null}, so the page body is blank during hydration. This makes list pages look broken on slow networks.
      • Remaining now: Fixed: files/page.tsx:320, DataTablesPanel.tsx:166, ScheduledTasksPanel.tsx:239 and JobsList.tsx:136 use ListSkeleton. agents/page.tsx:199 has a Skeleton Suspense fallback. Remaining: studio/page.tsx:202-204 still hand-rolls animate-pulse divs, and jobs/page.tsx:77 still has Suspense fallback={null}. I did not check knowledge, research or tools.
      • Fix: Standardise a ListSkeleton/TableSkeleton in components/ax/page-kit built on components/ui/skeleton and use it for every list/table first load; give Suspense fallbacks a PageHeader-sized skeleton; replace the hand-rolled animate-pulse blocks.

      [major] /help (OPEN, layout) (done)

      • Where: web/app/help/page.tsx:39-60 (card bodies), 162, 264-267; help/docs-content.tsx
      • Problem: Page chrome uses t() but the explainer cards ('It uses real tools', 'It asks when it matters'), the 'How VegaDuta helps you learn' block and docs-content.tsx bodies are hard-coded English, and brand name is written with HTML entity VegaDūta repeatedly.
      • Remaining now: help/page.tsx:162 and :264 still use VegaDūta HTML entities and hard-coded English card/learn-block copy (:264-267); no BrandName component used (grep BrandName empty); only page chrome uses useTranslations("helpPage") (:68).
      • Fixed (f2-gaps-workflows): page.tsx copy now lives in the helpPrimer i18n namespace and the brand renders through BrandName (help/BrandText.tsx). Still English: the shared docs-content.tsx topic bodies (also feed the public /docs page).
      • Fix: Move copy to i18n, use the BrandName component (used by changelog) instead of entity strings.

      [major] /jobs (PARTIAL, layout)

      • Where: web/app/jobs/page.tsx:45-62; web/app/components/jobs/JobLauncher.tsx; web/app/components/jobs/JobsList.tsx:19-25,93,169-176; web/app/components/jobs/AmbientDigestCard.tsx
      • Problem: No useTranslations anywhere in jobs/page.tsx, JobsList (filters 'In flight', 'Succeeded', 'Previous', 'Next', 'Showing N from #') or JobLauncher; eyebrow is the literal 'Build' although the nav puts Jobs under Monitor. The runs tab stacks AmbientDigestCard, JobLauncher and the list vertically with no Section headings, filter chips are a row of Buttons (aria-pressed) instead of Tabs/ToggleGroup, and Next is always enabled so users page into empty results.
      • Remaining now: jobs/page.tsx, JobsList and JobLauncher now use useTranslations. The eyebrow is t('eyebrow') = 'Test & Monitor' (en.json:13150). JobsList.tsx:192 uses PaginationControls, with real totalItems when the server sends one. Remaining: the filters are still Buttons with aria-pressed (JobsList.tsx:100-112), not a segmented control. There are no Section headings in the jobs components. When there is no X-Total-Count, hasMore is still rows.length > 0, so Next can lead to empty pages.
      • Fix: Wrap launcher and list in page-kit Section with headings, convert filters to a segmented control, move strings to i18n, correct the eyebrow, and return a total from listTenantAgentJobs so Next can disable.

      [major] /jobs, /research, /agents, /workflows, /providers, /secrets, /memory, /quick-task (PARTIAL, layout)

      • Where: web/app/components/jobs/JobsList.tsx:98-105; web/app/agents/page.tsx:53-58; web/app/components/jobs/SchedulePanel.tsx; web/app/studio/page.tsx; web/app/providers/page.tsx; web/app/secrets/page.tsx
      • Problem: Error UI is a mix of hand-rolled red divs (JobsList.tsx:99-104 uses 'border-destructive/30 bg-destructive/10' with no retry button), swallowed catches (agents/page.tsx:53-58 'no separate banner needed'; jobs/page.tsx:35-39 ignores listAgents failure) and proper Alert + Try again (skills, knowledge, tools, data-tables). An empty list after a failed load is indistinguishable from 'no data'.
      • Remaining now: There is still no shared ErrorState in components/ax/page-kit.tsx; its exports are Section, EmptyState and StatCard. Fixed: JobsList.tsx:134 uses ListError with onRetry from tenantLists/ListStates. agents/page.tsx:70 sets setLoadError(t('loadError')) when listAgents fails, though I did not find a retry control on it. The following showed no retry or list-error handling in a grep: components/jobs/SchedulePanel.tsx, studio/page.tsx, providers/page.tsx, secrets/page.tsx, jobs/page.tsx. The treatment is still inconsistent across pages.
      • Fix: Add an ErrorState (Alert variant=destructive + Retry button) to page-kit and use it everywhere a list fetch can fail; never fall through to EmptyState when the request failed.

      [major] /jobs, /scheduled, /agents, /build, /tools, /memory (OPEN, layout)

      • Where: web/app/components/jobs/JobLauncher.tsx:68; web/app/components/jobs/SchedulePanel.tsx:246,346,359,379,403; web/app/components/scheduledTasks/ScheduledTaskForm.tsx:241,289,318,336,421; web/app/components/agents/AgentWorkbench.tsx (17 raw <select>, 56 raw <button> across components/agents); web/app/build/gallery/GalleryPanel.tsx:208; web/app/build/page.tsx (48 raw <button>, 9 raw <select>); web/app/components/studio/* (50 raw <button>)
      • Problem: The repo has a custom components/Select.tsx (its own header comment says the native OS picker was a user complaint) and components/ui/button + input, but these forms still use native bypass focus rings, sizes and disabled styles. There is no components/ui/select primitive, so there is no single standard to point developers at.
      • Remaining now: web/app/components/ui/ has no select.tsx (accordion, alert, badge, button, card, input, label, popover, ... only). 13 native in the listed files; convert raw

      [major] /knowledge (PARTIAL, layout)

      • Where: web/app/knowledge/page.tsx:162
      • Problem: grid-cols-1 lg:grid-cols-[240px_minmax(0,1fr)_340px] jumps straight from a stacked layout to three columns at 1024px. With the 64-240px rail and 240+340px fixed columns, the middle documents panel is squeezed to a few hundred pixels on common laptop widths; there is no md/xl step. The header 'Upload files' and 'New collection' buttons work through imperative refs (collectionsRef/knowledgeRef.openUpload), which silently no-op if the child has not mounted. Header subtitle at lines 106-123 builds plurals by concatenating a 'plural' param (not i18n-safe) and mixes raw English ('Upload the documents your agents can search...', 'Collection count unavailable.'); alerts at 143-170 are raw English.
      • Remaining now: Layout fixed: knowledge/page.tsx:158 is grid-cols-1, then md:grid-cols-[220px_minmax(0,1fr)], then xl:grid-cols-[240px_minmax(0,1fr)_340px], with the third panel spanning below md. The raw English strings moved to the consolePolish namespace (tp(...) at lines 100-143). Remaining: the header actions still use imperative refs (collectionsRef.current?.openCreateForm() at 119, knowledgeRef.current?.openUpload() at 122, refs declared at 45-46). Plurals are still built by concatenation, with t('collectionCountLabel', {plural: ... ? '' : 's'}) at lines 106 and 111.
      • Fix: Use lg:grid-cols-[220px_minmax(0,1fr)] with the detail column moving below or into a Sheet until xl:, e.g. xl:grid-cols-[240px_minmax(0,1fr)_340px]. Replace ref-based header actions with lifted state or props. Use ICU plural messages instead of string concatenation and move the raw strings to messages/en.json.

      [major] /learn (PARTIAL, layout) (done)

      • Obsolete (0.1.8): web/app/learn/page.tsx and learn/CurriculumMap.tsx were removed when the Academy moved out of the console (CHANGELOG [0.1.8]); only /learn/tutor remains, and its components use useTranslations('tutorPage').
      • Where: web/app/learn/page.tsx:68,107-109,142-175,242,254-270,357 (and learn/CurriculumMap.tsx, learn/tutor/*)
      • Problem: The file never calls useTranslations; eyebrow 'Plans & Help', title 'Learn', subtitle, stat labels ('Lessons in the Academy', 'Lessons completed'), section titles, CTAs ('Open the Journey Map', 'Browse concepts'), empty state ('No screen mappings yet') are literal English in a console that is otherwise localized.
      • Remaining now: The tutor sub-components are localized: learn/tutor/PlanView, LessonView, TutorChat and ReviewSession all call useTranslations('tutorPage'). learn/page.tsx is not. It has no useTranslations call and still has literal English at line 107 (eyebrow 'Plans & Help'), lines 109-110 (title and subtitle), line 180 (title 'No screen mappings yet'), and lines 262 and 270 (cta 'Open the Journey Map' and 'Browse concepts'). CurriculumMap.tsx has no useTranslations call either.
      • Fix: Move all strings to messages/en.json under a 'learn' namespace and use useTranslations; add to i18n-route-namespaces.generated.ts via the generator; do the same for the tutor sub-components.

      [major] /roi (agents table, unpriced models list, price list) (PARTIAL, layout)

      • Where: web/app/roi/page.tsx:280-335, 400-430
      • Problem: table.agents.map, prices.unpricedModels.map and prices.prices.map render every row with no usePagination/PaginationControls; a tenant with 100+ agents or a long price history gets an endless table and list.
      • Remaining now: The agents table is paged: roi/AgentTable.tsx:56,130 (usePagination + PaginationControls). The price list is paged: page.tsx:296 usePricePaging, :320 pageItems, :335 PricesPager (RoiFamily.tsx:35-41). The unpricedModels list (page.tsx:305) still maps every row with no pager. No server limit/offset in lib/api/roi.
      • Fix: Wrap each collection with usePagination() and render components/PaginationControls under it (as code-intel and edge already do); consider server-side limit/offset in lib/api/roi.ts.

      [major] /scheduled, /jobs (Schedules tab) (PARTIAL, layout)

      • Where: web/app/components/scheduledTasks/ScheduledTasksPanel.tsx:355-380 (filter + <ul> of full-width cards, 556 lines); web/app/components/scheduledTasks/ScheduledTaskRuns.tsx (raw <table>); web/app/components/jobs/SchedulePanel.tsx:515
      • Problem: Each task is a large card in a flex-col list with filter pills (all/active/failing/paused) but no search, no sort, no pagination; run history opens inline inside the card, and ScheduledTaskRuns.tsx uses a raw
      rather than components/ui/table. Two separate schedule UIs exist (/scheduled and the Jobs 'Schedules' tab) with different layouts.
    • Remaining now: Fixed: tasks render in ui/table (ScheduledTasksTable.tsx:11), with search (ScheduledTasksPanel.tsx:76,335), sort and pagination. ScheduledTaskRuns.tsx:19 uses ui/table and is server-paged. Remaining: run history still opens inline in an expanded row (ScheduledTasksTable.tsx:191,215), not a Sheet or Dialog. The Jobs 'Schedules' tab (jobs/SchedulePanel.tsx) is still a separate UI with its own layout, not merged or linked.
    • Fix: Render tasks as a Table (name, schedule, next run, status, actions) with search + pagination; open run history in a Sheet/Dialog using ui/table; link or merge the Jobs 'Schedules' tab with /scheduled.
    • [major] /templates, /templates/mine, /directory, /plugins (PARTIAL, layout)

      • Where: web/app/templates/page.tsx:35-58; web/app/templates/mine/page.tsx:21-45; web/app/directory/page.tsx:32-70; web/app/plugins/page.tsx:34-67
      • Problem: These four rail destinations (AppShell.tsx nav hrefs /templates ~428, /directory ~300, /plugins ~962) are server components wrapped in TemplatesPublicChrome, not AppShell. A signed-in user clicking them loses the sidebar, topbar, theme and session context and lands in a marketing layout. Each hand-writes its own header (
        with uppercase eyebrow, h1 text-4xl, max-w-2xl) instead of PageHeader, so spacing and type scale differ from every other page. All copy is raw English (useTranslations absent). /templates/mine is the authed corner but renders in public chrome and relies on the component to prompt sign-in.
      • Remaining now: components/library/LibraryChrome.tsx:15-17 and L92 now swap to AppShell once a Keycloak session exists, and public SSR is kept for crawlers. Remaining: the pages still hand-write their header (templates/page.tsx:36, mine/page.tsx:23, directory/page.tsx:41, plugins/page.tsx:47) instead of PageHeader/Section. templates/page.tsx:40 still has raw English 'Community templates', so the strings are not in i18n. plugins/page.tsx:34 still uses TemplatesPublicChrome rather than LibraryChrome.
      • Fix: Keep the public SSR routes for crawlers but add a console variant: when a session exists render inside AppShell + PageHeader + Section (e.g. /templates and /directory under the console layout, or a client wrapper that switches chrome). Reuse PageHeader instead of the hand-rolled header block; move strings to i18n. Make /templates/mine a normal authenticated console route.

      [major] /admin/issues (PARTIAL, pagination)

      • Where: web/app/admin/issues/page.tsx:123-127,401 (feed: web/app/lib/api/governance.ts:120 listIssues; backend core/audit/.../PlatformIssueRepository.java:12-23, PlatformIssueService.java:108)
      • Problem: Page uses PaginationControls, but the endpoint is findTop200ByStatusOrderByLastSeenAtDesc: anything beyond the 200 most recently seen issues is invisible, the pager reports 'of 200' as if complete, and the OPEN count badge (line 178, issues.filter(OPEN).length) is wrong once the list is capped. No search box (only status pills), no sort, no error+retry for failed acknowledge/resolve beyond load error, loading is plain text not a skeleton.
      • Remaining now: Fixed: PlatformIssueController.java:44-57 now has limit/offset with the legacy newest-200 behaviour only when neither param is sent. admin/issues/page.tsx uses usePagedList (L125-129, L16), PaginationControls (L411-418), a search input (L244), Skeleton rows (L258), and an OPEN badge from the server total (L189). Remaining: web/app/lib/api/governance.ts:140 listIssues still only passes status. I did not see where pagedLoad.ts passes limit/offset, so this is unconfirmed. The audit's separate counts endpoint was not added; the OPEN badge uses the paged total with an 'atCap' plus sign. No sort control.
      • Fix: Add limit/offset (and X-Total-Count) to GET /api/admin/issues, plus a lightweight GET /api/admin/issues/counts?status= for the badge. Switch to server paging via PaginationControls (page, totalItems from header). Add a q search param and a search input next to the .ax-filter-pills.

      [major] /admin/leads (OPEN, pagination) (done)

      • Where: web/app/components/leads/LeadsPanel.tsx:37,107,335-368 (BOARD_LIMIT=200; backend core/omnichannel/.../crm/LeadController.java:37 MAX_PAGE_SIZE=200)
      • Problem: Default board view fetches page 0, size 200 once. Columns show the true per-stage count from /api/leads/pipeline (line ~353), but the cards inside a column are only those within the first 200 leads overall, so a column may say '340 leads' and render 12 cards with no indication and no way to load more (no per-column 'load more', no pager in board view). Leads in later stages can be missing entirely. Drag-and-drop target counts then disagree with visible cards.
      • Remaining now: LeadsPanel.tsx:37 BOARD_LIMIT=200 and the board fetch at line 107 are unchanged. Column headers show the true stage count (line 359). There is no per-column load-more and no truncation banner, so the card count can still disagree with the header.
      • Fix: Load per stage: call api.listLeads({stage, page, size:25}) per column and add a 'Show more (N remaining)' button per column driven by stageTotal.count; or show a banner 'Showing 200 of {allCount}; switch to table view or filter' when allCount > leads.length.

      [major] /admin/payments (PARTIAL, pagination)

      • Where: web/app/admin/payments/page.tsx:362-364,492-507 (feed web/app/lib/api/payments.ts listPaymentTransactions; backend core/payments/.../PaymentController.java:124-131)
      • Problem: Transactions table (the money ledger) loads api.listPaymentTransactions(id) with no limit, so the API default of 50 applies (max 200). Rows beyond the newest 50 can never be seen, there is no pager, no 'showing newest 50' notice, no total count, no date or status filter, no search. Mandates list (line 440) is unpaged and unbounded in DOM (api.listMandates has no cap or paging) and each active mandate triggers an extra getMandateSpend call (N+1 at lines 370-376).
      • Remaining now: Fixed: transactions are server-paged by usePagedList (page.tsx:345-348) with limit/offset and X-Total-Count (PaymentController.java:139-151). TransactionsTable.tsx:195-212 uses PaginationControls with total and a capped notice, plus a status filter at lines 52-75. Mandate spend is batched via /api/payments/mandates/spend (paymentsData.ts:7-17), so the N+1 is gone. Remaining: the status filter narrows only the current page in server mode (TransactionsTable.tsx:59). There is no date filter or search. The mandates list is unpaged (page.tsx has only a status filter at 351-354 and no usePagination or PaginationControls for mandates), and listPaymentTransactions in payments.ts:151 still has no offset.
      • Fix: Add offset to /api/payments/transactions and return X-Total-Count; render the table with PaginationControls (itemLabel='transactions', page size 10/50) and a status/type filter. Show 'Showing newest N of TOTAL' when truncated. Page mandates with usePagination(mandates) and batch the spend lookups into one endpoint (/api/payments/mandates/spend?agentId=).

      [major] /agents (PARTIAL, pagination)

      • Where: web/app/components/agents/AgentWorkbench.tsx lines ~2085-2130 (agents.map in the switcher ul); core/agents/src/main/java/com/agenticai/core/agents/core/AgentController.java line 147-150
      • Problem: GET /api/agents returns the full tenant list (agentService.listAll(), no limit/offset/total) and the page renders every agent in one switcher list with no search, no pagination, and no sort. Only a plan counter ('agentsUsedCount') exists. On an Unlimited plan (agentLimit shown as infinity) the list grows unbounded in the DOM and users must scroll to find an agent. By contrast the same file already implements search + 8-per-page paging for Knowledge collections (lines 1987-2001) so the pattern exists but not for agents.
      • Remaining now: The /agents landing is now components/agents/AgentsIndex.tsx, with a search Input (line 150), a provider filter and usePagination(filtered, 10) (line 71) with PaginationControls (line 240). The in-workbench switcher is unchanged: AgentWorkbench.tsx:2067 still does an unpaged, unsearchable agents.map in the switcher ul. GET /api/agents still has no limit/offset.
      • Fix: In AgentWorkbench add a search Input above the switcher list (filter agents by name/category client-side) and wrap the list in usePagination(filteredAgents) with PaginationControls (itemLabel 'agents'), or make the switcher a virtualized/searchable combobox. If tenants can exceed a few hundred agents, add limit/offset + X-Total-Count to AgentController.list and consume with pagedQuery/toPaged.

      [major] /code-intel (PARTIAL, pagination)

      • Where: web/app/code-intel/page.tsx
      • Problem: (a) The Search tab (lines 642-690) hard-codes codeIntel.search(repoId, q, 8) (line ~650). It returns at most 8 hits, shows no 'top 8' notice, no count, no pager, and no 'no results' state after a search (hits.map at 673 simply renders nothing). (b) The audit trail loads codeIntel.audit(60) (line 1150; lib.ts line 172 default 100; services/code-intel/app/audit.py tail() reads the last N lines). The pager then reads '1-10 of 60' as if 60 were the whole history. The cap is silent. (c) The approvals and audit loads use .catch(() => setApprovals([])) / setTrail([]) (lines 1149-1150), so a failed load looks identical to 'no activity yet'. (d) The file is 1402 lines, over the 1000-line ratchet unless baselined.
      • Remaining now: Search tab extracted to code-intel/SearchTab.tsx with top-k picker (:34,66-69), count / capped message (:85) and paging. Remaining: page.tsx:1101-1102 still swallow load errors with .catch(() => setApprovals([]))/setTrail([]) (no error state/Retry), audit(60) cap not labelled, and page.tsx is still 1354 lines (>1000).
      • Fix: Search: add a result count line ('Showing 8 of N, refine your query') and an EmptyState when q was run and hits is empty. Let the user pick top_k (8/20/50) or add a Load more that re-queries with a larger top_k, and wrap hits in usePagination + PaginationControls like the Ask tab does at lines 1062/1111. Audit: add a visible 'latest 60 events' label, or add offset/limit to the sidecar /audit and use server paging. Replace the swallowing catches with error state plus a Retry button (reuse t('retryButton') as at line 1225). Split the 1400-line page into per-tab files.

      [major] /coding (PARTIAL, pagination)

      • Where: web/app/components/codingTasks/CodingTasksPanel.tsx
      • Problem: The list (lines 187-208) is fed by GET /api/coding-tasks. CodingTaskController.list (lines 96-99) takes no params. CodingTaskService.list (line 390-ish) hard-codes store.recent(tenantId, requester, 20), so only the newest 20 tasks per user are ever returned. This is a silent server cap. The UI shows no 'latest 20' notice, no pager and no total. Older tasks, including ones awaiting diff approval, are unreachable. There is also no status filter or search. The page renders the list as an unbounded ul.
      • Remaining now: UI added: CodingTasksPanel.tsx:108-119 live/done status filter, usePagination, and PaginationControls at :254. Remaining: the silent server cap is unchanged. CodingTaskController.java list() (~line 96) takes no params, and CodingTaskService.java:396 still calls store.recent(tenantId, requester, 20). web/app/lib/api/codingTasks.ts listCodingTasks() has no limit/offset. There is no 'latest 20' hint, so the pager only pages those 20.
      • Fix: Add limit (default 20, max 200) and offset to CodingTaskController.list. Add an X-Total-Count header, or reuse the X-Has-More pattern from docs/api/LIST-ENDPOINT-PAGING.md. Pass them from lib/api/codingTasks.ts line 18 (listCodingTasks({limit, offset})). In the panel, keep the 18rem master list but add PaginationControls under it (page, totalPages, totalItems, pageSize, onPageChange, onPageSizeChange, itemLabel=t('tasks')). A status filter (running / needs review / done / failed) above the list is a good addition. At minimum, render a 'Showing the 20 most recent tasks' hint until server paging lands.

      [major] /directory (PARTIAL, pagination)

      • Where: web/app/components/directory/DirectoryBrowser.tsx line 40; web/app/components/directory/serverFetch.ts line 38; core/agents-interop/src/main/java/com/agenticai/core/agents/directory/AgentDirectoryController.java lines 101-107, AgentDirectoryService MAX_LIST_LIMIT=100 (line 40)
      • Problem: Public directory fetches ?limit=100 (server clamps to MAX_LIST_LIMIT=100) and the response carries only {entries,count}, no total and no offset parameter, so once more than 100 agents are published the rest are unreachable except by guessing a search term. There is no pagination, no 'showing 100 of N' notice, and no sort/category filter. Public route (TemplatesPublicChrome), not an AppShell console page, but it is the discovery surface for the feature.
      • Remaining now: Paging is fixed. AgentDirectoryController.java:102-128 accepts an offset and returns total, offset and hasMore, plus X-Total-Count and X-Has-More headers. DirectoryBrowser.tsx:60 sends &offset=, and :258 renders the shared PaginationControls with a total or hasMore. Not done: the gap's sort and category/capability filters are absent. DirectoryBrowser.tsx:176 only displays each entry's capabilities, with no filter control.
      • Fix: Add an offset param and a total field to AgentDirectoryController.list and AgentDirectoryService.listPublished (repository.listPublished with PageRequest.of(offset/limit, limit) plus a count query); in DirectoryBrowser/serverFetch render PaginationControls (or Load more) and a 'Showing X of N published agents' line. Add category/capability filters.

      [major] /files (PARTIAL, pagination)

      • Where: web/app/files/page.tsx lines 322-352 (refreshArtifacts), 381 (usePagination), ~790-830 (inline pager); web/app/lib/api/media.ts lines 13-16
      • Problem: Generated documents are fetched with listGeneratedDocuments({page:0,size:200}) and only content is kept, so anything past 200 documents silently disappears from the table with no notice. Images (/api/imagegen/images), ElevenLabs audio, Sarvam audio and video jobs (/api/videogen/jobs) are fetched in full with no limit param, so payload and client memory grow unbounded (the client then slices them in memory). The pager is a hand-rolled inline Prev/Next with a fixed PAGE_SIZE=20 and no page-size control, unlike every other list; the failure of any one of the five sources only produces a partial-failure count.
      • Remaining now: Fixed: files/page.tsx:416-424 uses the shared PaginationControls with onPageSizeChange. Documents are fetched paged (:174) and a 'documentsCapped' notice shows 'N of total' (FilesAlerts.tsx:79-82). LoadMoreBar (:400) is wired for documents. Remaining: images, ElevenLabs, Sarvam and video are still fetched unparameterised in lib/api/media.ts:13-16, with no limit or X-Total-Count. They hit a 200-item cap flagged by ARTIFACT_LIST_CAP (fileRows.ts:28) and the page can't fetch past it. The documents notice also doesn't fetch the next page.
      • Fix: Replace the inline pager (lines ~790-830) with PaginationControls (itemLabel 'files', onPageSizeChange=setPageSize from usePagination). For documents, read the response's totalElements/last flag and when content.length < totalElements show a LoadMoreBar/notice 'Showing 200 of N documents' that fetches the next {page,size} (the documentgen controller is already paged). Add limit/offset + X-Total-Count to the imagegen, elevenlabs/audio, sarvam/audio and videogen/jobs list endpoints (core/imagegen, core/elevenlabs ElevenLabsGeneratedAudioController.list line 37, core/sarvam SarvamGeneratedAudioController.list line 51, videogen controller) and use toPaged in lib/api/media.ts.

      [major] /platform-admin/audit (PARTIAL, pagination)

      • Where: web/app/platform-admin/audit/page.tsx
      • Problem: searchAudit takes only limit (default 200, max 1000) with a 'Max N rows' dropdown (lines 98-106). There is no page or offset: the user can never get past the newest N rows, and up to 1000 table rows are dumped into the DOM (result.rows.map, line 156). Truncation is disclosed (line 131) and CSV export exists, which is good. But there is no pager, no client sort and no row-level quick filter. Separately, (line 202) re-sweeps all tenants every 10s and shows only the merged newest 200 (see the sections/observability.tsx entry).
      • Remaining now: audit/page.tsx:40 usePagination(result.rows, 25) and PaginationControls at :189, so DOM holds 25 rows; truncation banner kept at :136. Still no server offset/cursor param (limit-only at :35,:49), so rows beyond the newest N remain unreachable; no sort or quick filter.
      • Fix: Add an offset/cursor (or page) param to PlatformAuditSearchController (/api/platform/audit/search) and PlatformAuditSearchService (keep per-tenant PageRequest). Use a fixed page size of 50 with page state in audit/page.tsx and render , using the response's total or truncated. Alternatively, as a smaller fix, wrap result.rows in usePagination(result.rows) + so the DOM never holds more than 10-50 rows, and keep the truncation banner.

      [major] /platform-admin/audit, /platform-admin/diagnostics, /platform-admin/spend (PARTIAL, pagination)

      • Where: web/app/platform-admin/components/sections/observability.tsx
      • Problem: IssuesSection (line 84, rows at 107), ActivitySection (line 356, rows at 377) and LlmUsageSection (line 143, rows at 163) each render the whole array returned by PlatformController /issues, /activity and /llm-usage, polled every 10s (pollMs={10000} on audit/page.tsx:202, diagnostics/page.tsx:39-41, spend/page.tsx:426). /issues and /activity are silently capped on the server (PER_TENANT_ROW_CAP 50 per tenant, MERGED_ROW_CAP 200 merged, PlatformObservabilityService lines 73-74) with no way to see older entries and no indication on screen that the list is cut (only 'merged and capped' in the activity subtitle). /llm-usage returns one row per active tenant, unbounded. None has a pager, search or filter. Loading is plain text.
      • Remaining now: platform-admin/components/sections/observability.tsx now has client-side usePagination plus PaginationControls on Issues (:93,141), LlmUsage (:161,193) and Activity (:384,429). Remaining: no search or status filter (the file's Input uses are only budget and threshold editors). No server paging or 'showing N of M (capped at 200)' notice. Loading is still plain 'Loading…' text. audit/page.tsx:217 and diagnostics/page.tsx:39-41 still poll at 10000 ms.
      • Fix: Add page/size (or offset/limit + total) to GET /api/platform/issues and /activity, and expose a total. In the sections, use usePagination over the returned array as a minimum (cap the DOM to 10-50 rows), add a search Input (issues: by title/tenant; activity: by actor/action), a status filter for issues, and <PaginationControls itemLabel="issues" | "events" | "tenants"/>. Show 'showing N of M (capped at 200)' when the server truncates. Drop the 10s poll to a manual Refresh for paged views, since polling resets the user's position.

      [major] /platform-admin/fleet (PARTIAL, pagination)

      • Where: web/app/platform-admin/fleet/page.tsx
      • Problem: FleetHealthController /api/platform/fleet/health returns every agent in every tenant in one response ('one row each', page comment line 16) with no paging params (only SLO thresholds). The page filters client-side (name/tenant text and a health chip row, lines 116-121, 237) but renders rows.map for all matches (line 287) with no pager, no sort and no total. This will grow with agent count.
      • Remaining now: Pager added: platform-admin/fleet/page.tsx:133 uses usePagination(rows, 25) and PaginationControls at line 398. Remaining: no sortable column headers (the only 'sort' is a comment at line 19), and the endpoint is still unpaged server-side.
      • Fix: Wrap the filtered rows in usePagination(rows) and render below the table; reset to page 1 when filter/health changes. Add sortable column headers (health, error rate, spend) since the server's sort order is fixed. Later, add page/size to the endpoint.

      [major] /platform-admin/fraud (PARTIAL, pagination)

      • Where: web/app/platform-admin/components/sections/abuse-monitor.tsx
      • Problem: AbuseScoresSection (rows at line 95), AbuseAnomaliesSection (line 175) and ThrottledSection (line 258) render every item returned by /api/platform/abuse-monitor/scores, /anomalies and /throttled, polled every 15s from fraud/page.tsx. Server caps: MAX_ACTORS_PER_TENANT=100 per tenant for scores, and findTop200BySourceOrderByLastSeenAtDesc per tenant for anomalies with no overall cap, so anomalies is bounded only by tenants x 200. There is no pager, search or filter, no total, and no truncation indicator. The /platform-admin/abuse page has no list of its own (it links to /fraud).
      • Remaining now: platform-admin/components/sections/abuse-monitor.tsx now has usePagination plus PaginationControls on all three sections (L69/148, L168/212, L233/304). Remaining: it is client-side paging over the full polled array, with no search or tier filter, no server total, and no truncation or cap indicator or anomalies cap.
      • Fix: Return a capped, sorted list plus a total from AbuseSignalService.currentScores()/recentAnomalies(). In each section add a search/tier filter and usePagination + <PaginationControls itemLabel="principals" | "anomalies" | "throttles"/>. Cap the anomalies merge (for example newest 500) and say so in the subtitle.

      [major] /platform-admin/mcp-review (PARTIAL, pagination)

      • Where: web/app/platform-admin/mcp-review/page.tsx
      • Problem: api.listPendingMcpReviews() returns every PENDING STDIO registration across all tenants, unbounded (McpRegistryReviewService.listPending iterates every active tenant schema, no limit, no ordering contract). The page renders every row twice (mobile card list at line ~178 and desktop table at ~255) with no pager, no search or filter by tenant or name, no count, and a plain 'Loading...' state. A flood of pending registrations grows the DOM without bound.
      • Remaining now: Fixed: search input (page.tsx:81,144,190), usePagination(filtered,10) at :152, PaginationControls at :382, both mobile (:216) and desktop (:293) render pg.pageItems, Skeleton loading (:199), Retry alert. Remaining: no server limit/offset/total (long-term fix), and no pending count in the page header.
      • Fix: Short term: client-side search by tenant/name/command + usePagination(filteredRows) + , rendering only the paged slice in both layouts. Long term: add limit/offset (and a total) to GET /api/platform/mcp-review/pending and sort oldest-first. Show the pending count in the page header and use a Skeleton instead of 'Loading...'.

      [major] /quick-task/history (PARTIAL, pagination)

      • Where: web/app/components/dashboard/QuickTaskHistoryList.tsx (line 285 usePagination, 352 PaginationControls); core/app/src/main/java/com/agenticai/core/common/quicktask/QuickTaskRunService.java line 86
      • Problem: GET /api/quick-task/recent is hard-capped server-side by repository.findTop50ByRequestedByKeycloakUserIdOrderByStartedAtDesc, takes no limit/offset, and returns no X-Total-Count. The page paginates those 50 rows client-side and prints 'of 50', so a user with 200 runs silently sees only the newest 50 and believes that is everything. The list also has no search, status filter or sort, and loading is plain text ('loadingHistory') instead of skeleton rows.
      • Remaining now: Fixed: server paging. QuickTaskController.java:140-152 takes limit/offset (default 50, max 200) and returns X-Total-Count via ListPaging; QuickTaskHistoryList.tsx:173-200,366-369 uses fetchQuickTaskPage with total/hasMore and PaginationControls; ListSkeleton replaces the plain loading text (:356). Remaining: no search input and no status filter or sort. The old getRecentQuickTasks in account.ts:134 is unpaged but no longer called.
      • Fix: Add limit/offset (clamped, default 50) plus X-Total-Count/X-Has-More to QuickTaskController.recentTasks (line 137) and to QuickTaskRunService.recentForOwner (use Pageable instead of findTop50; mirror the pagedQuery/toPaged contract in docs/api/LIST-ENDPOINT-PAGING.md). In web/app/lib/api/account.ts line 134 change getRecentQuickTasks to accept {offset,limit} and return PagedRows via toPaged. In QuickTaskHistoryList show 'Showing newest N of TOTAL' and a Load more/next-page action when hasMore; until the server is paged, at minimum show a 'Showing your 50 most recent tasks' notice when history.length===50. Add a status filter (COMPLETE/FAILED/running), a search Input and Skeleton rows.

      [major] /roi (PARTIAL, pagination)

      • Where: web/app/roi/page.tsx
      • Problem: AgentTable (lines 270-335) maps every agent row from GET /api/tenant/roi/agents (RoiController lines 112-121: params from/to/includeAssumed only, no paging). It uses a hand-written table with min-w-[1000px] (horizontal scroll), no sort, no search, no pager, and no per-page cap. PricesSection (lines 396-438) maps all unpricedModels (line 406) and all prices (line 421) as unbounded ul lists, with no pager, search or total. GET /api/tenant/roi/prices (RoiController line 77) takes only from/to. PilotSection.tsx lines 72-76 uses a native for the agent and job pickers instead of the searchable Select. A grep for sort in AgentTable.tsx and roiModel.ts found nothing, so there is no column sort. There is no server paging on /roi/agents or /prices.
      • Fix: Client-side: wrap table.agents in usePagination(table.agents) and render PaginationControls (itemLabel=t('agents')) under the table, keeping the totals tfoot computed from the full list (sum is already over table.agents, so keep that). Add a name search Input and column sort (ROI, cost, conversations). Do the same for prices.prices and prices.unpricedModels with their own usePagination instances. Replace the PilotSection
      for consistent styling.

      [major] all admin routes (PARTIAL, pagination)

      • Where: web/app/components/PaginationControls.tsx; web/app/lib/usePagination.ts (header comment states 'none of them take page/size params')
      • Problem: No single standard list component exists. Every page re-implements search, empty, loading and error UI (AuditLogPanel .ax-adt-, AdminUsersPanel .ax-tl-, api-keys inline styles, payments/commerce Tailwind cards, leads custom footer). PaginationControls only has page-size options 10 and 50 (PAGE_SIZE_OPTIONS), has no 25/100, no 'jump to page' and no first/last buttons, and hides the nav (but not the size buttons) with a single page. usePagination cannot represent a server-truncated list, which is exactly how the audit, issues, payments and ledger gaps hide. Pager styling uses fixed inline style minHeight 36/40 px.
      • Remaining now: PaginationControls.tsx is extended. PAGE_SIZE_OPTIONS = [10,25,50,100] (usePagination.ts:14). It has first/last buttons (lines 143,156), a jump-to-page input (line 173), and a capped notice via the capped prop (lines 28, 94, 112). Server paging exists as usePagedList (app/admin/issues/pagedLoad.ts:51) and useServerPages.ts. docs/api/LIST-ENDPOINT-PAGING.md covers audit-log (line 28) and admin/issues (line 46). Remaining: there is no shared kit. components/ax/page-kit.tsx exports only Section, EmptyState and StatCard, with no toolbar, skeleton, error-with-retry or filtered-empty list wrapper. usePagedList lives in admin/issues rather than a shared lib.
      • Fix: Add a small shared kit in components/ax/page-kit: toolbar (search + filter pills + sort), skeleton loading, error with Retry button, filtered-empty vs true-empty states, total count, and PaginationControls. Extend PaginationControls with an optional truncated={true} / cap={200} prop that renders 'Showing newest 200; narrow filters' so server caps are never silent; add 25 and 100 to PAGE_SIZE_OPTIONS; add a useServerPagination(fetchPage) hook (page, pageSize, total, items, loading, error) modelled on InboxPanel and lib/useServerPages.ts. Extend docs/api/LIST-ENDPOINT-PAGING.md to every endpoint listed above (audit-log, admin/issues, payments/transactions, wallets ledger, commerce events, api-keys, delegations, input-requests).

      [major] console-wide (jobs, lists using usePagination) (PARTIAL, pagination)

      • Where: web/app/components/PaginationControls.tsx
      • Problem: usePagination (web/app/lib/usePagination.ts) always slices a fully fetched array, so large tenants download everything and PaginationControls cannot express 'N shown of M, more on the server'. Only four endpoints support limit/offset per docs/api/LIST-ENDPOINT-PAGING.md. components/jobs/JobsList.tsx builds its own Previous/Next with offset (lines 29-57, 156-172) and does not use PaginationControls, so two pager looks exist. PAGE_SIZE_OPTIONS is just [10, 50]. The component hard-codes inline min sizes.
      • Remaining now: Fixed: PaginationControls has an optional hasMore prop (cursor mode) and renders a 'moreAvailable' hint (lines 45, 74-93, 120). PAGE_SIZE_OPTIONS is now [10, 25, 50, 100] (lib/usePagination.ts:14). components/jobs/JobsList.tsx uses PaginationControls with offset paging (line 189), so the second pager style is gone. Server paging has admin/issues/pagedLoad usePagedList, used by WalletLedger and payments. Still open: there is no shared useServerPagination hook in lib; usePagedList lives under admin/issues. There is no truncatedNotice prop. The inline min-w-20 at PaginationControls.tsx:149 is still there.
      • Fix: Extend PaginationControls with optional props: hasMore?: boolean (server cursor mode), totalKnown?: boolean (render 'Showing 1-50 of 50+') and a truncatedNotice?: string. Add a useServerPagination(fetchPage) hook that tracks offset and reads X-Total-Count and X-Has-More. Move JobsList onto it. Extend PAGE_SIZE_OPTIONS to [10, 25, 50, 100]. Then convert the Coding Tasks, Notifications and ROI lists above to it.

      [minor] (none) (OPEN, features)

      • Where: web/app/lib/api-reference-data.ts
      • Problem: The binding is documented in the in-app API reference (line 209) but no agent page shows the per-agent REST endpoint /api/agents/{agentId}/a2a/rest or its card URL. The Know-Your-Agent card is visible only on the public /directory page.
      • Remaining now: The only web matches for a2a/rest or a2a/rpc are web/app/lib/api-reference-data.ts and one platform-admin test. No agent publish or connect panel shows the A2A URLs or a copy button. docs/architecture/a2a-conformance.md:100 documents the binding, which covers the audit's 'note it in docs' fallback only partially.
      • Fix: Optional: on the agent publish/connect panel, add a 'Connect via A2A' block with the JSON-RPC and REST URLs and a copy button. If left API-only, note it in docs/architecture/a2a-conformance.md.

      [minor] /admin/api-keys (PARTIAL, features)

      • Where: web/app/components/AppShell.tsx
      • Problem: The rail entry (line 1026) is gated only by perm MANAGE_USERS. Users with MANAGE_INTEGRATIONS but not MANAGE_USERS never see it. The Admin overview (/admin) and the developer docs do not link to it. The page's curl example at line 158 hardcodes <host>. Nothing from /workflows (where CI triggers are used) points here.
      • Remaining now: Fixed: the curl example host now comes from apiHostForExample(), which uses the configured API_BASE and falls back to window.location.origin (admin/api-keys/ApiKeyDialogs.tsx:22-26). A link was added from admin/mcp-servers/McpAccessHint.tsx:30. Still open: the nav entry is still perm MANAGE_USERS only (navSections.ts:852-860, with a comment saying this is deliberate). admin/page.tsx has no API keys card. Nothing under workflows/ links to /admin/api-keys.
      • Fix: Add an 'API keys' card on the /admin overview and a 'Create API key for CI' link from the workflow run/trigger UI. Build the example host from the configured public origin (NEXT_PUBLIC_*).

      [minor] /admin/approvals (OPEN, features) (done)

      • Where: web/app/lib/api/governance.ts
      • Problem: listApprovals (lines 17-22) fetches without limit/offset although ApprovalController supports both (ApprovalController.java:77-79); the comment at lines 21-24 also contradicts itself (full list vs capped at 200). ApprovalsPanel paginates the full array on the client.
      • Remaining now: governance.ts listApprovals still sends no limit/offset. The self-contradiction is fixed: the comment now says the list is complete. ApprovalsPanel.tsx:256,273 still pages the full array client-side with usePagination. Server paging with X-Total-Count is not used for the list.
      • Fix: Use server-side limit and offset with the X-Total-Count header that countApprovals already reads, for the history views.

      [minor] /ai-models, /platform-admin/llm-providers (OPEN, features)

      • Where: web/app/ai-models/page.tsx
      • Problem: scripts/vegaduta models list|pull|rm and deploy/models/catalog.txt (documented in docs/local-models.md) are CLI-only. No console screen shows the catalog, which models are installed in the bundled Ollama, or lets a user pick a local model. The console only offers 'Ollama' as a provider name in ai-models/page.tsx (lines 48 and 66) and components/ProvidersPanel.tsx. The Free (self-hosted) pool row exists in Platform Admin → LLM providers.
      • Remaining now: No local-models panel or Ollama tags/catalog UI in ai-models/ or platform-admin/llm-providers/. Ollama appears only as a provider name (ai-models/page.tsx:48,66). I found no docs link either.
      • Fix: If console visibility is wanted, add a read-only 'Local models' panel to Platform Admin → LLM providers backed by a new endpoint that lists the Ollama /api/tags result plus the catalog. Otherwise link docs/local-models.md from the Ollama option and state that model install is operator CLI work.

      [minor] /channels (PARTIAL, features)

      • Where: web/app/components/channels/TwilioRoutingCards.tsx:~88; web/app/components/ChannelsPanel.tsx:1119-1131; core/omnichannel/.../status/ChannelStatusService.java:73 (sms registered as its own channel entry)
      • Problem: SMS is a first-class channel in the backend status list (connectionEntry 'sms'), but in the console it appears only as a sub-card inside the WhatsApp panel, mounted via TwilioRoutingCards under the WhatsApp selection. There is no SMS tile in the channel picker, so a user looking for SMS will not find it. The card is also rendered for WhatsApp panels connected through Meta Cloud API even though SMS needs the Twilio credential. There is no SMS-specific connected/pairing status, only the default-agent selector.
      • Remaining now: Fixed: ChannelsPanel.tsx:854 now lists an 'sms' tile in the channel picker. ChannelsPanel.tsx:1733 renders a dedicated SmsInspector (channels/SmsInspector.tsx) for it, with SMS_META and SMS_ICON. Remaining: TwilioRoutingCards (ChannelsPanel.tsx:1119-1131) is still mounted under the WhatsApp selection and still contains the SMS routing block (TwilioRoutingCards.tsx:87-89, smsSaved prop). I did not confirm that it is hidden for Meta Cloud WhatsApp connections. I did not check the SMS inspector for a Twilio-credential-gated state or a pairing link.
      • Fix: Add an SMS entry to the ChannelsPanel selector that opens a small SMS panel (Twilio credential status, default agent, pairing link) and render it only when a Twilio credential exists. Keep TwilioRoutingCards for WhatsApp-over-Twilio only.

      [minor] /platform-admin/safety (OPEN, features)

      • Where: web/app/platform-admin/components/sections/outbound-policy.tsx
      • Problem: The panel is read-only (GET only), which is consistent with an operator env setting. It is the last panel on a long moderation page (safety/page.tsx:36), under PolicyPacks, Thresholds, ReviewQueue and Blocklists, and has no nav entry of its own, so it is hard to find. It is also platform-admin only, so a tenant admin who gets a blocked-URL error from a tool or MCP server sees no explanation or link in the tenant console. The panel is not covered by i18n.
      • Remaining now: safety/page.tsx:36 still renders OutboundPolicyPanel last. AdminShell.tsx has no egress anchor. outbound-policy.tsx has no useTranslations. No tenant-console URL hints about the egress allow-list were found. The only egress matches outside the panel and its test are in agents/quality, fleet and layout files.
      • Fix: Add a 'Network egress' anchor or sub-nav under Trust & Safety (AdminShell.tsx:135-137). Add a hint to tenant-console URL fields (MCP server form, OpenAPI sources, webhooks) pointing at the operator's allow-list and docs/configuration.md. Say in the panel that it is configured by env var app.security.outbound.egress-allow-list.

      [minor] /roi (OPEN, features)

      • Where: web/app/lib/api/roi.ts
      • Problem: The backend GET /api/tenant/roi/costs?groupBy=agent|workflow|channel|owner|day is not called anywhere in the console. The CHANGELOG promises cost per agent, workflow, channel, owner and day, but roiApi only has getAgents, getBudget, getPrices, setPrice, getAssumption, setAssumption and downloadAgentsCsv. Only the per-agent table is shown.
      • Remaining now: No caller of /api/tenant/roi/costs anywhere in web/app (grep for roi/costs, getCosts, groupBy finds only unrelated platform-admin reliability code). web/app/lib/api/roi.ts has no costs method and web/app/roi/page.tsx has no Workflow/Channel/Owner/Day breakdown tabs.
      • Fix: Add roiApi.getCosts(groupBy, range) and a tabbed or segmented breakdown section on web/app/roi/page.tsx (Agent, Workflow, Channel, Owner, Day) with a table or chart.

      [minor] /roi (OPEN, features)

      • Where: web/app/lib/api/roi.ts
      • Problem: POST /api/tenant/roi/outcomes (mark an outcome, RoiController.java:151) and DELETE /api/tenant/roi/prices/{id} (RoiController.java:92) have no client method or control. Prices can be added in PricesSection (roi/page.tsx around line 421) but never removed. Outcomes cannot be corrected by hand.
      • Remaining now: web/app/roi/page.tsx:294-335 PricesSection has only an edit (Pencil) button per price row; no delete control and no deletePrice in web/app/lib/api/roi.ts (grep for deletePrice and roi/outcomes finds only roi.test.ts). No manual outcome control, and docs/cost-and-roi.md:135-137 lists the DELETE and POST endpoints without saying they are API-only.
      • Fix: Add a delete button with a confirm on each price row, calling a new roiApi.deletePrice. Add a mark-outcome control only if the product wants manual outcome marking; otherwise state in docs/cost-and-roi.md that it is API-only.

      [minor] /roi (PARTIAL, features)

      • Where: web/app/roi/page.tsx
      • Problem: The agents table (table.agents.map, line 298) and the prices list (prices.prices.map, line 421) render every row. They have no pagination, sorting, search or filter. The pilot agent picker (roi/PilotSection.tsx:146) is a bare (roi/PilotSection.tsx:146), not searchable.
      • Fix: Reuse web/app/components/PaginationControls.tsx and client-side sorting on the agents table (add a name search). Page the prices list. Make the pilot agent picker searchable.

      [minor] /skills, registry browser (PARTIAL, features)

      • Where: web/app/components/SkillsPanel.tsx
      • Problem: Badges show in the skills list (SkillsPanel.tsx:371), the agent Skills card and the platform-admin review queue. The tenant-side install flow shows a registry preview (SkillRegistryBrowser.tsx:93) but not a persistent risk filter or sort on the skills list. There is no tenant-visible 'needs review' queue or count in the nav, while platform-admin has one at /platform-admin/skill-security. skillsNeedingReview is computed in lib/skillSecurity.ts but there is no rail badge for it.
      • Remaining now: Fixed: a 'attention' filter exists (SkillListFilter in web/app/lib/skillAuthoring.ts:350, wired at SkillsPanel.tsx:115, flagged names from skillsNeedingReview at SkillsPanel.tsx:92). Remaining: no risk sort in SkillsPanel, and no nav badge for /skills. AppShell.tsx:606 only badges /tools via mcpAttentionCount, and AppChromeProvider has no skills count.
      • Fix: Add a 'needs review' filter chip and risk sort to the SkillsPanel list, and a nav badge on /skills (like mcpAttentionCount at AppShell.tsx:1295) when skillsNeedingReview() is non-empty.

      [minor] /tools (OPEN, features)

      • Where: web/app/components/McpPanel/ToolPinReview.tsx
      • Problem: All strings are hardcoded English (pinModeNote at lines 12-15, card title and labels), unlike the rest of the console, which uses useTranslations and web/messages/en.json. The same applies to McpServerAuthOptionsCard.tsx (title 'How callers authenticate to /mcp' at line 36, plus the on/off badges and descriptions). The tools/page.tsx tab labels 'Classification', 'Custom Code' and 'OpenAPI Sources' (lines 298-306) are hardcoded as well.
      • Remaining now: ToolPinReview.tsx:12-15 pinModeNote still hardcoded English with no useTranslations; McpServerAuthOptionsCard.tsx:36 still hardcodes 'How callers authenticate to /mcp'; tools/page.tsx:306-312 TabsTrigger labels 'Classification', 'Custom Code', 'OpenAPI Sources' still literal (other tabs use t()).
      • Fix: Move the strings into web/messages/en.json and read them with useTranslations. Use the existing Alert, Badge and Button components.

      [minor] /workflows/[id] (OPEN, features)

      • Where: web/app/components/workflow/NodeConfigDrawer.tsx
      • Problem: TriggerPollConfig is rendered at line 1191, after the closing of the channel === "WEBHOOK" block, so it appears for every trigger channel (manual, schedule, webhook) with no explanation of when it applies, and sits below the FormTriggerEditor.
      • Remaining now: NodeConfigDrawer.tsx:1191 still renders unconditionally after the channel==="WEBHOOK" block closes at :1188, so it shows for every trigger channel.
      • Fix: Show it only when the trigger channel is an HTTP-poll choice, or put it behind a Polling channel option so it is arranged with the other trigger channels.

      [minor] POST /api/workflows/{id}/run (OPEN, features)

      • Where: docs/api/openapi.json
      • Problem: The header (max 128 chars, replay returns an error, WorkflowController.java:277-301) is documented only in CHANGELOG.md and the generated openapi.json. There is no prose section for API users.
      • Remaining now: grep for Idempotency-Key in docs finds only docs/api/openapi.json; no prose section in docs (workflow-dsl-v1.md, api/README.md, etc.).
      • Fix: Add a short section to the workflow API docs describing the header, its 128-character limit and the replay response.

      [minor] n/a (Helm) (OPEN, features)

      • Where: deploy/helm (no IDENTITY_PROVIDER, AUTH0_* or OIDC_* values; only deploy/compose/.env.example and core.env.example contain them)
      • Problem: The Auth0 adapter and claim mapping are intentionally env-configured and documented in docs/configuration.md and docs/extending.md, but the Helm chart does not expose them; Kubernetes users have no documented path other than raw extraEnv.
      • Remaining now: No IDENTITY_PROVIDER/AUTH0_/OIDC_ in deploy/helm; the chart README and VALUES.md have no Auth0 or OIDC note (only generic global.core.extraEnv doc with a KMS example); docs/configuration.md:280-290 Auth0 paragraph does not mention Helm/extraEnv (only line 372 does, for KMS).
      • Fix: Add identity.provider, identity.auth0.* and oidc.rolesClaimPath/tenantClaim/scopesClaim to the umbrella chart values and templates, or add a short 'set via extraEnv' note to the chart README and docs/configuration.md.

      [minor] n/a (operator config) (OPEN, features)

      • Where: docs/configuration.md
      • Problem: The monthly BYOK caps app.llm.gateway.budget.premium-monthly-cap-usd (default 200) and free-monthly-cap-usd (default 0) are env-only and not listed in docs/configuration.md or deploy/compose/.env.example (grep found neither). Line 697 refers to them only as app.llm.gateway.budget.*. AGENTS.md says new configuration belongs in both.
      • Remaining now: grep for monthly-cap, MONTHLY_CAP, gateway.budget and GATEWAY_BUDGET finds premium-monthly-cap-usd and free-monthly-cap-usd only in core/app-config/src/main/resources/application.yml:1468-1469. docs/configuration.md:730 still mentions only the wildcard 'app.llm.gateway.budget.*'. There is no row for either cap, and neither is in deploy/compose/.env.example.
      • Fix: Add a table row for each property with its env var and default to docs/configuration.md, and the env var to deploy/compose/.env.example.

      [minor] /admin, /admin/maintenance (OPEN, layout)

      • Where: web/app/admin/page.tsx (inline widths at 797 and 889: style width %, background var(--accent-500); only 1 Section, 0 EmptyState/Skeleton, 5 hex literals); web/app/admin/maintenance/page.tsx (300 lines; 1 hex literal)
      • Problem: The overview builds its own meters with inline-styled divs rather than StatCard/Progress, and has hard-coded hex colours that the design-tokens/no-hex-literal rule warns about. It has no empty state, so a fresh tenant sees zero tiles with no guidance.
      • Remaining now: admin/page.tsx (1122 lines) still builds meters from inline-styled divs with style width % and background var(--accent-500) at lines 797 and 889. It imports no StatCard, Progress, EmptyState, Skeleton or Section. The hex-literal part is not an issue now, since I found no hex colours in admin/page.tsx or admin/maintenance/page.tsx.
      • Fix: Use StatCard and ui progress for meters, replace hex values with tokens from globals.css, and add an EmptyState/onboarding card.

      [minor] /admin/access (OPEN, layout)

      • Where: web/app/components/AdminAccessMatrix.tsx:93-99 (minWidth 100%, sticky first column, fontSize 0.78rem, 12 inline style props, no pagination, no empty state); web/app/components/AdminUsersPanel.tsx (727 lines, 5 raw <button>, 8 inline styles)
      • Problem: The users x scopes matrix is a wide table with many columns and no row paging or search, while the users panel above it is paged. At 375px the matrix forces horizontal page overflow unless the wrapper scrolls. No empty state when there are no users.
      • Remaining now: components/AdminAccessMatrix.tsx is unchanged in the ways the gap names. It has a raw
      with inline styles (lines 84-154: fontSize 0.78rem, minWidth 100%, sticky first column), only an overflowX:auto wrapper (92), and no PaginationControls, usePagination, search or EmptyState. AdminUsersPanel.tsx is still 727 lines with 5 raw
      directly instead of components/ui/table (which wraps tables in an overflow-x container), so wide tables overflow the page at 375px and miss the standard header typography and row hover.
    • Remaining now: Fixed: ScheduledTaskRuns.tsx:19 and :137 and JudgeCalibrationSection.tsx:22 and :266 now use ui/table. Remaining: ImportWizard.tsx:313 is still a raw
    • , though inside an overflow-x-auto div at L312. VersionDiffPanel.tsx:160 is still a raw table with class wf-vdiff-table, a fixed-layout table in version-diff.css:381 with no overflow wrapper. build/studio/DomainsPanel.tsx:134 is still a raw table.
    • Fix: Swap to Table/TableHeader/TableRow/TableHead/TableCell; verify at 375px.
    • [minor] /build, /studio (build/studio), agents, jobs (PARTIAL, layout)

      • Where: web/app/build/gallery/GalleryPanel.tsx:222 (text-emerald-600); web/app/build/page.tsx (11 palette classes); web/app/build/studio/StudioSurface.tsx (24); web/app/components/agents/AgentVersionsCard.tsx (5); web/app/components/studio/showtime/*.tsx (Scenes 59, DayStage 49, LiveBuilder 33, Showtime 27, AppPhone 20) and PhoneStage.tsx (39)
      • Problem: text/bg/border-(emerald|red|amber|blue|...)-NNN classes bypass design tokens (--success-text, --state-*, destructive) so they ignore theme and dark-mode overrides and contrast fixes. The ESLint rule design-tokens/no-hex-literal only warns on hex, so palette utilities slip through. (The studio/showtime scenes may be intentional illustration art; GalleryPanel, build/page.tsx, StudioSurface and AgentVersionsCard are real UI.)
      • Remaining now: build/page.tsx and AgentVersionsCard.tsx now have 0 palette utilities. Remaining: GalleryPanel.tsx:222 still uses text-emerald-600, and :305,306,309,735 use amber/emerald palette classes. StudioSurface.tsx still has 17 palette-class matches. eslint.config.mjs has no rule flagging palette utilities (only the hex-literal rule).
      • Fix: Replace with token classes already used elsewhere (text-[var(--success-text)], bg-[var(--success-bg)], text-destructive, state-pill) and extend the lint rule to flag palette utilities outside illustration directories.

      [minor] /code-intel, /edge, /coding (OPEN, layout)

      • Where: web/app/code-intel/page.tsx:537, web/app/components/codingTasks/CodingTasksPanel.tsx:193
      • Problem: Selectable list items are hand-built
      • Remaining now: components/ui has no selectable-item or SelectableCard component. code-intel/page.tsx:538 still uses a hand-built
      • Fix: Create components/ui/selectable-item (aria-pressed/aria-current, focus-visible styles) and replace the copies.

      [minor] /jev, /devices, /capabilities (OPEN, layout) (done)

      • Where: web/app/jev/page.tsx, devices/page.tsx, capabilities/page.tsx
      • Problem: Three routes are bare redirects; /devices is a client useEffect redirect that renders null (blank flash, no loading state) and no tests assert the destination tab is selected. The Mobile SDK pairing surface is thus three clicks deep in Channels.
      • Remaining now: app/devices/page.tsx still uses a client useEffect router.replace and returns null. jev and capabilities already use a server redirect(), as before. navSections.ts:75-76 states deliberately that /devices is absent from the nav, so no explicit 'Mobile SDK devices' nav item deep-links to /admin/channels?channel=mobile-sdk. I did not check for tests asserting the destination tab.
      • Fix: Use server redirect() for /devices (as jev does), and surface 'Mobile SDK devices' as an explicit nav item deep-linking to /admin/channels?channel=mobile-sdk.

      [minor] /learn, /learn/tutor, /capabilities/decision, /changelog, /insights jobs (OPEN, layout)

      • Note (0.1.8): the /learn parts are obsolete: web/app/learn/page.tsx and learn/CurriculumMap.tsx were removed (CHANGELOG [0.1.8]).
      • Where: web/app/learn/page.tsx:184, web/app/learn/CurriculumMap.tsx, web/app/capabilities/decision/page.tsx, web/app/changelog/page.tsx, web/app/insights/page.tsx:469
      • Problem: Mapped-screen lessons, curriculum levels, decision history/criteria rows, the full changelog entry list (CHANGELOG_ENTRIES rendered in one page) and insights ranking rows have no pagination or 'show more'. Only code-intel (repos, trace, passages) and edge (models, engines) use PaginationControls in this area.
      • Remaining now: A grep for PaginationControls or usePagination in learn/, capabilities/decision/, changelog/ and insights/ finds nothing. changelog/page.tsx has no Load older, year filter or show-more control. The changelog still renders all entries in one page.
      • Fix: Use PaginationControls for long lists; for changelog, group by release with a 'Load older' control or year filter.

      [minor] /memory, /skills, /providers, /secrets (PARTIAL, layout)

      • Where: web/app/memory/page.tsx (522 lines, ImportChatsCard 629); web/app/components/ProvidersPanel.tsx:1025 (card grid with no search/filter); web/app/secrets/page.tsx; web/app/components/SkillsPanel.tsx
      • Problem: These are closest to the standard (PageHeader, pagination via PaginationControls, Tabs on memory) but: ProvidersPanel's catalog grid has no search/filter or pagination, SecretsPanel/SkillsPanel mix raw
      • Remaining now: Fixed: ProvidersPanel.tsx:1023 renders catalog.controls, so the provider grid has search/filter controls. memory/page.tsx:340 and :383 now use md:grid-cols-2 steps. Remaining: raw
      • Fix: Add search + filter to the provider grid, convert the remaining raw buttons, and add md: breakpoints to memory grids.

      [minor] /platform-admin sidebar (PARTIAL, layout) (done)

      • Where: web/app/platform-admin/components/AdminShell.tsx:90-185 (NAV_SECTIONS); :103 label 'Demo Sandboxes (reclaim)'
      • Problem: Operations holds 13 items. Overlapping pairs: Demo Sandbox vs Demo Sandboxes (reclaim); Fleet Health, Reliability & SLOs, Ops Hub and Diagnostics; Safety & Moderation, Abuse & Rate Control and Fraud & Anomaly; Tenants & Users, Platform Users and Tenant Detail. Tenant Detail is a standalone entry that needs a selected tenant rather than a row drill-down. No page search or collapsible groups.
      • Remaining now: Tenant Detail is now reachable from tenant rows (tenants/page.tsx:549 links /platform-admin/tenant-detail?id=...). Unchanged in AdminShell.tsx: Operations group still ~13 items (:150-175), 'Demo Sandboxes (reclaim)' label at :104 next to Demo Sandbox :102, no collapsible groups or page search.
      • Fix: Split Operations into Reliability and Maintenance groups, merge or rename the demo sandbox pair, make Tenant Detail reachable from Tenants rows, and add collapsible groups or a Cmd+K page search.

      [minor] /platform-admin/llm-providers, /tenants, /demo-sandbox (PARTIAL, layout)

      • Where: llm-providers/page.tsx (1426 lines, over the 1000-line ratchet), tenants/page.tsx (812), demo-sandbox/page.tsx (792)
      • Problem: llm-providers mixes provider rows, hosted pool, credentials and model pickers in one component; hostedRowParts.tsx is only a partial split. This makes consistent layout and tabs hard, and breaches the repo file-size rule.
      • Remaining now: llm-providers/page.tsx is 995 lines, down from 1426, and under the 1000-line ratchet. It was split into TaskRoutingCard.tsx, TranslationAndCatalogCards.tsx and hostedRowParts.tsx. The file has no Tabs, so the Providers/Hosted pool/Models tab layout was not done. tenants/page.tsx (813 lines) and demo-sandbox/page.tsx (792 lines) are still single files with no split.
      • Fix: Split llm-providers into ui/tabs sections (Providers, Hosted pool, Models) with per-tab components, and tenants into roster, limits and actions.

      [minor] /platform-admin/{ai-reports,mcp-review,plugins,academy-learners} and every section compone (PARTIAL, layout)

      • Where: ai-reports/page.tsx:161; mcp-review/page.tsx:165; plugins/page.tsx:270,315; academy-learners/page.tsx:410; components/sections/abuse-monitor.tsx:88,170,254,326; observability.tsx:102,159,247,311,372,460; budgets.tsx:143,303,465; smart-ai.tsx:173; platform-controls.tsx:89; feature-flags.tsx:36
      • Problem: A Skeleton component exists and about 15 pages use it, but these show a muted Loading line, so layout jumps and the console feels unfinished.
      • Remaining now: ai-reports/page.tsx:180 and mcp-review/page.tsx:199 now use Skeleton, and circuit-breakers.tsx:79 does too. Still plain text: plugins/page.tsx:270 and 315, academy-learners/page.tsx:410, abuse-monitor.tsx:91, 183, 277 and 358, and other sections (budgets 9 hits, observability 10, feature-flags 4). components/ui/skeleton.tsx exports only Skeleton, with no TableSkeleton, CardSkeleton or StatSkeleton.
      • Fix: Add TableSkeleton, CardSkeleton and StatSkeleton to ui/skeleton and use them in these places.

      [minor] /platform-admin/{audit,spend,fleet,demo-sandboxes,impersonation,mcp-review,observability,f (PARTIAL, layout)

      • Where: audit/page.tsx:143 (no min-w, text-xs, max-w-[10rem] cells); spend/page.tsx:61; fleet/page.tsx:270; demo-sandboxes/page.tsx:294; impersonation/page.tsx:48 (min-w 820px); mcp-review/page.tsx:243 (min-w 64rem); funnel/page.tsx:246 (min-w 760px); components/sections/moderation.tsx:261 (grid-cols-2, no breakpoint); components/templates/ConfigViewer.tsx:93
      • Problem: All tables sit in an overflow-x-auto wrapper, so the page itself should not overflow, but several force 760-1024px of horizontal scroll on a phone with no card layout (only mcp-review has a second layout). Tables without a min-width (audit, spend, fleet) squeeze unpredictably. Two grids stay at 2 columns on a phone. Not verified in a browser.
      • Remaining now: Fixed or improved: moderation.tsx:263 now has grid-cols-2 sm:grid-cols-4. mcp-review/page.tsx:211-215 has a stacked-card layout below md. audit, spend and fleet use AdminTableWrap. Remaining: audit/page.tsx:148 (min-w-56rem), impersonation/page.tsx:51 (820px), funnel/page.tsx:246 (760px), and mcp-review/page.tsx:281 (64rem) still scroll horizontally. No shared stacked-card pattern was added. ConfigViewer.tsx:93 (moved to platform-admin/components/templates) has a grid-cols-2 base with sm:grid-cols-3 and a breakpoint. Not verified in a browser.
      • Fix: Adopt a shared pattern: table at sm and above, stacked cards below. Add breakpoints at moderation.tsx:261 and ConfigViewer.tsx:93, and check at 375px.

      [minor] /roi pilot report (OPEN, layout)

      • Where: web/app/roi/PilotSection.tsx:146,157 (raw <select>); roi/page.tsx:492-496 (price form grid)
      • Problem: Two raw elements styled by hand (h-9 rounded-md border...). No aria-invalid or aria-describedby appears anywhere in web/app/roi/*.tsx. No ui/select wrapper is used there, and no per-million unit suffix was found.
      • Fix: Add a ui/select (or native-select wrapper in components/ui) used by both; wire error text with aria-describedby/aria-invalid and add unit suffixes ($ per million tokens).

      [minor] /studio (OPEN, layout) (done)

      • Where: web/app/studio/page.tsx:150-215
      • Problem: Gallery is grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 with raw tiles wrapped in Tilt3d, 'text-[11px]' micro type, and skeleton blocks built from animate-pulse divs. Six columns at lg leaves ~120px tiles with 11px captions; actions (Reuse, Download) are 28px-high buttons (h-7) below touch size.
      • Remaining now: studio/page.tsx:159 still grid-cols-2 sm:grid-cols-3 lg:grid-cols-6; raw at :169; text-[11px] at :176,188; h-7 buttons at :188,193; skeletons still animate-pulse divs (:202-204); no pager/load-more.
      • Fix: Use xl:grid-cols-6 and lg:grid-cols-4, Skeleton for placeholders, 36-40px hit targets, and add the pagination/'load more' control shown elsewhere.

      [minor] /tools, /tools/servers, /tools/credentials, /tools/code (OPEN, layout)

      • Where: web/app/tools/page.tsx:163-330; web/app/tools/servers/page.tsx (595 lines, 0 i18n); web/app/tools/credentials/page.tsx (655 lines, 1 t() call)
      • Problem: /tools combines a search box, five buttons, up to 4 tabs and an inspector in one screen, while /tools/servers and /tools/credentials are separate pages with almost no i18n (eyebrow 'Tools & MCP' literal at servers/page.tsx:513, raw alerts) and hand-rolled
      • Remaining now: The tools hub tab state is local (tools/page.tsx:59 useState, with no ?tab= deep link beyond mcp_oauth). servers, credentials and code are still separate pages and were not folded into tabs. tools/servers/page.tsx:536 still has the literal eyebrow "Tools & MCP" and the literal title "Enabled MCP servers". credentials/page.tsx still has hand-rolled
      • Fix: Fold servers/credentials/code-tools into tabs of /tools with deep-linkable ?tab=, one shared list component, and i18n the three files.

      [minor] /usage, /edge, /coding, /capabilities/decision, /help (OPEN, layout)

      • Where: web/app/usage/page.tsx, edge/page.tsx, coding/page.tsx, capabilities/decision/page.tsx, help/page.tsx
      • Problem: These pages never import Section from components/ax/page-kit (0 <Section matches) and stack hand-rolled headings/cards, while insights, roi, and learn use Section. Vertical rhythm (space-y-4/6/8, mb-3/mb-10) varies page to page; edge uses 'mt-4' div and SetupHint 'mb-3'.
      • Remaining now: Counts of <Section in usage/page.tsx, edge/page.tsx, coding/page.tsx, capabilities/decision/page.tsx and help/page.tsx are all 0; only usage/page.tsx imports page-kit (for PageHeader/EmptyState), the other four do not import page-kit at all.
      • Fix: Wrap each logical block in Section (title, description, optional actions) and use one spacing scale.

      [minor] all audited routes (PARTIAL, layout)

      • Where: web/app/components/ax/page-kit.tsx (Section); web/app/components/AppShell.tsx:1223-1249 (wide/bleed props); web/app/{knowledge,build}/page.tsx use wide; only studio, quick-task/history, tools/servers, tools/credentials and workflows/templates use Section
      • Problem: Pages choose between default, wide and ad-hoc 'mb-3/mb-4/mb-6/mb-8' wrappers and 'space-y-6' by hand. Only 6 audited pages use Section; the rest drop panels straight under PageHeader. Subtitle widths are fixed at max-w-2xl, while eyebrow is passed both to AppShell and again to PageHeader on some pages (quick-task/history) and not others, producing duplicated eyebrows.
      • Remaining now: 23 page.tsx files now import page-kit, up from the 6 the audit counted. I found no required PageLayout wrapper, and there are 69 PageHeader pages, so most pages still choose their own spacing. I did not verify the eyebrow duplication.
      • Fix: Define a PageLayout (PageHeader + vertical rhythm token + Section) and require it in page.tsx files; document which routes are wide; pass eyebrow only to AppShell.

      [minor] all console list pages (OPEN, layout)

      • Where: web/app/components/EmptyState.tsx (props: hint, action; class empty-state) vs web/app/components/ax/page-kit.tsx:59 (props: description, action); 9 imports of the old one vs ~49 page-kit imports
      • Problem: DataTablesPanel/others import the legacy EmptyState (hint prop, .empty-state CSS), while jobs/research/shared-with-me use page-kit's EmptyState (description prop). They render with different spacing and icon treatment. Several panels still use bare

        (DataTablesPanel.tsx:326, ScheduledTasksPanel.tsx:366-371, studio gallery) rather than either.

      • Remaining now: web/app/components/EmptyState.tsx still exists and is imported by about 9 files (ScheduledTasksPanel.tsx:22 with hint prop, DataTablesPanel, DataTableGrid, CodingTasksPanel, AdminUsersPanel, GoogleIntegrationPanel, memory/page.tsx, admin/api-keys, admin/mcp-servers); ScheduledTasksPanel.tsx:368 still has a bare dashed

        .

      • Fix: Delete components/EmptyState.tsx, re-export page-kit's from one place, migrate the 9 importers, and replace the ad-hoc dashed

        blocks.

      [minor] /quick-task/history (PARTIAL, nav)

      • Where: web/app/components/SimpleTaskRunner.tsx
      • Problem: Reachable only from a link inside the Quick Task runner (SimpleTaskRunner.tsx lines 75 and 223) and has no nav row or palette entry, although /quick-task, /jobs and /research all do.
      • Remaining now: Palette entry added at web/app/components/CommandPalette.tsx:15. No navSections.ts row (intended). The only in-page entry is still the footer ghost link at SimpleTaskRunner.tsx:920-925, not a PageHeader action or tab as the fix asked.
      • Fix: Surface it as a PageHeader action or tab at the top of /quick-task rather than a new rail row, and add it to the command palette extras.

      [minor] /scheduled, /research (OPEN, nav)

      • Where: web/app/components/AppShell.tsx
      • Problem: BUILD (lines 268-415) holds 11 rows, including run-time surfaces (/scheduled, /coding, /research). Comments at lines 349, 385-390 and 401-404 say rows sit 'after Quick Task' and 'after Code Intel', positions that no longer exist. /scheduled sits in BUILD while its sibling /jobs is in TEST & MONITOR. This contributes to the 'elements not arranged properly' complaint.
      • Remaining now: web/app/components/nav/navSections.ts: /scheduled (line 230) and /research (253) still sit in the navGroupBuild section (starts line 146). /jobs is at 609 in navGroupMonitor (starts 567), so /scheduled is still apart from /jobs. Comments at 267 ('after Quick Task') and 280 ('after Code Intel') are unchanged. Line 605 still says 'right after Quick Task'.
      • Fix: Move /scheduled next to /jobs in TEST & MONITOR (after line 774) and /research next to /studio, and refresh the stale comments while extracting the file. The feature-flag filters in navSections key off href, so moving rows between sections does not break them or NAV_TOUR_TARGETS.

      [minor] /workflows/import, /workflows/templates, /templates/mine (PARTIAL, nav)

      • Where: web/app/workflows/page.tsx
      • Problem: Reachable only through buttons on /workflows (import, templates) and /templates (mine). /workflows/templates is a separate gallery from /templates (LIBRARY) with a near-identical name, which is confusing, and neither is in the command palette.
      • Remaining now: web/app/components/CommandPalette.tsx:16-17 now has PALETTE_EXTRAS for /workflows/import and /workflows/templates (labels 'Import a workflow', 'Workflow templates', messages/en.json:153-154). Still open: /templates/mine is not in the palette. The galleries are not folded together. The LIBRARY row is still labelled 'Community Templates' (en.json:72 railTemplates), not 'Template gallery'.
      • Fix: Keep the buttons but add them to the command palette extras. Ideally fold /workflows/templates into /templates?kind=workflow so there is one gallery; at minimum label the LIBRARY row "Template gallery" and the workflow one "Workflow templates".

      [minor] /admin/access (OPEN, pagination)

      • Where: web/app/components/AdminUsersPanel.tsx:212-226,708 (api.listAdminUsers; backend core/admin/.../UserAdminController.java:91 -> KeycloakAdminClient.listUsersByTenant)
      • Problem: Search, client paging and empty state are all present. Server returns the complete tenant roster each call (internally pages Keycloak 100 at a time up to a 100k safety stop), so very large tenants ship the whole list; no sort and no role/status filter.
      • Remaining now: AdminUsersPanel.tsx:212-226 filters and pages client-side only. api.listAdminUsers (lib/api/admin.ts:20) is still a no-argument GET /api/admin/users. There are no limit/offset/q parameters, no role or enabled filter, and no column sort.
      • Fix: Optional: add limit/offset/q and X-Total-Count to GET /api/admin/users and drive PaginationControls from the header; add role and enabled/disabled filter pills. Also add a column sort (name, last login).

      [minor] /admin/approvals (OPEN, pagination) (done)

      • Where: web/app/components/ApprovalsPanel.tsx:240-273,418-430 (api.listApprovals in web/app/lib/api/governance.ts:17)
      • Problem: Panel is paged with PaginationControls and has status filter + channel search, but paging is client-side over a deliberately limit-less fetch (listApprovals sends no limit), so payload grows unbounded for long-lived tenants even though GET /api/approvals supports limit/offset/X-Total-Count (LIST-ENDPOINT-PAGING.md). No free-text search over requests, only status and channel select.
      • Remaining now: ApprovalsPanel.tsx:256 api.listApprovals(status) is still limit-less (governance.ts:18-24) and paging is client-side usePagination(requests) at :273 with PaginationControls at :422; no X-Total-Count server paging and no free-text search (only status and channel select).
      • Fix: Switch to server paging: api.listApprovals(status, ..., {limit: pageSize, offset}) reading X-Total-Count (the countApprovals helper already shows how) and feed PaginationControls from the header; keep the limit-less call only for the badge/pending lookups. Add a text search param.

      [minor] /admin/contacts (OPEN, pagination)

      • Where: web/app/components/ContactsPanel.tsx:153-156,244-288,1040-1116 (api.listChannelContactsPage, server cap 500)
      • Problem: Directory is server-capped and fetched in offset pages, but search and channel/group filters run client-side over the rows loaded so far; the code auto-loads further pages while a filter is active (lines 259-278) which can pull the whole directory into memory and the DOM total badge (line 915) shows filteredContacts.length, not the server total. Pending pairings list is client-paged but unbounded. Client-side usePagination is layered on top of the load-more pages, so two paging mechanisms stack.
      • Remaining now: web/app/components/ContactsPanel.tsx:245-280 still filters search, channel and group client-side over loaded rows, and L259-278 still auto-loads further pages while a filter is active. L915 still shows filteredContacts.length in the badge. Client-side usePagination (L286-287) is still stacked on load-more pages (L1114-1117). The pending list is still client-paged. No server-side q param.
      • Fix: Pass channel/groupId/q to the server (channel/groupId are already accepted by listChannelContactsPage; add q) and drive PaginationControls from X-Total-Count instead of auto-loading all pages; show the server total in the badge.

      [minor] /admin/guardrails (PARTIAL, pagination)

      • Where: web/app/admin/guardrails/page.tsx:118,149-150,641,670 (api.listGuardrailRules, web/app/lib/api/agents.ts:268)
      • Problem: Both rule lists are paged client-side with PaginationControls, but there is no search/filter over rules, no sort and no total in the section headers; endpoint returns everything.
      • Remaining now: admin/guardrails/page.tsx:467-472 adds a rule search input (name, description, pattern). Filtered lists feed usePagination at :155-158 with PaginationControls (:660, :689) that show totals. Remaining: no category filter and no sort.
      • Fix: Add a rule search input (name/pattern) and category filter feeding usePagination(filteredCustom).

      [minor] /admin/input-requests (OPEN, pagination)

      • Where: web/app/components/InputRequestsPanel.tsx:56-142 (api.listInputRequests; backend core/workflow/.../humaninput/WorkflowInputRequestController.java:64-72)
      • Problem: Uses usePagination + PaginationControls, but the endpoint has no paging contract; SUBMITTED (the audit view) is the full history, fetched unbounded, and has no search or date filter. Default (no status) returns only PENDING, which is correct.
      • Remaining now: InputRequestsPanel.tsx:82 only does client-side usePagination plus PaginationControls (:142). governance.ts:64-69 listInputRequests sends only status and runId, with no limit/offset or total, so the SUBMITTED history is still fetched unbounded. No workflow-name search or date filter in the panel.
      • Fix: Add limit/offset/X-Total-Count to GET /api/workflows/input-requests, use server paging for the SUBMITTED/all filter, add a workflow-name search.

      [minor] /admin/leads (OPEN, pagination)

      • Where: web/app/components/leads/LeadsPanel.tsx:196,405-446
      • Problem: Table view is correctly server-paged (page/size, total) but uses a one-off pager (prev/next buttons, fixed TABLE_PAGE_SIZE=25) instead of the shared PaginationControls: no page-size choice, visually and behaviourally different from every other list. No sort controls, no column sorting. Loading shows a bare

        , no skeleton.

      • Remaining now: web/app/components/leads/LeadsPanel.tsx:38 still has the constant TABLE_PAGE_SIZE=25, and :196 uses it for pageCount. The footer at :437-443 is still the one-off Prev/Next buttons with the pageOf text. PaginationControls is not used, there is no page-size choice, and I found no sort controls (grep for sort, Pagination and Skeleton in the file returned only the page-size lines).
      • Fix: Replace the custom footer with <PaginationControls page={page+1} totalPages={pageCount} totalItems={total} pageSize={pageSize} onPageChange={p=>setPage(p-1)} onPageSizeChange={...} itemLabel='leads'/>, making pageSize state (10/50) instead of the constant; add sort (sort=createdAt|score|value,dir) to LeadController params.

      [minor] /admin/mcp-servers (PARTIAL, pagination)

      • Where: web/app/admin/mcp-servers/page.tsx:404-419 (checkbox pickers), 833,949-980 (grants, paged), api.listMcpServerGrants/listMcpServers in web/app/lib/api/devices.ts:34 and mcp.ts:22
      • Problem: Grants list is paged correctly, but the inline checkbox pickers (agents/workflows/tools selection, maxHeight 200 scroll box) render every item with no search box and no select-all, which becomes unusable with hundreds of agents/tools. Grants have no search or status filter.
      • Remaining now: The tool pickers are fixed. admin/mcp-servers/CheckboxPicker.tsx:29-48 adds a filter box that hides rows without changing the selection. I did not confirm a select-all control. The grants list still has no search or status filter: page.tsx only has a grants.filter on delete at line 740.
      • Fix: Add a filter input above the picker list (client-side filter on item.name) and 'select all/none'; add search and enabled/disabled pills above the grants list.

      [minor] /edge (PARTIAL, pagination)

      • Where: web/app/components/edge/EdgeConsole.tsx
      • Problem: The model market and engines are paged with usePagination + PaginationControls (good). Gaps: textModels is filtered to tier === 1 (line 300), so other tiers are hidden with no 'show all' toggle. There is no search/filter box over the 10/50-per-page model lists. The 'other relays' list in EdgeRelaySection.tsx (lines 148-157) is an unpaged, uncapped ul. The file is 1476 lines, over the file-size ratchet unless baselined.
      • Remaining now: Fixed: components/edge/EdgeModelFilter.tsx adds a search box and an 'only models that fit' toggle. EdgeConsole.tsx:326 feeds the filtered list into usePagination. EdgeRelaySection.tsx:148-170 pages the other-relays list when more than 10 exist. Remaining: EdgeConsole.tsx:301 still hard-filters textModels to m.tier === 1, and I saw no 'show all tiers' toggle. EdgeConsole.tsx is still 1476 lines (baselined at 1476, file-size-baseline.txt:60) and was not split.
      • Fix: Add a 'Show all tiers' toggle and a text search above the model market, both feeding usePagination. Paginate the relays list when more than 10 devices are registered. Split EdgeConsole into market / installed / settings components.

      [minor] /help/workflow-recipes (OPEN, pagination)

      • Where: web/app/help/workflow-recipes/WorkflowRecipesClient.tsx
      • Problem: The route is public. It has search over 89 industries, but the filtered result list (lines ~820-850) renders every matching industry accordion at once, with no pager or 'show more'. Industry packs are lazily loaded, so the payload is fine, but the DOM and scroll length grow.
      • Remaining now: WorkflowRecipesClient.tsx (949 lines) has no usePagination, PaginationControls, show-more control or slice. Every matching industry accordion still renders at once.
      • Fix: Show the first 12 matches with a 'Show more (N)' button, or a usePagination + PaginationControls pair. Low priority because it is a public reference page.

      [minor] /platform-admin/edge-models, /platform-admin/skill-security, /platform-admin/demo-sandbox (PARTIAL, pagination)

      • Where: web/app/platform-admin/edge-models/page.tsx
      • Problem: Smaller lists render fully with no pager: edge model rows (line 269) and registry candidates (line 313, no search), skill-security review list (reviews?.map line 274, flagged.map line 309, scoped to the operator's own tenant), and demo-sandbox 'recent' (fixed at the 20 most recent, demo-sandbox/page.tsx line 557, no way to see more). Likely small but they will grow.
      • Remaining now: edge-models/page.tsx:168 and 313 page the registry candidates with usePagination and PaginationControls. skill-security/page.tsx:225-226 and 307,344 page both reviews and flagged. Remaining: the edge candidate list has no search Input. The edge model rows at line 275 are unpaged. demo-sandbox/page.tsx:296 and 557 still fixes recent at 20 and has no link to /platform-admin/demo-sandboxes.
      • Fix: Add a search Input and usePagination to the edge registry candidate list and the skill-security review/flagged lists. For demo-sandbox recent, link to /platform-admin/demo-sandboxes (which is already paged) instead of a fixed 20.

      [minor] /platform-admin/newsletter (PARTIAL, pagination)

      • Where: web/app/platform-admin/newsletter/page.tsx
      • Problem: Every active tenant is rendered as a checkbox row inside a max-h-96 scroll box (line ~260) from the unbounded /api/platform/newsletter/recipients. The search box is hidden when recipients.length <= 8 (line 236). Loading has no Skeleton. A page-based pager would be wrong here because the selection is across all rows.
      • Remaining now: Fixed: rendering is capped by usePagination(filteredRecipients,50) (page.tsx:74,265) and the search input shows whenever recipients.length>0 (:245); select-all still covers filteredRecipients (:91-95). Remaining: no Skeleton while loading (only a spinning Refresh icon, :139-140); used a pager rather than the 'Show more' the fix suggested.
      • Fix: Keep the scroll box but virtualise or cap rendering (render the first 100 and add a 'Show more' button), always show the search input, and add a Skeleton while loading. Do not add paging, because select-all must keep covering filtered rows.

      [minor] /platform-admin/safety (PARTIAL, pagination)

      • Where: web/app/platform-admin/components/sections/moderation.tsx
      • Problem: ReviewQueuePanel (items.map, line 357) renders everything from /api/platform/moderation/review-queue, which is capped at MAX_ITEMS_PER_TENANT=200 per tenant (PlatformModerationQueueService line 56) and is otherwise merged across tenants. The blocklist panel (entries.map, line 489) is unbounded. Neither has a pager, count or truncation notice. (They do have LoadState with an onRetry, which is the right error pattern.)
      • Remaining now: moderation.tsx has usePagination(items,10) plus PaginationControls with totalItems for the review queue (:333,403-406) and the blocklist (:455,522-525). The section subtitle shows data.pendingCount (:338). No server limit/offset was added and no truncation notice for the 200-per-tenant cap.
      • Fix: Add usePagination + <PaginationControls itemLabel="items" | "entries"/> to both panels, show the total in the section subtitle, and add server limit/offset to the review-queue and blocklists endpoints.

      [minor] /platform-admin/spend (PARTIAL, pagination)

      • Where: web/app/platform-admin/spend/page.tsx
      • Problem: The by-tenant table (tenants.map, line 308) renders up to 500 rows (PlatformSpendService.MAX_TENANTS=500). Truncation is handled well (meta.truncated banner at line 88, 92-day range cap). But no pager and no sort on the tenant table, so 500 DOM rows is the worst case. By-model rows are capped server-side with no UI pager.
      • Remaining now: Tenants, days and models are paged with usePagination and PaginationControls (spend/page.tsx:191-193, 324, 365, 447). Remaining: there is no sortable cost column; I found no sort code in the file.
      • Fix: Wrap tenants (and models) in usePagination and add ; add a sortable cost column.

      [minor] /platform-admin/tenants (PARTIAL, pagination)

      • Where: web/app/platform-admin/tenants/page.tsx
      • Problem: This is the model client-side page (search, status filter, cap filter, sort, PaginationControls at lines 693-701), but it paginates a FULL array fetched by api.getPlatformTenants() (GET /api/platform/tenants, PlatformController.listTenants, no page/limit). Every tenant is shipped on every refresh and page size is limited to [10, 50]. Tenant detail picker (PlatformTenantDetailController.list, tenant-detail/page.tsx 262) and newsletter recipients (NewsletterController /recipients) also return unbounded arrays.
      • Remaining now: Page-size options were extended to [10,25,50,100] (lib/usePagination.ts:14). The page still paginates a full client-side array: usePagination(filteredTenants) at tenants/page.tsx:195, fed by api.getPlatformTenants() at :243, with PaginationControls at :694. No server-side page/size/q parameters were added. The original gap said this was acceptable for now, so the remaining part is deferred work.
      • Fix: Acceptable for now. When tenants reach thousands, add page/size/q/status/sort to /api/platform/tenants (return total) and wire PaginationControls to server pages like email-suppressions. Extend PAGE_SIZE_OPTIONS in web/app/lib/usePagination.ts with 25 and 100.

      [minor] /plugins (OPEN, pagination)

      • Where: web/app/components/plugins/PluginsGallery.tsx lines 32 and 98-110; web/app/components/plugins/serverFetch.ts line 50
      • Problem: Catalog is paged client-side via usePagination(catalog.plugins) with PaginationControls and has skeleton + retryable error states, but there is no search, category or kind filter, so finding a plugin means paging. Note /plugins renders under TemplatesPublicChrome, so it is a public-style page rather than an in-console AppShell route.
      • Remaining now: PluginsGallery.tsx:32 still calls usePagination(catalog.plugins) with no search input, category chips or filter. The stale comment at line 18 still says the catalog has no filters.
      • Fix: Add a search Input and category chips above the grid and feed the filtered list into usePagination; if the console needs an authenticated plugins view, mount PluginsGallery inside AppShell as well.

      [minor] /studio (OPEN, pagination)

      • Where: web/app/studio/page.tsx lines 64, 83-93, 211-217
      • Problem: The 'recent images' strip downloads the full list via GET /api/imagegen/images (unpaged), sorts it in the browser and keeps only GALLERY_LIMIT=6, then downloads those six full-size images (no thumbnail endpoint). It is intentionally a strip with a 'View all' link to /files, so it is not a silent truncation, but it transfers the whole image index just to show six.
      • Remaining now: web/app/lib/api/media.ts:13 listGeneratedImages() still calls /api/imagegen/images with no limit/sort; studio/page.tsx:80-93 still downloads full list, sorts client-side, slices GALLERY_LIMIT=6; comment at 88 confirms no thumbnail endpoint.
      • Fix: Add ?limit=6&sort=createdAt,desc to the imagegen list endpoint (same limit/offset contract as docs/api/LIST-ENDPOINT-PAGING.md) and add a thumbnail variant; keep the existing 'View all' link.

      [minor] /templates, /templates/mine (PARTIAL, pagination)

      • Where: web/app/components/templates/TemplatesGallery.tsx lines 207-232; web/app/components/templates/MySubmissions.tsx lines 43, 197-215
      • Problem: TemplatesGallery is genuinely server-paged by ?page= but uses its own Prev/Next with 'Page N' only (no total, no page size) because the API returns a bare list; it differs from PaginationControls. MySubmissions correctly uses usePagination + PaginationControls. Both sit under TemplatesPublicChrome (public-style) rather than AppShell.
      • Remaining now: Fixed: TemplatesGallery.tsx:210-224 now uses the shared PaginationControls in hasMore mode with page-size options. MySubmissions already used the shared pager. Remaining: the API still returns no total. serverFetch.ts:94 guesses hasMore as items.length >= pageSize, so there is no 'N templates' count. Not verified: whether the chrome is still TemplatesPublicChrome.
      • Fix: Return X-Total-Count/X-Has-More from the community templates endpoint (fetchCommunityTemplatesServer in templates/serverFetch.ts) so the gallery can show 'N templates' and reuse a shared pager; until then keep the Prev/Next but align its styling with .ax-pager.

      [minor] /workflows/templates, /agents/templates (PARTIAL, pagination)

      • Where: web/app/workflows/templates/page.tsx lines 45-111; web/app/agents/templates/page.tsx lines 142-251
      • Problem: Both render client-side constant catalogs (WORKFLOW_TEMPLATES, AGENT_TEMPLATES) in full. Workflow templates has search + category chips and an empty state; Agent templates groups by audience/category with persona and WhatsApp filters but has no free-text search and no per-category 'show more', so the page is one long scroll as the catalog grows. No server pagination is needed because the data is static.
      • Remaining now: agents/templates/page.tsx has a search Input (:159) and a folded 'Show all (N)' toggle for starter templates (:76,132,197-200, preview count 8). The per-category card grids (:301-330) still render every template with no collapse. Workflow templates already had search and chips.
      • Fix: Add the same search Input used on workflows/templates to agents/templates, and collapse each category to its first 6-8 cards with a 'Show all (N)' toggle; if the catalog moves server-side, use the TemplatesGallery pattern.

      Deferred (billing)

      [major] /admin/api-keys, /admin/billing, /admin/mcp-servers, /admin/issues, /admin/maintenance (PARTIAL, layout)

      • Where: web/app/admin/api-keys/page.tsx (33 inline style props; className="row", "hint", "wf-panel"); web/app/admin/billing/page.tsx (87 inline styles); web/app/admin/mcp-servers/page.tsx (50; wf-modal, wf-modal-backdrop at 177/325; custom dialogs); web/app/admin/issues/page.tsx (23; width:40/28 at 234,256,308)
      • Problem: These pages mix the old wf-/ax-/row/hint CSS classes with inline pixel styles. Spacing, font sizes (0.72rem, 0.85rem) and borders differ from Tailwind/ui pages, so the pages look different from the console-preview target. mcp-servers builds its own modal dialogs (wf-modal) rather than components/ui/dialog or AlertDialog.
      • Remaining now: Improved: admin/billing/page.tsx now 0 inline styles (split into PlanUsageCards/InvoicesCard etc., 2 in billingShared.tsx); api-keys/page.tsx down to 8 inline styles (dialogs in ApiKeyDialogs.tsx). Remaining: admin/mcp-servers/page.tsx still has 47 inline style props and 11 wf-modal references (custom modals, not components/ui/dialog); admin/issues/page.tsx still has 16 inline styles.
      • Fix: Rebuild the form and list areas with Card, Section, Input, Label, Button, Badge from components/ui. Replace custom wf-modal with components/ui/dialog. Delete the inline style props in favour of Tailwind utilities and tokens.

      [major] /admin/api-keys, /admin/commerce, /admin/payments, /admin/agent-trust (PARTIAL, layout)

      • Where: web/app/admin/api-keys/page.tsx:368-381 (native <input type=checkbox> with inline-styled <label>; <Label> with no htmlFor for the permissions group at 363); web/app/admin/payments/page.tsx:514 (raw <button> inside table cell); web/app/admin/commerce/page.tsx and payments Input at ~line 250 has placeholder only ("amountMinorPlaceholder") with no <Label>
      • Problem: Permission checkbox group has no fieldset/legend and uses unstyled native checkboxes. The credit dialog Input (commerce) has only a placeholder and no accessible name. Raw
      • Remaining now: Fixed: api-keys/page.tsx:208-209 uses fieldset+legend for permissions; commerce/page.tsx:222 credit amount Input has aria-label; the raw
      • Fix: Use ui Checkbox with Label inside a fieldset+legend, add an aria-label or Label to every bare Input, and replace raw

      [major] /admin/billing, /admin/mcp-servers, /admin/commerce, /admin/payments, /admin/agent-trust, (PARTIAL, layout)

      • Where: web/app/admin/billing/page.tsx (1227 lines); web/app/admin/mcp-servers/page.tsx (1013 lines); web/app/components/ContactsPanel.tsx (1141 lines); web/app/admin/inbox/InboxPanel.tsx:562 (3-col grid with minmax(280px,340px) cols needs lg/xl)
      • Problem: Oversize single-file pages mix several concerns (billing: plan, usage, invoices, tickets, support; mcp-servers: servers, grants, dialogs). Each defines its own spacing and container width, so adjacent routes look different. Billing, mcp-servers and ContactsPanel exceed the 1000-line file-size ratchet from AGENTS.md. The inbox 3-column layout only collapses to one column below lg, with fixed min column widths, so it will overflow at 375px.
      • Remaining now: Fixed: billing/page.tsx is now 350 lines, split into PlanUsageCards, InvoicesCard, SupportTicketCard, ReferralCard and BillingProfileForm. admin/mcp-servers/page.tsx is 963 lines, under the 1000 limit. The inbox is now one column by default: InboxPanel.tsx:562 applies the grid only at lg and xl, the panes toggle with hidden lg:block (lines 564 and 704), and there is a back-to-list control (lines 718 and 742). Still open: components/ContactsPanel.tsx is 1141 lines, over the 1000 limit. I did not check whether it is baselined. I found no shared container-width token, and I did not check a Section-based tab split for these pages.
      • Fix: Split these into Section-based tabs (components/ui/tabs) per concern and extract subcomponents. Use one container width token. Test at 375px and make the inbox panes stack with a back-to-list pattern on small screens.

      [major] /admin/data-privacy, /admin/approvals, /admin/input-requests, /admin/guardrails, /admin/br (PARTIAL, layout)

      • Where: web/app/admin/data-privacy/page.tsx:27-30 (no useTranslations at all); web/app/admin/approvals/page.tsx:23 and web/app/admin/input-requests/page.tsx:20 (eyebrow "Administration"); web/app/admin/guardrails/page.tsx:300,378,388,394,402,413,430,437-450,493-620,655; web/app/admin/guardrails/ContentPolicyCard.tsx (i18n:0); web/app/admin/branding/page.tsx:206,218,238,244,279,372; web/app/admin/inbox/InboxPanel.tsx:535-540 (stat labels) and web/app/admin/inbox/page.tsx; web/app/admin/billing/page.tsx:895
      • Problem: Large blocks of user-facing copy are literal English in JSX (alert titles, card titles, labels, tooltips, stat tile labels). guardrails and branding import useTranslations at most once but the bodies are untranslated. data-privacy/page.tsx and ContentPolicyCard.tsx do not use i18n.
      • Remaining now: data-privacy/page.tsx now uses useTranslations("privacySso") (:24) and billing uses t() (18 calls). Still English: approvals/page.tsx:21 and input-requests/page.tsx:19 eyebrow="Administration"; guardrails/page.tsx:36 comment 'Not translated (no useTranslations)', literal 'Redact' at :298-300; ContentPolicyCard.tsx has no useTranslations; branding/page.tsx:206 'Could not load branding'; inbox/InboxPanel.tsx:535-540 stat labels literal.
      • Fix: Move strings into web/messages/en.json under the page namespaces and read them via useTranslations. Check the generated i18n-route-namespaces so each route loads its namespace.

      [major] /platform-admin/{ai-reports,jobs,migrations,mcp-review,users,audit,fleet,billing,spend,ten (PARTIAL, layout)

      • Where: ai-reports/page.tsx:152-154 and :100 (console.error only); jobs/page.tsx:25; users/page.tsx; billing/page.tsx; fleet/page.tsx; migrations/page.tsx; spend/page.tsx; funnel/page.tsx
      • Problem: Retry or try again appears only in mcp-catalog, north-star, plugins, reliability and tenants. Elsewhere a failed load shows a bare Alert (or only a console error) and the admin must find the page's Refresh button. The section-based pages handle errors per section and inconsistently.
      • Remaining now: Retry now exists on: ai-reports/page.tsx:165-170, migrations/page.tsx:84-89, mcp-review/page.tsx:174-179, and AdminErrorState with onRetry in jobs/page.tsx:137 (also used in users, audit, fleet, billing, spend, tenants). moderation.tsx has LoadState with Retry. Remaining: funnel/page.tsx:171 is still a bare Alert with no retry. budgets, fraud, safety, settings, diagnostics and abuse page.tsx have no Retry (0 matches). observability.tsx useSectionData exposes an error but has no retry UI. ai-reports and mcp-review still only console.error on the initial load failure (ai-reports:110, mcp-review:102). The ErrorState lives in components/admin/AdminKit.tsx, not platform-admin/components/ui.
      • Fix: Create ErrorState (message, Retry button) in platform-admin/components/ui; make every fetch hook expose reload(); replace the bare Alerts.

      [major] /platform-admin/{audit,users,spend,fleet,tenant-detail,billing,maintenance} and sections/m (OPEN, layout)

      • Where: audit/page.tsx:76,97; users/page.tsx:281-283; spend/page.tsx:216-218; fleet/page.tsx:143,189,229; tenant-detail/page.tsx:249,256; billing/page.tsx:187,195; maintenance/page.tsx:396; components/sections/moderation.tsx:96,110,340,451,462; checkboxes at migrations/page.tsx:159, jobs/page.tsx:125, skill-security/page.tsx:176; raw inputs users/page.tsx:134, newsletter/page.tsx:267, demo-sandboxes/page.tsx:298,316, SpendCapCard.tsx:205
      • Problem: components/Select.tsx and ui/input.tsx exist but 11 places paste long class strings onto native select, so heights differ (h-8 in spend, h-9 elsewhere). Native checkboxes are unstyled and raw inputs duplicate ui/input.
      • Remaining now: Raw remain in audit/page.tsx:82,103, spend:223, fleet:154,200,240, tenant-detail:284,291, billing:201,209 (hand-styled className), maintenance:398, newsletter:272, demo-sandboxes:298,316, users:140,297, and moderation.tsx:98,112,343,464,475. Raw checkboxes remain in jobs:130, migrations:167 and skill-security:180. app/components/ui has no checkbox, and there is no FilterBar component.
      • Fix: Replace with components/Select.tsx, ui/input and a new ui/checkbox. Wrap filter rows in a shared FilterBar (search, selects, apply, reset) with consistent sizing and wrapping.

      [major] /platform-admin/{migrations,observability,mcp-review,audit,demo-sandbox,email-suppressions (PARTIAL, layout)

      • Where: migrations/page.tsx:171; observability/page.tsx:302,521; mcp-review/page.tsx:243; audit/page.tsx:143; demo-sandbox/page.tsx:565; email-suppressions/page.tsx:125; spend/page.tsx:61; ops/page.tsx:311; jobs/page.tsx:144; fleet/page.tsx:270; impersonation/page.tsx:48; demo-sandboxes/page.tsx:294; funnel/page.tsx:246
      • Problem: Each page hand-writes th/td classes: audit, spend and demo-sandboxes use text-xs, the rest text-sm; min-widths differ (560, 640, 760, 820px, 52rem, 64rem) and some have none (audit, spend, fleet, demo-sandboxes). Other lists are div-built (ai-reports, tenants, users, billing). No sticky headers, sortable columns, th scope or caption.
      • Remaining now: platform-admin/components/admin/AdminKit.tsx:85 adds AdminTableWrap, a scroll container with role=region and tabIndex. It is used in 12 places. All 13 listed pages still hand-write raw
      markup (grep -c '<table' returns 1 or 2 for each). There is no shared Table component with sticky header, th scope or density variants, and no sortable-header work was visible. Only the wrapper was standardised.
    • Fix: Add platform-admin/components/ui/table.tsx (or reuse the console components/ui/table) with responsive wrapper, sticky header, th scope and density variants, then migrate the 14 tables. Add sortable headers on long lists (tenants, users, jobs, audit).
    • [major] All /admin/* pages (notably /admin/issues, /admin/billing, /admin/commerce, /admin/payment (PARTIAL, layout)

      • Where: web/app/admin/issues/page.tsx:222-224 (loadingText); all admin files have Skeleton count 0
      • Problem: Pages show a one-line loading string, nothing, or blank panels until data arrives, then jump. Several return null for the whole page while the scope check or fetch resolves (e.g. AdminChannelsInner Suspense fallback={null}, AdminIntegrationsPage Suspense fallback={null}). This causes layout shift and an empty-looking console.
      • Remaining now: Skeleton is now used in admin/issues (page.tsx:256), billing, commerce, payments, agent-trust, api-keys, channels (page.tsx:108 Suspense fallback), data-privacy tabs and audit-tail. Remaining: admin/integrations/page.tsx:37 still has Suspense fallback={null}. I did not audit mcp-servers or the other admin pages.
      • Fix: Use components/ui/skeleton (row skeletons for tables, card skeletons for stat tiles) and a shared Suspense fallback with the PageHeader skeleton.

      [major] all /platform-admin/* pages (PARTIAL, layout)

      • Where: web/app/platform-admin/components/AdminShell.tsx (top-bar h1 text-sm, max-w-6xl content wrapper near line 610); components/sections/common.tsx (PageHint, SectionCard); page.tsx:232, migrations/page.tsx:89,122 (hand-built stat tiles); ai-reports/page.tsx:163-166 and mcp-review/page.tsx:168 (hand-built empty states)
      • Problem: The page title is a small truncated text-sm top-bar h1 and each page opens with a PageHint paragraph, so there is no real page heading with description and primary actions. Primary actions sit in different places and forms per page: icon-only Refresh in users and billing, labelled outline Refresh in jobs, migrations, mcp-review, ai-reports, Save catalog in edge-models. Stat tiles are re-implemented in overview, migrations, ops, demo-sandbox, spend and academy-learners. Content is capped at max-w-6xl, wasting width on table-heavy pages.
      • Remaining now: Added platform-admin/components/admin/AdminKit.tsx (AdminEmptyState, AdminErrorState, AdminFilterBar, AdminTableWrap) used by ~9 pages; but AdminPageHeader (AdminKit.tsx:15) is imported by no page (mcp-review/page.tsx still uses PageHint at :7 and a small title/actions form), no shared StatCard exists (stat tiles remain hand-built), and AdminShell.tsx:614 still caps content at max-w-6xl.
      • Fix: Introduce one AdminPageHeader (title, description, actions slot), plus shared StatCard and EmptyState in platform-admin/components/ui. Standardise Refresh as the same outline button. Consider max-w-7xl or a full-width option.

      [major] /platform-admin/billing (PARTIAL, pagination)

      • Where: web/app/platform-admin/billing/page.tsx
      • Problem: Three lists, none paged. (1) Subscriptions: billingApi.subscriptions({...applied, limit: 200}) (line 157), server max 500 from a 5000-tenant scan. The subtitle says 'showing N' (line 175) but rows 201+ cannot be reached and there is no pager. (2) CompGrantsPanel (lines 244-275): billingApi.compGrants() uses the default limit 200 (server max 500), grants?.map renders everything, and nothing says the list may be cut. (3) InvoicesPanel: hard-coded limit: 50 (line 285), subtitle 'max 50' (line 299), server max 200. There is no way to see older invoices, no total count and no pager. None of the three has an error retry besides the filter form.
      • Remaining now: All three lists now use usePagination and PaginationControls via the Pager at billing/page.tsx:64-75 (lines 184, 279, 324). A capped hint is shown (lines 254, 299, 367). Remaining: the limits are still client-side caps (subscriptions 500 at line 170, invoices 100 at line 312), so rows beyond them are unreachable, there is no server offset, and invoices cannot show older ones.
      • Fix: Add page (or offset) to PlatformBillingController /subscriptions, /comp-grants and /invoices and return the total (subscriptions already returns total; comp-grants and invoices return only count=rows.size(), so add a real total). In billing/page.tsx add page/pageSize state per panel and <PaginationControls itemLabel="tenants" | "grants" | "invoices"/>, resetting page on filter change. For comp grants and invoices, show 'N of TOTAL' in the SectionCard subtitle. Remove the hard-coded 200/50 limits.

      [minor] /admin, /admin/billing, /admin/approvals, /admin/audit, /admin/users, /admin/issues, /admi (PARTIAL, layout) (done)

      • Where: web/app/admin/page.tsx (5), web/app/admin/billing/page.tsx (8), web/app/admin/issues/page.tsx (2), web/app/admin/channels/page.tsx (2), web/app/admin/approvals/page.tsx (1), web/app/admin/audit/page.tsx (1), web/app/admin/users/page.tsx (1), web/app/admin/maintenance/page.tsx (1), web/app/components/AdminAccessMatrix.tsx:96 (#fff)
      • Problem: Hex matches appear in several admin files (some may be issue-number comments like #554 or #974, so the real count needs a lint run). Real offenders include the AdminAccessMatrix #fff fallback. These break dark mode and the token rule.
      • Remaining now: A regex grep over the named admin files finds only issue-number comments such as '#472' and '#554', so no real hex literals remain there. Two literals remain in web/app/components/AdminAccessMatrix.tsx: lines 96 and 141 use var(--surface-card, #fff) fallbacks. They sit inside a token var(), but they are still a hex literal. I did not run the eslint rule.
      • Fix: Run the eslint design-tokens/no-hex-literal rule, and replace genuine colour literals with CSS variables (--card, --accent-500, --state-*).

      [minor] /changelog, /compare, /billing, /code (OPEN, layout)

      • Where: web/app/changelog/page.tsx (all strings and KIND_STYLE labels), compare/page.tsx, billing/BillingPageClient.tsx, code/page.tsx
      • Problem: These are public routes with their own chrome (vd-pub wrapper, bespoke nav), not AppShell; changelog and compare are entirely literal English (no useTranslations) and KIND_STYLE uses raw emerald/sky/amber/violet utility colours instead of semantic state tokens; /code has its own 'Back to console' chrome. A signed-in user clicking from the console lands in a different visual system with no sidebar.
      • Remaining now: web/app/changelog/page.tsx:34-38 still defines KIND_STYLE with raw emerald/sky/amber/violet utility colours and literal English labels. compare/page.tsx has no useTranslations. No in-console 'What's new' view exists: the only CHANGELOG_ENTRIES consumer is the public changelog page.
      • Fix: Offer an in-console 'What's new' view rendered in AppShell using CHANGELOG_ENTRIES (kept public page as-is), and map kind badges to ui/badge variants with state tokens.

      [minor] /insights, /usage, /billing (PARTIAL, layout)

      • Where: web/app/insights/page.tsx:267,307 (min-[480px] arbitrary breakpoint); usage/page.tsx; billing/BillingPageClient.tsx
      • Problem: insights uses non-standard min-[480px]:grid-cols-2 instead of sm:, so KPI grids break differently than every other page; usage has flex rows with min-w-0 flex-1 but no wrap fallback on narrow screens; billing mixes five max-w values.
      • Remaining now: Fixed: usage/page.tsx now wraps its flex rows (flex-wrap at lines 89, 141 and 213). Still open: insights/page.tsx:268 and :308 still use min-[480px]:grid-cols-2 instead of sm:. billing/BillingPageClient.tsx still mixes max-w-6xl, 3xl and xl (lines 149, 177, 185, 266, 281, 319). I did not check page-kit.mobile.test.tsx.
      • Fix: Use sm:/lg: breakpoints only, flex-wrap on action rows, and one container width; verify at 375px with the page-kit mobile test pattern (page-kit.mobile.test.tsx).

      [minor] /learn, /help, /roi, /edge, /coding, /capabilities/decision (PARTIAL, layout)

      • Note (0.1.8): the /learn parts are obsolete: web/app/learn/page.tsx and learn/CurriculumMap.tsx were removed (CHANGELOG [0.1.8]).
      • Where: web/app/learn/page.tsx:107, help/page.tsx:85, edge/page.tsx:20, roi/page.tsx:199
      • Problem: AppShell eyebrow is inconsistent: some pass t(...), some literal strings, and /usage and /code-intel also pass title= while others don't, so the topbar breadcrumb differs per page. Only code-intel uses the wide prop; other pages rely on unspecified container widths (code/page.tsx hard-codes max-w-6xl, billing uses max-w-3xl/4xl/6xl/xl mix).
      • Remaining now: Fixed: help (page.tsx:85), edge (:23) and roi (:195) now pass i18n eyebrows such as t('pageEyebrow'). Remaining: learn/page.tsx:107 still uses the literal with hard-coded title and subtitle strings. I did not check the per-page max-w wrappers: code/page.tsx:79 and :116 still hard-code max-w-6xl, and I did not check billing. I did not check whether AppShell's wide prop was adopted as the single width token.
      • Fix: Standardise: eyebrow always from i18n, a single content max-width token via AppShell (default and wide), and remove per-page max-w wrappers.

      [minor] /platform-admin overview, /tenants, /newsletter, /migrations, /ops, /llm-providers, /billi (PARTIAL, layout) (done)

      • Where: page.tsx:202,268-308; tenants/page.tsx:68-77; newsletter/page.tsx:280,313-314; migrations/page.tsx:122,126,189; ops/page.tsx:81,267; llm-providers/page.tsx:725,1296,1416; components/AdminShell.tsx:470,566,571; components/BrandIdentityModal.tsx (16 hits)
      • Problem: About 70 uses of text/bg/border-(emerald|amber|orange)-NNN with per-file dark: variants. The same ok/warn/bad badge class string is copied across tenants, newsletter, llm-providers and AdminShell, so contrast is not governed by tokens.
      • Remaining now: Fewer hits remain: 56 emerald/amber/orange class hits under platform-admin (about 70 originally). platform-admin/tenants/page.tsx and llm-providers/page.tsx have none, and BrandIdentityModal.tsx has none. Remaining: platform-admin/page.tsx (6), components/sections/abuse-monitor.tsx (5), templates/ConfigViewer.tsx (4), AdminShell.tsx (4), newsletter/page.tsx (3), migrations/page.tsx (3), and others. globals.css:155-174 defines --success-, --danger- and --warning tokens. platform-admin/components/ui/badge.tsx has no ok/warn/bad/info tone variant.
      • Fix: Add success/warning/danger tone tokens to globals.css and a Badge tone variant (ok|warn|bad|info) in platform-admin/components/ui/badge.tsx; delete the copied constants. Check BrandIdentityModal separately, as swatches may be legitimate.

      [minor] /platform-admin/users, /billing, /tenant-detail, /fleet, /jobs, /ops, /reliability (OPEN, layout)

      • Where: users/page.tsx:309; billing/page.tsx:89; tenant-detail/page.tsx:243; fleet/page.tsx:168
      • Problem: Some pages have an icon-only Refresh with aria-label, others a labelled button. Operational screens show no Updated N seconds ago indicator and no auto-refresh, so an admin cannot tell if data is stale. Drill-down pages have no breadcrumb or back link.
      • Remaining now: Refresh is still inconsistent. users/page.tsx:326 and billing/page.tsx:102 have icon-only buttons with aria-labels (and billing's label is a hard-coded English string), while fleet/page.tsx:180 and tenant-detail/page.tsx:279 have labelled buttons. AdminKit.tsx exports AdminPageHeader, AdminEmptyState, AdminErrorState, AdminFilterBar and AdminTableWrap but no RefreshButton. A grep of platform-admin for breadcrumb, 'updated ... ago' and lastUpdated finds nothing relevant (only an unrelated template card timeAgo).
      • Fix: Standardise a RefreshButton with a last-updated label in the AdminPageHeader actions slot; add breadcrumbs for tenant-detail and demo-sandbox.

      [minor] /platform-admin/{audit,users,billing,spend,fleet,tenant-detail,maintenance} and moderation (OPEN, layout)

      • Where: audit/page.tsx:97; users/page.tsx:281; billing/page.tsx:187,195; spend/page.tsx:216; fleet/page.tsx:189,229; tenant-detail/page.tsx:249,256; moderation.tsx:96,110,340,451,462
      • Problem: Filters have only aria-label and placeholder, so sighted users lose the field meaning once a value is chosen. Only llm-providers, maintenance, tenants and plugins use ui/label consistently.
      • Remaining now: The filters are still aria-label plus placeholder with no visible or sr-only