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>
81 lines
2.9 KiB
TypeScript
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>
|
|
);
|
|
}
|