* test(ui): characterise the old usage page before migrating it
Role- and text-based coverage of the route as it behaves on Tremor, so the
shadcn migration has a regression net it did not get to write. Pins the
DISABLE_EXPENSIVE_DB_QUERIES branch (warning copy, the docs link and its
target, and that every expensive query is skipped), the admin vs non-admin
tab set, the cost cards, and the provider and customer tables
* refactor(ui): migrate old-usage to shadcn
Replaces Tremor with the installed shadcn primitives and the shared recharts
wrappers on the only file the route owns. Tabs, cards, tables, the key select
and the tag multi-select come from src/components/ui; the bar, area and donut
charts come from src/components/shared/charts. Tremor BarList has no shared
equivalent, so Total Spend Per Team is composed from ui/meter, which also means
the per-team totals stay numbers in state instead of pre-formatted strings; a
team total of 1,000 or more used to make the bar widths NaN.
The Database Query Limit Reached warning moves with it: same copy, same docs
link, still short-circuiting every expensive query.
Drops the file's no-restricted-imports suppression and the dead customTooltip,
getTopKeys, DataDict and UserData symbols. The characterisation test from the
previous commit is unchanged and green on both sides
* test(ui): pin prompts panel toolbar and delete behaviour before migration
* refactor(ui): migrate prompts list panel to shadcn
* fix(ui): resolve prompts environment label and hold the delete dialog while deleting
* test(ui): characterise the API reference page before the shadcn migration
Pins the behaviour the migration must preserve: the three SDK tabs and their
accessible names, the default selection, that selecting a tab surfaces that
SDK's snippet wired to the resolved base url, and the title, blurb and docs
link. Written against the current Tremor markup with role and text queries so
it carries over unedited.
* refactor(ui): migrate the API reference page to shadcn
Replaces the Tremor Grid, Text and Tab primitives on the API reference route
with the shadcn Tabs primitive and token utilities, and prunes the file's now
stale no-restricted-imports suppression. Markup only; the characterisation
test added in the previous commit passes unedited.
The wrapper keeps an explicit grid-cols-1 because Tremor's Grid defaults to
numItems=1 and emitted it; the implicit auto column that replaces it sizes to
the widest child and made the code block overflow the viewport.
* test(ui): scope the API reference snippet assertion to the selected tab panel
Asserts against the rendered tabpanel instead of searching every mounted code
block, so the check keeps proving the selected tab drives the snippet even if
the panels are ever kept mounted.
* refactor(ui): migrate Tool Policies table onto the shared DataTable
Splits the old components/ToolPolicies.tsx into a data-owning panel, a thin
DataTable consumer and a getToolPoliciesTableColumns module, all under
components/ToolPolicies/. The hand-rolled tremor table, sort dropdowns and
Prev/Next pager are replaced by the shared DataTable in client mode, so
sorting, pagination and filtering now come from TanStack rather than local
state. Search moves to the toolbar global filter and the four facets (input
policy, output policy, team, key) move into a filter drawer; the facets match
exactly instead of by substring, so filtering on "trusted" no longer also
matches "untrusted"
Inline policy editing is preserved. The two policy columns still render a
PolicySelect directly in the row, with the per-row-per-column saving state and
the in-place row update kept in the panel that owns the data
The 15s live-tail poll is removed in favour of the toolbar refresh action, which
takes the auto-refresh out of the write path of the inline edits. The green
live-tail banner goes with it. The panel now reads through React Query with
window-focus and reconnect refetching disabled, so refresh stays manual; that
also removes the effect that previously needed a set-state-in-effect suppression
The metric cards, the Needs Review banner and the detail swap are unchanged.
Review still scrolls to the row when it is on screen, but no longer jumps
across pages, since the paginated order now lives inside the table
Drops the unused userRole prop threaded from the route through the view into
the table, and prunes the suppressions stranded by the file move
* fix(ui): make Tool Policies inline saves safe against concurrent edits and refresh
Two races in the inline policy editing path, both found by review.
Saving state was a single tool name per column, so starting a second row's save
re-enabled the first row while its PATCH was still in flight, and whichever
save finished first cleared the indicator for whichever row was in the slot.
Track the set of tool names currently saving per column instead, so each cell
disables and re-enables on its own request
A list fetch already in flight when a save landed would resolve afterwards and
overwrite the row with its pre-save snapshot, silently reverting a policy the
user had just changed and the server had already accepted. Cancel in-flight
queries before writing the row, which is the documented React Query ordering
for this; the stale response is then discarded and the refresh can be retried
Tightens the test helpers that hid the second bug: policy values are now
compared exactly rather than with toHaveTextContent, which substring-matches
and so let "untrusted" satisfy an assertion for "trusted"
Both tables consume the shared DataTable's controlled row-selection API, so they
move together.
Users runs fully server-side (sorting, pagination, filtering) with the page,
sort and filter state lifted to ViewUserDashboard, which now also owns the
detail-view swap that used to live inside the table component. Sort controls are
restricted to the five keys the backend accepts so a header click can no longer
send an invalid sort_by. The hand-rolled checkbox column, select-all and
selectedUsers[] are replaced by controlled rowSelection keyed by user id, and the
per-row icon strip becomes an overflow menu.
Model health checks keep client-side sorting, including the custom status and
timestamp orderings, while pagination moves to the shared footer driven by the
grandparent's page state. Selection is cleared whenever the page changes, since
the rows underneath it are swapped out.
ModelDataTable had no consumers left once HealthCheckComponent stopped using it,
so it is removed along with dead local state it carried.
* fix(ui): bundle provider logos as static imports and unify fallback in Logo component
providerLogoMap values are now content-hashed bundle URLs emitted by
static imports instead of /ui/assets/logos/ path strings, so any
deployment that serves the app JS also serves the logos: dev server,
proxy /ui mount, server_root_path sub-paths, and the split-chart nginx
image where the old route 404d in production. A missing file is now a
build error instead of a silent runtime 404.
resolveLogoSrc passes /_next/ URLs through untouched so bundled values
never get double-prefixed with the server root path. The new Logo
molecule owns resolution and the letter-avatar fallback and warns with
the failing URL on load error; ProviderLogo delegates to it. The three
bare img sites in the agents wizard render through Logo, fixing their
broken-image bug.
Dashscope now uses qwen.png, RunwayML the on-disk runway.png, and the
GradientAI entry is removed (no plausible asset exists). soniox.svg and
ai21.svg drop a single mismatched intrinsic dimension attribute that
Turbopack's import-time image parser rejects. Dead logoSrc lookup in
AddModelForm deleted. Vitest resolves image imports to Next's
StaticImageData shape via a config plugin so tests exercise the same
/_next/ URLs as production.
* fix(ui): retry logo load when src changes after an error
Track which src errored instead of a boolean so a Logo instance whose
source changes in place (agents modal title) attempts the new URL
rather than staying on the letter-avatar until remount.
* refactor(ui): migrate inline provider logo lookups to the shared Logo component
Patterns B, C, and E from the logo consolidation: every inline
providerLogoMap lookup feeding a bare img with a hand-rolled DOM
fallback now renders through Logo (credential modal, vector store
create/info views, cost tracking margin and discount forms and tables).
getProviderDisplayInfo, handleImageError, and ProviderDisplayInfo are
deleted; getProviderLogoAndName is a strict superset of the exact-match
helper. The vector store logo map no longer duplicates provider logo
paths: shared entries reference providerLogoMap and the three
vector-store-only logos become static imports. The map itself stays
because milvus and s3_vectors have no Providers enum equivalent.
Sites that rendered nothing for an unmapped provider now render the
letter avatar. Representative tests per pattern assert the rendered img
src against providerLogoMap so a wrong provider-to-enum mapping fails,
plus letter-avatar fallbacks for unmapped providers.
* fix(ui): resolve vector store slugs through the vector store logo map
The vector store info provider badge fed backend slugs like pg_vector,
milvus, and s3_vectors to getProviderLogoAndName, which only knows LLM
providers, so those stores showed a letter avatar and a raw slug. The
pre-existing inline lookup had the same wrong-domain bug via
provider_map. Reinstate getVectorStoreProviderLogoAndName resolving
through vectorStoreProviderMap first with a fallback to the LLM
resolver, so vector-store-only providers get their own logo and display
name for the first time.
* fix(ui): distinguish response cache from provider prompt caching
The log detail drawer labeled LiteLLM's response cache result as
"Cache Hit" and rendered a red "false" tag next to provider prompt
cache token counts, which read as prompt caching being broken. The
row is now labeled "Response Cache" with an explanatory tooltip,
shows a neutral "Miss" tag instead of a red one, and the prompt
cache token rows are prefixed with "Prompt Cache" and get their own
tooltips. Cost breakdown line items get the same prefix.
The Caching dashboard only reports response cache analytics but never
said so; it is renamed to "Response Cache" in the sidebar, gains a
scope description pointing to the Usage page and Logs for prompt
caching, and the ambiguous "Cached Tokens" stat card is renamed to
"Cached Completion Tokens".
* test(ui): cover renamed Response Cache sidebar item in e2e
The sidebar navigation spec now clicks the renamed "Response Cache"
item and asserts it routes to /ui/caching, and the menu label fixture
maps the new label while keeping "Caching" as a legacy alias.
Verified by running sidebar.spec.ts through run_e2e.sh (full harness:
built UI served by the proxy, seeded postgres); both tests pass.
* feat(ui): link cache tooltips and dashboard description to docs
The Response Cache tooltip links to the proxy caching docs and the
two prompt cache token tooltips link to the prompt caching docs, so
users can jump straight to the explanation of whichever mechanism
they are looking at. The Response Cache dashboard description links
both docs pages the same way.
* refactor(ui): migrate agents table onto the shared DataTable
Replace the hand-rolled tremor table inside AgentsPanel with the shared
DataTable, splitting the surface into a data-owning panel, a thin
AgentsTable consumer, and a getAgentsTableColumns definition composed
from the shared cell library.
Row delete moves from an inline icon button into the per-row overflow
menu, and the health-check toggle moves into the table toolbar since it
controls which rows the server returns. The loading skeleton is now
initial-load-only, so refetches keep the current rows on screen.
Drops the last @tremor/react import from AgentsPanel, so its
grandfathered eslint suppressions are pruned from the baseline.
* fix(ui): keep agents ordering and token changes correct in the migrated table
Sorting by created_at went through a raw accessor, and TanStack places
undefined ahead of real values, so an agent with no created_at jumped to
the top of the newest-first list. The pre-migration sort coerced a
missing date to epoch 0 and sorted it last; restore that by sorting on a
derived timestamp.
Reload the list when the access token changes rather than leaving the
previous token's rows on screen: show the skeleton for the new token,
drop the rows if that load fails, and ignore a superseded response so a
slow earlier request cannot overwrite newer rows. Refetches triggered by
delete or the health-check toggle still keep their rows.
Tests also reset the networking mocks between cases so an unconsumed
mockResolvedValueOnce queue cannot leak into the next test.
The organizations admin table was a hand-rolled tremor/antd table in a single
snake_case file. This moves it onto the shared DataTable and cell library the other
migrated tables use, splitting it into a data-owning OrganizationsPanel, a thin
OrganizationsTable consumer, and a getOrganizationsTableColumns module
The models column no longer uses a per-row accordion whose expand state lived in the
parent; it renders the shared ModelsCell with truncation and a "+N more" tooltip,
matching every other table with a models column. Row actions (Edit, Delete) move into
a per-row overflow menu gated to proxy admins, while the detail view, create modal,
and delete modal stay in the panel. The server-side org id / org alias search stays
wired to the useOrganizations hook, and the table gains an initial-load skeleton plus
a search-aware empty state. The dead sort_by / sort_order filter fields, the misnamed
"Info" column that only ever showed a member count, and an unused refresh affordance
are dropped; the default created_at descending sort is preserved
Move the admin dashboard Memory table off the hand-rolled antd
<Table> onto the shared DataTable and cell library, matching the
pattern already used by Teams, Virtual Keys, and Guardrails.
MemoryView keeps the data (server useQuery, mutations) and owns the
detail drawer, edit modal, and delete modal; it now renders a thin
MemoryTable consumer plus a getMemoryTableColumns columns file. The
server pagination moves the full PaginationState up to the parent so
the shared footer's rows-per-page selector works, the key-prefix
search runs through the shared toolbar and resets the page on change,
and per-row view/edit/delete collapse into a single overflow menu.
Sorting stays off since the backend returns updated_at DESC.
The old page-reset effect is gone (the page now resets inside the
search handler), so its react-hooks/set-state-in-effect suppression
is pruned. The detail drawer moves into its own MemoryDetailDrawer
component to keep the parent under the complexity budget.
Move the Credentials panel off its hand-rolled tremor table onto the shared
DataTable and cell library, matching the SimpleTable design and the sibling
Vector Stores / Guardrails tables. Split the panel into a modal-owning parent
(CredentialsPanel), a thin client-mode DataTable consumer (CredentialsTable),
and a CredentialsTableColumns factory.
Credential Name and Provider render as shared cells (IdentityCell + provider
logo via getProviderLogoAndName); the per-row edit/delete icons become a right
-aligned overflow menu (Edit, Copy credential name, Delete). Admin-viewer read
parity is preserved: viewers still see the list but get no actions column.
Detail/edit and delete modals stay in the parent, so the public prop stays
uploadProps only.
Drops the now-unused tremor import (pruning its eslint suppression) and the
dead antd Form handle.
* refactor(ui): consolidate Add/Edit credential modals into one CredentialModal
AddCredentialModal and EditCredentialModal were ~90% identical: the same
provider select, ProviderSpecificFields, and submit/filter logic, differing
only in title, button text, edit-mode prefill, and the disabled credential
name. Replace both with a single CredentialModal driven by a mode: 'add' |
'edit' prop, and point the two call sites in credentials.tsx at it.
Removes ~120 lines of duplication and drops the no-explicit-any and
no-restricted-imports baselines. The two per-file tests merge into one
CredentialModal.test.tsx covering both modes (add: editable empty name;
edit: prefilled, disabled name; provider fields render).
* refactor(ui): derive credential name disabled state from mode, not data
The disabled flag on the credential name field was tied to whether
existingCredential?.credential_name is truthy, an artifact of the old
EditCredentialModal. Drive it from the isEdit flag like the rest of the
component so mode='add' with a stray existingCredential can't disable the
field and mode='edit' with an empty name can't leave it editable. Behavior
is unchanged for real call sites; adds a regression test for the edit-with-
empty-name case.
* refactor(ui): prefill credential form declaratively instead of via useEffect
The edit-mode form was seeded with an imperative form.setFieldsValue inside
a useEffect that also set React state (setSelectedProvider), an antd anti-
pattern carried over from the old EditCredentialModal. Both call sites mount
the modal fresh with existingCredential already present (conditional && plus
destroyOnHidden), so there is no 'prop arrives after mount' case to handle.
Replace it with antd's declarative initialValues on the Form and a lazy
useState initializer for the provider. Removes the effect, its
react-hooks/set-state-in-effect suppression and exhaustive-deps warning, and
one any cast; behavior is unchanged (edit now shows the real provider on
first paint instead of flashing the default). Existing tests cover prefill
and the disabled name field.
The value cell was a ternary chain over field_type; adding Select made it a fourth
level and tripped no-nested-ternary. Early returns read better than a deeper chain
and let the suppression baseline ratchet down.
* refactor(ui): migrate AI Hub, public hub, and MCP Toolsets tables onto shared DataTable
* test(ui): stub skillHubPublicCall in the public model hub networking mock
Migrate Projects, Project Keys, Vector-store Documents, Logging
Callbacks, and Pass-through Endpoints onto the shared DataTable and
cell library, each split into a thin container plus a columns file.
Row actions move into the unified overflow menu, empty states get the
rich icon+title+body treatment, and parents that never tracked loading
gain an initial-load-only isLoading flag so tables render skeletons
instead of flashing the empty state.
Project Keys switches from a hand-rolled antd Pagination header to
server pagination through the shared footer, and its section is
rewired into ProjectDetailsPage, which had regressed to a hardcoded
"No keys to display" empty card while the section sat orphaned.
pass_through_settings.tsx moves to PascalCase under
components/PassThroughSettings/ and drops its dead modelData prop and
tremor imports; its stale eslint bulk-suppressions are pruned.
Rewrites the three Batch A tables from hand-rolled TanStack + tremor renderers
into thin DataTable consumers with a separate ColumnDef module each, matching
the Guardrails and Tags migrations. Row actions move into a per-row overflow
menu (edit/copy/delete for vector stores; copy for everyone plus admin-gated
delete for prompts and skills). The vector stores parent gains an isLoading
flag resolved on every exit path so the table shows the shared skeleton
instead of flashing the empty state. Table files are renamed to PascalCase
and stale eslint bulk-suppressions for the rewritten files are pruned.
* feat(ui): migrate guardrails table onto shared DataTable
Move the guardrails list onto the shared DataTable + cell library as the
proof-of-concept for the simple-tables design migration, following the Teams
reference pattern.
Split the table into a thin container (guardrail_table.tsx) and column defs
(guardrailTableColumns.tsx): client-side sort defaulting to created_at desc, a
search + refresh toolbar, IdCell / DateCell / StatusBadge cells, real provider
logos, a rich empty state, and skeleton loading rows. Row actions move into a
per-row overflow menu; deletion stays disabled for config-file guardrails, now
surfaced as a disabled menu item instead of a greyed trash icon. Detail view
and the delete modal remain owned by GuardrailsPanel.
Restyle the "Add New Guardrail" control to the shared Button + dropdown menu.
Update the regression tests for the menu-based actions and drop the now-stale
eslint suppression entry that the rewrite eliminated.
* fix(ui): match guardrails table to the design
Address design-review feedback on the guardrails migration:
- Drop the search + refresh toolbar. The original table had neither and the
SimpleTable design has no toolbar; the container now just renders the sorted
table and its empty state.
- Give the Guardrail ID cell the design's hover affordance by rendering it with
the shared IdentityCell (monospace, chevron on hover) instead of the blue
IdCell pill.
- Stop pinning the actions column. Pinning added a sticky divider that the
design and the Teams table don't have; it is now a plain right-aligned menu
column, matching Teams.
* fix(ui): match loading skeleton row height to loaded rows
The compact skeleton row did not carry the h-8 height that real compact
rows get, so loading rows rendered shorter than loaded ones and the table
height jumped when data arrived. Mirror the same size-based height on the
skeleton row in the shared DataTable so every compact table loads at a
stable height
* test(ui): drop stale onGuardrailUpdated from guardrails table baseProps
The prop was removed from GuardrailTableProps when the toolbar went away;
the test baseProps still listed it. Harmless at the call site since it is
spread rather than an object literal, but dead and worth removing
* fix(ui): remove dead edit_guardrail_form after guardrails migration
The guardrails table migration dropped the last import of EditGuardrailForm,
which knip flags as an unused file. The form was already unreachable before
the migration: the table wired a delete button only, and nothing ever called
handleEditClick to open the modal, so the import was the sole thing keeping
the file referenced. Delete it and prune its now-stale eslint suppression
entry. Guardrail editing is unchanged and lives in the detail view
(GuardrailInfoView)
* feat(ui): rebuild the Teams table on the shared DataTable
The Your Teams tab moves off the Ant Design table onto the shared DataTable that the Virtual Keys page uses, following the new dashboard design. It gains server-side sort, pagination and filtering, a toolbar with a filter drawer and a columns menu, and a per-row actions menu
Sorting is wired only to the columns /v2/team/list can actually order by (team_alias, created_at); Spend / Budget and Updated stay unsorted because the endpoint silently ignores those fields. The design's "Created by" column is dropped since the team object has no such field, and the drawer's "Has keys" filter is dropped for the same reason. The Resources cell shows members, models and keys as colored pills, and the actions menu keeps the existing Edit, Copy team ID and Delete behaviors, with Edit and Delete gated to Admin
The teams grid gets its own unit tests in TeamsPage/TeamsTable.test.tsx. Teams.tsx keeps the create-team modal, delete modal, detail view and tabs, now refreshing the list through React Query invalidation instead of a manual refetch
* fix(ui): match Teams loading skeletons to the rendered row height
The default twoLine and chips skeleton shapes rendered the Team and Resources cells shorter than the loaded row (a real row measures 55px, the old skeleton ~49px), so the loading state looked visibly squat. Give the Team column a custom renderSkeleton that mirrors the two-line IdentityCell (measured 54px) and the Resources column one that mirrors the pills, and mark the hidden Rate Limits column as two-line so it matches when shown
* fix(ui): keep team admins' Members tab by deriving is_team_admin from the selected team
The redesign computed is_team_admin from useTeam(selectedTeamId), but that hook returns teamInfoCall's nested { team_info: { members_with_roles } } shape, so the top-level members_with_roles read was always undefined and is_team_admin was always false. For a non-proxy-admin team admin that hid the Members, Member Permissions and Settings tabs in the team detail view, which broke the team-admin add/remove member e2e tests. Pass the Team object up from the table instead (/v2/team/list returns it with a top-level members_with_roles), matching the pre-redesign behavior; proxy admins were unaffected because is_proxy_admin already granted access
Also point the Delete-a-team e2e at the new kebab: open the row actions menu, then click Delete team, rather than clicking the old inline delete icon
* refactor(ui): standardize debounce waits behind shared DEBOUNCE_WAIT_MS constant
* build(ui): bump @tanstack/react-pacer from 0.2.0 to 0.22.1
* refactor(ui): migrate straightforward value debounces to react-pacer
* refactor(ui): migrate callback debounce sites to react-pacer with regression tests
* chore(ui): restore trailing newline in eslint-suppressions.json
* test(ui): mock all pacer debounce hooks in VirtualKeysTable test
* fix(ui): update merged debounce tests for OldTeams to Teams rename
* feat(ui): rebuild the Virtual Keys table on the shared DataTable
Replaces the hand-rolled Tremor table and bespoke toolbar/pagination on the admin
Virtual Keys page with the shared DataTable: server-side sort, paginate, and
filter, a sticky scrolling body, a search plus column-visibility plus filters
toolbar, a right-side filter drawer, and a rows-per-page footer. A page header
with the existing key icon carries the Create New Key action.
Adds reusable, shadcn-default building blocks for the tables migrating onto the
DataTable next: shared IdentityCell, ModelsCell, and SpendBudgetCell in
shared/table_cells, plus a shared PageHeader. The models cell reveals overflow in
a hover tooltip and the spend/budget cell uses the Meter primitive.
All data and domain logic is preserved, including the useKeys query, team and org
alias resolution, the user popover, and the KeyInfoView detail swap. The rich
async Team/Org/Alias filters move into the drawer, and the toolbar search maps to
the key-alias substring search. Status now also reflects key expiry alongside
blocked and SCIM-blocked.
The VirtualKeysTable tests are updated to the new markup and extended with focused
coverage for each new shared cell
* fix(ui): address Virtual Keys redesign review feedback
Fold the status badge into the clickable Key cell and drop the separate Status
column so a key's alias, secret, and status read as one unit. The Key cell is
now the single click target that opens the key detail; the whole-row click is
removed
Migrate the filter drawer off AntD to shadcn. A new Combobox composed from
Popover and Input backs the Team, Organization, and Key Alias filters, keeping
search and the alias infinite-scroll
Show $0.00 for zero spend instead of a hyphen, and extend the shared DataTable
with badge, chips, and meter skeleton shapes so the loading state matches the
loaded cells (status pill, model chips, spend meter) rather than uniform bars
Fix key sorting: the Key column sent its column id "key" as sort_by, which
/key/list rejects with 400. It now sorts by the backend field key_alias
* fix(ui): use the shadcn base combobox and refine the keys filters and skeletons
Replace the hand-rolled filter combobox with the supported shadcn Base UI combobox
(ui/combobox, added via the CLI and reused through a small SearchSelect wrapper).
Its vended input-group and textarea deps are written for React 19 (plain functions
with ref-as-prop); this app is on React 18, where those subcomponents drop the refs
Base UI passes for focus and anchoring, so InputGroupInput, InputGroupButton, and
ComboboxTrigger are adapted to forwardRef. Those ui/ files now diverge from the
registry, and a future shadcn add would overwrite the adaptation until the app moves
to React 19. Adds class-variance-authority, which input-group needs
Give loading skeletons a per-column renderSkeleton escape hatch on the shared
DataTable and mirror the Key cell exactly (alias line, secret, status pill), so
skeleton rows match the real rows instead of being shorter and simpler
Resolve the automated review: the toolbar search and the drawer Key Alias filter
both mapped to the key-alias query, so the search silently overrode the drawer value
while its chip stayed visible. Consolidate to a single alias search in the toolbar
(placeholder now "Search by key alias…") and drop the redundant drawer field. Re-add
coverage for the Created By column's alias-over-email display
Refine the Team and Organization filters: they match on name and id, so the labels
read "Team" and "Organization" rather than "... ID", each option shows the name with
the id on a muted second line instead of "name (id)", and the active-filter chip
shows the friendly name
* chore(ui): drop duplicate class-variance-authority, use the repo cva package in input-group
* refactor(ui): rename OldTeams component file to Teams
* fix: show and allow editing team model aliases after team creation
* fix(ui): mark team model_aliases as nullable to match the prisma schema
* refactor(ui): convert endpoint usage charts to shadcn/recharts
Adds a LineChart wrapper to the shared charts kit, mirroring the
BarChart/AreaChart composition with connectNulls and curveType props,
and converts EndpointUsageBarChart and EndpointUsageLineChart from
tremor to the shared wrappers. Both endpoint chart tests now assert on
real recharts SVG output instead of tremor mocks.
* refactor(ui): drop unused endpointData prop from EndpointUsageLineChart
* fix(ui): point endpoint chart test type imports at the UsagePage types alias after colocation move
* refactor(ui): colocate the usage view, keeping the shared usage components
Split for the usage (UsagePage) segment. Most of the folder is the usage page's
own view, but four pieces are reused elsewhere and stay in @/components/UsagePage:
TopKeyView (old-usage), KeyModelUsageView and value_formatters (activity_metrics),
and the shared types (activity_metrics, chartUtils). The other 21 files move into
usage/_components, preserving the folder structure.
The external consumers import only the retained files, so they are untouched. The
moved files' imports of the retained files become @/components/UsagePage paths,
other escaping relative imports are absolutized, and lint suppressions are re-keyed
for moved files only. No behavior change.
* refactor(ui): colocate the mcp-servers view, keeping the shared mcp_tools surface
Split for the usage (UsagePage) segment. Most of the folder is the usage page's
own view, but four pieces are reused elsewhere and stay in @/components/UsagePage:
TopKeyView (old-usage), KeyModelUsageView and value_formatters (activity_metrics),
and the shared types (activity_metrics, chartUtils). The other 21 files move into
usage/_components, preserving the folder structure.
The external consumers import only the retained files, so they are untouched. The
moved files' imports of the retained files become @/components/UsagePage paths,
other escaping relative imports are absolutized, and lint suppressions are re-keyed
for moved files only. No behavior change.
* refactor(ui): convert entity usage and usage page charts to shadcn/recharts
Swap the tremor BarChart/DonutChart render sites in EntityUsage,
SpendByProvider, TopKeyView, TopModelView, KeyModelUsageView and
UsagePageView to the shared shadcn/recharts wrappers. Convert the two
sole-chart Daily Spend cards and the KeyModelUsageView card to the
shadcn Card primitives.
Close the donut parity gap with strictly additive optional DonutChart
props: showLabel/label render a center total (tremor showed
valueFormatter(sum) by default) and startAngle/endAngle forward to the
Pie so both provider donuts keep tremor's clockwise-from-12 layout.
Defaults preserve the previous wrapper behavior.
DailyData and two site-local row types move from interface to type
alias so they satisfy the wrappers' Record<string, unknown> constraint;
interfaces lack implicit index signatures.
Tests now assert on real recharts output: bar/sector counts, cyan
fills, axis labels, donut center totals, and the TopKeyView bar-click
drill-down into the key info modal. The dead tremor chart mocks in
UsagePageView.test.tsx are removed and lint metrics/suppressions are
regenerated for the dropped tremor imports.
* fix(ui): compute donut center label only when shown and assert Model Usage renders as a card title
* fix(guardrails): filter Add-Guardrail mode dropdown per provider
The GET /guardrails/ui/add_guardrail_settings endpoint returned every
GuardrailEventHooks value in one flat supported_modes list, so the Admin
UI rendered pre_mcp_call as a selectable Mode for every guardrail. Saving
Content Filter or Tool Permission with pre_mcp_call then failed with a
400 because those guardrails' server-side supported_event_hooks list
excludes it.
Expose each guardrail's supported hooks as a get_supported_event_hooks
classmethod on CustomGuardrail (mirrors the existing get_config_model
pattern) and have the endpoint iterate guardrail_class_registry to build
a supported_modes_by_provider map. The UI Mode dropdown filters by that
map when the selected provider is known and falls back to the global
list otherwise. __init__ now sources its own supported_event_hooks list
from the classmethod so the two sides can't drift.
Also register BedrockGuardrail, ToolPermissionGuardrail, lakera,
lakera_v2, and presidio in guardrail_class_registry so they participate
in the map (they were previously only in guardrail_initializer_registry
and had no class-registry entry).
Behavior change: guardrails that previously had no supported_event_hooks
declared (aim, javelin, azure/text_moderation, cato_networks,
crowdstrike_aidr, headroom, hiddenlayer, lasso, noma, onyx,
prompt_security, qualifire, repelloai, zscaler_ai_guard, aporia_ai,
lakera_ai, lakera_ai_v2, mcp_jwt_signer, model_armor, presidio) now
validate the configured mode at instantiation. Existing configs where
the mode was silently a no-op will fail at proxy startup with a clear
validation error rather than running as a broken guardrail.
Resolves LIT-4226
* fix(guardrails): add LITELLM_STRICT_GUARDRAIL_MODES escape hatch, preserve current mode in edit form
Address Greptile P1 (startup break) and P2 (edit form UX):
LITELLM_STRICT_GUARDRAIL_MODES defaults to true (raise on unsupported
event_hook, unchanged behavior for the guardrails validated pre-PR).
Setting it to false logs a warning and continues, giving deployments an
opt-out while they fix configs that now surface as errors instead of
silently no-op'ing. Regression test covers both modes.
Edit form now surfaces the currently-saved mode even when it is not in
the filtered per-provider list, so a legacy row (e.g. content_filter
saved with pre_mcp_call before this fix) no longer disappears from the
dropdown; the option renders with a 'not supported by <provider>' note
so the user knows to pick another.
* fix(guardrails): correct audited hook lists, prune stale modes on provider switch, clean form lint
Audited every get_supported_event_hooks classmethod against the hooks
each guardrail's own tests exercise and its handler methods. Five were
too narrow and their tests caught it in CI: rubrik gains pre_call,
presidio gains during_call and pre_mcp_call, prompt_security, onyx and
qualifire gain during_call. The remaining classes match either their
original __init__ declarations or their exercised modes exactly.
Cursor review fixes: the Add form now drops selected modes the new
provider does not support when the user switches providers, so a
pre_mcp_call selection cannot ride along into a provider that rejects
it at save; the edit form handles list-shaped stored modes instead of
treating mode as always a string.
Extracted shared toModeArray and getSupportedModesForProvider helpers
into guardrail_info_helpers so both forms use one implementation, typed
the remaining any usages in both forms, removed nested ternaries, and
committed the ratcheted-down eslint metrics and pruned suppressions
* refactor(ui): colocate the skills view, keeping the AIHub-shared skill components
Group-1 split for the skills (claude_code_plugins) segment. It is a file-plus-
folder combo where part of the folder is shared with AIHub and the public model
hub: the skill types, skill_detail, MakeSkillPublicForm, and helpers stay in
@/components/claude_code_plugins (imported by AIHub's ModelHubTable and
SkillHubDashboard, plus public_model_hub, networking, and skill_hub_table_columns),
while the plugins-management view moves to skills/_components.
claude_code_plugins.tsx becomes skills/_components/index.tsx; add_plugin_form and
plugin_table move alongside it. The moved files' imports of the retained shared
files become @/components/claude_code_plugins paths, other escaping imports are
absolutized, and lint suppressions are re-keyed for moved files only (the staying
components keep their src/components keys). The shared components did not move, so
their consumers are untouched. No behavior change.
* refactor(ui): name the agents/guardrails view files instead of index.tsx
The moved view files were renamed to _components/index.tsx, which reads like a
barrel of re-exports rather than the single view component each actually is.
Rename them to match their default export (AgentsPanel.tsx, GuardrailsPanel.tsx)
and their colocated tests, and point page.tsx at the named file.
* refactor(ui): colocate agents and guardrails views, keeping shared selectors and types
The last two group-1 colocation splits. Both are file-plus-folder combos: the
page view is a top-level file (agents.tsx / guardrails.tsx) sitting beside a
supporting folder of the same name, and part of that folder is shared.
agents: agents/types (imported by the agents hook) stays in @/components/agents;
agents.tsx and the rest of the agents/ folder move into agents/_components (the
view file becomes _components/index.tsx).
guardrails: GuardrailSelector (imported by the Playground, the key edit view,
and the agents add-guardrail form) and types stay in @/components/guardrails;
guardrails.tsx and the other 47 folder files (including the tool_permission,
custom_code, content_filter, and llm_judge subfolders) move into
guardrails/_components.
Moved files' imports of the retained shared files become @/components paths,
other escaping relative imports are absolutized against @/, and moved test
files have their from/vi.mock/vi.importActual paths rewritten to match. Lint
suppressions are re-keyed for moved files only (the staying selector/types keep
their src/components keys). The shared selectors did not move, so their external
consumers are untouched. No behavior change.
* fix(ui): move the orphaned agents/guardrails view tests and drop dead GuardrailItem
Greptile follow-ups on the agents/guardrails colocation:
- agents.tsx and guardrails.tsx moved to their _components/index.tsx, but their
sibling test files (src/components/agents.test.tsx, guardrails.test.tsx) were
left behind still importing ./agents and ./guardrails, which broke a full
vitest run. Move them to the matching _components/index.test.tsx and rewrite
their view import to ./index, folder mocks to local ./ siblings, and
networking to @/components/networking.
- Remove the unused GuardrailItem interface and its GuardrailDefinitionLocation
import from the guardrails view (dead code carried over from before the move;
state is typed as Guardrail[]).
* refactor(ui): colocate the policies view, keeping PolicySelector shared
Group-1 colocation split. The policies folder lived in the shared src/components
dump but is only partly shared: PolicySelector (used by the Playground,
ComplianceUI, and key edit view) and its types stay in @/components/policies,
while the policy-management view (27 files: index, tables, forms, modals,
pipeline builder, and their tests) moves to policies/_components.
Moved files' imports of the retained shared files become @/components/policies
paths; other escaping relative imports are absolutized against the @/ alias
(including @/data/... and the repo test-utils via @/../tests/...), and moved
test files have both their `from` imports and `vi.mock` paths rewritten so the
mocks keep matching the source. Grandfathered lint suppressions for moved files
are re-keyed. PolicySelector did not move, so its external consumers are
untouched. No behavior change.
* fix(ui): keep PolicySelector.tsx eslint suppression at its original path
PolicySelector.tsx stays in src/components/policies (only the management view
moved to _components), but the suppression re-key wrongly moved its
no-nested-ternary entry to the phantom _components path, orphaning the real
file's suppression. Revert that one key. (Greptile P1 on #32720.)
Two of the group-1 colocation splits. Each of these folders lived in the shared
src/components dump but is only partly shared: the page's management view is
segment-owned, while a selector widget is reused by other features. So this
splits them rather than moving wholesale.
tag-management: TagSelector (used by playground) and its types stay in
@/components/tag_management; the management view (index, tag_info, TagTable,
CreateTagModal) moves to tag-management/_components.
vector-stores: VectorStoreSelector (used by organizations and playground) and
its types stay in @/components/vector_store_management; the rest of the
management UI moves to vector-stores/_components.
The moved files' imports of the retained shared files are rewritten to absolute
@/components paths, escaping relative imports are absolutized, and moved test
files have both their `from` imports and `vi.mock` paths rewritten to match.
Grandfathered lint suppressions for moved files are re-keyed. The external
consumers of the selectors are untouched (the selectors did not move). No
behavior change.
Renames each segment's local components/ folder to _components/ (private to the
route, matching Next's _ route-exclusion). Both folders are imported only by
their own page.tsx via the folder index (verified zero external importers
across src, tests, and e2e_tests), so each is a straight rename plus repointing
that one index import; a folder rename keeps every file at the same depth, so
all internal and relative imports are unaffected.
Grandfathered lint suppressions under the two folders (31 entries: cost-tracking
15, prompts 16) are re-keyed to the new paths with counts unchanged. No behavior
change.
Colocation follow-up to the App Router migration: move each page's owned
components out of the shared src/components dump and into its route segment's
_components/ folder, draining the shared bucket. Convention: a component used
by exactly one segment goes in that segment's _components/ (private, matching
Next's _ route-exclusion); a component shared by 2+ segments stays in
@/components. No new _shared/ folder.
Rename-in-place (segment already had a local components/ folder):
- api-reference (also relocates the shared CodeBlock, used by playground and
cost-tracking, to @/components/CodeBlock)
- memory, budgets, access-groups
- caching, projects, guardrails-monitor
Extract from src/components (page view lived in the shared dump):
- AdminPanel -> admin-panel, organizations -> organizations,
general_settings -> router-settings, usage -> old-usage
Each folder/view was verified to have no importer other than its own page
(cross-checked across src, tests, and e2e_tests). Relative imports inside moved
single files are rewritten to absolute @/components/*; colocated tests move with
their subject and have their vi.mock paths rewritten to match. Grandfathered
lint suppressions (tremor, react-hooks, and similar, all pre-existing) are
re-keyed to the new paths with counts unchanged. No behavior change.
Second proof-of-concept consumer for the shared DataTable. Replaces the
hand-rolled tremor table in the Team Info Virtual Keys tab with DataTable in
server-sort and server-pagination mode plus column resizing; the file drops
about 150 lines. Sortable headers now use DataTableSortHeader, pagination is
a detached DataTablePagination driven by the page state, the id-cell still
opens the key drawer, and the body scrolls under a sticky header via
maxBodyHeight. Two behavior changes: the pagination control is the
standardized bar (row range plus page-size select) rather than the old
Previous/Next buttons, and a sort header cycles ascending/descending without
a third unsorted state, which also removes a latent case where clearing the
sort left the server sorted.
Updates the TeamVirtualKeysTable and TeamInfo tests to the new pagination,
adds a test that a sort-header click routes to useKeys as a server sort, and
lowers the no-large-inline-object-arg metric by one and the file's
no-nested-ternary suppression from two to one to match the leaner code.
* feat(ui): add shared table_cells kit and convert logs columns
DateCell, MoneyCell, IdCell and StatusBadge consolidate the duplicated
per-table cell implementations behind one component each. The logs page
columns are the reference conversion; the dead auditLogColumns export
(superseded by audit_logs.tsx) is removed with it
* refactor(ui): consolidate table cells onto the shared table_cells kit
106 cell sites across 44 table files converge onto DateCell, MoneyCell,
IdCell and StatusBadge, replacing 8 date formats, 6 spend formats, 7 id
truncation strategies and 6 status badge styles with one implementation
each. Badge now forwards refs so Base UI tooltip triggers composed over
it can attach (they previously never opened under React 18). TimeCell
is deleted; its two consumers now render DateCell
* fix(ui): suppress cost tooltip for zero spend and drop dead getStatusBadge param
The logs Cost tooltip showed the raw $0 over a "-" cell for zero or
null spend (pre-existing, surfaced by review); the tooltip now only
renders when there is a real amount. healthCheckColumns no longer takes
the unused getStatusBadge callback and its dead definition is removed
* fix(ui): restyle StatusBadge as tinted pill matching the prior antd Tag look
* fix(ui): keep StatusBadge fully rounded like the other kit pills
* feat(ui): add eslint rules for nested ternaries, large inline object args, and long condition chains
Adds three dashboard lint rules to keep new code readable. Nested ternaries
are banned outright via the built-in no-nested-ternary, with the 265 existing
occurrences grandfathered in eslint-suppressions.json so only new ones fail.
Two custom rules ship as a small local plugin under scripts/eslint-rules:
no-large-inline-object-arg flags object literals with 4+ properties passed
straight into a call, nudging toward a named variable, and no-long-condition-chain
flags boolean expressions that combine 4+ conditions, nudging toward a named
boolean. Both are warnings tracked on the existing budget ratchet
(eslint-budgets.json + eslint-metrics.json) with headroom above the current
counts, so they ratchet down over time rather than freezing a baseline. Both
thresholds are configurable rule options and covered by RuleTester unit tests.
* fix(ui): scope no-long-condition-chain to boolean operators, not nullish
Greptile flagged that the rule counted nullish-coalescing chains the same as
&&/|| chains, so a 4-part `a ?? b ?? c ?? d` fallback surfaced "Boolean
expression combines 4 conditions", which is inaccurate since a `??` fallback
is value defaulting, not a condition. Restrict the visitor to && / || nodes so
`??` chains are treated as leaves, while a boolean chain nested inside a `??`
is still caught. Drops 6 miscounted occurrences (240 -> 234).
* chore(ui): sync lint metrics and suppressions with staging
Merge advanced the base branch, adding one no-large-inline-object-arg
occurrence (508 -> 509) and making one grandfathered react-hooks suppression
stale. Regenerate eslint-metrics.json and prune the suppression so the
budget/drift gate passes.
* chore(ui): sync lint metrics with staging
Merge advanced the base, adding four no-large-inline-object-arg occurrences
(509 -> 513). Regenerate eslint-metrics.json so the drift gate passes.
* test(ui): characterize DataTable behavior before shadcn reskin
Pins the shared view_logs DataTable contract with library-agnostic
queries ahead of the tremor-to-shadcn table migration: loading and
empty states, TanStack column defs with custom cell renderers,
onRowClick payload, both expansion render paths (colspan sub-component
and sibling child rows), the getRowCanExpand gate, and client-side
sorting on and off. These must pass unchanged after the reskin.
* refactor(ui): reskin shared DataTable from tremor onto shadcn table primitives
Swaps the view_logs DataTable's presentational layer from @tremor/react
to the in-repo components/ui/table primitives and hardens the seam that
every later table migration copies:
- getRowId is injected instead of hardcoded to request_id through an
any cast; identity defaults to the row index and the logs page now
passes request_id explicitly, keeping expansion state attached to the
right row across refetch reorders
- one expansion render path: renderChildRows had zero consumers and is
removed; renderSubComponent (colspan cell) is the single path
- the four consumers passing dead no-op renderSubComponent and
getRowCanExpand boilerplate drop it
- loading and empty defaults become generic (Loading... / No results)
instead of log-specific
The characterization tests from the previous commit pass unchanged
except the dead child-rows path test, replaced by a reorder-stability
test for injected getRowId plus coverage of the new generic defaults.
First tremor removal of the tables track; view_logs/table.tsx no longer
imports @tremor/react.
* test(ui): assert child rows hidden before expansion in DataTable test
* fix(ui): suppress row hover on DataTable placeholder rows
* feat(ui): polish DataTable with skeleton loading, header band, and numeric column alignment
* feat(ui): shape DataTable skeletons per column and keep stale rows during refetch
* revert(ui): drop DataTable skeleton loading, restore text loading row
* fix(ui): clip DataTable to its rounded wrapper and right-align Duration/TTFT values
* feat(ui): shadcn migration foundation: Tailwind v4, shadcn init, antd cascade fix
Upgrade the dashboard from Tailwind v3 to v4 with CSS-first config: the
official upgrade codemod renamed utilities across 151 files, and
tailwind.config.js (plus the dead tailwind.config.ts) is replaced by
@theme tokens, @source globs, and @plugin directives in globals.css. The
Tremor safelist becomes @source inline patterns and the legacy tremor
theme tokens carry over verbatim. ui_colors.json was build-time only and
fed the dying Tremor palette, so its brand values are inlined and the
file removed; runtime theming replaces that path next.
shadcn is initialized with a hand-authored components.json (rsc,
cssVariables, baseColor gray) pointing utils at the existing
lib/cva.config.ts, which now exports cn (cva beta cx + twMerge) instead
of adding class-variance-authority as a second variant library. The two
ad-hoc cn helpers fold into it. Button lands as the canary primitive,
adapted to cva beta and React 18 forwardRef, with tests covering the
variant, twMerge, asChild, and ref seams. --radius is 0.5rem so the
shadcn radius scale reproduces Tailwind defaults and legacy rounded-*
classes render unchanged.
antd v5 emits unlayered CSS-in-JS that would beat every layered v4
utility, so AntdGlobalProvider now wraps the app in StyleProvider layer
and ConfigProvider cssVar, and globals.css declares
@layer theme, base, antd, components, utilities. antd wins over
preflight but yields to utilities, which is what lets migrated shadcn
pages coexist with legacy antd pages. Preflight stays global with the
three v3 behaviors pinned (default border color, button cursor,
placeholder color).
* fix(ui): restore tremor opacity tints removed by tailwind v4
Tailwind v4 removed the *-opacity-* utilities, but the precompiled
@tremor/react dist still composes them with shade-500 palette classes
(bg-opacity-10 over bg-<color>-500 etc.), so Badge, BadgeDelta, Callout,
light Icon and Button, BarList, and ProgressBar lost their tints and
rendered solid 500-shade fills. Adversarial review caught it; the
original smoke pages only exercised antd Tags.
tremor-v3-compat.css restores exactly the pairs tremor emits: for each
of the 22 safelisted colors, bg-opacity-{10,20,40}, hover/group-hover
bg-opacity-{20,30}, and ring-opacity-{20,40} against the -500 shade,
via color-mix into the utilities layer. Tremor's colorPalette maps both
background and iconRing to 500, so the -500 pairing covers every
composition in the dist; dark: variants are inert until dark mode ships.
The shim dies with @tremor/react at the end of the migration.
The upgrade codemod also missed two hand-rolled modal scrims using
bg-black bg-opacity-{30,50} (solid black under v4); now bg-black/30 and
bg-black/50. Removed the docker/build_admin_ui.sh copy of
enterprise_colors.json into the deleted ui_colors.json; that build-time
rebrand path is retired and its runtime replacement lands with the
theming phase.
* fix(ui): pair ring-opacity-40 with shade 300 in tremor compat shim
Tremor's colorPalette maps ring to shade 300, and the only consumer of
ring-opacity-40 (Icon variant outlined) composes it with that shade,
so the shade-500 rows were dead and outlined icon rings would render
at full opacity. Latent today (no dashboard usage of the outlined
variant); caught by adversarial review. ring-opacity-20 stays at 500
(iconRing), matching Badge and BadgeDelta.
* refactor(ui): drive cache settings form from a typed frontend schema
The Cache Settings form was dynamically generated from field metadata
shipped by the backend, and read its values back out of the DOM with
document.querySelector. That loses type safety and makes client-side
validation awkward, which is a poor fit for a form whose shape only
changes when a developer edits code.
Move the field definitions (name, label, type, default, help text, which
redis type they apply to, section, and validation rules) into a typed
frontend module and render them through antd Form with controlled state.
The GET /cache/settings endpoint is still used to populate current values,
and the save/test payload shape sent to POST /cache/settings and
/cache/settings/test is unchanged. Per-field validation now lives on each
field's antd rules, so an inline error can surface before and on submit;
this is where the upcoming Redis URL validation will slot in.
The backend's fields output in GET /cache/settings is no longer consumed
by the UI, but is left in place since removing it is a separate backend
change.
* refactor(ui): validate list-field JSON inline so bad input blocks save
sentinel_nodes and redis_startup_nodes had no validation rule, so
malformed JSON passed validateFields, was caught while building the save
payload, and the field was silently omitted; the user's cluster/sentinel
config was discarded with no feedback. Add a jsonListRule (same shape as
portRule) to both list fields so an invalid value surfaces inline and
blocks save.
* fix(ui): show valid-JSON examples for cache list fields and clarify the error
The Startup Nodes and Sentinel Nodes help text showed Python-style
single-quoted examples (e.g. [{'host': '127.0.0.1', 'port': '7001'}]),
which the JSON validator correctly rejects, so pasting the example we
display failed. Switch both examples to valid JSON with double quotes and
change the parse-error message to "Must be a valid JSON array (use double
quotes)" so the hint points at the fix. Also add a regression test
asserting a numeric field (Database Index) is included in the save payload.
* fix(ui): validate numeric cache fields as text so bad input blocks save
Numeric fields (Database Index, TTL, Max Connections, Similarity
Threshold) rendered as antd InputNumber, which silently coerces
non-numeric input to empty. Because the fields are optional, an invalid
entry like a full connection URL pasted into Database Index passed
validation and was silently dropped from the save payload.
Render numeric fields as text inputs with a validation rule (non-negative
integer for Database Index and Max Connections, number for TTL and
Similarity Threshold), mirroring how Port already works, so invalid input
is preserved, flagged inline, and blocks submit instead of vanishing. The
save payload still coerces these to real numbers. Adds a regression test
for a non-numeric value entered into a numeric field.
Moves the user-management component tree (view_users plus BulkEditUsers, edit_user, DefaultUserSettings, user_edit_view, and the view_users table/columns/info-view) out of the shared src/components dump into the users route segment under _components, now that the app router owns the route. The page imports from a trimmed ./_components barrel
UserInfo moves into networking.tsx beside UserListResponse, its real owner: networking defines the user API response shapes that embed it, and previously reached up into a view folder (components/view_users/types) to import the type. Defining it in networking removes that backwards data-layer-to-view dependency and drains the view_users/ folder entirely. CreateUserButton and onboarding_link stay in components/ since the create-key flow also consumes them
Relative imports in the moved files are rewritten to @/components/* absolute paths, and the eight pre-existing eslint-suppressions entries are re-keyed to the new paths so the move stays behavior and lint neutral
Verified: the moved suites pass with the same 75 assertions as before the move, tsc and eslint are clean, and next build compiles the /users route