Integration — plugging into the live API

What the other developer needs to know to connect this pack to the API layer and Supabase, and what has been built on this side so that connection is a configuration change rather than a rewrite.
Nothing here needs rewriting to go live. Every page reads from four globals — CLIENTS, SHELF, NOMINEES, RUNBOOKS — and derives everything else. _api.js replaces the contents of those globals from the API and leaves every page untouched. Until it is configured, the pack runs exactly as it does today off the flat file.

Turning it on

One block, before the scripts, on whichever pages should run live:

<!-- 1. config (omit entirely to stay on static data) -->
<script>
window.SUPREME_CONFIG = {
  mode:            'supabase',   // 'static' | 'rest' | 'supabase'
  supabaseUrl:     'https://<project>.supabase.co',
  supabaseAnonKey: '<anon key>',
  token:           session.access_token,   // the signed-in admin's JWT
  cacheMinutes:    10,
  staleAfterDays:  7
};
</script>

<!-- 2. load order matters: data, then adapter, then the page -->
<script src="_data.js"></script>
<script src="_api.js"></script>
<script>
  SupremeData.setAdmin(currentAdminId);
  await SupremeData.load();   // fills CLIENTS / SHELF / NOMINEES / RUNBOOKS in place
  render();                    // existing page code, unchanged
</script>
Failure behaviour is deliberate. If the API is unreachable, load() logs the error, returns source: 'static-fallback' and leaves the static arrays in place. An admin gets yesterday's data and a warning rather than an empty screen — which matters when the person affected is in another timezone and cannot just ask someone.

What the API has to return

Two options. Either PostgREST directly against the schema, in which case _api.js already knows the table and column names, or a single bootstrap endpoint if you would rather assemble it server-side.

GET/dispatch/bootstrap

Returns { clients[], shelf[], nominees[], runbooks[], rates{} } in the database's own snake_case shape. _api.js maps it — do not pre-map it to the front-end shape, because then two places would own that translation.

POST/dispatch/claim

{ adminId, limit } → the tasks now claimed by that admin. Must be backed by FOR UPDATE SKIP LOCKED; see below.

PATCH/dispatch/tasks/:id

Status transitions. The server must reject done from a non-supervisor rather than trusting the UI.

PATCH/dispatch/packages/:id

Package state. Same rule — done is a supervisor action and needs confirmed_by.

POST/dispatch/reconcile

{ adminId, packages[], completedTaskIds[] } — merges progress from an admin who worked offline. The existing export/import in the portal already produces exactly this shape.

The one thing that will break at 15 admins

Three teams of five all pull work at the same time. A naive select … limit 10 for update makes fourteen of them wait behind the first. The schema ships a claim function that does not:

update public.tasks t
   set status = 'claimed', claimed_by = p_admin_id, claimed_at = now()
 where t.id in (
   select id from public.tasks
    where status = 'open'
    order by priority, id
    limit p_limit
    for update skip locked          -- each admin gets DIFFERENT rows
 )
returning t.*;

Call it as rpc/claim_tasks with { p_admin_id, p_limit }. There is also release_stale_claims() — run it on a schedule so work claimed by someone who then logged off does not sit invisible forever.

Schema notes worth keeping

Scaling to 3 teams of 5

The schema carries teams and admins with a day_capacity defaulting to ten. v_admin_load gives live per-person load — claimed, awaiting confirmation, and closed in the last seven days — which is what a team lead needs to see rather than a total.

Capacity, honestly. Fifteen admins at ten tasks a day is 150 a day. The entire current backlog is tasks. At full strength that is under working days, so the constraint stops being labour almost immediately and becomes the two things labour cannot fix: HMRC processing time, and clients who do not reply. Plan the second week around chasing and confirmation rather than around volume — and make sure someone senior is free to confirm work to Done, or fifteen people will fill the submitted queue faster than one person can check it.

Order to build in

Run schema.sql on a branch database first

It is idempotent — every object uses if not exists or create or replace — so it can be re-run while you iterate.

Seed runbooks and rates from _data.js

RUNBOOKS and RATES are the two objects that should move into the database first, because they are reference data everything else joins to.

Import the 44 clients, 8 shelf companies and 24 nominees

Same file. Set ch_verified_at to the 15 Aug verification date rather than to now — they are real readings with a real age, and pretending they are fresh is how stale data becomes invisible.

Generate tasks server-side using the same rules

The rules are in buildTasks(). Port them to SQL or run the JS on the server — but keep ONE implementation. Two copies of this logic will drift within a month.

Wire auth, then flip one page to live

Do DISPATCH.html first. If it works there, it works everywhere, because every other page reads the same globals.

Schedule the Companies House refresh

This is what makes the whole thing worth building — the register moves and our copy does not. Nightly is enough; update ch_verified_at on every read.

One thing the API cannot fix, and it should be said plainly. The practice holds no HMRC Agent Reference Number and no agent services account. No amount of live data changes that: without it nothing can be submitted to HMRC as an agent, and the 95 prepared 64-8 forms on file stay unusable. It also has an AML dependency in front of it. Build the integration by all means, but do not let a working dashboard disguise the fact that the bottleneck is a registration, not a system.