feat(portal): M10.1 — fill the 10 customer-area shells
ci / shared (push) Successful in 8s
ci / test (push) Successful in 25s
ci / e2e (push) Has been skipped
ci / image (push) Has been skipped

Four real surfaces wired to tenant-registry (settings, settings/api-keys CRUD, audit pagination, products live entitlements), five forward-looking empty states with CTAs. 56 vitest tests + 10 Playwright canaries. lib/format.ts consolidates date helpers.

Refs: M10.1
This commit was merged in pull request #12.
This commit is contained in:
2026-05-20 07:20:31 +00:00
parent ecbe6ae74b
commit e387b9a963
16 changed files with 1093 additions and 49 deletions
+111 -7
View File
@@ -1,16 +1,120 @@
import { auth } from "@/auth";
import { NotAuthorized, ShellEmpty } from "@/components/ShellEmpty";
import { NotAuthorized } from "@/components/ShellEmpty";
import { formatDateTime } from "@/lib/format";
import type { SessionWithExtras } from "@/lib/session";
import { canSee } from "@/lib/session";
import { fetchTenantBySlug } from "@/lib/tenant-registry";
export default async function Page() {
export default async function SettingsPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const session = (await auth()) as SessionWithExtras | null;
if (!canSee(session, "settings")) return <NotAuthorized />;
const tenant = await fetchTenantBySlug(slug);
if (!tenant) {
return (
<section>
<h1 style={{ fontSize: 28 }}>Settings</h1>
<p style={{ color: "#a82626" }}>Tenant not found.</p>
</section>
);
}
return (
<ShellEmpty
title="Settings"
description="Tenant identity, SSO, organization defaults."
milestone="M10.1"
/>
<section>
<h1 style={{ fontSize: 28, marginBottom: 8 }}>Settings</h1>
<p style={{ color: "#444", marginBottom: 24 }}>
Tenant identity and lifecycle metadata. Editing these lands in the
M10.1 follow-up; for now contact <a href={`/${slug}/support`}>support</a>.
</p>
<h2 style={{ fontSize: 18, marginTop: 16, marginBottom: 8 }}>Identity</h2>
<Field label="Tenant ID" value={tenant.id} mono />
<Field label="Slug" value={tenant.slug} mono />
<Field label="Name" value={tenant.name} />
<Field label="Kind" value={tenant.kind} />
<h2 style={{ fontSize: 18, marginTop: 24, marginBottom: 8 }}>Plan & status</h2>
<Field label="Plan" value={tenant.plan} />
<Field label="Status" value={tenant.status} badge={statusColor(tenant.status)} />
{tenant.trial_ends_at && (
<Field label="Trial ends at" value={formatDateTime(tenant.trial_ends_at)} mono />
)}
<h2 style={{ fontSize: 18, marginTop: 24, marginBottom: 8 }}>Audit</h2>
<Field label="Created" value={formatDateTime(tenant.created_at)} mono />
<Field label="Last updated" value={formatDateTime(tenant.updated_at)} mono />
<h2 style={{ fontSize: 18, marginTop: 24, marginBottom: 8 }}>External links</h2>
<p style={{ fontSize: 13, color: "#666" }}>
ERPNext customer + Polar subscription land in M8.3; rendered here when
the IDs land on the tenant row.
</p>
</section>
);
}
function Field({
label,
value,
mono,
badge,
}: {
label: string;
value: string;
mono?: boolean;
badge?: string;
}) {
return (
<div style={{ display: "flex", padding: "6px 0", borderBottom: "1px solid #f0f0f0" }}>
<span style={{ width: 160, color: "#666", fontSize: 13 }}>{label}</span>
<span
style={{
fontFamily: mono
? "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
: "inherit",
fontSize: mono ? 13 : 14,
}}
>
{badge ? (
<span
style={{
padding: "2px 8px",
borderRadius: 4,
background: badge,
color: "#fff",
fontSize: 12,
textTransform: "uppercase",
letterSpacing: 0.4,
}}
>
{value}
</span>
) : (
value
)}
</span>
</div>
);
}
function statusColor(s: string): string {
switch (s) {
case "active":
return "#1a7a3e";
case "trial":
return "#a87a00";
case "frozen":
return "#a82626";
case "archived":
return "#666";
case "demo":
return "#0070f3";
default:
return "#444";
}
}