* fix(ui): restore hover feedback and dark-mode variants lost in the token migration
PR #37576 mapped hardcoded Tailwind palette classes onto semantic tokens. Two-tone
hover pairs collapsed onto a single token, so 116 hover utilities across 49 files
became identical to their base class and produced no visible feedback, and in seven
files a dark: variant was dropped while its hardcoded light partner survived, leaving
those elements stuck light in dark mode.
Hover states now follow the alpha-step idiom the shadcn primitives already use
(hover:bg-primary/80, hover:bg-success/20): a duplicated hover:text-X or hover:bg-X
becomes /80, hover:border-border becomes hover:border-ring, and a duplicate is
dropped where another hover utility on the element already carries the change. One
transition-colors that no longer animated anything is removed.
For the dark-mode gaps, indigo maps onto info and amber onto warning. There is no
purple token in globals.css, so the purple sites keep their palette classes and get
their dark: partner back.
* fix(ui): add an eslint rule that fails a hover: utility identical to its base
The token migration collapsed two-tone hover pairs by hand, so nothing catches
the next one. `local/no-noop-hover-variant` reads every string literal and
template chunk and errors when a `hover:X` sits alongside a bare `X`, which is
exactly the shape that renders no hover feedback. It ships at error with no
suppression baseline, so the eleven sites that already carried a dead hover
before the migration are fixed here too.
The rule reads one class string at a time, so a base class supplied by a
different ternary branch than its hover partner is left alone: a selected row
whose resting colour already matches its hover colour is deliberate, not a bug.
Nothing in the dashboard renders antd any more, so the package and the
scaffolding around it can go. This removes `antd` and
`@ant-design/cssinjs` from package.json, deletes the global StyleProvider
the root layout wrapped every page in, drops the `antd` cascade layer and
the z-index override that lifted Base UI popups over an antd Modal, and
retires the lint rules that policed antd imports and antd class selectors
in tests.
Fifteen test files still carried `vi.mock("antd", ...)` factories for
components that stopped importing antd during the migration. They were
inert, and they resolve the real module, so they would have broken the
moment the package left node_modules.
The compatibility shims keep their behaviour and lose the antd name:
`antdRules`/`antdRequired` become `validatorRules`/`requiredRule`,
`isAntdUrl` becomes `isValidUrl`, and `ABOVE_ANTD_MODAL` becomes
`NESTED_DIALOG_LAYER`. Comments that explain why a contract looks the way
it does still name antd, because that history is the reason.
The dashboard drew its icons from two libraries at once: lucide-react,
which shadcn/ui ships with, and @ant-design/icons, left over from antd.
This moves the last 39 files onto lucide and drops the dependency, so
the icon set matches the component library everywhere.
antd icons sized themselves from the inherited font-size and rendered as
role="img" with an aria-label, neither of which a lucide svg does, so the
swap carries explicit size classes and gives the two icon-only plugin
buttons real accessible names.
Enables 14 rules at error with zero violations, so the vacuous-assertion class
the previous commit fixed cannot come back. No budget file and no suppressions
baseline: a rule is on only if it is already at zero.
prefer-to-have-value stays off. It matches any attribute whose name contains
"value", so it rewrites toHaveAttribute("aria-valuenow", n) into toHaveValue(n),
and jest-dom's toHaveValue supports only form controls, which breaks every
role="meter" element the dashboard renders.
Records the enabled set, the rules left off with their measured counts, and the
seven ways these plugins' autofixers produce broken output.
Dashboard tests located controls through antd's own class names
(.ant-form-item, .ant-select-selector, .ant-select-item-option). Those
break when a page moves to shadcn without any behaviour changing, and
they miss regressions a user would notice.
Replace them with role, label, title and accessible icon-name queries
where antd exposes one, and add local/no-antd-class-selectors to keep
them out. The rule is enabled as an error at zero violations, so there is
no budget file and no suppressions baseline. It found eight more sites a
'.ant-' search missed, written as bare class names.
Eleven couplings remain and carry an inline suppression naming why:
antd puts role="option" only on a hidden mirror list, so the visible
options have no role, no aria-disabled and a tooltip in title; Skeleton
and the modal mask expose nothing at all; and one assertion's whole
purpose is that no antd modal renders.
7231 tests passed before, 7233 pass after: one conflated ModelSelector
case became three focused ones.
* chore(ui): add filename, size, JSX-handler, prefer-const, and antd lint rules
Wires up five error-level ESLint rules on the dashboard, grandfathering every
current offender into eslint-suppressions.json so the gate only bites new code
and ratchets down as files are fixed
- local/filename-pascal-case: new local rule requiring PascalCase .tsx names,
exempting Next.js reserved files (page, layout, route, ...) and test/spec files
(239 grandfathered)
- max-lines: 800 lines over src/**, excluding tests, src/data, and generated
schema.d.ts (20 grandfathered)
- local/no-complex-jsx-arrow: new local rule flagging inline JSX arrow handlers
with block bodies over two statements; each failure is a small extract-to-named
-handler refactor (65 grandfathered)
- prefer-const: flipped from off to error (103 grandfathered)
- no-restricted-imports: added antd to the phase-out ban alongside tremor, and
pointed both messages at shadcn/ui primitives (405 antd import sites grandfathered)
Both new local rules ship with RuleTester coverage
* fix(ui): preserve secondary extensions in filename-pascal-case suggestion
The suggestion text built the rename from only the head segment, so a
multi-dot file like my-component.utils.tsx was told to become
MyComponent.tsx instead of MyComponent.utils.tsx. Rebuild it from the
PascalCased head plus the untouched remaining segments, and add tests
covering multi-dot filenames and the hyphenated Next.js reserved names
(global-error, apple-icon, opengraph-image, twitter-image)
* 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.
Introduce a gradual ratchet to remove raw console.* calls from the
dashboard, mirroring the existing no-explicit-any budget.
The no-console eslint rule is set to warn with allow: [warn, error] so
the 486 console.log/debug/info calls are tracked without force-deleting
the legitimate console.error/warn error reporting in catch blocks. The
count is grandfathered via eslint-budgets.json (max 486, target 0) and
eslint-metrics.json, so any newly added console.log fails the budget
check and follow-up PRs grind the max down toward zero.
Independently, next.config strips console output from production builds
via SWC removeConsole (exclude: [error]), gated on NODE_ENV=production so
dev keeps full console output. This gives an immediate prod-hygiene net
regardless of how long the source cleanup takes. Verified against a real
production build: app-code console.log dropped from 675 to 14 in the
bundle (remainder is node_modules, which the transform leaves alone),
console.warn app calls stripped, console.error preserved 906 to 906.
* feat(ui): generate dashboard API types from the proxy OpenAPI spec
Introduces the shared type foundation for the dashboard without touching any
runtime code. The proxy's FastAPI app is the source of truth; app.openapi()
emits the spec and openapi-typescript turns it into src/lib/http/schema.d.ts.
Adds an npm run gen:api script (a Python spec dump piped into openapi-typescript)
and a Check UI API Types Sync CI job that regenerates the file from the live
spec and fails if it drifts, so the committed types can never silently fall out
of step with the backend. The generated file is pinned to openapi-typescript
7.13.0 and excluded from prettier, eslint, and knip, and marked linguist-generated
so it collapses in diffs.
No openapi-fetch and no call-site changes yet; this only makes the types exist.
* chore(ui): tidy gen-api-types script per review
Write the spec dump inside a with-block and clean up the temp dir in a
finally, so repeated local runs don't leave stray ~MB JSON files behind.
* refactor(ui): add shared HTTP client and pin raw fetch() to one file
Introduce src/lib/http/client.ts, a single typed wrapper that owns the only
fetch() in the dashboard. It centralizes the base URL, the auth header, error
parsing (deriveErrorMessage), non-2xx -> thrown ApiError, and JSON parsing, and
is framework-agnostic (no React) so it can run from client and, later, server
components. The base URL, auth header name and the logout side effect are injected
through createApiClient.
networking.tsx builds one configured apiClient and the 29 functions whose
boilerplate maps exactly to the client's default behavior (canonical
deriveErrorMessage + handleError + res.json() template) now call it instead of
hand-rolling fetch. Names, signatures, return types and error behavior are
unchanged; this is a pure refactor that drops ~440 lines.
The no-restricted-syntax fetch rule now points at the client and a
files: ["src/lib/http/**"] override makes that the only place fetch() is allowed.
Re-baselined eslint-suppressions.json: networking.tsx fetch suppressions drop
270 -> 241; no other rule's counts change.
The remaining networking.tsx fetches and the ~61 scattered component/hook fetches
diverge from the default client behavior (text() error bodies, no res.ok check,
no handleError side effect) and stay grandfathered for a follow-up burndown.
* fix(ui): make the HTTP client tolerate non-JSON error bodies
The non-2xx branch parsed the error body with response.json(), so a gateway
returning HTML (502/503 from a reverse proxy) threw a SyntaxError before onError
fired or ApiError was built, dropping the user-facing notification. This matched
the old per-function behavior, but the client is now the single error path so it
is the right place to harden. Read the body as text once, try JSON.parse for the
existing deriveErrorMessage path, and fall back to the raw text (or the HTTP
status) otherwise. The success path stays strict json() so return types are
unchanged.
* fix(ui): await the returned apiClient promise in 6 migrated functions
The codemod rendered the `return response.json()` tail as `return apiClient.x()`
without `await`. Inside the surrounding try/catch that returns an unawaited
promise, so the catch never runs and its console.error log is dropped on failure;
4 of the 6 were `return await response.json()` originally, so this restores their
exact behavior. Use `return await apiClient.x()` in all six.
* refactor(ui): widen onError type and handle empty success bodies
Address review notes on the shared client. Type onError as
(message: string) => void | Promise<void> so the fire-and-forget async contract
(networking passes the async handleError) is explicit rather than silently
discarded by void. On the success path, read the body as text and return
undefined for an empty body (e.g. a 204 No Content) instead of throwing a
SyntaxError, while still parsing non-empty bodies strictly so a malformed JSON
response surfaces rather than being masked. Add tests for the 204 case.
* fix(ui): only flag bare fetch() outside React Query queryFn/mutationFn
The frontend lint rule banned every fetch() call by static AST name match,
so a fetch wrapped in a React Query queryFn/mutationFn tripped it just like
a loose fetch in a component. esquery (no-restricted-syntax) can't express
"has ancestor", so this replaces that selector with a small custom rule
(local/no-bare-fetch) that exempts a fetch lexically inside a queryFn or
mutationFn and reports everything else.
Re-baselined eslint-suppressions.json under the new rule id (same 44 files /
331 violations) so existing code keeps its grandfathered suppressions.
Adds a RuleTester suite covering wrapped (valid) vs unwrapped, the standalone
*Api.ts function pattern, queryKey, and computed-key cases.
* chore(ui): remove the bare-fetch lint rule
Drop the fetch lint gate (and its 331 grandfathered suppressions) ahead of
the networking refactor. The plan is to centralize all fetching in a single
shared http client and enforce that with a location-based rule, so keeping a
fetch rule in place now would only block CI while functions are routed
through the new client. Removing it unblocks that work; the location-based
rule lands with the client in a follow-up.
* ci(ui): add frontend-lint job enforcing prettier and eslint on changed files
Lints only the files a PR adds or modifies under ui/litellm-dashboard,
so new and touched code must be prettier-clean and eslint-clean while the
existing tree is grandfathered. Skips cleanly when a PR touches no
lintable UI files. This lets us adopt the formatters incrementally
without a repo-wide reformat
* ci(ui): write frontend-lint file lists to $RUNNER_TEMP
Keep the prettier/eslint changed-file lists out of the checkout dir so
they cannot collide with a future source file of the same name
* lint(ui): baseline existing eslint findings so only new ones block
Capture the current error-level eslint findings (318 across 183 files)
in a committed suppressions baseline via eslint --suppress-all. Every
rule stays at its error severity, so any newly introduced violation
fails the frontend-lint gate, while the existing tree is grandfathered;
touching a legacy file never forces fixing its pre-existing issues. CI
runs eslint with --pass-on-unpruned-suppressions so that fixing a
baselined issue does not fail on a now-stale suppression, and the
generated baseline is prettier-ignored since eslint owns its format.
Burn the baseline down over time with eslint --prune-suppressions
* lint(ui): enforce a count budget for explicit any
Make @typescript-eslint/no-explicit-any a warning and cap the total
instead of hard-blocking each new one. A frontend-lint step counts the
repo-wide explicit any and fails only when it exceeds the committed
budget in eslint-any-budget.json. max starts at 2031, ten above the
current 2021, so the next ten land as warnings and the build fails once
that headroom is gone. Lower max over time toward target to ratchet the
count down. New anys still surface as warnings on changed files via the
normal eslint step
* lint(ui): enable zero-cost rules no-var, no-self-assign, react/no-danger
These have no existing violations, so they need no baseline; turning them
on purely blocks new instances. react/no-danger guards against new
dangerouslySetInnerHTML (XSS), no-var enforces let/const, and
no-self-assign catches self-assignment typos. no-debugger is already
enforced by the recommended preset
* lint(ui): add baselined complexity rules
Enable complexity:20, max-depth:4, max-params:4, max-nested-callbacks:4,
with thresholds set near the codebase p99 so only genuine outliers are
flagged. The 272 existing over-threshold functions are grandfathered in
the suppressions baseline; new over-threshold functions block. Lower the
thresholds over time to ratchet complexity down. max-lines-per-function
is intentionally left off since React components are legitimately long
* lint(ui): ban new raw fetch, standardize on React Query
Add a no-restricted-syntax rule flagging bare fetch() calls, pointing
contributors at React Query (@tanstack/react-query). The rule is not
exempted anywhere, including the already-bloated networking.tsx, so all
331 existing fetch calls are grandfathered but no new ones can be added
there or elsewhere. New data access goes through React Query, and the
networking layer can be migrated out and pruned from the baseline over
time
* lint(ui): ban new @tremor/react imports
Add a no-restricted-imports rule flagging imports from @tremor/react so
tremor is phased out rather than spread further. The 232 existing tremor
imports are grandfathered in the baseline; new ones block and point at
antd. Migrate components off tremor and prune the baseline over time
* lint(ui): widen explicit-any budget headroom to 2040
Raise max from 2031 to 2040, giving ~19 of slack over the current 2021
instead of 10
* style(ui): prettier-format eslint.config.mjs
The frontend-lint gate flagged its own config file. Format it so the
prettier check on this PR's changed files passes
* lint(ui): soften complexity and max-depth to warnings
These two are smell metrics with arbitrary thresholds where a legit new
function can trip them, so make them advisory rather than hard-blocking.
They drop out of the baseline (now 963). max-params, max-nested-callbacks,
and the react-hooks rules stay strict since those are clear-cut
* lint(ui): move complexity and max-depth to the count-budget pattern
Generalize the explicit-any budget into a shared lint-budget mechanism:
eslint-budgets.json maps a rule to {max, target} and check-lint-budgets.mjs
counts each across the repo and fails when a count exceeds its max.
complexity (129, max 140) and max-depth (61, max 70) now use the same
slack-plus-counter model as explicit-any (2021, max 2040): they warn
per-file and the build only fails if the repo-wide total crosses the
ceiling. Lower each max toward its target over time
* docs(ui): note pruning the eslint suppressions baseline when fixing lint debt
ESLint 9 defaults to flat config and eslint-config-next was pinned at 15
while Next is on 16, so eslint only ran with ESLINT_USE_FLAT_CONFIG=false
and next lint is gone on Next 16. Replace .eslintrc.json with a native
flat eslint.config.mjs (config-next 16 ships flat configs, so no
FlatCompat shim is needed), bump eslint-config-next to 16.2.6, add
@eslint/js and typescript-eslint as explicit devDeps for the recommended
rule sets, and point the lint script at eslint directly.
This only makes eslint runnable on modern tooling; it does not wire it
into CI. The same rules carry over (next/core-web-vitals, eslint and
typescript-eslint recommended, prettier, unused-imports)