Commit Graph
14 Commits
Author SHA1 Message Date
mrhid6 9df89e2db4 fix: surface vuln_scanning across licence, staff and pricing UI
Chart Release / chart (push) Successful in 11s
Server Deploy / deploy (push) Canceled after 59s
The catalogue row alone was not enough; the feature was invisible in
three places and mislabelled in a fourth.

PlanConfigurator rendered every key that was not "console" as "Single
sign-on", so the staff checkbox granting vulnerability scanning was
labelled single sign-on. Feature wording was duplicated between the staff
configurator and the purchase form and the copies had drifted, so it now
lives in adminsite/lib/features.ts and both read from it.

The customer licence panel showed raw keys; it now labels them.

Pricing gains a comparison row. The add-on block with a monthly price is
deliberately NOT added: that is a pricing decision, and the Paddle price
IDs for the new catalogue rows have to be pasted in before it can be sold
anyway.
2026-08-06 15:10:26 +01:00
mrhid6 f60c509b47 feat: vuln_scanning entitlement and documentation
Chart Release / chart (push) Successful in 12s
Server Deploy / deploy (push) Failing after 1m11s
Agent Release / build (push) Successful in 1m0s
Agent Release / msi (push) Successful in 2m18s
Adds license.FeatureVulnScanning as the one name for the feature and a
catalogue row per deployment/tier, following console and oidc: features
are opt-in per customer, so no plan bundles it.

Documents the subsystem in CLAUDE.md, including that ScopedCollections is
the canonical registry instance deletion derives from — there is no
separate deletion list, which the plan had wrong.
2026-08-06 14:44:11 +01:00
mrhid6 287bd9657b fix: Fixed paddle relink sub
Chart Release / chart (push) Successful in 27s
Server Deploy / deploy (push) Successful in 1m17s
2026-08-03 17:34:39 +01:00
mrhid6 b5f684c4fe fix: Fixed paddle subs
Chart Release / chart (push) Successful in 28s
Server Deploy / deploy (push) Successful in 1m17s
2026-08-03 15:32:01 +01:00
mrhid6 dabe6fe3aa feat: Updated package path to match repo
Agent Release / build (push) Failing after 25s
Agent Release / msi (push) Skipped
Server Deploy / deploy (push) Successful in 2m47s
2026-07-28 10:01:40 +01:00
mrhid6 4ad68e3ac4 feat: Updated purchase page
Server Deploy / deploy (push) Successful in 4m40s
2026-07-27 14:59:43 +01:00
mrhid6andClaude Opus 5 fbd93d0ea5 feat(admin): Paddle client behind an interface, config, and the event idempotency record
Client is a thin REST client (net/http) rather than the vendor SDK: the surface
we need is two calls, and a hand-rolled client has no version-drift risk and no
dependency in go.sum. All Paddle wire shapes live only in http.go.

PADDLE_API_KEY and PADDLE_WEBHOOK_SECRET are boot-required — an unverified
webhook endpoint is one anyone can issue licences through. paddle_events carries
a unique index on event_id for webhook idempotency.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 10:36:26 +01:00
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 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 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 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 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