Persiste proposal, specs delta (auth, api-client, access-requests, platform-shell, testing), design con 8 ADRs, tasks (27 items en 10 fases), verify-report (28 tests, 0 críticos, 0 warnings tras el cierre) y archive-report final. Cierra el ciclo SDD del cleanup de deuda técnica del console.
9.8 KiB
Proposal: console-tech-debt-cleanup
Change name: console-tech-debt-cleanup
Target service: services/frontend-console
Stack: React 19 + Vite 7 + TypeScript 5.9, Zustand (persist), TanStack Query 5
Type: Technical debt + safety net (no feature work, no backend changes)
Why
The console has accumulated correctness debt concentrated in the auth layer and the API client typing, on top of a completely untested mutation surface that approves/rejects platform access. Each item alone is small; together they form a risk cluster:
- Two sources of truth for the access token (
tokenandaccessTokenfields in the auth store) silently diverged across consumers — the next refactor touching auth has a real chance of breaking sign-in. LoginResponsetypes do not match runtime — the login page falls back toresponse.access_token ?? response.token, which means either the type or the runtime contract is wrong. The compiler currently lies about the shape.apiClientcastsnull as Ton 204 responses, defeating TypeScript's ability to catch consumers that forgot to handle "no body".- First-load UX flashes "Restaurando sesion…" for users who never had a session, because the heuristic uses an unpersisted field.
- Failed queries are invisible on
AccessRequestsPage— an operator sees an empty table on API failure with no way to distinguish "no requests" from "backend down". - No
ErrorBoundaryat the shell — any unhandled render error blanks the whole console. - Zero tests in this app, despite Vitest being the project default. The approve/reject mutations on
AccessRequestsPagedirectly gate platform access. Shipping refactors against this surface without a baseline suite is unacceptable.
This proposal pays down that debt and lays the testing foundation so future changes land safely.
What changes
Eight discrete fixes, grouped into three bundles by priority. None alter the network contract; the backend is untouched.
Bundle P0 — Correctness, types, and test scaffolding
- Unify access token field in auth store. Keep
accessTokenas the single source of truth. Rename consumers inProtectedRoute.tsxandLoginPage.tsxto readaccessToken. Drop the duplicatedtokenfield fromauth-store.ts. - Align
LoginResponsewith the actual backend response. Determine the real shape (likelyaccess_tokensnake_case from the API). Either make the type match exactly and remove the?? response.tokenfallback, or, if both shapes are legitimately possible, mark both as optional and guard explicitly. No silent??chains across non-equivalent fields. - Remove
null as Tcasts inapiClient. Split into two call paths: the defaultapiClient<T>returnsTand throws on 204 (or refuses to be called on void endpoints), and a siblingapiClientVoidreturnsvoidfor endpoints that legitimately respond 204. Callers update accordingly. - Bootstrap Vitest + Testing Library in
frontend-console(it is the project default persdd-initbut not yet wired up here). Addvitest,@testing-library/react,@testing-library/jest-dom,jsdom, avitest.config.tsaligned withfrontend-admin, asetupTests.ts, and annpm script(pnpm test). Write the baseline suite that protects every P0/P1 change:SessionCoordinator.refresh— dedupes concurrent refresh calls.apiClient— 401 triggers refresh-and-retry; 204 path is type-safe.auth-store—setSessionandlogoutbehave correctly (single token field).AccessRequestsPage— approve, reject, and "mark as review" mutations on the happy path; error path surfaces a toast.
Bundle P1 — UX and robustness
- Fix the "Restaurando sesion…" flash on first load. In
ProtectedRoute.tsx, base the "previous session existed, attempt refresh" heuristic on persisteduserpresence, not on the in-memorytoken. Users without a prior session go straight to login with no flash. - Surface query failures on
AccessRequestsPage. ReadrequestsQuery.isErrorand render an explicit error state (retry CTA + message). No more empty-table-on-failure ambiguity. - Add an
ErrorBoundaryat the platform shell so render errors render a recoverable error screen instead of a blank console.
Bundle P2 — DRY hygiene
- Use the existing
isActionablehelper inAccessRequestsPage.tsxinstead of inlining['pending','in_review'].includes(request.status)three times.
Impact
Files touched (expected)
src/features/auth/stores/auth-store.ts— droptoken, keepaccessTokenonly.src/features/auth/api/auth-api.ts—LoginResponsealigned with runtime.src/features/auth/pages/LoginPage.tsx— readaccessToken, drop??fallback (or make it explicit and typed).src/app/router/ProtectedRoute.tsx— readaccessToken; refresh heuristic uses persisteduser.src/lib/api/client.ts— splitapiClient/apiClientVoid; removenull as T.src/features/access-requests/pages/AccessRequestsPage.tsx— error UI, useisActionable.src/app/(shell) — newErrorBoundary.vitest.config.ts,src/setupTests.ts,package.json(devDeps + scripts) — test scaffolding.- New
__tests__files colocated with the units listed in P0.4.
Behavioral changes
- Internal-only: the auth store exposes one token field instead of two. Any external code reading
state.tokenwould break, but no such consumers exist outside the four files listed. - User-visible:
- First-load no longer flashes "Restaurando sesion…" for users without a prior session.
- Failed access-request queries now show an error state instead of an empty table.
- Render errors render an error boundary instead of a blank screen.
- Network contract: unchanged. No backend modification.
Tooling
- New dev dependencies:
vitest,@testing-library/react,@testing-library/jest-dom,@testing-library/user-event,jsdom. Aligned with whatfrontend-adminalready uses. - New script:
pnpm test(andpnpm test:watch).
Out of scope
- New features or new modules.
- Redesign of
AccessRequestsPageUX (filtering, pagination, sorting, etc.). - Backend API changes — even if
LoginResponseshape is awkward, the proposal aligns the type to the existing runtime, not the other way around. - Migrating other apps (
frontend-admin,landing) — they are out of scope for this change. - Auth flow architectural changes (refresh token rotation, session storage strategy, etc.) — only the duplicated-field bug is addressed.
- E2E tests. Only unit/component tests via Vitest + Testing Library.
Priorities
| Priority | Items | Rationale |
|---|---|---|
| P0 | 1, 2, 3, 4 | Correctness + type safety + the test scaffolding required to land everything else without regressions. P0.4 must come first within the bundle so P0.1–3 are protected by tests. |
| P1 | 5, 6, 7 | UX correctness and robustness. Visible-but-non-correctness issues. Land after P0 so the test suite already protects against regressions. |
| P2 | 8 | Pure DRY hygiene. Lowest risk, lowest reward. Can ship in the same PR as P1 or as a tail commit. |
Recommended sequencing inside P0: (4) test scaffolding + baseline tests → (1) unify token → (2) align types → (3) split apiClient. Tests come first so the auth and client refactors land green.
Risks
Overall risk: low. This is a local refactor inside a single app, no backend changes, no network contract changes.
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
Renaming token → accessToken misses a consumer and silently breaks login |
Low | High (sign-in broken) | Test scaffolding (P0.4) ships first and includes auth-store + protected-route tests; TypeScript catches structural reads; manual smoke of login + refresh in dev. |
Splitting apiClient into apiClient / apiClientVoid requires touching every void-returning caller |
Medium | Low (compile-time) | TypeScript flags every miss at build time; mechanical fix, no runtime ambiguity. |
LoginResponse runtime contract turns out to differ from assumption |
Low | Medium | Verify against backend handler before changing the type; if both shapes truly exist, model both as optional and document why. |
Test scaffolding diverges from frontend-admin conventions |
Low | Low | Mirror frontend-admin's vitest.config.ts and setupTests.ts 1:1 where possible. |
ErrorBoundary swallows errors useful for debugging |
Low | Low | Boundary logs to console + shows a "reload" CTA; only catches render errors, not async ones. |
Success criteria
- The auth store exposes a single access-token field; no consumer reads
state.token(it no longer exists). LoginResponsematches the backend response exactly; the?? response.tokenfallback is gone (or both fields are explicitly optional and guarded).apiClientno longer containsnull as T. Void endpoints use a dedicatedapiClientVoid(or equivalent pattern) and TypeScript correctly types the absence of a body.- First-load with no prior session goes directly to login — no "Restaurando sesion…" flash.
- A failed
AccessRequestsPagequery renders a visible error state with a retry, distinguishable from an empty list. - A render error anywhere in the console renders the
ErrorBoundary, not a blank page. AccessRequestsPageuses theisActionablehelper everywhere; no inline duplication of the status check.pnpm testruns Vitest infrontend-consoleand the baseline suite passes green, covering:SessionCoordinator.refreshdedupe,apiClient401 retry + 204 typing,auth-storesetSession/logout,AccessRequestsPageapprove/reject/review mutations (happy + error toast).pnpm run lintis clean.- No conventional-commit trailers reference AI authorship.