# Console handover For the next developer. What this console was, what it is now, what changed, and what is still waiting for somebody. Two companion documents sit one directory up: - `../REVIEW.md` — the assessment across both halves. §9 covers the console. - `../SAAS_HARDENING.md` — the chronological log, Part 4 onwards. The API this talks to has its own `../backend/HANDOVER.md`. **Read that one first** — most of what this console does is expose something the server enforces, and the reasoning lives on the server side. --- ## 1. In one paragraph React 19 + Vite + TypeScript, react-router, i18next with English and Arabic (including RTL), Tailwind with CSS custom properties for theming. It was a working admin console covering roughly a third of what the API could do, with no tests. It now covers all of it, has 194 tests, and paints its first screen in **507 kB against a 550 kB budget** — *lower* than before this work began, despite eleven more screens. --- ## 2. What was here before | Screen | State | |---|---| | Sign in / sign up / reset password | Present | | Dashboard | Present | | Profile | Present, without sessions or security | | Tenants (workspaces) | Present | | Subscriptions / plans | Present | | Roles | Present | | Users | Present | | Theme | Present | | Settings | A shell | | Modules (admin) | Present | | Logs | Present | **No tests at all.** No bundle budget. Both languages' translations were loaded eagerly at startup. Everything in this repository is uncommitted on branch `furqan`. `git diff` shows every change to a pre-existing file; `git ls-files --others --exclude-standard` shows the 83 new ones. --- ## 3. Screens added Each one exposes a capability the API gained. All are lazy-loaded. | Route | What it is | |---|---| | `/settings/api-keys` | Issue and revoke keys, with scopes. The secret is shown once. | | `/settings/webhooks` | Endpoints, delivery history, HMAC secret rotation. | | `/settings/sign-in` | Inbound SSO — connect the customer's Azure AD / Okta / Google. | | `/settings/email` | Send from the workspace's own address. | | `/settings/reference` | Reference lists — the things dropdowns are made of. | | `/settings/organisation` | Org units, membership, scoped administration, **seats**. | | `/documents` | The workspace's file library. | | `/users/invitations` | Invite somebody rather than choosing their password. | | `/operations` | What the background jobs have been doing. Superadmin only. | Added to existing screens: - **Profile** — `SessionsPanel` (see and end your sessions), `SecurityPanel` (MFA enrolment), and `NotificationPreferencesPanel`. - **Users** — `DeletedUsersPanel`, since deletion is now soft. - **Header** — `NotificationBell`. - **Layout** — `SubscriptionBanner`, warning before a subscription lapses. --- ## 4. Conventions worth knowing before you change anything ### `apiClient` is the only way to reach the API `src/lib/apiClient.ts`. It refreshes an expired access token and retries once. Do not use `fetch` directly — a request that goes around it is the one request that fails on an expired token, for a reason nobody can see. That includes file downloads: use `apiClient.blob`, which exists precisely so a download is not the exception. Toast options: `toast: false` suppresses **both** success and error toasts; there is no error-only mode. Where a silent failure would be dangerous, the component reports it inline instead — see `NotificationPreferencesPanel`. ### On create, absent; on edit, null The rule that turns form state into a request, stated once because getting it wrong is invisible: - **Create** — "not set" is `undefined`, and the key is dropped. - **Edit** — clearing a field means `null`, because `undefined` leaves the old value in place and looks like the save did not work. This is not theoretical. The workspace edit form sent `undefined` for a cleared billing address while sending `null` for a cleared logo two lines above, so **a billing address could be set and never removed**. The rule now lives in `buildTenantPayload` / `buildPlanPayload`, extracted from the components and tested. ### Translations: English is bundled, Arabic is fetched `src/i18n/config.ts`. Both languages used to be imported statically — 81 kB of JSON, half of it in a language the visitor had not chosen. English is now the bundled fallback and Arabic is fetched on demand, **before** switching rather than after, because changing to a language whose bundles have not arrived renders a screen of raw keys. **When you add a screen, add both `en/` and `ar/` files.** There is no test that catches a missing Arabic key. ### The bundle has a budget, and it ratchets ```bash npm run build && npm run check:size # fails over 550 kB first paint ``` It has fired once, at 536 kB, and the cause was the translations above rather than any screen. It is there to make the cost visible at the moment it becomes worth paying attention to, rather than in six months. --- ## 5. Running it ```bash npm install npm run dev # NOT `npm test` — that is the dev server in test mode npm run test:unit # 194 tests npm run lint npm run build && npm run check:size ``` `npm test` runs Vite against the `test` environment. The unit suite is `test:unit`. This is the repository's existing naming and I left it alone. --- ## 6. Testing approach, and its limit 194 tests across 20 files. They cover the api client, the route guard, the permission gate, the payload every form builds, and the components where being wrong is expensive — sessions, MFA enrolment, sign-in, notifications, documents, API keys, invitations, operations, seats. **They are rule-level and component-level, not screen-level.** No screen is rendered end to end against a real API. That was a deliberate trade: the payload each form builds was the actual risk and is now tested, and rendering tests on top would add little. Three things learned the hard way, in case you hit them: - `vi.fn().mockResolvedValue()` inside a hoisted `vi.mock` factory returns `undefined`. Use a plain `async () => …`. - Required fields render as `Label *`, so exact-string label queries miss. Use a regex. - Running this suite concurrently with the backend suite produces timeout failures that are not real. Run them one at a time. --- ## 7. A defect worth knowing about `CustomInput` had `htmlFor={props.id}` and **no caller passed an id**, so every label in the product was decoration — not associated with its input, unusable with a screen reader, and not clickable. Found while writing a test that could not find a field by its label. Fixed with a `useId()` fallback. If you write a new input component, this is the mistake to not repeat. --- ## 8. Pending ### 8.1 Nothing is blocked on the console Every API capability now has a screen. The check that found the last gap is worth re-running whenever the API grows — it compares what the server exposes against what the console actually calls: ```bash grep -rho '"/api/[a-z0-9/-]*' src/ | sort -u ``` Three capabilities were built on the server and had no screen for a while precisely because nobody ran that comparison. It should run at the end of each feature, not at the end of a batch. ### 8.2 No screen is rendered end to end Stated above as a trade rather than an omission, but it is the honest next step if you want more confidence than the current suite gives. ### 8.3 Arabic has no parity check Every new key must be added to `en/` and `ar/` by hand. A test asserting the two key sets match would catch what review does not. Not written. ### 8.4 CI has never been run by GitHub Actions The repository has no remote. The workflow has been run locally step by step, which is most of the value, but the YAML has only been parsed. ### 8.5 Depends on the backend's pending items The console will not behave correctly until the server-side steps in `../backend/HANDOVER.md` §7 are done — in particular the RLS role, without which either everything or nothing is visible depending on which role the API connects as. The operations page's audit-retention panel will read "not reporting" until `AUDIT_RETENTION_DATABASE_URL` is set; that is the panel working, not failing. --- ## 9. Where to look ``` src/ lib/ apiClient.ts Read first. Every request goes through here. queryParams.ts Table filters ↔ URL. tablePageSize.ts routes/ index.tsx All routes. Lazy imports at the top. ProtectedRoutes.tsx The auth guard. context/ AuthContext.tsx Session, permissions, superadmin flag. ThemeContext.tsx i18n/ config.ts English bundled, Arabic fetched. See §4. locales/en/, ar/ application/ / Page.tsx The screen. Api.ts Its API calls. Types.ts Its types. *.test.tsx components/ custom/ Shared inputs, modals, tables, loaders. layout/ Header, sidebar, subscription banner. scripts/ check-bundle-size.mjs The budget. ``` The `application//` shape — page, api, types, tests in one folder — is the existing convention. Follow it; the codebase is consistent about it and it makes a feature easy to delete.