import { getSessionUser, canMutate } from "@/lib/session"; import { getTranslations, getFormatter } from "next-intl/server"; import { redirect, notFound } from "next/navigation"; import { getTenant } from "@/lib/k8s"; import { canUserSeeTenant } from "@/lib/visibility"; import { StatusBadge } from "@/components/ui/status-badge"; import { UsageDisplay } from "@/components/dashboard/usage-display"; import { PackageList } from "@/components/packages/package-list"; import { WorkspaceEditor } from "@/components/packages/workspace-editor"; import { ChannelUsers } from "@/components/channel-users/channel-users"; import { AssignedUsersPanel } from "@/components/tenants/assigned-users-panel"; import { formatDateTime, formatRelative } from "@/lib/format"; const CHANNEL_PACKAGES = ["telegram", "discord", "email"]; export default async function TenantDetailPage({ params, }: { params: Promise<{ name: string; locale: string }>; }) { const user = await getSessionUser(); if (!user) redirect("/login"); const { name } = await params; const t = await getTranslations("tenantDetail"); const f = await getFormatter(); const tenant = await getTenant(name); if (!tenant) notFound(); // Slice 6: visibility check encompasses org membership AND, for // user-role members, the tenant_user_assignments check. notFound() // (404) rather than redirect/403 to avoid leaking tenant existence. if (!(await canUserSeeTenant(user, tenant))) { notFound(); } // Slice 5: editable surface gated on owner role. Platform users always // can edit; customer-side, only `owner` may. `user`-role members see // the same page but with edit controls hidden / fields read-only. const canEdit = canMutate(user); const enabledPackages = tenant.spec.packages || []; const workspaceFiles = tenant.spec.workspaceFiles || {}; const enabledChannels = enabledPackages.filter((pkg) => CHANNEL_PACKAGES.includes(pkg) ); const channelUsers = tenant.spec.channelUsers || {}; // Admins inspecting another tenant's usage: pass teamId AND keyAlias so // the backend filters spend logs by this specific tenant's virtual key. // Without keyAlias the response would include sibling tenants in the // same org, since teams are now shared (Slice 2). // Customers viewing their own: pass nothing — backend resolves both // from the session-bound tenant. const usageTeamId = user.isPlatform ? tenant.status?.litellmTeamId || undefined : undefined; const usageKeyAlias = user.isPlatform ? tenant.status?.litellmKeyAlias || undefined : undefined; return (
{/* Header */}

{tenant.spec.displayName || name}

{tenant.spec.agentName && (

{t("agent")}: {tenant.spec.agentName}

)} {tenant.metadata.creationTimestamp && (

{t("provisioned")}{" "} {formatRelative(tenant.metadata.creationTimestamp, f)}{" "} ({formatDateTime(tenant.metadata.creationTimestamp, f)})

)}
{/* Usage */}

{t("usage")}

{/* Packages */}

{t("packages")}

{/* Channel Users (authorized users per channel) */} {enabledChannels.length > 0 && (
)} {/* Workspace files */}

{t("workspaceFiles")}

{/* Slice 7: Assigned users — visible to anyone who can see the tenant, editable only by owners/platform users. The component fetches its own data so the page doesn't need to await. */}

{t("assignedUsers")}

); }