☰ MenuPage specs

Page specs — Redpoint Engine human surfaces

Companion to docs/DESIGN-BRIEF.md. The brief gives system-level direction (personas, aesthetic, tokens, component list). This file gives the per-page detail: every element, its exact data source in the current code, every state it can show, and acceptance criteria. Design from both together.

Grounding. Every "← binding" in this file points to real code as of this writing (src/app/**, src/lib/**, supabase/). Where the current scaffold renders something poorly (raw JSON brief, raw enum status shown to clients), the spec says so and specifies the intended design — those are marked elevate. Two pages are not built yet (/workspace, /jobs/[id]/review); their sections are marked NORMATIVE (unbuilt) and are grounded in BUILD-PLAN Phases 5–6 and the transition machine (src/lib/transition-rules.ts).

Rule of precedence: where this file and the code disagree, the code wins — flag it, don't guess (per brief §intro). Where this file adds a page not yet built, the spec is the target.

No invented fields. If a datum is not in the code and not promised by the BUILD-PLAN, it is not in a spec. A short list of judgement calls I had to make is at the very end.


PART A — SHARED SYSTEMS

These are referenced by every page. Design them once.

A1. Client-facing status label map

Job status is an exact enum (src/lib/transition-rules.ts, JobStatus): NEW, ASSIGNED, IN_PROGRESS, IN_REVIEW, QC_REJECTED, CLIENT_REVIEW, REVISION, APPROVED, DELIVERED, CANCELLED.

Admin (/ops) sees raw enum values. Editors (/workspace) see raw enum values too — they operate the machine and the enum is their vocabulary. Clients must never see the raw enum (brief §4). The current /jobs list and /jobs/[id] detail render job.status raw to everyone — elevate: apply this map on every client surface.

Raw status Client label Chip tone Notes
NEW Order received neutral Job created; footage may still be arriving.
ASSIGNED In production info Editor booked (client never sees who).
IN_PROGRESS In production info Same client label as ASSIGNED — client needs no finer granularity.
IN_REVIEW Quality check info Brief §4's named example.
QC_REJECTED Quality check info Must NOT read as failure (brief §4). Deliberately shares the "Quality check" label — internal rework is invisible to the client.
CLIENT_REVIEW Ready for your review accent / action The one status that asks the client to act. Loudest chip; may pair with a call-to-action.
REVISION Revisions under way info After the client requested changes.
APPROVED Approved success Payment captured, finals releasing.
DELIVERED Delivered success Terminal, happy.
CANCELLED Cancelled muted Terminal. Authorization voided.

Design must expose this as a single lookup so a second tenant needs zero new copy. Every state must be conveyed by more than colour (brief §7): the label text carries the meaning; the chip tone reinforces it.

A2. Status-chip system

Chips are "the loudest visual element on any screen" (brief §6). Two chip families:

A2a. Job-status chip (10 states above)

A2b. Ingest chip (6 states)

Source of truth src/app/jobs/[jobId]/upload/ingest-panel.tsx CHIP map and src/lib/ingest.ts IngestStatus:

Status Chip label (exact) Tone Extra content
queued queued info (indigo)
fetching fetching… info (cyan) appends humanized bytes_total when present, e.g. fetching… 3.2 GB
done done success appends · {files_imported} file(s) when files_imported > 0
failed failed error (red) shows an ⓘ affordance; error string is the tooltip
queued_manual our team will handle this warning (amber) phrase as service, never error (brief §4)
cancelled cancelled muted

A3. SLA badge (admin only)

Source slaBadge() in src/app/ops/page.tsx. Input: jobs.internal_deadline (nullable) + jobs.status.

A4. File-row component

Two live renderings today — unify them.

Client detail (/jobs/[id], table): columns Name · Kind · Size · Download. Ops detail (/ops/jobs/[id], list): kind · filename · size · download.

A5. Form-field kit (brief-renderer input side)

Order form field types, from src/app/order/order-form.tsx + BriefField (src/lib/billing.ts) + the seed template (supabase/seed.sql):

type Control Value shape Options source
text (and any unknown type — fallback) single-line input string
textarea multi-line (min-height ~70px) string
select dropdown with leading — choose — empty option string field.options[]
multiselect inline checkboxes string[] field.options[]
date date input string (ISO date) — — promised by brief §3; not in seed but the renderer must handle it

Design the kit for mobile-first (brief §7, client pages 360px up): generous touch targets, one field per row, thumb-reachable submit.

A6. Brief renderer (read side) — elevate

Today the brief is dumped as raw JSON: /jobs/[id] in a <details><pre> and /ops/jobs/[id] in a <pre>. Both must become a readable brief render (brief §2, §3).

Inputs:

Render spec:

A7. Events timeline (admin; also feeds editor/client note surfaces) — elevate

Source: events rows (type, payload, created_at), newest first, capped at 20 on /ops/jobs/[id]. Today rendered as type + JSON.stringify(payload). Elevate to human sentences, screen-reader readable (brief §7).

Event types that exist in code (do not invent others):

type Written by payload Suggested human line
status_change DB trigger (log_job_status_change) { from, to } "Status: {from} → {to}" (admin raw) / client-label mapped on client surfaces
editor_assigned transitionJob on NEW→ASSIGNED { editor_id, internal_deadline } "Editor assigned · deadline {date}" (admin only; never expose editor_id to clients)
qc_rejected_note transitionJob on IN_REVIEW→QC_REJECTED { note } "QC returned to editor: {note}"
revision_requested transitionJob on CLIENT_REVIEW→REVISION (only when a note was given) { note } "Revision requested: {note}"

A8. Theme tokens (tenant layer)

Client-facing surfaces consume tenant theme at runtime (brief §5). Actual token keys today live on tenants.theme (supabase/seed.sql, Wakecut):

{ "primary": "#0B2A3C", "accent": "#C8A45C", "logo_url": null, "font": "system-ui" }

A9. Voice (empty / loading / error)

Brief §4: "honest, specific, calm." The API already speaks this way — keep its concreteness, e.g. "Footage not ready (3 originals, 1 import pending)". Button labels are verbs, never "Submit" (brief §8). Reusable strings appear per page below.


PART B — BUILT PAGES (restyle; behaviour is fixed)

B1. /login

Purpose & user. All personas. Magic-link sign-in, tenant-branded. (src/app/login/page.tsx, login-form.tsx.)

Layout. Single narrow centred column (currently max-width: 400px). Mobile- first; this is the only unauthenticated page (brief §7 — no marketing furniture). Tenant logo slot (A8) above the form; heading; email field; one primary button.

Elements & bindings.

State matrix.

Interactions. Submit is async, no optimistic UI; on success swaps to sent. No resend affordance exists today — may design one but do not assume a backend for it beyond re-submitting the form.

Empty/loading/error. No list here. Loading = button busy state (design a disabled/spinner variant; code currently doesn't set one — safe to add visually). Error microcopy: keep the raw Supabase message but frame it calmly ("We couldn't send that link — check the address and try again." over the raw string is acceptable design polish).

Acceptance criteria.

  1. Tenant name (or logo) appears in the branded header; falls back to "Sign in" with no tenant.
  2. Email field is labelled and keyboard/AA accessible; visible focus ring in the accent colour.
  3. sent state fully replaces the form with the check-email message.
  4. error state is legible, calm, and not colour-only.
  5. Uses tenant tokens (A8); no yacht-specific or hard-coded brand copy.
  6. Works at 360px width; single primary action is thumb-reachable.

B2. /order

Purpose & user. Client. Choose a package, fill the tenant brief, go to payment. (src/app/order/page.tsx, order-form.tsx.)

Layout. Single column (currently max-width: 640px), mobile-first. Order top- to-bottom: branded heading → package chooser → brief fields → rush toggle → VAT block → error list → primary button. Desktop may widen but stays single-column (brief §6: whitespace over widgets).

Elements & bindings.

State matrix (button + form).

Interactions.

Empty/loading/error.

Acceptance criteria.

  1. Package cards show name, description, price in the tenant currency, and a clear entitlement/one-off distinction.
  2. The rush toggle appears only for rush-eligible packages and, when on, the surcharge and new total are visible before the client commits.
  3. Brief fields render per type (text/textarea/select/multiselect/date) with accessible required markers.
  4. Required-field errors surface as a fixable list in calm voice, not colour-only.
  5. Primary button label reflects the billing mode (payment vs place-order) and shows the amount for one-off.
  6. Prices never mis-scale (minor units ÷ 100 exactly once) and use tabular numerals.
  7. Fully usable at 360px; single accent used only on the primary action + focus.

B3. /order/success

Purpose & user. Client. Post-payment reassurance. (src/app/order/success/page.tsx.)

Layout. Short centred column (currently max-width: 560px). Confirmation headline → reassurance paragraph → next-step link.

Elements & bindings.

State matrix.

Empty/loading/error. No session_id → order is null → pending state shown. That's acceptable. No hard error surface needed.

Acceptance criteria.

  1. The "authorized, not charged" promise is the visual focus.
  2. Both branches (job ready / job setting-up) render calmly with the right link.
  3. The setting-up branch never reads as failure.
  4. Tenant-themed; works at 360px.

B4. /jobs

Purpose & user. Client and editor (RLS scopes the list). Job index. (src/app/jobs/page.tsx.) Admin also technically sees all here, but the admin's real console is /ops.

Layout. Single column (currently max-width: 720px), mobile-first. "+ New order" link → list of job rows.

Elements & bindings.

State matrix. Each row shows one job-status chip (A2a) mapped per role. Rush is an independent marker.

Interactions. Row → job detail. "+ New order" → /order. No inline actions.

Empty/loading/error.

Acceptance criteria.

  1. Clients see friendly labels (A1) + chips, never raw enum values.
  2. Editors see the raw enum + chip; the same page adapts by role.
  3. Short id is monospace/tabular and links to the detail page.
  4. Empty state is persona-appropriate and actionable, never blank.
  5. Rows are scannable at 360px; date and rush don't crowd the status chip.

B5. /jobs/[id] (client job detail)

Purpose & user. Client (also the editor's/admin's basic detail, but their richer views are /workspace and /ops). Status, files, footage readiness, cancel. (src/app/jobs/[jobId]/page.tsx.)

Layout. Single column (720px), mobile-first. Back link → title (short id) → status line → footage-readiness banner (NEW only) → brief (collapsible) → files table → upload link → cancel.

Elements & bindings.

State matrix.

Interactions.

Empty/loading/error.

Acceptance criteria.

  1. Client sees a mapped status label + chip, never the raw enum.
  2. The footage-readiness banner appears only for NEW jobs and shows exact counts with a colour + text (not colour-only) ready/waiting distinction.
  3. Brief renders as labelled fields (A6), not raw JSON.
  4. File rows expose name, kind, size, and a working download; empty state present.
  5. Cancel is available exactly for the pre-capture statuses and uses a designed destructive confirm that names the authorization void.
  6. Upload entry point appears only when the viewer may upload.
  7. Mobile-first at 360px; single accent reserved for the primary action.

B6. /jobs/[id]/upload (THE critical client screen)

Purpose & user. Client (editors reuse it for drafts). Get multi-GB footage in, three ways, with live trust. (src/app/jobs/[jobId]/upload/page.tsx, uploader.tsx, ingest-panel.tsx.) Brief §3 calls this the critical screen.

Layout. Single column (720px), mobile-first. Back-to-job link → "Upload files" heading → Uppy Dashboard (drag-drop + "connect a service" pickers) → "Or paste a transfer link" ingest panel (originals only). Brief §3 describes "three side-by-side affordances" — on mobile they stack; on wider screens the drag-drop and connect-a-service live in the one Dashboard, with the paste-link panel below. Design the stack order so the simplest path (drag-drop) is first.

Elements & bindings.

State matrix (ingest rows). Six chip states (A2b). Polling runs only while any row is queued/fetching. fetching shows live bytes; done shows file count; queued_manual is phrased as service; failed exposes the error; both queued and queued_manual are cancellable.

Interactions.

Empty/loading/error.

Acceptance criteria.

  1. The three affordances (drag-drop, connect-a-service, paste-link) are all present and clearly separated; on mobile they stack simplest-first.
  2. The paste-link panel appears only for original uploads (not editor drafts).
  3. Ingest chips render all six states with exactly the code's labels, incl. queued_manual as a service message and live bytes on fetching.
  4. The pasted source is shown redacted (host · filename); the raw URL is never displayed.
  5. "Safe to close this tab" is shown for pasted-link ingests and not for browser uploads.
  6. Cancel appears only on queued/queued_manual ingests.
  7. The Uppy Dashboard is CSS-reskinned in-theme, not replaced, and remains accessible.
  8. Connect-a-service sources appear only when Companion is configured — the design degrades gracefully without them.
  9. Works on a phone at 360px on flaky Wi-Fi; progress and resumability are legible.

B7. /ops (admin console list)

Purpose & user. Admin/operator. Every job across tenants, SLA-first. (src/app/ops/page.tsx.) Not tenant-themed — Redpoint's own tool (brief §5): neutral, dense, dark-mode-eligible. Desktop-first (1280px, usable at 1024 — brief §7).

Layout. Wide container (currently 980px; design for 1280). Title + one-line subtitle → filter row(s) → dense table. Information-dense, sortable feel (brief §2 operator persona).

Elements & bindings.

State matrix. Each row: status (raw enum + revision marker), rush marker, SLA badge (breach / amber / normal / none). The breach state is the loudest thing on the page and must survive a fast scan.

Interactions. Filter links (server-rendered navigation). Row → ops job detail. No inline mutation here (mutations live on the detail page).

Empty/loading/error.

Acceptance criteria.

  1. Neutral, high-density, non-tenant-themed styling; dark-mode viable.
  2. All seven columns present with exact bindings; editor column shows pseudonym only.
  3. SLA badges (A3) render breach/amber/normal correctly and breaches dominate the scan; tabular numerals throughout.
  4. Filter row exposes Open/All/per-status and (multi-tenant only) tenant filters, with the active filter visibly marked.
  5. RUSH and revision counter are visible without shouting over SLA breaches.
  6. Rows are legible and sortable-feeling at 1024–1280px.

B8. /ops/jobs/[id] (admin job detail + action panel)

Purpose & user. Admin. The operator's control surface for one job: meta, footage, brief, files, audit timeline, and every state transition. (src/app/ops/jobs/[jobId]/page.tsx, ops-actions.tsx.) Not tenant-themed.

Layout. Single dense column (currently 780px). Back link → title (short id + raw status + RUSH) → meta tableaction panel → brief → files → events. Desktop-first.

Elements & bindings.

State matrix. The action panel is a state-driven form: it shows a different control set for each of the 8 actionable statuses (DELIVERED/CANCELLED show only whatever remains, effectively none but Cancel where legal). Each mutating control has idle/busy states and shares one error line. The disabled conditions above are the exact implemented gates — the design must represent them (greyed options, disabled Assign until three preconditions, disabled QC-reject on empty note).

Interactions.

Empty/loading/error.

Acceptance criteria.

  1. Meta table shows tenant, package (+revision count/limit), client (+email), pseudonymous editor, deadline, footage readiness with counts, created — all bound as above.
  2. The action panel shows exactly the controls legal for the current status and nothing more.
  3. Assign shows each editor's load/cap and disables over-cap/unavailable editors; the Assign button is disabled until editor + deadline + footage-ready.
  4. The footage-not-ready warning and the empty-editors message appear under their real conditions.
  5. QC reject is disabled with an empty note; QC pass and reject route to the right statuses.
  6. Cancel appears only in pre-capture states and uses a designed destructive confirm that names the authorization void.
  7. Brief renders readably (A6) and the events timeline reads as human sentences (A7), admin-only, without leaking client/editor identity across the boundary.
  8. Neutral non-tenant-themed styling; usable at 1024–1280px.

PART C — UNBUILT PAGES (NORMATIVE)

These pages do not exist in the code yet. Their specs are the target for BUILD-PLAN Phases 5–6. Every transition named below is checked against the real permission matrix (src/lib/transition-rules.ts PERMISSIONS). Where the current matrix and the Phase-6 intent differ, it is called out explicitly.

C1. /workspace (editor) — NORMATIVE (unbuilt), BUILD-PLAN Phase 5

Purpose & user. Editor (freelance professional, desktop, many jobs). Their whole job: see assigned work, read the brief, pull originals, upload a draft, ship it — with zero ceremony (brief §2). Editors see only their assigned jobs (RLS). Pseudonymous to clients; never juxtapose client and editor identity (brief §2, ARCHITECTURE §8).

Layout. Desktop-first (1280px, usable at 1024). Two-pane feel: a left assigned-jobs list and a right job detail; on narrower widths, list collapses above detail. Engine-neutral styling (spans tenants — not tenant-themed; A8). Dense but scannable (brief §2 editor persona).

Assigned-jobs list

Job detail (right pane)

Editor actions (grounded in PERMISSIONS, adminOrAssignedEditor)

The editor may perform exactly these edges (from the matrix):

Button (proposed) Edge Shown when status is Notes
Start work ASSIGNED → IN_PROGRESS ASSIGNED begins the job
Submit for QC IN_PROGRESS → IN_REVIEW IN_PROGRESS hands the draft to the operator; gate on ≥1 draft file present — see below
Resume work QC_REJECTED → IN_PROGRESS QC_REJECTED after a QC rejection; pairs with the QC note
Resume work REVISION → IN_PROGRESS REVISION after a client revision request; pairs with the revision note

State matrix.

Empty/loading/error.

Acceptance criteria.

  1. Editor sees only assigned jobs; nothing from other editors or unassigned work; no client email/identity beyond brief-level info.
  2. The brief renders readably (A6) and is the page's most legible element.
  3. Originals are downloadable; drafts are uploadable via the reused uploader with kind='draft'; no paste-link ingest panel appears.
  4. Exactly the four editor-legal transitions are offered, each under its correct status; no assign/QC/approve/cancel controls exist.
  5. QC-rejection notes and revision notes surface prominently and are visually distinguished, in calm professional voice.
  6. "Submit for QC" is disabled until a draft file exists (normative UI gate), with helper copy explaining the watermarked-preview requirement.
  7. Editor status labels are the raw enum + chip; deadlines read as focus, not alarm.
  8. Engine-neutral (non-tenant) styling; dense and keyboard-friendly at 1024–1280px.

C2. /jobs/[id]/review (client) — NORMATIVE (unbuilt), BUILD-PLAN Phase 6

Purpose & user. Client. Watch the watermarked draft, then Approve (→ payment capture — a deliberate, confident moment) or Request changes (with a note and a remaining-revisions count). After approval, download finals. Mobile-first, non-technical, calm (brief §2, §3).

Layout. Single column, mobile-first (360px up). Order: branded header → status line → preview player frame → decision block (Approve / Request changes) → (post-approval) finals section. Whitespace over boxes (brief §6); tenant-themed (A8).

Preview player frame

Decision block (only in CLIENT_REVIEW)

Grounded in the state machine: from CLIENT_REVIEW the client-relevant edges are APPROVED and REVISION.

Finals section (post-APPROVED)

State matrix (by job status, client-mapped label A1).

Interactions.

Empty/loading/error.

Acceptance criteria.

  1. The page plays the watermarked preview (kind='preview') inside a designed frame, clearly labelled as a preview, with player internals left standard.
  2. Approve is a deliberate, confident, accent-led confirmation whose copy states that approval releases payment and finals (Phase-6 capture assumed).
  3. Request-changes requires a note (UI-enforced) and shows the correct "remaining of total revisions" count from revision_limit − revision_count.
  4. The over-cap state is handled calmly with a paid-revision/contact path, not a dead error.
  5. Finals (kind='final') appear only after APPROVED, clearly distinguished from the watermarked preview, downloadable via the standard file row.
  6. Every job status maps to the right client label (A1) and the right action set; pre-review and revision states are calm, never blank.
  7. Mobile-first at 360px; tenant-themed; single accent on the Approve action; all states conveyed by more than colour.

Judgement calls I had to make (flagged explicitly)

These are decisions taken because the brief asked for pixel-completeness while the code left a gap. Each is defensible and grounded, but a reviewer should confirm:

  1. Client status label map (A1). The brief mandates a mapping but only gives the IN_REVIEW example and the "QC_REJECTED must not read as failure" rule. I mapped both IN_REVIEW and QC_REJECTED to "Quality check" (so rework is invisible to clients), and collapsed ASSIGNED+IN_PROGRESS to "In production." The exact words are proposals; the rules they satisfy are the brief's.
  2. Editors see raw enum, clients see mapped labels. The brief says admin sees raw; it's silent on editors. I put editors on the raw enum (they operate the machine). If the operator wants editor-friendly labels, that's a small change.
  3. Brief renderer needs the template on /ops/jobs/[id] and /workspace. brief_data stores keys, not labels/types; the ops page currently loads only brief_data. I specced the renderer to consume brief_templates.schema and degrade to key-humanization. This implies a small data-loading addition on those pages (normative).
  4. /jobs/[id]/review Approve is admin-only in the current matrix. I specced the client Approve button as Phase-6 normative and flagged that shipping it requires the capture+payout wiring that flips CLIENT_REVIEW→APPROVED to allow the owner-client. The design is correct for Phase 6; it must not ship before.
  5. UI-only gates the server doesn't enforce yet: (a) editor "Submit for QC" disabled until a draft file exists; (b) client "Request changes" note required. Both are normative UI gates; I explicitly did not claim the server enforces them (it doesn't today).
  6. Finals gating. The download route gates on RLS participation only, not on APPROVED/kind. I specced the review page to reveal finals only post-APPROVED (per ARCHITECTURE §6) and flagged that Phase 6 must add the server-side gate — the design assumes it.
  7. Preview vs draft on the review page. ARCHITECTURE §6 says clients see watermarked previews; the editor uploads a preview alongside the draft until the Phase-9 transcoder. I specced the player to bind to kind='preview', not draft. If the operator prefers to show draft directly pre-transcoder, that's a one-line change — but it would show unwatermarked footage, which the architecture forbids.
  8. Theme token names. The brief proposes --brand-accent etc.; the actual tenants.theme keys are primary/accent/logo_url/font. I documented the real keys and mapped them to the brief's intended token roles so the design and the data agree.