Files
vantage/adminsite/components/ConfirmPlanChange.tsx
T
mrhid6andClaude Opus 5 c4e6ad5485
Server Deploy / deploy (push) Successful in 5m22s
feat(license): metered licensing — catalogue, entitlements, and enforcement
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

81 lines
2.9 KiB
TypeScript

import type { Plan } from "@/lib/api";
import { limitLabel } from "@/lib/format";
import { Button } from "./Button";
/*
* Editing a plan changes what every future customer gets, so the confirmation
* names each field rather than asking "are you sure". Existing licences
* snapshotted their plan at issue time and are genuinely unaffected — saying so
* is what stops a well-meaning edit being followed by a panicked reissue.
*/
export function ConfirmPlanChange({
plan,
next,
issuedCount,
onConfirm,
onCancel,
}: {
plan: Plan;
next: Plan;
issuedCount: number;
onConfirm: () => void;
onCancel: () => void;
}) {
const rows: { field: string; was: string; now: string }[] = [];
const fields = [
"max_servers",
"max_monitors",
"max_secret_groups",
"max_channels",
"audit_retention_days",
] as const;
for (const f of fields) {
if (plan.base_limits[f] !== next.base_limits[f])
rows.push({
field: f,
was: limitLabel(plan.base_limits[f]),
now: limitLabel(next.base_limits[f]),
});
}
if (plan.support_level !== next.support_level)
rows.push({
field: "support_level",
was: plan.support_level || "none",
now: next.support_level || "none",
});
if (plan.base_features.join(",") !== next.base_features.join(","))
rows.push({
field: "features",
was: plan.base_features.join(", ") || "none",
now: next.base_features.join(", ") || "none",
});
return (
<div className="grid max-w-xl gap-3 rounded border border-warn bg-panel p-5">
<h2 className="text-xl">Change what {plan.name} grants?</h2>
<ul className="grid gap-1 font-mono text-[0.82rem]">
{rows.map((r) => (
<li key={r.field} className="flex flex-wrap gap-2">
<span className="text-ink-3">{r.field}</span>
<span className="text-ink-3 line-through">{r.was}</span>
<span className="font-semibold text-ink"> {r.now}</span>
</li>
))}
{rows.length === 0 && <li className="text-ink-3">Nothing would change.</li>}
</ul>
<p className="text-[0.82rem] text-ink-3">
This applies to licences issued from now on. The {issuedCount} licences already
issued keep what they were signed with until each is reissued.
</p>
<div className="flex flex-wrap gap-3">
<Button type="button" onClick={onConfirm}>
Change plan
</Button>
<Button type="button" variant="line" onClick={onCancel}>
Keep as is
</Button>
</div>
</div>
);
}