# 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` 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-store` — `setSession` 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 5. **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. 6. **Surface query failures on `AccessRequestsPage`.** Read `requestsQuery.isError` and render an explicit error state (retry CTA + message). No more empty-table-on-failure ambiguity. 7. **Add an `ErrorBoundary` at the platform shell** so render errors render a recoverable error screen instead of a blank console. ### Bundle P2 — DRY hygiene 8. **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.ts` — `LoginResponse` 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.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). - `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.