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).
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.
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.
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.
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.
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.
, 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
Remaining now: web/app/components/ui/ has no select.tsx (accordion, alert, badge, button, card, input, label, popover, ... only). 13 native
Fix: Add components/ui/select (wrapping components/Select.tsx) with Label wiring, then replace native
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.
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').
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.
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.
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.
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.
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.
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.
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=).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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...'.
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.
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
Remaining now: Fixed: AgentTable.tsx uses ui/table (line 10, 70), a name search Input (67), usePagination (56) and PaginationControls (130). Secondary columns hide below lg, so the min-w-[1000px] scroll is gone. Prices are paged with usePricePaging and PricesPager (roi/page.tsx:296,328,332). Remaining: unpricedModels (page.tsx:306) is still an unbounded list. PilotSection.tsx:146 and 157 still use native
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