Four counts, each one work somebody has to do today: failed injections,
licences expiring inside 14 days, past-due subscriptions, and purchases
unlinked for more than 48 hours. No totals and no revenue -- nothing that
cannot be acted on. Every row links straight to the thing that needs doing.
An empty queue says "nothing to do here" rather than rendering a bare zero,
so a quiet dashboard reads as quiet rather than broken.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The link screen carries the whole burden of the five-minute bar: it names
where to find the instance ID, validates the format before asking the server
so a typo is instant rather than a round trip, surfaces the backend's own
message when a UUID is already linked, and on success lands the customer
directly on the download rather than back on a list.
Billing is deliberately thin and says plainly that billing changes go
through support, rather than linking to a Paddle portal that does not exist
until spec 5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The blob is shown inline as well as offered as a file, because a licence is
signed public data bound to one instance -- useless anywhere else -- and a
blocked download must never leave a paying customer stuck. Admin now returns
it to its owner for the same reason.
Relink shows the remaining allowance from the backend's max_relinks rather
than a hardcoded 3, and at zero it disables and says to contact support
instead of failing at the API.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
State reads three ways on every card -- a stripe, a shaped-and-labelled
pill, and the copy -- so it survives a colourblind reader and a glance at
arm's length. Colour alone would fail on the one screen where getting it
wrong costs money.
The expired card leads with what still works, because that is the first
thing a worried customer wants to know and the backend really does keep
servers, monitors and agent keys running. The awaiting-link card is
deliberately loud: a customer who has paid and not linked has paid for
nothing yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Route-group layouts do the guarding. A customer session on /staff/* is
redirected to its own home rather than shown a refusal -- there is nothing to
tell them about. This is UX only: admin enforces the same boundary with
RequireStaff/RequireCustomer and answers 404 rather than 403 for another
account's data, which is the layer that actually matters.
Signup carries the honeypot the backend expects and reports "check your
email" rather than claiming an account exists, matching a backend that
creates nothing until the link is opened.
Buttons match site/'s .btn--solid and .btn--line, neutral border included.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A fifth Next.js app, built like web/ and site/: App Router, React 18,
Tailwind 3, TanStack Query, standalone output.
app/globals.css carries site/app/globals.css's token blocks copied verbatim
rather than retyped, so the two cannot drift by transcription. Tailwind holds
var() references only -- no component or config entry may contain a hex
value. The semantic three are aliased: site/'s --up/--pend/--down become
valid/warn/expired, so each app names the colours for what it shows.
Unlike web/, there is no rewrite proxy: the browser calls admin directly, so
NEXT_PUBLIC_ADMIN_API_URL must be browser-reachable and listed in admin's
ADMIN_ORIGIN.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>