Commit Graph
417 Commits
Author SHA1 Message Date
mrhid6andClaude Opus 5 11ebd324ad ci: only rebuild images whose inputs changed
Server Deploy / deploy (push) Successful in 2m38s
Every push rebuilt all six images regardless of what it touched. A git diff
against github.event.before now gates each build step.

Two things this needs to work at all: fetch-depth 0, because the default
shallow clone has a single commit and nothing to diff against, and git
installed in the dind container, which had node and npm but not git.

The path mapping follows the build contexts rather than intuition — the Go
images use a root context and COPY shared/, so shared/ fans out to all
three, while the Next images use their own directory and cannot be affected
from outside it. Anything that leaves no trustworthy base commit — manual
run, new branch, force-push whose old head is gone — lists every tracked
file instead, so the fallback is one code path rather than two.

Known gap, documented: a repo variable change pushes no commit, so nothing
rebuilds. workflow_dispatch is the escape hatch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 18:15:26 +01:00
mrhid6andClaude Opus 5 3a6c5ebae8 fix(admin): licence and plan features serialise as [], never null
Server Deploy / deploy (push) Successful in 5m22s
Free has no features, so a nil Go slice served "features": null. The
portal's type said string[], called .length on it, and took the page down.

The guarantee lives on a named Features type with its own MarshalJSON
rather than at each of the six places a licence or plan is serialised,
because the seventh is the one that would have been forgotten. Applying it
at marshal time also fixes rows already holding null in Mongo, which a
write-side fix alone would not.

Same class, found while checking the rest: staffListInstances built its
expiring filter from a nil []string, marshalling to $in: null, which Mongo
rejects — so the quiet week when nothing is expiring is precisely when the
staff Operations page's own query would have failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 18:10:07 +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 6bf288f83e docs: phase 3, grants project rather than federate
Records why instance_members is an index and not the authority, why hqsync
is not part of inject, and why an invitation carries no password.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:43:52 +01:00
mrhid6andClaude Opus 5 7b077905e2 feat(web): hq-sourced members are read-only here
Server Deploy / deploy (push) Successful in 2m56s
The lock is a courtesy — the API answers 409 either way. NEXT_PUBLIC_HQ_URL
defaults empty so a self-hosted install shows a plain label rather than a
link to a portal that does not serve them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:42:47 +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 a05a74cf4d feat(server): refuse local edits to hq-sourced users
The API is the boundary; hiding the control in web/ is a courtesy. A role
editable in two places is a role with two answers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:31:27 +01:00
mrhid6andClaude Opus 5 14b855fa9f feat(admin): hqsync repairs stale projected passwords
Its own package rather than a pass inside inject: inject writes three
licence fields and nothing else, and that narrowness is what makes admin's
reach into the control plane reviewable.

Repairs by copying HQ's hash, not by re-hashing — two bcrypt hashes of one
password differ by salt, so a re-hash would never converge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:30:33 +01:00
mrhid6andClaude Opus 5 0f5ad1d836 feat(admin): one password change reaches every instance
Best-effort by design: refusing the change because one instance was
unreachable would leave the customer holding the password they were trying
to replace. A failure is flagged and hqsync repairs it.

Also corrects two pieces of copy this makes false — CreateInstance's doc
comment and the instance-ready email both claimed the two passwords
diverge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:29:50 +01:00
mrhid6andClaude Opus 5 cca0ffbeae feat(admin): grant, re-role and revoke instance members
A grant writes a real control-plane user; the instance_members row is only
admin's index of it, which is why a failed insert unwinds the projection.
Self-hosted instances refuse all three mutations: their users live in a
deployment we cannot see.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:28:41 +01:00
mrhid6andClaude Opus 5 0c663945ee feat(admin): invite people to an account and give them roles
An invitation carries no password. The HQ password is what signs someone
into every instance they are granted, so a password the inviter chose would
be a shared credential to all of them — the invited row has an empty hash,
which cannot authenticate, until /accept-invite sets one.

Removing a person revokes every projected instance user first, and refuses
outright if any of those is an instance's last owner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:26:56 +01:00
mrhid6andClaude Opus 5 b37ed967b6 feat(admin): cloudprov projects, revokes and repairs users
A grant is a real control-plane users row with auth_source hq, not a
federation shim: the instance authenticates it with no runtime dependency
on admin. CountOtherOwners counts control-plane owners so a locally-created
owner satisfies the last-owner rule too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:23:46 +01:00
mrhid6andClaude Opus 5 a7b9af4422 feat(admin): account roles and the instance_members index
Phase 2 created cloud instances without recording who owns them on this
side, because the collection did not exist. The boot backfill reconstructs
one member row per instance from the hq-sourced control-plane owner, and
marks every existing customer_user an owner — they all created their own
account.

Backfill lives in models rather than db: db is the connection layer and
models already imports it for SeedPlans, so db -> models would cycle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:23:02 +01:00
mrhid6 310b3ab03f fix: Fixed plans on admin site erroring
Server Deploy / deploy (push) Successful in 3m39s
2026-07-26 16:08:15 +01:00
mrhid6 fbad3e44e5 doc: Added phase 3 plan
Server Deploy / deploy (push) Successful in 1m37s
2026-07-26 15:42:07 +01:00
mrhid6 e4a3f22871 fix: Fixed pricing license names 2026-07-26 15:41:52 +01:00
mrhid6 e8cc9b4ecd fix: Fixed misleading error message when license on cloud 2026-07-26 15:16:02 +01:00
mrhid6 0b1efca737 Updates
Server Deploy / deploy (push) Successful in 4m6s
2026-07-26 14:46:38 +01:00
mrhid6andClaude Opus 5 aef5811c16 fix(admin): SMTP over implicit TLS, and rollbacks that survive
Server Deploy / deploy (push) Successful in 2m13s
Two bugs, one symptom: signup created an account and a customer_user but
no verification email ever arrived.

net/smtp.SendMail only speaks STARTTLS. Against a port-465 server, which
expects a TLS handshake immediately, it never delivers. The transport now
wraps the connection before speaking SMTP on 465, exactly as
sitesvc/internal/mail already did — the two are duplicated, so change both
or consolidate into shared/. Also adds Date and Message-ID, whose absence
gets a message scored as spam, and a 15s deadline on the conversation.

The rollbacks ran on the HTTP request's context. A stalled mail server
holds the request until the browser gives up, which cancels that context
and turns both rollbacks into silent no-ops — stranding the exact rows
they exist to remove. They now run detached with their own timeout, and
log when they fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 14:26:34 +01:00
mrhid6andClaude Opus 5 b209fed4a7 fix(deploy): ADMIN_ORIGIN must list the marketing site too
/start now posts account signups straight to admin, so the browser sends
a cross-origin preflight from vantage.hostxtra.co.uk. With only the
console origin in ADMIN_ORIGIN it was refused.

The failure mode is worth naming: cors() omits Access-Control-Allow-Origin
but still answers the preflight 204, so the browser blocks the request and
admin logs nothing. It looks like the endpoint is down when curl says it
is fine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 14:02:25 +01:00
mrhid6andClaude Opus 5 104f21d0a9 docs: phase 2 configuration and the reaper's containment
Server Deploy / deploy (push) Successful in 5m8s
FREE_INSTANCE_REAP_AFTER is set only in docker-compose.site.yml, so a
self-hosted deployment can never reap. Admin and server must carry the
same value: one names the deletion date in warnings, the other acts on it.

Records that admin now has a second control-plane write path, cloudprov,
and that deletion lives in the control plane because that is where the
knowledge of what an instance is made of belongs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:48:23 +01:00
mrhid6andClaude Opus 5 b86d9ddd86 refactor(sitesvc): remove signup and verification
Account creation moved to admin, which owns accounts, and the marketing
form now posts there. sitesvc keeps the contact mailer only.

DEPLOY LAST: sitesvc's verify endpoint must stay live until every
outstanding pending signup has expired, or an in-flight verification link
breaks. Do not roll this out until the site change has been live 24 hours
and site_pending_signups is empty.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:41:47 +01:00
mrhid6andClaude Opus 5 9cf04a6940 feat(site): /start creates an account, not an instance
The form posts to admin's signup and the slug preview goes: there is no
instance at this point, and previewing one promises something the
submission does not create. Creating the instance is now a step in the
portal, which the page's What happens next panel spells out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:37:30 +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 a940390791 feat(admin): reconciler marks reaped instances deleted
Without this the row stays active forever, the reconciler re-logs the
same miss every fifteen minutes, and the lifecycle sweep keeps emailing
about an instance that no longer exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:24:44 +01:00
mrhid6andClaude Opus 5 4e90e8619f feat(server): reap Free instances whose licence lapsed
The control plane owns deletion because it is the only service that knows
what an instance is made of; mirroring that collection list into admin
would drift, and a drift here deletes the wrong rows.

Defaults OFF. Eligibility is three positive assertions — Free tier, an
expiry that exists, and an expiry past the window — so a missing or stale
field is never eligible. The instance document is deleted last, making an
interrupted purge retryable rather than orphaning rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:23:37 +01:00
mrhid6andClaude Opus 5 372a8c5ddf feat(admin): lapse sweep and the four renewal notices
Hourly sweep marks expired Free instances lapsed and sends at most one
notice per instance per pass, most urgent first, recorded on the document
so a restart cannot re-send.

Deletion warnings are suppressed when the reaper is off. Promising a
deletion that will never happen is a lie, and a scarier one than silence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:20:00 +01:00
mrhid6andClaude Opus 5 909ddb884e feat(admin): renew a Free instance from the portal
Available from seven days before expiry and, deliberately, at any point
after it up to deletion, so the same button rescues a lapsed instance.

Renewal is manual because it is the entire reclaim signal: an instance
nobody renews is one nobody is using, which is what makes reaping safe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:17:45 +01:00
mrhid6andClaude Opus 5 1836237f82 feat(admin): POST /api/instances creates a Free cloud instance
Provisions the control-plane instance and its owner, records the
admin_instances row, issues and injects a Free licence, and emails the
customer where it is and when it expires.

Licence issuance and email cannot fail the request. The instance exists
and the customer can sign in; rolling back something they can already see
would be worse than shipping it unlicensed for staff to fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:16:12 +01:00
mrhid6andClaude Opus 5 983655d2a1 feat(admin): cloudprov, the instance provisioning write path
Admin's second and final write path into the control plane. It creates
instances and users and nothing else; inject still owns exactly three
licence fields and is untouched.

The owner's password hash is copied from the HQ account, not shared. The
two diverge on the next password change, which is accepted: propagating a
hash across two databases is worse than two passwords that started equal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:14:12 +01:00
mrhid6andClaude Opus 5 78a0a610be docs: phase 2 plan, creation and Free lifecycle
Ten tasks: cloudprov, instance creation, renewal, the notice sweep, the
reaper, the reconciler tie-in, the HQ and marketing UI, the sitesvc
cutover, and an end-to-end verification.

The reaper sweeps once at boot as inject's reconciler does, which is what
makes it observable at all — the ticker is hourly and deletion is
measured in days.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:12:29 +01:00
mrhid6andClaude Opus 5 5f35b57268 docs: users is unique per instance, not globally
The old index was load-bearing because two lookups were unscoped. Both
are scoped now and the unscoped helper is gone, so the property that
matters is the absence of any unscoped lookup by email. Says so, and
documents auth_source hq.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:01:09 +01:00
mrhid6andClaude Opus 5 cf318470b8 feat(admin): drop the unscoped control-plane login branch
HQ sign-in names no instance, so a lookup of control-plane users by email
alone cannot be scoped — and users.email is no longer globally unique, so
it would return an arbitrary match. Every customer authenticates against
customer_users instead.

Legacy cloud customers get an HQ login from staff via the new
POST /api/staff/accounts/:id/users, alongside the manual instance attach
the spec README already describes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 12:55:44 +01:00
mrhid6andClaude Opus 5 d703bbc4e8 feat(server): scope every user lookup by instance
users is unique on (instance_id, email) now, so an unscoped FindOne could
return an arbitrary one of several matching users. On the login path that
means signing someone into a tenant that is not theirs.

GetUserByEmail is deleted rather than left unused. Local sign-in resolves
its instance from the host, falling back to the single instance a
self-hosted deployment has, and refuses to guess otherwise. The OIDC
cross-instance guard goes: a scoped lookup cannot return another
instance's user, which is a stronger guarantee than the check it replaces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 12:54:13 +01:00
mrhid6andClaude Opus 5 17bcf4b5b9 feat(shared): auth_source constants and hq_user_id on User
Nothing writes them yet. They land now so phases 2 and 3 do not require a
second rebuild of every service that consumes the shared module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 12:52:15 +01:00
mrhid6andClaude Opus 5 da3afca7fa feat(shared): unique users index is (instance_id, email)
One address is one user within an instance, not globally, so an account's
people can be projected into every instance they are granted.

The replacement index is created before email_1 is dropped, so a failure
at any point leaves a working constraint. The drop is idempotent and
tolerates two services racing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 12:48:27 +01:00
mrhid6andClaude Opus 5 88f49a96ae docs: phase 1 plan, identity
Seven tasks: the index swap, the hq fields, the three scoped lookups,
removing admin's unscoped control-plane login, and an end-to-end
verification that two users sharing one address sign in to different
instances.

Also corrects the spec's phase list, which claimed phase 1 projects the
creator as owner. Projection needs instance creation, which is phase 2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 12:28:25 +01:00
mrhid6andClaude Opus 5 d15ab78bd5 docs: spec 6 revised for account membership
An account has people, and those people belong to several cloud
instances. That requires dropping the global unique index on users.email
for a compound (instance_id, email), scoping the two unscoped lookups
that made the global index load-bearing, and projecting HQ users into
instances as real control-plane users.

Self-hosted instances are never projected into; their users stay local.

Restructured into three phases: identity, creation and lifecycle,
membership.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 12:16:38 +01:00
mrhid6andClaude Opus 5 8eb14c1502 docs: spec 6, cloud instance creation
Separates having an account from having an instance. Signup creates only
the HQ account; the customer then creates a Free cloud instance from the
portal, which provisions the control-plane instance, the admin_instances
row and a Free licence in one path.

Supersedes spec 5's signup-migration section.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 12:03:14 +01:00
mrhid6 f8cd909828 doc: Doc updated for cloud instance creation.
Server Deploy / deploy (push) Successful in 2m16s
2026-07-25 22:54:04 +01:00
mrhid6andClaude Opus 5 6a3e0a29a4 chore: verify the admin site end to end
Server Deploy / deploy (push) Successful in 1m54s
Everything checkable without a browser, against scratch databases:

- All five Go modules and the adminsite production build compile clean.
- Scoping holds: own instance 200, another account's 404, nonexistent 404 --
  indistinguishable, so no existence oracle -- and a customer session on the
  staff API gets 401 where staff gets 200.
- max_relinks comes from the API (3), not a constant mirrored in TypeScript.
- The inline blob and the download endpoint return byte-identical content, so
  the fallback is faithful rather than approximate.
- ADMIN_API_URL really is baked at build time: 9999 in the deliberately
  broken image, 8083 in the good one.
- The served stylesheet carries site/'s tokens with matching values, plus
  prefers-color-scheme and both data-theme overrides.
- With admin stopped the control plane still reports valid and mutations
  still succeed -- instances never call admin.
- Admin touched only the three licence fields on instances; every other
  control-plane collection is as the control plane left it.

Caught one stale-image bug doing this: the running admin predated the
customer blob change, so the licence response had no blob field at all.
Rebuilt and re-verified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:30:43 +01:00
mrhid6andClaude Opus 5 c19c11e6eb docs: record admin, adminsite and the shared token set
The previous commit missed this file: the repo tracks it as lowercase
claude.md, so staging CLAUDE.md matched nothing in the index.

admin/ was never documented here at all -- the backend plan's wiring task
covered compose and CI but not the file every session loads. So this adds
both services to the structure, admin's whole REST surface with its
404-never-403 rule, the three visual identities, and the coupling that
matters most: adminsite/ and site/ share one token set with nothing enforcing
the match, the same hazard shape as sitesvc's mirrored slug rules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:24:55 +01:00
mrhid6andClaude Opus 5 85f6d47024 feat(adminsite): image, compose service, image build and docs
Publishes adminsite on 3004 -- 3000 is web, 3003 is the marketing site since
the port shuffle -- and adds the sixth CI image.

CLAUDE.md gains both new services. admin/ was never documented there at all:
the backend plan's wiring task covered compose and CI but not the file every
session actually loads. So this records admin's whole REST surface, its
404-not-403 rule, the three visual identities and, most importantly, that
adminsite/ and site/ share one token set with nothing enforcing the match --
the same hazard shape as sitesvc's mirrored slug rules.

Also notes that admin's REDIS_ADDR reaches admin only, because the base
compose hardcodes redis:6379 for server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:24:37 +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 24060b2c5a chore(adminsite): remove the frontend test suite
Removes vitest, React Testing Library, the config, the setup file and all
eleven test files, plus the test scripts and dev dependencies. Done at the
user's direction; it matches the rest of the repo, which has no automated
tests in any language.

All eleven were observed passing before removal, and their assertions are
kept in the plan as acceptance criteria to check by hand rather than deleted
outright -- they are the clearest statement of what each component has to do.

Consequence worth stating: Task 16's manual pass is now the only verification
that exists for spec 4. Four behaviours it must cover carefully, because each
is easy to break invisibly: 404-not-403 scoping, the expired card naming what
still works, relink disabling at zero, and the blob fallback when a download
fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:19:34 +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 73efb206ac feat(adminsite): staff accounts, search by UUID and account detail
Search covers name, email, Paddle customer ID and instance UUID. The UUID
case is the one that matters: a support email often contains a UUID and
nothing else, and the empty state says so rather than just reporting nothing
found.

Account detail gathers everything about one customer on one screen --
instances, subscriptions, people, audit -- and says plainly when an account
has no people because it is a cloud account whose owner signs in with
control-plane credentials.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:12:48 +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 b583e9803f feat(adminsite): the self-hosted link flow and billing view
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>
2026-07-25 21:08:46 +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