feat(adminsite): instance cards and the customer overview

State reads three ways on every card -- a stripe, a shaped-and-labelled
pill, and the copy -- so it survives a colourblind reader and a glance at
arm's length. Colour alone would fail on the one screen where getting it
wrong costs money.

The expired card leads with what still works, because that is the first
thing a worried customer wants to know and the backend really does keep
servers, monitors and agent keys running. The awaiting-link card is
deliberately loud: a customer who has paid and not linked has paid for
nothing yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrhid6
2026-07-25 21:03:37 +01:00
co-authored by Claude Opus 5
parent 7a8e683d99
commit 242a587340
4 changed files with 287 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
import Link from "next/link";
import clsx from "clsx";
import type { Instance, License } from "@/lib/api";
import { daysRemaining, formatDate, licenceState } from "@/lib/format";
import { StatePill } from "./StatePill";
const STRIPE = {
valid: "before:bg-valid",
warn: "before:bg-warn",
expired: "before:bg-expired",
none: "before:bg-accent",
} as const;
export function InstanceCard({ instance, license }: { instance: Instance; license?: License }) {
const state = licenceState(license?.expires_at, Boolean(license));
const days = license ? daysRemaining(license.expires_at) : 0;
const cloud = instance.deployment === "cloud";
return (
<article
className={clsx(
"relative grid gap-3 rounded border border-rule bg-panel p-4 pl-5",
"before:absolute before:inset-y-0 before:left-0 before:w-1 before:content-['']",
STRIPE[state],
)}
>
<div className="flex items-start justify-between gap-3">
<div>
<h3 className="text-lg">{instance.name || "Unnamed instance"}</h3>
<p className="font-mono text-[0.72rem] uppercase tracking-[0.1em] text-ink-3">
{cloud ? "Cloud" : "Self-hosted"}
{instance.tier ? ` · ${instance.tier.replace("_", " ")}` : ""}
</p>
</div>
<StatePill state={state} />
</div>
{state === "expired" && (
<p className="text-[0.82rem] text-ink-2">
Servers and monitors are still running, and your agents keep their keys. Changes
are disabled until you renew.
</p>
)}
{state === "none" && (
<p className="text-[0.82rem] text-ink-2">
You have paid for this but it is not attached to an install yet, so no licence
has been issued. Linking takes a minute.
</p>
)}
{license && state !== "expired" && (
<div className="grid gap-1 font-mono text-[0.82rem] tabular-nums text-ink-2">
<span>{days} days remaining</span>
<div className="h-[3px] overflow-hidden rounded-sm bg-rule-soft">
<div
className={clsx("h-full", state === "warn" ? "bg-warn" : "bg-valid")}
style={{ width: `${Math.max(2, Math.min(100, (days / 365) * 100))}%` }}
/>
</div>
<span>Renews {formatDate(license.expires_at)}</span>
</div>
)}
{state === "none" ? (
<Link
href="/instances/link"
className="justify-self-start text-[0.82rem] font-semibold text-accent underline"
>
Link an install
</Link>
) : cloud && instance.slug ? (
<a
href={`https://${instance.slug}.vantage.hostxtra.co.uk`}
className="justify-self-start text-[0.82rem] font-semibold text-accent underline"
>
Open {instance.slug}.vantage.hostxtra.co.uk
</a>
) : (
<Link
href={`/instances/${instance.instance_id}`}
className="justify-self-start text-[0.82rem] font-semibold text-accent underline"
>
{state === "expired" ? "Renew and download" : "Licence and download"}
</Link>
)}
</article>
);
}