import { getSessionUser, canMutate } from "@/lib/session"; import { getTranslations, getFormatter } from "next-intl/server"; import { redirect } from "next/navigation"; import { listTenants } from "@/lib/k8s"; import { listActiveTenantRequestsByOrgId } from "@/lib/db"; import { listVisibleTenants, canSeeInflightRequests, isUserScoped, } from "@/lib/visibility"; import { personalAccountAtCapacity } from "@/lib/personal-org"; import { Card, CardHeader } from "@/components/ui/card"; import { StatusBadge } from "@/components/ui/status-badge"; import { OnboardingFlow } from "@/components/onboarding/onboarding-flow"; import { ProvisioningStatus } from "@/components/onboarding/provisioning-status"; import { formatDateTime } from "@/lib/format"; import Link from "next/link"; export default async function DashboardPage() { const user = await getSessionUser(); if (!user) redirect("/login"); const t = await getTranslations("dashboard"); const tAdmin = await getTranslations("admin"); const f = await getFormatter(); const allTenants = await listTenants(); // Platform users see overview of all tenants — unchanged from pre-Slice-3. if (user.isPlatform) { const phaseCount = allTenants.reduce>((acc, t) => { const phase = t.status?.phase ?? "Pending"; acc[phase] = (acc[phase] || 0) + 1; return acc; }, {}); return (

{t("title")}

{t("welcome", { name: user.name || user.email })}

{tAdmin("title")} {/* Summary cards */}
{tAdmin("allTenants")} {allTenants.length} {Object.entries(phaseCount).map(([phase, count]) => ( {phase}
{count}
))}
{/* Tenant table */} {allTenants.length > 0 && (
{allTenants.map((tenant) => ( ))}
{tAdmin("name")} {tAdmin("phase")} {tAdmin("packages")} {tAdmin("created")}
{tenant.metadata.name}
{tenant.spec.displayName && (
{tenant.spec.displayName}
)}
{tenant.spec.packages?.join(", ") || "—"} {formatDateTime(tenant.metadata.creationTimestamp, f)} {t("manage")} →
)}
); } // --------------------------------------------------------------------- // Customer view (Slice 3 multi-tenant + Slice 6 visibility scoping) // --------------------------------------------------------------------- // Slice 6: orgTenants becomes "visible tenants for this user". For an // owner that's all of the org's tenants; for a `user`-role member // it's only the tenants they've been assigned to via // tenant_user_assignments. The dashboard renders fewer cards in the // user-role case but otherwise uses the same template. const orgTenants = await listVisibleTenants(user, allTenants); // For the "no instances yet" empty state, we want to know whether // this user is being scoped down. A `user`-role with 0 visible // tenants gets a different message than an owner with 0 tenants // (the user might just need an assignment; the owner needs to // create one). const userScoped = isUserScoped(user); // Pending/in-flight requests are only shown to roles that can act on // them. `user`-role customers see no request cards. const orgRequests = canSeeInflightRequests(user) ? await listActiveTenantRequestsByOrgId(user.orgId) : []; // Pending requests that don't yet have a tenant CR. Once the CR // exists, the tenant card carries the live phase, so a separate // "request" card would just duplicate it. We compare against // *all* org tenants here (not just visible ones) — otherwise a // request whose tenant is invisible to the caller would erroneously // show as in-flight. const orgScopedTenants = allTenants.filter( (t) => t.metadata.labels?.["pieced.ch/zitadel-org-id"] === user.orgId ); const inflightRequests = orgRequests.filter( (r) => !r.tenantName || !orgScopedTenants.some((t) => t.metadata.name === r.tenantName) ); // Slice 5: only owners (and platform users, who'd typically be using // the admin panel anyway) see the "Create new instance" link. A // `user`-role member sees the dashboard but not the create flow — // they need to ask an owner. // // Bug 5: personal accounts are 1-instance by design. Once a personal // account has either an active tenant OR an in-flight request, the // create button must disappear. The matching server-side guard is // in `/api/onboarding` so direct POSTs are also rejected. const personalAtCapacity = personalAccountAtCapacity( user.isPersonal, orgScopedTenants.length, inflightRequests.length ); const canCreate = canMutate(user) && !personalAtCapacity; // First-time / no-visibility branch. // // Three sub-cases: // 1. owner / platform with 0 tenants and 0 requests → show wizard. // 2. owner / platform with 0 visibility but the org HAS tenants → // shouldn't happen (owners see all org tenants). Defensive // fall-through to the wizard. // 3. user-role with 0 visible tenants → show "ask your owner" // message, with copy distinguishing whether the org has any // tenants at all. if (orgTenants.length === 0 && inflightRequests.length === 0) { if (userScoped) { // Slice 6 empty state for `user` role. The org might or might // not have tenants — either way this user has none assigned. // The two messages are subtly different: "no instances exist" // means owner needs to create one; "you're not assigned" means // owner needs to grant access. const orgHasTenants = orgScopedTenants.length > 0; return (

{t("title")}

{t("welcome", { name: user.name || user.email })}

{orgHasTenants ? t("noAssignmentsTitle") : t("noInstancesYetTitle")}

{orgHasTenants ? t("noAssignmentsDescription") : t("noInstancesYetDescription")}

); } if (!canCreate) { // Belt-and-braces: any role that's neither owner-with-create nor // user-scope ends up here (e.g. weird cases like a session with // no roles at all). Same generic message as before. return (

{t("title")}

{t("welcome", { name: user.name || user.email })}

{t("noAccessNoInstances")}

); } return (

{t("title")}

{t("welcome", { name: user.name || user.email })}

); } // Returning customer: list of tenants + in-flight requests, plus // a button to add another instance (owners only). return (

{t("title")}

{t("welcome", { name: user.name || user.email })}

{canCreate && ( + {t("createInstance")} )}
{/* In-flight (pending/approved/provisioning/rejected) requests */} {inflightRequests.length > 0 && (

{t("inflightRequests")}

{inflightRequests.map((r) => ( ))}
)} {/* Active tenants */} {orgTenants.length > 0 && (

{t("instances")}

{orgTenants.map((tenant) => (
{tenant.spec.displayName || tenant.metadata.name}
{tenant.metadata.name}
{tenant.spec.agentName && (
{tenant.spec.agentName}
)} {tenant.spec.packages && tenant.spec.packages.length > 0 && (
{tenant.spec.packages.slice(0, 4).map((pkg) => ( {pkg} ))} {tenant.spec.packages.length > 4 && ( +{tenant.spec.packages.length - 4} )}
)}
{t("manage")} →
))}
)}
); }