Files
saas_frontend/HANDOVER.md
T
2026-08-31 20:05:33 -04:00

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 budgetlower 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:

  • ProfileSessionsPanel (see and end your sessions), SecurityPanel (MFA enrolment), and NotificationPreferencesPanel.
  • UsersDeletedUsersPanel, since deletion is now soft.
  • HeaderNotificationBell.
  • LayoutSubscriptionBanner, 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

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 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:

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.