☰ MenuUAT — Admin/Supervisor walkthrough

UAT — admin / supervisor workflow (ops console acceptance)

Test account: man_on_rock@redpoint-vision.com (admin, real inbox). Site: https://wakecut.com — the ops surfaces live under /ops. Runs alongside docs/ops/UAT-CLIENT-WALKTHROUGH.md (client = dorian@redpoint-vision.com) and docs/ops/UAT-EDITOR-WALKTHROUGH.md (editor = rp.editor@example.com). Use separate browser profiles — many admin steps QC / approve / assign the jobs the other two sheets create.

This is the overall supervisor's sheet: the console tour, finance review, the editor-supervision surface (1E), and the GDPR / retention operator jobs. Every step is Pass / Fail-able. /ops is admin-only — a non-admin session is redirected to /jobs, so all of the below fails closed for the wrong role (worth confirming once as the client/editor).

The promise this whole surface must keep: you can see and steer every job and editor across every tenant, money never moves from a review screen, and every destructive or income-affecting action is deliberate, attributed, and audited.

🧱 Migration state. The ops surfaces below read migrations 00340040, applied and probe-verified live 2026-07-18 (the 0040 paste was verified this session: the replaced record_supervision resolves with the 0040 signature and the rejected status filter returns 200). The supervision confirm-queue + manual-action steps (§4.3–§4.4) depend specifically on 0040 (admin attribution in the audit + the rejected audit status). Expected present — but rule 8 still stands: verify before relying, and use the §4.4 functional probe as belt-and-braces.

📧 Emails you verify here. man_on_rock@… is a real inbox, so the admin-facing SLA-breach email is directly checkable. Editor/client emails are confirmed via the admin-readable notifications table (one row per event, status = sent, unique dedupe_key) — see the editor sheet's email callout. Requires RESEND_API_KEY in the Vercel prod env.


1 · Ops console tour (/ops)

Precondition: logged in as man_on_rock@…; at least one job exists (run the client sheet §1 once, or use seed jobs).

# Do Expect
1.1 Open /ops Ops console — all jobs across all tenants, one table (Job · Tenant · Package · Client · Editor · Status · SLA), sorted by deadline with breaches first (a past-deadline live job floats to the top, row tinted)
1.2 Use the status filters (Open / All / each status) The table filters in place; Open hides DELIVERED/CANCELLED. A tenant filter row appears only when more than one tenant exists (wakecut + expocut)
1.3 On a NEW, unassigned job read the pool chip "in pool" (info) when the effective dispatch mode is pool, "withheld" (muted) when managed — the effective mode is override → package → pool
1.4 If any pool job has stalled (editors passed it, or a client scope-change is pending) A "Needs attention" section above the table: pushback cards show "{N} of {M} editors passed" + the pass-reason chips (ops-only), with a release-prompt button; scope-change cards show "Scope change — needs ack" + the client's note, with an ack button. (To generate one: editor sheet §2.2 passes, or ops releases the prompt.)
1.5 Click a job id Lands on /ops/jobs/[id] — the job detail (§2)

Pass bar (§1): every tenant's jobs visible in one deadline-sorted list, breaches surfaced, and the needs-attention queue reflects real pushback / scope state.

2 · Job detail — lifecycle, QC, dispatch (/ops/jobs/[id])

Precondition: a job mid-pipeline. Best run interleaved with the editor sheet (§3 there submits for QC; §4 here QC-passes).

# Do Expect
2.1 Open a job detail Header: short job id + an admin status chip. Meta block: tenant, package (with revisions used / limit), client (company/name + email — ops sees full client identity), assigned editor pseudonym + quality readout, deadline, footage readiness, created. A money card (§3 cross-ref), the rendered brief, the files list, and an events timeline
2.2 On a NEW job with footage not ready The assign panel shows "Footage not ready — assignment is blocked…"; the Assign button stays disabled until at least one original has landed and no import is in flight
2.3 On a NEW, footage-ready job: pick an editor + a deadline, Assign Editor dropdown lists this tenant's approved + available editors with load/cap (options disabled when unavailable or at cap). Assign transitions NEW → ASSIGNED, sets the internal deadline. Assigning fires the "New job assigned" email (an admin assign, unlike a board self-claim) — verify a job_assigned row in notifications
2.4 Walk a job through the lifecycle buttons ASSIGNED → Mark in progress; IN_PROGRESS → Mark submitted for QC; the actions panel only ever offers legal next states (from the transition rules)
2.5 QC pass on an IN_REVIEW job: "QC pass → client review" The job does not jump straight to client review — it enqueues a watermarked preview transcode and stays in QC, surfacing a "Preview queued — awaiting transcode" note; it advances to CLIENT_REVIEW automatically once the preview lands (Phase 9). The assigned editor's QC-pass counter increments
2.6 QC reject on an IN_REVIEW job: type the required QC note, then "QC reject" Button stays disabled until the note is non-empty; on reject the job → QC_REJECTED and the editor gets "Changes needed before review (…)" carrying the note verbatim (editor sheet §3.4📧 — verify via notifications)
2.7 On a CLIENT_REVIEW job use "Approve (admin override)" or "Request revision" Admin can approve on the client's behalf or push a revision (optional note); these mirror the client review actions
2.8 Dispatch toggle on a NEW, unassigned job A pool ⇄ withheld override (jobs.dispatch_override) lets you hold a job back from the self-claim board or release it, independent of the package default
2.9 Cancel a cancellable job → "Cancel job (void authorization)…" A confirm dialog explains it voids the payment authorization; on confirm the job → CANCELLED and the Stripe auth is voided (never captured)

Pass bar (§2): every transition button offers only legal next states, QC pass queues a preview (not an instant advance), QC reject demands a note and emails the editor, and cancel voids the auth.

3 · Finance — margins & commissions (/ops/finance)

Precondition: admin session. Realized figures are empty until a job has been captured (client sheet §4.7).

3a · Margins (/ops/finance)

# Do Expect
3a.1 Open /opsFinance Heading Finance, "Modeled vs realized — realized from the verified Stripe ledger". Sub-nav to Pricing / Margin builder / Commissions / Jobs
3a.2 Read Per package — modeled vs realized Each package's modeled margin (£ + %) from price vs editor fare, and a Realized column (from the F2/F3 Stripe ledger) — "no data" until a capture lands. A realized figure far below modeled tints red (drift; the threshold is billing_config.drift_alert_bps, tenant config not code)
3a.3 If the Realized — by month table shows Per-currency rows from the verified ledger (revenue, Stripe fees, fares, refunds, gross margin, jobs) — GBP and EUR never summed together
3a.4 If a "N jobs missing a price snapshot" warning shows Investigate — those jobs' money terms are undefined (should be zero)

3b · Partner commissions — dry-run (/ops/finance/commissions)

Precondition: migration 0035 applied (else the page shows a "Migration 0035 is not applied…" notice and records nothing). This page is read-only — no money moves from it.

# Do Expect
3b.1 Open Finance → Commissions Heading Partner commissions, "Dry-run accruals — review before arming"
3b.2 Read the Arming state panel Global env PARTNER_COMMISSION_ENABLED shown as "off (dry-run)" (or ARMED). A real transfer needs both that env and a tenant's commission_live on — both default off, so nothing transfers
3b.3 Look for first-party tenants (wakecut) in the Partner tenants table Absent — wakecut is the merchant, keeps everything, and is not listed as a partner. Only tenants with a Connect destination appear. Its capture path is byte-identical (no accrual)
3b.4 On a partner row with no platform_fee_bps A "fee unset" chip (tooltip: "No platform_fee_bps set — Redpoint's full cut accrues to the partner (accrual uses 0)") instead of a bare 0%
3b.5 If any accrual mixed currencies (EUR gross vs GBP fare) An FX-held banner — those accruals are HELD: never auto-transfer even when armed, and excluded from the would-transfer subtotals, pending a real EUR→GBP conversion
3b.6 Read the Accruals table + commission_live column Per-accrual audit (gross − editor − platform fee − Stripe fee = commission), newest first; commission_live shows off (muted) for every tenant by default. Confirm the subtotal note: currency = gross currency, Stripe fee is a model estimate, nothing moves money

Pass bar (§3): modeled and realized margins read from config + the verified ledger (never guessed); commissions are dry-run with first-party tenants excluded, the fee-unset chip and FX-held banner visible where they apply, and no control on either page moves money.

4 · Editor supervision — the 1E ops surface (/ops/editors/supervision)

The supervisory layer scores each (editor, tenant) pair from client ratings + QC/on-time/revision counters and can warn / throttle / suspend / reinstate. It is OFF by default and, even when on, queues every action for a human (auto-actions default false). This section proves the surface; enable it on a test tenant first, then disable after.

4.0 · Precondition — enable / disable supervision (config, no migration)

Supervision reads tenants.dispatch_config.supervision. With it off, the roster shows every editor as "unscored" / "off" and no action is possible — that is the correct default state and worth confirming first. To exercise §4.1–§4.5, enable it on the tenant you are testing (wakecut, or the e2e/test tenant), then disable it afterwards if you don't intend to run supervision live.

The paste merges the supervision node into dispatch_config (top-level || replaces only that key, preserving the sibling pushback/dispatch knobs). Run it as the operator (Supabase SQL editor, or the Management API per CLAUDE.md) — pick the tenant by slug.

ENABLE (documented engine defaults — enabled:true, auto_actions:false, so actions queue for you, never auto-apply):

update public.tenants
set dispatch_config = coalesce(dispatch_config, '{}'::jsonb) || jsonb_build_object(
  'supervision', jsonb_build_object(
    'enabled',          true,
    'auto_actions',     false,
    'min_sample_size',  5,
    'score_window',     jsonb_build_object('kind', 'recent_jobs', 'n', 20),
    'weights',          jsonb_build_object(
                          'client_rating', 0.40, 'qc_pass', 0.30,
                          'on_time', 0.20, 'revision', 0.10),
    'ladder',           jsonb_build_object(
                          'warn',     jsonb_build_object('below_score', 0.70),
                          'throttle', jsonb_build_object('below_score', 0.55,
                                        'cap_to', 1, 'deprioritise', true),
                          'suspend',  jsonb_build_object('below_score', 0.40)),
    'reinstate',        jsonb_build_object('mode', 'manual', 'auto_above_score', 0.65),
    'guards',           jsonb_build_object('max_auto_suspends_per_day', 2)
  ))
where slug = 'wakecut';   -- or your e2e / test tenant slug

These are the exact DEFAULT_SUPERVISION values from src/lib/supervision.ts. score_window.n (20) is ≥ min_sample_size (5) on purpose — a window below the floor would trip the config warning chip (§4.1) and leave the layer silently inert. auto_actions:false is the safe stance for an income-sensitive layer: transitions queue, a human confirms.

DISABLE (restore the default — everyone reads unscored/off, no teeth):

update public.tenants
set dispatch_config = coalesce(dispatch_config, '{}'::jsonb) || jsonb_build_object(
  'supervision', jsonb_build_object('enabled', false, 'auto_actions', false))
where slug = 'wakecut';   -- match the slug you enabled
# Do Expect
4.0.1 With supervision off, open /ops/editors/supervision Every roster tenant shows a muted "supervision off" chip and every editor an "unscored" / "off" standing — no ladder line, no breaker chip. This is the correct dormant default
4.0.2 Apply the ENABLE paste to your test tenant, reload That tenant's chip flips to "supervision on" and shows its floor + ladder line (warn/throttle/suspend cutoffs) + a breaker count — all read from the parsed config (rule 5)
4.0.3 After finishing §4, apply the DISABLE paste, reload The tenant returns to "supervision off"; standings read unscored again; the board (editor sheet §2) is byte-identical to dormant

4.1 · Roster

Precondition: supervision enabled on the test tenant (§4.0). Meaningful scores need editors with ≥ min_sample_size client ratings (client sheet §4b) — below the floor an editor stays "unscored" even when the layer is on.

# Do Expect
4.1.1 Read the per-tenant roster Editors grouped by tenant, worst standing first. Each row: a standing chip (good / warn / throttle / suspend, or muted unscored), the editor pseudonym + real name (ops-only identity — never leaks to a client/editor surface), and readouts: score (composite %), rating (avg + n=count), QC pass / on-time / revisions rates, sample size (with "below floor" when thin)
4.1.2 Read the tenant header chips floor N · ladder warn<…/throttle<…/suspend<… (from config), and a breaker M/K chip (auto-suspends applied today vs the per-day cap) that tints danger when the cap is hit
4.1.3 If you set score_window.n below min_sample_size A config warning appears: "score window is smaller than the sample-size floor — no editor can ever clear the floor, so the layer is silently inert…". Behaviour is unchanged (editor-safe); the warning just surfaces the foot-gun
4.1.4 Click an editor pseudonym Opens the per-editor audit detail (§4.2)

4.2 · Per-editor audit detail (/ops/editors/supervision/[editorId])

# Do Expect
4.2.1 Open an editor's audit page Their standing by tenant (chip + score + rating + sample per tenant) and a global QC/on-time/revision readout
4.2.2 Read the Action history table Append-only supervisory_actions: when · tenant · action · status · by (a system chip for engine/auto, or the acting admin's name for a manual/confirm action) · prior → new standing · score-at-the-time · reason. This is the audit trail every §4.3–§4.4 action lands in

4.3 · Confirm queue — approve / reject

Precondition: supervision enabled; migration 0040 (applied live — see the migration callout). A queued proposal appears when the engine proposes an action under auto_actions:false — e.g. an editor's score drops below the warn cutoff after a low rating (client sheet §4b, 1–3★), then the per-rating hook (or the supervision cron) queues it.

# Do Expect
4.3.1 On the roster (or the editor detail) find a queued proposal A Confirm queue section at the top of the roster lists each (editor, tenant) with a "Proposed: {verb}" chip, current standing, and score; the roster row also carries a "queued: {verb}" chip
4.3.2 Approve a proposal: type the required reason, click Approve The proposed action applies (standing changes, teeth engage); a new applied audit row lands attributed to you with your reason. The queue entry clears
4.3.3 Reject a different proposal: type the reason, click Reject The standing is unchanged, the queue clears, and a rejected audit row is written attributed to you (re-proves 0040 in passing — a pre-0040 DB would reject the status)
4.3.4 Both buttons with an empty reason Blocked — "a reason is required" (enforced client- and server-side)

4.4 · Manual actions — warn / throttle / suspend / reinstate

# Do Expect
4.4.1 On a roster row (or the editor detail) click "Manual action…" A form: a verb select (Warn / Throttle / Suspend / Reinstate) + a required reason field
4.4.2 Warn an editor with a reason, Apply The standing → warn; an applied, auto=false, admin-attributed audit row lands with your reason. Warn is informational (no dispatch teeth)
4.4.3 Throttle an editor Standing → throttle; the row shows a "cap {N}" chip (reduced concurrent cap, magnitude from config); their pool jobs sink below good-standing work
4.4.4 Suspend an editor Standing → suspend; that (editor, tenant) is removed from that tenant's board. A manual suspend is not subject to the auto-suspend breaker (the breaker guards automatic mass-suspends only)
4.4.5 Reinstate a held editor A distinct reinstate audit row lifts the hold to good; the fresh ladder re-evaluation is left to the next refresh (never fused into the reinstate row)
4.4.6 Any manual action with an empty reason Blocked — "a reason is required"
4.4.7 (0040 belt-and-braces probe — expected to pass) Every manual action succeeds and its audit row carries your name. If one 500s, or an audit row reads system instead of you, the live DB has regressed from the verified 0040 state — re-probe and re-apply before continuing

4.5 · Supervision at assign time (ties §2.3 to §4)

Precondition: an editor is throttled or suspended for a tenant (§4.4), and that tenant has a NEW, footage-ready, unassigned job.

# Do Expect
4.5.1 Open that job's assign panel; read the editor dropdown A throttled/suspended candidate's option is annotated ", throttled" / ", suspended" alongside its load/cap — a warning, not a block (the 1D warn-not-block rule: an admin may still deliberately assign)
4.5.2 Select the suspended/throttled editor A warning chip appears: "{editor} is suspended for this tenant. Assigning overrides supervisory dispatch — allowed, but review the supervision roster first." The Assign button still works
4.5.3 A good / unscored candidate No annotation, no warning — the assign panel is byte-identical to §2.3

Pass bar (§4): off by default (unscored everywhere); enabling shows config-driven ladder + breaker chips and the config-warning foot-gun; the confirm queue and manual levers each demand a reason and write an attributed audit row (applied or rejected); a suspended/throttled candidate is warned, not blocked, at assign time.

5 · Voice notes — admin view (/ops/jobs/[id])

Precondition: the client recorded ≥1 voice note on a job (client sheet §2b); migration 0034 applied.

# Do Expect
5.1 Open the job detail → Files The client's voice note appears as a file of kind voice_note in the list, one-click downloadable with your admin session (the double-blind player is the editor workspace; ops sees it as a downloadable file)
5.2 Read the Events timeline A voice_note_added event per note (the audit that a note was recorded)
5.3 Confirm the client identity boundary is intact The voice note is audio only — nothing about it exposes anything the brief doesn't already show to the editor

Pass bar (§5): voice notes are visible and downloadable to the admin as voice_note files with a matching event, nothing more.

6 · Rate-your-edit — admin visibility

Precondition: at least one client rating exists (client sheet §4b); migration 0037 applied. Supervision on (§4.0) to see the aggregate on the roster.

# Do Expect
6.1 On the supervision roster / editor detail read the rating readout The editor's average stars + n=count per tenant (the aggregate that feeds the composite score). The raw stars — never the client's free-text comment — are what ops sees here
6.2 Confirm the comment boundary The client's rating comment is client PII and is not surfaced on the roster/audit (the roster reads stars only). Admin does not have a general screen that shows rating comments
6.3 Cross-check the tie-in is editor-demand signal, not an admin lever A 4–5★ rating offers the client the paid specify-editor re-request (client sheet §4b) — the admin sees its effect only as held jobs (editor sheet §2b) and, if priced, commission/margin, never as a rating override

Pass bar (§6): admin sees rating aggregates (avg + n) on the supervision surface, never individual client comments, and the 4–5★ tie-in reads as a demand signal, not an admin control.

7 · In-app UAT widget — admin view

The floating UAT checklist widget is gated by enrollment in uat_testers (not by admin role). To see it as the admin, enroll man_on_rock@….

Precondition: a uat_testers row for the admin profile; migration 0023 applied. (No row → the widget renders nothing and ships zero client JS — the safe default.)

# Do Expect
7.1 Enroll the admin in uat_testers, then open any live page as man_on_rock@… A small "UAT n/total" pill appears top-right — it never overlays a primary CTA, the cookies control, or the dev overlay
7.2 Open the pill The panel shows the Client and Editor checklist tabs, each step with Pass / Fail toggles + a note field + a build stamp
7.3 Mark a step, then check from a second tester account State is shared via uat_results — the aggregate reads "you: pass · team: 1✓ 1✗", so the whole team's verdicts are visible
7.4 Confirm the fence A signed-out visitor and a signed-in non-tester see nothing (unchanged DOM, no widget) — it is invisible and zero-cost to real users

Pass bar (§7): the widget appears only for enrolled testers, carries the shared team state, and is wholly absent for everyone else.

8 · Right-to-erasure (GDPR Art.17) — DESTRUCTIVE, admin-only

⚠️ IRREVERSIBLE. Run against a THROWAWAY client subject only — never a real client. This is an operator action (POST /api/ops/erasure) with no ops page; it deletes/scrubs the subject's PII across R2 + the DB and proves the result with an independent sweep. It targets client profiles only — an editor subject returns 422 (editor erasure is a separate manual escalation). Migration 0031 applied.

# Do Expect
8.1 Create a throwaway client (a fresh magic-link account), give it a small order → job → footage upload, a brand kit, and (optional) a rating comment A disposable subject with PII across files / events / brief_data / brand_kits
8.2 As man_on_rock@…, POST /api/ops/erasure with { "subjectProfileId": "<throwaway id>" } (admin session cookie) 200 with verifiedEmpty: true — R2 objects deleted-and-verified-empty, PII scrubbed/pseudonymized, and an independent sweep reporting zero residual PII across every store
8.3 Re-read the subject Profile pseudonymized in place (tombstone email/name, erased_at set) — the row survives so financial FKs stay valid; ledger/order money columns are untouched (only the creative brief_data jsonb is nulled)
8.4 Check the erasure_requests table One audit row recording the request + verdict
8.5 Try it on an editor profile id 422"erasure targets client profiles only…" (fails loudly rather than running an incomplete erasure)
8.6 Try it signed-out / as a non-admin 401 / 403 — and the pure runner refuses a non-admin role as defense in depth

Cleanup: none required — the tombstone is the intended end state; do not try to "undo" it. Just don't reuse the throwaway account.

Pass bar (§8): an erasure on a throwaway client returns verifiedEmpty, pseudonymizes the profile while preserving financial records, writes an audit row, and refuses editors/non-admins.

9 · Retention dry-run — reading the cron audit

The retention cron (/api/cron/retention) deletes a DELIVERED job's originals once delivered_at + retention_days passes — and nothing else. It is dry-run by default: the scheduled Vercel run carries no query string, so it computes the would-delete set, records it, and deletes nothing. A live purge needs all of ?commit=1 and RETENTION_ENABLED=1 and no footage-vault tables present. Bearer CRON_SECRET-gated (a missing Bearer → 401 before any DB/R2 access). Migration 0030 applied.

# Do Expect
9.1 curl GET /api/cron/retention without a Bearer 401 — the gate is the route's first statement
9.2 curl with Authorization: Bearer $CRON_SECRET, no ?commit 200 JSON summary with mode = dry-run; jobs_considered / would-delete counts computed; nothing deleted. A voiceNotes sub-result reports the (separate, additive) voice-note refund-window pass, also dry-run
9.3 Read the retention_runs table One row per invocation: mode (dry_run), jobs_considered, jobs_purged (0 in dry-run), objects_deleted, bytes_estimated — the audit you review before ever arming
9.4 Confirm going live is deliberate Live mode requires editing the cron path to …?commit=1 and setting RETENTION_ENABLED=1, only after reviewing these dry-run rows — and the vault gate refuses live mode entirely if vaults/vault_items tables exist

Pass bar (§9): the cron 401s without the secret, runs dry (deletes nothing) by default, records a reviewable retention_runs audit row, and cannot go live without the deliberate two-guard + vault-gate conditions.

10 · Operator / system notes (not a click-path)


Overall pass bar: every tenant's jobs and editors are visible and steerable from /ops; QC pass queues a preview and QC reject demands a note; finance reads modeled + verified-realized figures with commissions dry-run and first-party tenants excluded; the supervision surface is off by default, config-driven when on, warns-not-blocks at assign time, and writes an attributed applied/rejected audit row for every human action; and the erasure + retention operator jobs are gated, audited, and — for retention — dry-run by default with no money or footage moving from any review screen.