9.1 KiB
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), andNotificationPreferencesPanel. - 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, becauseundefinedleaves 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
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
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 hoistedvi.mockfactory returnsundefined. Use a plainasync () => ….- 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:
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/
<feature>/
<Feature>Page.tsx The screen.
<Feature>Api.ts Its API calls.
<Feature>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/<feature>/ 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.