feat(adminsite): the licence ledger and staff instance actions
The screen that answers "why did this stop working on the 14th". Read top to bottom it is one instance's whole history: what was issued, why, by whom, and what replaced it. Superseded entries stay visible and overprinted rather than disappearing, because licences are append-only and hiding them would destroy the only record that answers the question. Each links to its successor. Injection state is shown live for cloud instances and omitted for self-hosted, where the customer holds the blob and there is nothing for us to have written. Staff relinks carry no cap, with the reason stated inline: the customer cap exists to put a human in the loop, and this is that human. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,77 +0,0 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { InstanceCard } from "./InstanceCard";
|
||||
import type { Instance, License } from "@/lib/api";
|
||||
|
||||
const base: Instance = {
|
||||
instance_id: "6a0fe3f0-49d2-4aa1-967c-a3094b200b5d",
|
||||
account_id: "a1",
|
||||
name: "Acme Production",
|
||||
slug: "acme",
|
||||
deployment: "cloud",
|
||||
tier: "professional",
|
||||
status: "active",
|
||||
current_license: "l1",
|
||||
relink_count: 0,
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
function licence(daysFromNow: number): License {
|
||||
return {
|
||||
license_id: "l1",
|
||||
instance_id: base.instance_id,
|
||||
account_id: "a1",
|
||||
tier: "professional",
|
||||
deployment: "cloud",
|
||||
limits: { max_servers: -1, max_secret_groups: -1, max_channels: -1 },
|
||||
features: ["console", "oidc"],
|
||||
issued_at: "2026-01-01T00:00:00Z",
|
||||
expires_at: new Date(Date.now() + daysFromNow * 86_400_000).toISOString(),
|
||||
issued_by: "staff@example.com",
|
||||
reason: "new",
|
||||
};
|
||||
}
|
||||
|
||||
describe("InstanceCard", () => {
|
||||
it("shows a valid licence with days remaining", () => {
|
||||
render(<InstanceCard instance={base} license={licence(367)} />);
|
||||
expect(screen.getByText("Valid")).toBeInTheDocument();
|
||||
expect(screen.getByText(/367 days remaining/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("warns inside fourteen days", () => {
|
||||
render(<InstanceCard instance={base} license={licence(9)} />);
|
||||
expect(screen.getByText("Expiring")).toBeInTheDocument();
|
||||
expect(screen.getByText(/9 days remaining/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("names what still works when expired", () => {
|
||||
render(<InstanceCard instance={base} license={licence(-3)} />);
|
||||
expect(screen.getByText("Expired")).toBeInTheDocument();
|
||||
// The reassurance is the point: this is the first thing a worried
|
||||
// customer needs, and the backend really does keep these running.
|
||||
expect(screen.getByText(/servers and monitors are still running/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/changes are disabled/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("prompts to link when paid but never linked", () => {
|
||||
render(
|
||||
<InstanceCard
|
||||
instance={{
|
||||
...base,
|
||||
status: "awaiting_link",
|
||||
deployment: "self_hosted",
|
||||
current_license: undefined,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("Awaiting link")).toBeInTheDocument();
|
||||
expect(screen.getByText(/not attached to an install yet/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole("link", { name: /link an install/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("links to the instance's own subdomain for cloud", () => {
|
||||
render(<InstanceCard instance={base} license={licence(30)} />);
|
||||
expect(screen.getByRole("link", { name: /open/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import clsx from "clsx";
|
||||
import type { License } from "@/lib/api";
|
||||
import { formatDate, formatStamp, limitLabel } from "@/lib/format";
|
||||
|
||||
const REASON: Record<License["reason"], string> = {
|
||||
new: "New",
|
||||
renewal: "Renewal",
|
||||
tier_change: "Tier change",
|
||||
relink: "Relink",
|
||||
manual: "Manual",
|
||||
};
|
||||
|
||||
/*
|
||||
* Licences are append-only: a renewal supersedes its predecessor rather than
|
||||
* replacing it. So this is a ledger, not a table. Superseded rows stay visible
|
||||
* and are overprinted the way a cancelled instrument is — hiding them would
|
||||
* destroy the only record of why an instance stopped working on a given date.
|
||||
*/
|
||||
export function Ledger({ licenses }: { licenses: License[] }) {
|
||||
if (licenses.length === 0) {
|
||||
return (
|
||||
<p className="text-ink-2">
|
||||
No licence has ever been issued for this instance, so it is read-only.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="grid">
|
||||
{licenses.map((l) => {
|
||||
const dead = Boolean(l.superseded_by);
|
||||
return (
|
||||
<li
|
||||
key={l.license_id}
|
||||
className={clsx(
|
||||
"grid gap-4 border-b border-rule-soft py-4 last:border-0 sm:grid-cols-[9.5rem_1fr]",
|
||||
dead && "text-ink-3",
|
||||
)}
|
||||
>
|
||||
<div className="font-mono text-[0.72rem] tabular-nums text-ink-3">
|
||||
<b
|
||||
className={clsx(
|
||||
"block text-[0.82rem] font-semibold",
|
||||
dead ? "text-ink-3" : "text-ink",
|
||||
)}
|
||||
>
|
||||
{formatDate(l.issued_at)}
|
||||
</b>
|
||||
{formatStamp(l.issued_at)}
|
||||
</div>
|
||||
<div className="grid justify-items-start gap-1.5">
|
||||
{dead && (
|
||||
<span className="-rotate-2 rounded-sm border-2 border-archival px-1.5 py-0.5 font-mono text-[0.72rem] uppercase tracking-[0.18em] text-archival opacity-75">
|
||||
Superseded
|
||||
</span>
|
||||
)}
|
||||
<p className="flex flex-wrap items-center gap-2 font-semibold">
|
||||
{l.tier.replace("_", " ")}
|
||||
<span className="rounded-sm border border-rule px-1.5 py-0.5 font-mono text-[0.72rem] font-normal uppercase tracking-[0.09em] text-accent">
|
||||
{REASON[l.reason]}
|
||||
</span>
|
||||
</p>
|
||||
<p className="font-mono text-[0.72rem] tabular-nums text-ink-3">
|
||||
{l.license_id.slice(0, 8)} · expires {formatDate(l.expires_at)} ·{" "}
|
||||
{limitLabel(l.limits.max_servers)} servers · issued by {l.issued_by}
|
||||
{l.superseded_by && (
|
||||
<>
|
||||
{" "}
|
||||
· replaced by{" "}
|
||||
<span className="text-accent underline">
|
||||
{l.superseded_by.slice(0, 8)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { LicenceDelivery } from "./LicenceDelivery";
|
||||
|
||||
describe("LicenceDelivery", () => {
|
||||
it("offers the file and always shows the blob as a fallback", () => {
|
||||
render(
|
||||
<LicenceDelivery
|
||||
instanceId="6a0fe3f0-49d2-4aa1-967c-a3094b200b5d"
|
||||
blob="VANTAGE-LIC abc123"
|
||||
downloadUrl="https://admin.example.com/api/instances/x/license/download"
|
||||
/>,
|
||||
);
|
||||
const link = screen.getByRole("link", { name: /download licence/i });
|
||||
expect(link).toHaveAttribute("href", expect.stringContaining("/license/download"));
|
||||
// A blocked download must never leave a paying customer stuck.
|
||||
expect(screen.getByText(/VANTAGE-LIC abc123/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Settings → Licence/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { NotConnectedPanel } from "./NotConnected";
|
||||
|
||||
describe("NotConnectedPanel", () => {
|
||||
it("names the variable that is wrong and where it is set", () => {
|
||||
render(<NotConnectedPanel url="https://admin.example.com" />);
|
||||
|
||||
expect(screen.getByRole("heading")).toHaveTextContent(
|
||||
/not connected to the licensing service/i,
|
||||
);
|
||||
expect(screen.getByText(/ADMIN_API_URL/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/https:\/\/admin\.example\.com/)).toBeInTheDocument();
|
||||
// The two mistakes that actually cause this, both named.
|
||||
expect(screen.getByText(/reachable from your browser/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/ADMIN_ORIGIN/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("says the value is missing when no URL was baked in", () => {
|
||||
render(<NotConnectedPanel url="" />);
|
||||
expect(screen.getByText(/was not set when this app was built/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Queue } from "./Queue";
|
||||
|
||||
describe("Queue", () => {
|
||||
it("shows the count and links every row to the work", () => {
|
||||
render(
|
||||
<Queue
|
||||
title="Failed injections"
|
||||
tone="expired"
|
||||
count={2}
|
||||
items={[
|
||||
{ label: "Acme Production", href: "/staff/instances/i1", meta: "4h ago" },
|
||||
{ label: "Globex EU", href: "/staff/instances/i2", meta: "26m ago" },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("Failed injections")).toBeInTheDocument();
|
||||
expect(screen.getByText("2")).toBeInTheDocument();
|
||||
expect(screen.getByRole("link", { name: "Acme Production" })).toHaveAttribute(
|
||||
"href",
|
||||
"/staff/instances/i1",
|
||||
);
|
||||
});
|
||||
|
||||
it("says so plainly when there is nothing to do", () => {
|
||||
render(<Queue title="Past due" tone="expired" count={0} items={[]} />);
|
||||
expect(screen.getByText(/nothing to do/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { RelinkPanel } from "./RelinkPanel";
|
||||
|
||||
describe("RelinkPanel", () => {
|
||||
it("shows the remaining allowance", () => {
|
||||
render(<RelinkPanel instanceId="i1" used={1} max={3} onRelink={vi.fn()} />);
|
||||
expect(screen.getByText("2 of 3 relinks left this term")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /relink/i })).toBeEnabled();
|
||||
});
|
||||
|
||||
it("disables at zero and says what to do instead", () => {
|
||||
render(<RelinkPanel instanceId="i1" used={3} max={3} onRelink={vi.fn()} />);
|
||||
expect(screen.getByRole("button", { name: /relink/i })).toBeDisabled();
|
||||
expect(screen.getByText(/contact support/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user