feat(license): metered licensing — catalogue, entitlements, and enforcement
Server Deploy / deploy (push) Successful in 5m22s
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>
This commit is contained in:
@@ -46,6 +46,43 @@ const SOURCE: Record<string, string> = {
|
||||
none: "None installed",
|
||||
};
|
||||
|
||||
/*
|
||||
* The three stable identifiers, in prose. An unknown or empty value renders
|
||||
* NOTHING rather than a fallback: a licence signed before support_level existed
|
||||
* has no support level, and inventing one would promise something we have not
|
||||
* sold.
|
||||
*/
|
||||
const SUPPORT_LABELS: Record<string, string> = {
|
||||
community: "Community support",
|
||||
email_24_5: "Email support, 24/5",
|
||||
email_call_24_7: "Email and phone support, 24/7",
|
||||
};
|
||||
|
||||
/*
|
||||
* Retention is a duration, not a cap, so it gets a fact rather than a meter.
|
||||
* Allowance's bar needs a numerator and there isn't one — how much of a
|
||||
* retention window have you "used"?
|
||||
*/
|
||||
function Retention({ days }: { days: number }) {
|
||||
const forever = days === UNLIMITED;
|
||||
return (
|
||||
<Card padding={false} className="p-4">
|
||||
<p className="font-mono text-[0.62rem] uppercase tracking-[0.14em] text-text-tertiary">
|
||||
Audit history
|
||||
</p>
|
||||
<p className="mt-2 text-2xl font-extrabold tracking-[-0.03em] tabular-nums text-text-primary">
|
||||
{forever ? "Kept" : days}
|
||||
<span className="ml-1.5 text-sm font-medium tracking-normal text-text-secondary">
|
||||
{forever ? "indefinitely" : "days"}
|
||||
</span>
|
||||
</p>
|
||||
<p className="mt-3 font-mono text-[0.62rem] uppercase tracking-[0.14em] text-text-secondary">
|
||||
{forever ? "Never trimmed" : "Older entries removed daily"}
|
||||
</p>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
/** A keyed field on the record, the way a certificate prints them. */
|
||||
function Keyed({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
@@ -125,6 +162,9 @@ function RecordPanel({ license }: { license: LicenseInfo }) {
|
||||
already carries the colour. A dot would be the third telling. */}
|
||||
<h2 className={`text-2xl font-extrabold tracking-[-0.03em] ${s.text}`}>{s.label}</h2>
|
||||
{license.tier && <p className="text-sm text-text-secondary">{license.tier} tier</p>}
|
||||
{license.support_level && SUPPORT_LABELS[license.support_level] && (
|
||||
<p className="text-sm text-text-secondary">{SUPPORT_LABELS[license.support_level]}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{license.reason && <p className="mt-3 max-w-prose text-sm text-text-secondary">{REASON[license.reason] ?? license.reason}</p>}
|
||||
@@ -225,8 +265,10 @@ export default function LicensePage() {
|
||||
<Group label="Allowances">
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<Allowance label="Servers" used={license.usage.servers} limit={license.limits.max_servers} />
|
||||
<Allowance label="Monitors" used={license.usage.monitors} limit={license.limits.max_monitors} />
|
||||
<Allowance label="Secret groups" used={license.usage.secret_groups} limit={license.limits.max_secret_groups} />
|
||||
<Allowance label="Notification channels" used={license.usage.channels} limit={license.limits.max_channels} />
|
||||
<Retention days={license.limits.audit_retention_days} />
|
||||
</div>
|
||||
|
||||
<Card padding={false} className="px-5 py-2">
|
||||
|
||||
Reference in New Issue
Block a user