Commit Graph
12 Commits
Author SHA1 Message Date
mrhid6andClaude Opus 5 c4e6ad5485 feat(license): metered licensing — catalogue, entitlements, and enforcement
Server Deploy / deploy (push) Successful in 5m22s
Implements spec 7 tasks 2-10 on top of the six-plan payload from task 1.

Admin: plans re-keyed on (deployment, tier); new catalogue collection holds
every Paddle price ID (one row per priceable component); new entitlements
collection holds desired beside granted. admin/internal/catalogue owns both
folds — entitlement to licence limits, and entitlement to Paddle line items —
so the base allowance is subtracted in exactly one place. licensing.Issue now
snapshots the instance's granted entitlement, never desired. Free is enforced
per account AND deployment. Staff endpoints for plans, catalogue and
entitlements; Free self-hosted can be claimed and renewed on its annual term;
the reaper stays cloud-only.

Server: enforces the monitor cap, audit-log retention (daily sweep, skips
Unlimited and lapsed instances), and gates the OIDC callback. Unset limits are
filled from the seed plan at the single decode site so old blobs never read as
zero.

Frontends: adminsite gains a catalogue price-ID editor, six-plan allowance
screen, and a catalogue-driven PlanConfigurator mounted on the staff instance
page. web shows monitors, audit retention and support level on the licence page.

Docs: CLAUDE.md, spec index and plan 5 preamble updated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 09:37:39 +01:00
mrhid6andClaude Opus 5 34a0373eca feat(adminsite): one masthead, a shared page frame, collapsible instance records
Server Deploy / deploy (push) Successful in 53s
The console had components but no shell: a brand bar and a nav strip stacked
into 100px carrying eight words, no sign-out, no account identity, and an
Overview link hardcoded to text-accent so it read as the current page on
every screen. Nine pages each hand-rolled their own header.

AppBar replaces both bars and derives its active state from usePathname.
Settings moves into AccountMenu — it is your password, not a destination —
taking appearance with it, which finally sets the data-theme attribute the
token blocks have supported in both directions since they were written.
That leaves three customer destinations: Overview, People, Billing.

PageFrame adds a support rail so a page has a floor, and InstanceRecord
replaces InstanceCard with one component that opens and closes: an account
with a single instance used to render a third of a row of summary with its
substance a click away. It defaults open when the instance is the only one
or needs attention.

No plan card in the rail: tier, limits and expiry belong to a licence and a
licence belongs to one instance, so an account holding a Free cloud instance
and a Professional self-hosted one has no single plan. The rail carries only
what is account-wide.

Tokens and globals.css are untouched — they stay verbatim shared with site/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 17:45:53 +01:00
mrhid6andClaude Opus 5 2d12669f9b feat(adminsite): people, instance members and one password
The members panel is absent for self-hosted instances rather than disabled:
the backend refuses those, and a panel rendering controls the server will
reject is a panel that lies.

/auth/me now reports the caller's account role, so the UI hides what the
backend would refuse rather than discovering it in an error toast.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:38:21 +01:00
mrhid6andClaude Opus 5 cb90ed12fb feat(adminsite): create and renew a free instance
Adds the create form with a live slug preview, a renew action inside the
seven-day window, and a deletion countdown that renders only when the
backend has actually promised a date.

The progress bar denominator now follows the tier; a 30-day Free licence
was rendering as an 8% sliver against the hardcoded 365.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:30:42 +01:00
mrhid6andClaude Opus 5 4175608772 feat(adminsite): staff licence history, audit and plan editing
Licences and audit are both filterable client-side: the endpoints cap at 500
rows and staff are narrowing a list already in front of them.

Plans carry both guard rails spec 4 asks for. The confirmation names each
field that changes and states how many licences are already issued and
unaffected -- existing licences snapshotted their plan at issue time, and
saying so is what stops a well-meaning edit being followed by a panicked
reissue. Deployment is displayed and never editable, because moving a tier
between cloud and self-hosted would break the cloud-only rule spec 1 leans
on; that is a code review, not a form field.

The two edit buttons are the concrete changes staff need on day one. A
general-purpose limits editor waits until somebody asks for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:20:54 +01:00
mrhid6andClaude Opus 5 cefbac625c feat(adminsite): the licence ledger and staff instance actions
The screen that answers "why did this stop working on the 14th". Read top to
bottom it is one instance's whole history: what was issued, why, by whom, and
what replaced it.

Superseded entries stay visible and overprinted rather than disappearing,
because licences are append-only and hiding them would destroy the only
record that answers the question. Each links to its successor.

Injection state is shown live for cloud instances and omitted for
self-hosted, where the customer holds the blob and there is nothing for us to
have written. Staff relinks carry no cap, with the reason stated inline: the
customer cap exists to put a human in the loop, and this is that human.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:15:41 +01:00
mrhid6andClaude Opus 5 7fea321376 feat(adminsite): staff operations dashboard
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>
2026-07-25 21:10:48 +01:00
mrhid6andClaude Opus 5 92ac1eeb62 feat(adminsite): licence delivery, paste instructions and relink
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>
2026-07-25 21:06:21 +01:00
mrhid6andClaude Opus 5 242a587340 feat(adminsite): instance cards and the customer overview
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>
2026-07-25 21:03:37 +01:00
mrhid6andClaude Opus 5 7a8e683d99 feat(adminsite): session guards and the auth screens
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>
2026-07-25 21:00:54 +01:00
mrhid6andClaude Opus 5 3e447fd024 feat(adminsite): test harness, typed client and the not-connected state
The repo's first frontend test setup: Vitest, React Testing Library, jsdom.
Scoped to the flows that lose money or leak data when broken, per spec 4.

lib/api.ts collapses every failure into three the UI can act on:
NotConnected (unreachable, or no URL baked in), ApiError 401 (redirect), and
ApiError with the backend's own message, which is customer-facing and shown
verbatim rather than replaced with something vaguer.

The not-connected panel names the variable, the value baked in, and both
reasons it fails -- unreachable from the browser, or missing from admin's
ADMIN_ORIGIN. Proven by test before it existed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 20:57:51 +01:00
mrhid6andClaude Opus 5 6953f5e972 feat(adminsite): scaffold and site/'s token system
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>
2026-07-25 20:55:25 +01:00