The auth context now clears the expired token via clearTokenCookies(),
which deletes with an already-past expires date rather than the removed
deleteCookie wrapper's Max-Age=0 write. The assertion was pinned to the
mechanism instead of the behavior; it now accepts either deletion form
Greptile flagged that the admin_ui_disabled path cleared the token cookie
but left AuthContext state populated until a hard reload. The context now
exposes clearAuth(), which useAuthorized calls instead of reaching for
clearTokenCookies() directly, and the decoded JWT fields are derived from
the token with useMemo instead of living in their own useState slots, so
nulling the token clears them for free. The userRole/userEmail reset runs
in the token-sync effect rather than clearAuth itself so a stale sync
queued in the same commit cannot resurrect them
showSSOBanner was computed and passed through but rendered nowhere; the
banner UI it fed was deleted long ago. Removed from the context, the hook,
and the typed test mocks instead of restoring the login_method guard
Greptile suggested, since polishing dead code is the wrong fix
The admin_ui_disabled test now asserts the decoded fields are cleared
after the redirect; it previously pinned the stale values, so it fails
without clearAuth
The dashboard had two parallel auth systems: AuthContext (provided at the
root, used by the legacy ?page= shell) and useAuthorized (used by ~85 files
in the migrated tree), each reading the cookie, decoding the JWT, and
deciding when to redirect on its own. Only AuthContext applied the decoded
auth_header_name via setGlobalLitellmHeaderName, so the two trees could
disagree on custom auth headers, and each ran its own login-redirect effect
AuthContext stays the engine: it still resolves uiConfig before clearing
authLoading (so proxy-rooted URLs are correct) and performs the single
decode. useAuthorized now consumes the context and only layers on policy:
the admin_ui_disabled check and the redirect-to-login side effect. Its
return shape is unchanged, so none of the ~85 consumers are touched
Context semantics are aligned to what the hook's consumers already
expected: userRole defaults to formatUserRole("") instead of "", and
showSSOBanner is computed strictly from login_method (default false)
instead of defaulting to true. The context's unused public setters
(setToken, setUserID, setAccessToken, setPremiumUser, setShowSSOBanner)
are dropped from the exported surface; only setUserRole/setUserEmail have
consumers. Decoded-JWT fields keep their legacy `any` typing at the hook
boundary since ~25 call sites rely on it; tightening is a follow-up
The hook's tests now exercise the real provider + hook together with real
JWTs instead of mocking the decode away, which also covers the provider's
expiry and undecodable-token paths
* 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.
* refactor(ui): extract auth state into AuthContext
Move auth state (token, userID, userRole, accessToken, premiumUser, userEmail,
disabledPersonalKeyCreation, showSSOBanner) out of src/app/page.tsx into a
new AuthProvider at src/contexts/AuthContext.tsx. Wrapped at the root layout
so login/onboarding/dashboard routes all have access via useAuth().
Day 1 foundation for the App Router migration: migrated (dashboard)/X/page.tsx
route entry points won't have a parent passing props, so shared auth state
must live in a context they can read from.
Sub-components are unchanged — they still receive accessToken/userID/userRole
as props from page.tsx (which now reads them from useAuth()). Only the
page.tsx → top-level-page-component handoff is de-drilled; deeper prop
drilling is left for the per-page migration to address.
Net change: -86 lines from page.tsx (state + two effects moved), +5 in
layout.tsx (provider wrap), new AuthContext.tsx (~140 lines), test update
to wrap CreateKeyPage in AuthProvider.
Fixes LIT-3366
Part of LIT-3128
* fix(ui): await getUiConfig before clearing authLoading
The AuthContext refactor flipped authLoading to false synchronously on mount
while letting getUiConfig() run fire-and-forget. On SERVER_ROOT_PATH deployments
this races the unauthenticated login-redirect effect: the redirect fires with
proxyBaseUrl still at its module-init value, sending users to /ui/login instead
of {SERVER_ROOT_PATH}/ui/login.
Restores the original sequencing inside AuthProvider's mount effect and adds a
Playwright spec wired into the existing SERVER_ROOT_PATH workflow matrix. The
spec delays the config endpoint via page.route() to make the race deterministic
across CI runners.
* [Refactor] UI - Spend Logs: consolidate filter state, extract components, remove dead code
- Lift filter state into index.tsx and pass to hook (removes selectedX vars + sync useEffect)
- Move main useQuery into useLogFilterLogic hook (removes isMainQueryEnabled toggle)
- Delete dead RequestViewer component (300 lines, replaced by LogDetailsDrawer)
- Extract LogsTableToolbar component (search, date range, pagination, live tail)
- Extract filter options config to filter_options.ts
- Remove dead code: handleRefresh, handleSelectLog, handleCloseDrawer, formatTimeUnit,
showFilters/showColumnDropdown state, dropdownRef/filtersRef
* Fix PR feedback: use antd Switch instead of Tremor in new file, fix typo
* Collapse dual-path filtering into single React Query
All 10 filter keys now go through the useQuery — the imperative
performSearch / debouncedSearch / backendFilteredLogs path is deleted.
Filter values are debounced via useDebouncedValue(300ms) before hitting
the query key so text inputs don't fire per-keystroke.
Removed: performSearch, debouncedSearch, backendFilteredLogs,
lastSearchTimestamp, hasBackendFilters, clientDerivedFilteredLogs,
the sort/page/time refetch useEffect, and the filteredLogs chooser memo.
* Clean up remaining smells: remove isFetchingDeferred, internalize selectedTimeInterval, fix circular import
- Remove useDeferredValue/isButtonLoading — pass logsQuery.isFetching directly
- Move selectedTimeInterval into LogsTableToolbar as internal state
- Move PaginatedResponse type from index.tsx to log_filter_logic.tsx
* Fix quick-select dropdown overlapping sidebar
* Fix stale quick-select label after Reset Filters
Move selectedTimeInterval back to parent so handleFilterReset can
reset it to the 24-hour default. The toolbar receives it as a prop.
* refactor useLogFilterLogic tests for controlled-hook + backend-query shape
The hook no longer owns filter state or does client-side filtering — it
receives filters/setFilters as props and drives filteredLogs from a
useQuery over uiSpendLogsCall. Reshape the tests around that contract:
introduce a controlled harness that owns filter state, collapse the 10
per-filter assertions into a single it.each over filterKey → API param,
and drop the client-side passthrough tests (the .min test file and the
"return all logs when no filters" / "empty when logs null" cases) that
no longer correspond to any hook behavior.
* cover new useLogFilterLogic invariants: activeTab gate, filterByCurrentUser fallback, debounce negative, partial merge
Follow-up to the test refactor. Adds coverage for invariants the
refactored hook contract introduced but that the first pass didn't
assert:
- query enablement: expand the single accessToken-null case into an
it.each over all four credential props (accessToken, token, userRole,
userID), plus a separate test for activeTab !== "request logs"
- filterByCurrentUser: when true with a blank User ID filter, the
outbound request carries user_id = userID
- debounce: also assert the negative case — no call in the first 100ms
after a filter change (first waiting out the initial mount fire)
- handleFilterChange: partial updates merge without clobbering other
filter keys (protects the spread + default-fill semantics)
- handleFilterReset: calls setCurrentPage(1) alongside restoring
filters
* fix typo dropping the live-tail banner border
Tailwind silently ignores unknown classes, so border-greem-200 was
leaving the auto-refresh banner with only its bg-green-50 fill and no
outline.
* memoize columns and derived table data in SpendLogsTable
The table's columns array, four-pass data pipeline, and sort-change
handler were all being rebuilt on every parent render. That made every
filter click re-instance all 23 TanStack-Table columns, re-run
filter/reduce/map over all rows, and recreate per-row click closures —
all before the intentional 300ms debounce timer even got a chance to
fire.
Local measurement (40 rows, dev mode):
filter click → query fires: 1957ms → 1217ms (−38%)
Wrap createColumns in useMemo keyed on sortBy/sortOrder, hoist
onSortChange into a useCallback, and move the searchedLogs /
sessionComposition / sessionRepresentativeMap / filteredData derivations
into a single useMemo keyed on filteredLogs.data + searchTerm.
These were pre-existing issues on main — not regressions from the
hook refactor — but the refactor made them user-visible because the
new query debounce put render cost on the critical path.
* apply dropdown filters instantly, debounce only text inputs
Dropdown selects now bypass the 300ms debounce so a click updates the
table immediately. Text inputs (Key Hash, Error Message, Request ID,
User ID) still debounce. handleFilterReset also clears the pending
debounced value so a half-typed text filter can't re-fire after reset.
* fix(ui/spend-logs): restore lost loading/debounce behavior + cover dropped tests
Regressions from the spend-logs-view refactor:
- debounce the 'Public model / search tool' text filter (was firing a
backend query per keystroke) via TEXT_FILTER_KEYS
- restore Fetch-button smoothing through table repaint using
useDeferredValue on the rendered data (explicit staleness)
- show AntDLoadingSpinner during the auth-resolve phase instead of a
blank screen on first load
- only live-tail-poll while the tab is visible
(refetchIntervalInBackground: false)
- extract getLiveTailRefetchInterval helper for the poll decision
Tests:
- LogDetailContent: retries display (>0 / 0 / absent), overhead-absent
- log_filter_logic: regression guard that the public-model filter
debounces; getLiveTailRefetchInterval unit tests
- logs_utils: getTimeRangeDisplay quick-select window labels
* test(ui/spend-logs): cover the cold-load auth-not-ready spinner guard
Asserts SpendLogsTable shows a loading spinner (not a blank screen)
while credentials are unresolved, and renders the table once present.
The test's partial vi.mock of @/components/networking was missing the daily
activity call exports now imported by EntityUsage via ENTITY_FETCH_FNS.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Local vi.mock("@tremor/react") overrides in router_settings tests were
clobbering the global setupTests.ts mock, re-introducing the real Tooltip
component which schedules a setTimeout. When jsdom tears down after each
test file, the pending timer fires and hits window is not defined, which
Vitest flags as an unhandled error that can cause false positive failures
in subsequent tests (including the create_mcp_server timeout in CI).
Fix: add Switch to the global @tremor/react mock in setupTests.ts (the
only reason the local overrides existed), then remove the three local
vi.mock("@tremor/react") blocks so all test files inherit the global mock
with properly stubbed Button, Tooltip, and Switch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Enables deep-linking directly to the key creation modal with prefilled
form data via URL parameters, including support for preserving these
deep-links through SSO authentication flows.
Key Creation Deep-links:
- Auto-open key creation modal via ?create=true parameter
- Prefill form fields from URL parameters (team_id, key_alias, models, etc.)
- Role-based access control for auto-open (requires write access)
- Race condition protection for redirect handling
Example: /ui?create=true&team_id=abc&key_alias=my-key&models=gpt-4,claude-3
SSO Return URL Preservation:
- Cookie-based return URL storage (works across ports for SSO flows)
- URL validation to prevent open redirect attacks
- Support for both dev and production environments
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>