Commit Graph
14 Commits
Author SHA1 Message Date
mrhid6andClaude Opus 5 3d3be7465f fix(admin): re-key legacy plans before seeding, tolerant of a prior partial boot
Server Deploy / deploy (push) Successful in 1m5s
Boot ran SeedPlans before Backfill, so the fresh (self_hosted, professional)
seed row was inserted before pass 3 tried to rename the legacy self_hosted TIER
row into it — colliding on deployment_tier_unique and failing boot.

The plan re-key moves to MigrateLegacyPlans, called before SeedPlans so the
rename lands first and the seed no-ops on it. It is also self-healing: on a
database a crashed boot already seeded (self_hosted, professional) into, the
legacy row can no longer be renamed onto it, so it is dropped instead — its
instances are re-tiered and re-entitled from the surviving professional row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 10:03:17 +01:00
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 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 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
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 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 58c37bf81b feat(admin): support authenticated Redis, and point at the external server
Server Deploy / deploy (push) Successful in 2m14s
Adds REDIS_USERNAME and REDIS_PASSWORD. Both are optional, so an
unauthenticated instance still works unchanged. Redis 6+ ACL auth takes
both; a legacy requirepass instance takes the password with an empty
username, which is what go-redis needs to send single-argument AUTH.

Admin now defaults to the external Redis at 10.10.10.2:6379 rather than the
compose-local one, and no longer declares depends_on: redis -- it is not
starting that container any more. The base stack keeps its own Redis for
`server`, which still has no auth support.

Also fixes SMTP_PASSWORD in the admin block: it read `$SMTP_PASSWORD:-}`
rather than `${SMTP_PASSWORD:-}`, which appended a literal `:-}` to the
password and would have failed SMTP auth at the first verification email.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 19:50:19 +01:00
mrhid6andClaude Opus 5 268134d821 feat(admin): staff API and the route table
Completes the service -- this is the first commit where the whole thing
compiles and serves.

staffCreateInstance adopts a cloud instance that already exists in the
control plane, taking its name and slug from there and refusing when no
such instance exists: an admin row pointing at nothing would issue licences
nobody can use. Adopt then issue is how the existing read-only cloud
instances get licensed.

staffRelink has no attempt cap. The customer-facing limit exists to put a
human in front of the fourth attempt, and this is that human.

Editing a plan changes what a tier grants from now on only; existing
licences snapshotted theirs at issue time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 19:11:04 +01:00
mrhid6andClaude Opus 5 07a3756b18 feat(admin): self-hosted customer accounts with email verification
Mirrors the pattern sitesvc already proves: 32 random bytes, only the
SHA-256 hash stored, a 24-hour expiry, and the token cleared on use -- so a
leaked database yields no working links.

Unverified login returns a distinct "verify your email address first" rather
than the generic error. The address is already known to be theirs, so there
is nothing to disclose and that is the only useful thing to say.

Licence blobs are emailed inline. A blob is signed public data, not a
secret: it is useless on any instance other than the one it names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 19:07:47 +01:00
mrhid6andClaude Opus 5 480a578deb feat(admin): sessions, staff auth and adminctl
One Redis session store and one cookie for all three identities. Staff
login returns the same error for every failure mode and spends a bcrypt
comparison against a dummy hash when no user exists, so neither the message
nor the timing confirms which addresses have accounts.

Staff users are created only by adminctl. There is no signup endpoint: a
licensing authority that can be joined over the internet is not one.

Pins gin and go-redis to the versions server/ already uses rather than the
latest tidy would pick.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 19:05:28 +01:00
mrhid6andClaude Opus 5 a636a48e07 feat(admin): cloud injection and the 15-minute reconciler
Injection is a single UpdateOne of three licence fields, so it is
idempotent and safe to re-run. The control plane caches licence state for
60 seconds, so an injected licence takes effect within a minute with no
restart.

Deliver never fails its caller. The reconciler, not the issuance path, is
what actually guarantees a cloud instance ends up holding the licence admin
says it holds -- injection at issue time is best-effort and this is the
backstop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 19:02:45 +01:00
mrhid6andClaude Opus 5 427191b14c feat(admin): licence issuance with plan snapshots and supersession
Issue is the only place that signs. It records the licence, supersedes its
predecessor and updates the instance -- but deliberately does not deliver.

The ordering matters: a licence recorded but not delivered is recoverable,
because the customer can download it. A licence delivered but not recorded
is a support mystery with no paper trail.

Free stays cloud-only through one comparison of plan against instance
deployment, not a flag. Renewals reset relink_count because the cap is per
term.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 19:01:47 +01:00
mrhid6andClaude Opus 5 b8fcf89ee7 feat(admin): documents, indexes and the plan seed
Adds admin's own documents, its unique indexes and the plan seed from
shared/license.

Licences are append-only -- a renewal writes a new row and supersedes the
old one -- because the history is the support tool. Plans are seeded with
$setOnInsert only, so a redeploy never stamps over staff edits to limits,
features or Paddle IDs.

admin_instances.instance_id unique is a correctness property, not an
optimisation: without it two customers could both claim one self-hosted
UUID and both be issued licences for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 19:00:37 +01:00
mrhid6andClaude Opus 5 64eac6dbc7 feat(admin): module skeleton, config and two database connections
Adds the admin module: env config with fail-fast validation, two MongoDB
connections (its own vantage_admin database plus a narrow path into the
control plane), the boot sequence and the image.

Config refuses to start without a signing key, and both Mongo URIs must
name their database inline -- admin talks to two databases, so a bare
MONGO_DB would be ambiguous about which.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 18:58:44 +01:00