Files
OmniOilPersonal/openspec/changes/archive/2026-05-07-console-tech-debt-cleanup/proposal.md
henryor af5535dfcc docs(openspec): archiva el change console-tech-debt-cleanup
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.
2026-05-07 21:14:56 -05:00

9.8 KiB
Raw Blame History

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 (token and accessToken fields in the auth store) silently diverged across consumers — the next refactor touching auth has a real chance of breaking sign-in.
  • LoginResponse types do not match runtime — the login page falls back to response.access_token ?? response.token, which means either the type or the runtime contract is wrong. The compiler currently lies about the shape.
  • apiClient casts null as T on 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 ErrorBoundary at 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 AccessRequestsPage directly 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

  1. Unify access token field in auth store. Keep accessToken as the single source of truth. Rename consumers in ProtectedRoute.tsx and LoginPage.tsx to read accessToken. Drop the duplicated token field from auth-store.ts.
  2. Align LoginResponse with the actual backend response. Determine the real shape (likely access_token snake_case from the API). Either make the type match exactly and remove the ?? response.token fallback, or, if both shapes are legitimately possible, mark both as optional and guard explicitly. No silent ?? chains across non-equivalent fields.
  3. Remove null as T casts in apiClient. Split into two call paths: the default apiClient<T> returns T and throws on 204 (or refuses to be called on void endpoints), and a sibling apiClientVoid returns void for endpoints that legitimately respond 204. Callers update accordingly.
  4. Bootstrap Vitest + Testing Library in frontend-console (it is the project default per sdd-init but not yet wired up here). Add vitest, @testing-library/react, @testing-library/jest-dom, jsdom, a vitest.config.ts aligned with frontend-admin, a setupTests.ts, and an npm 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-storesetSession and logout behave 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

  1. Fix the "Restaurando sesion…" flash on first load. In ProtectedRoute.tsx, base the "previous session existed, attempt refresh" heuristic on persisted user presence, not on the in-memory token. Users without a prior session go straight to login with no flash.
  2. Surface query failures on AccessRequestsPage. Read requestsQuery.isError and render an explicit error state (retry CTA + message). No more empty-table-on-failure ambiguity.
  3. Add an ErrorBoundary at the platform shell so render errors render a recoverable error screen instead of a blank console.

Bundle P2 — DRY hygiene

  1. Use the existing isActionable helper in AccessRequestsPage.tsx instead of inlining ['pending','in_review'].includes(request.status) three times.

Impact

Files touched (expected)

  • src/features/auth/stores/auth-store.ts — drop token, keep accessToken only.
  • src/features/auth/api/auth-api.tsLoginResponse aligned with runtime.
  • src/features/auth/pages/LoginPage.tsx — read accessToken, drop ?? fallback (or make it explicit and typed).
  • src/app/router/ProtectedRoute.tsx — read accessToken; refresh heuristic uses persisted user.
  • src/lib/api/client.ts — split apiClient / apiClientVoid; remove null as T.
  • src/features/access-requests/pages/AccessRequestsPage.tsx — error UI, use isActionable.
  • src/app/ (shell) — new ErrorBoundary.
  • 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.token would 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 what frontend-admin already uses.
  • New script: pnpm test (and pnpm test:watch).

Out of scope

  • New features or new modules.
  • Redesign of AccessRequestsPage UX (filtering, pagination, sorting, etc.).
  • Backend API changes — even if LoginResponse shape 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.13 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 tokenaccessToken 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).
  • LoginResponse matches the backend response exactly; the ?? response.token fallback is gone (or both fields are explicitly optional and guarded).
  • apiClient no longer contains null as T. Void endpoints use a dedicated apiClientVoid (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 AccessRequestsPage query 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.
  • AccessRequestsPage uses the isActionable helper everywhere; no inline duplication of the status check.
  • pnpm test runs Vitest in frontend-console and the baseline suite passes green, covering: SessionCoordinator.refresh dedupe, apiClient 401 retry + 204 typing, auth-store setSession / logout, AccessRequestsPage approve/reject/review mutations (happy + error toast).
  • pnpm run lint is clean.
  • No conventional-commit trailers reference AI authorship.