{/* The cap above is the free tier's while a paid plan is unpaid —
say so, or it just looks like the plan isn't being honoured. */}
{usage.awaiting_payment && (
Your {user?.plan_name || usage.plan} plan's device limit applies once your first payment is confirmed. Until then the free plan's limit is in effect.
);
}
// ─── Instance Card ───────────────────────────────────────────────────────────
function InstanceCard({ instance: inst, onRefresh }) {
const [showCreds, setShowCreds] = useState(false);
const [copied, setCopied] = useState("");
// admin_password / api_key are stripped from the /instances list. Fetch them
// only for an explicit reveal or dashboard handoff, not on every page load.
const [creds, setCreds] = useState(null);
const [rotating, setRotating] = useState(false);
const [rotationMsg, setRotationMsg] = useState("");
const loadCreds = async () => {
if (creds) return creds;
const next = await apiCall(`/instances/${inst.id}/credentials`);
setCreds(next);
return next;
};
// rotateAPIKey invalidates the current key and recreates the MDM container on
// the new one. The platform DB, the instance .env, and the running container
// converge on the new key (#718); the regression test in
// api_key_rotation_regression_test.go proves the three platform↔tenant paths
// keep authenticating after a rotation (#605). Confirmed because every
// integration holding the old key (CLI, Terraform, billing sync, vendor CSR)
// must be updated before the next call.
//
// The backend's handleRotateInstanceAPIKey also has a recovery path: when the
// instance is in its post-rotation-error state (status_detail prefixed with
// postRotationHealthFailurePrefix), a POST retries the container recreate
// without minting a new key. isPostRotationError surfaces that state so the
// dashboard can offer the retry, and confirmForRotation() picks a message
// that matches what the backend will actually do.
const isPostRotationError = () =>
inst.status === "error" && !!inst.status_detail &&
inst.status_detail.startsWith("post-rotation health check failed");
const confirmForRotation = () =>
isPostRotationError()
? confirm("Retry rotation recovery? This re-runs the container recreate without minting a new key.")
: confirm("Rotate this instance's API key? The current key stops working immediately, the MDM container restarts briefly, and anything using the old key (CLI, Terraform, billing sync, push-cert signing) must be updated to the new one.");
const rotateAPIKey = async () => {
if (rotating) return;
if (!confirmForRotation()) return;
setRotating(true);
setRotationMsg("");
try {
await apiCall(`/instances/${inst.id}/rotate-api-key`, { method: "POST" });
// The rotation succeeded — the old key is dead. Drop the cached creds so a
// later reveal re-fetches; the credentials GET below is the fast path that
// keeps the user from having to toggle. If that GET fails (transient
// network, brief restart), the rotation still stands and the message
// says so, rather than reporting a failure that didn't happen — and the
// stale dead key is never left on screen.
setCreds(null);
try {
const next = await apiCall(`/instances/${inst.id}/credentials`);
setCreds(next);
setRotationMsg("API key rotated. Copy the new key above — the old one no longer works.");
setTimeout(() => setRotationMsg(""), 8000);
} catch (credErr) {
setRotationMsg("API key rotated, but the new key could not be fetched. Close and re-open credentials to see it.");
}
} catch (e) {
setRotationMsg(e.message || "Rotation failed.");
} finally {
setRotating(false);
}
};
const copy = (text, label) => {
navigator.clipboard.writeText(text);
setCopied(label);
setTimeout(() => setCopied(""), 2000);
};
const statusLabels = {
provisioning: "Provisioning…",
active: "Active",
suspended: "Suspended",
pending_payment: "Awaiting Payment",
};
return (