From b12bca88188ea6ba3bfcc103fd132180af938571 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 1 May 2026 21:39:16 +0200 Subject: [PATCH] Suspendedremoval display in Frontend --- src/app/[locale]/dashboard/page.tsx | 22 +++++++++- src/lib/db.ts | 63 ++++++++++++++++++++++++----- src/types/index.ts | 9 +++++ 3 files changed, 82 insertions(+), 12 deletions(-) diff --git a/src/app/[locale]/dashboard/page.tsx b/src/app/[locale]/dashboard/page.tsx index fad0c31..bacdd91 100644 --- a/src/app/[locale]/dashboard/page.tsx +++ b/src/app/[locale]/dashboard/page.tsx @@ -2,7 +2,10 @@ 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 { + listActiveTenantRequestsByOrgId, + syncProvisioningStatuses, +} from "@/lib/db"; import { listVisibleTenants, canSeeInflightRequests, @@ -160,6 +163,23 @@ export default async function DashboardPage() { // Pending/in-flight requests are only shown to roles that can act on // them. `user`-role customers see no request cards. + // + // syncProvisioningStatuses runs on every dashboard load: it walks + // active and provisioning rows and reconciles them against the + // current cluster state. Without this, the operator-initiated + // 60-day TTL deletion (Bug 37b) leaves the portal showing "Your + // assistant is ready!" cards for tenants that no longer exist — + // the operator deletes the CR, but the DB row stays at active=true + // until something updates it. Running the sync at every dashboard + // load keeps the portal eventually consistent with the cluster + // without needing a separate cron/job. + // + // Cost: one K8s GET per row in (active, provisioning) status. At + // pilot scale this is small; if it grows we'd cache or move to a + // periodic background job. + if (canSeeInflightRequests(user)) { + await syncProvisioningStatuses(); + } const orgRequests = canSeeInflightRequests(user) ? await listActiveTenantRequestsByOrgId(user.orgId) : []; diff --git a/src/lib/db.ts b/src/lib/db.ts index c119628..6bee5d5 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -639,8 +639,26 @@ export async function deleteTenantRequest(id: string): Promise { } /** - * Sync provisioning statuses: for all requests with status "provisioning", - * check if the PiecedTenant CR has reached "Ready" and update to "active". + * Reconcile the portal's tenant_requests table against actual cluster + * state. Two passes, both walking only rows with `tenant_name` set + * (rows in pending/rejected/cancelled state don't have one and are + * irrelevant to this reconciliation): + * + * 1. provisioning → active: when a tenant CR's phase reaches Ready + * or Running, the portal flips the row to active so the + * "provisioning…" card transitions into the running tenant view. + * + * 2. active/provisioning → deleted: when the corresponding CR no + * longer exists in the cluster (404), or is mid-deletion (has + * metadata.deletionTimestamp set), the row gets flipped to + * `deleted`. The DB is otherwise blind to operator-initiated + * deletions — when the 60-day TTL fires (Bug 37b) and the + * operator deletes a suspended tenant, the portal would happily + * keep showing the "Your assistant is ready!" card forever. + * Without this reconciliation the dashboard drifts from reality. + * + * Errors are tolerated per-row: a transient API hiccup on one tenant + * shouldn't fail the whole sweep. Skipped rows get retried next call. * * Slice 3 note: with multi-tenant per org, this iterates each row * individually (keyed by its own tenant_name), so multiple in-flight @@ -648,24 +666,47 @@ export async function deleteTenantRequest(id: string): Promise { */ export async function syncProvisioningStatuses(): Promise { await ensureSchema(); + // Pull every row that *might* be reconcilable in one query — the + // status filter narrows to ones whose CR-vs-DB consistency is + // worth checking. Pending/rejected/cancelled rows have no + // tenant_name to compare against; deleted rows are terminal. const result = await getPool().query( - "SELECT * FROM tenant_requests WHERE status = 'provisioning'" + "SELECT * FROM tenant_requests WHERE status IN ('provisioning', 'active') AND tenant_name IS NOT NULL" ); for (const row of result.rows) { const mapped = mapRow(row); if (!mapped.tenantName) continue; + let tenant: Awaited> = null; try { - const tenant = await getTenant(mapped.tenantName); - if ( - tenant?.status?.phase === "Ready" || - tenant?.status?.phase === "Running" - ) { - await updateTenantRequestStatus(mapped.id, "active"); - } + tenant = await getTenant(mapped.tenantName); } catch { - // Tenant might not exist yet — skip + // Transient API error — skip this row, retry on next sweep. + continue; + } + + // CR gone, or mid-deletion. Flip the row to 'deleted'. The + // `markTenantRequestDeletedByTenantName` helper also nulls the + // tenant_name column so any future tenant created with the same + // name (unlikely given UUID-suffixed naming, but possible) won't + // collide with the unique index on (tenant_name) WHERE + // request_type = 'provision'. + if (!tenant || tenant.metadata.deletionTimestamp) { + await markTenantRequestDeletedByTenantName(mapped.tenantName); + continue; + } + + // CR exists and is healthy. Promote provisioning → active when + // the operator reports the tenant has reached steady state. + // Keep `active` rows on `active` regardless of phase — a + // temporarily-Reconfiguring tenant is still active from the + // portal's billing/visibility perspective. + if ( + mapped.status === "provisioning" && + (tenant.status?.phase === "Ready" || tenant.status?.phase === "Running") + ) { + await updateTenantRequestStatus(mapped.id, "active"); } } } diff --git a/src/types/index.ts b/src/types/index.ts index 550f2eb..86acfe3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -134,6 +134,15 @@ export interface PiecedTenant { name: string; namespace?: string; creationTimestamp?: string; + /** + * Set by the API server when something issues a Delete on the CR. + * The CR continues to exist while finalizers run cleanup; once + * they all remove themselves, the API server permanently removes + * the CR. Used by the portal's status sync to detect tenants + * being torn down — the customer should see "Deleted" rather + * than "Ready" while the cleanup runs. + */ + deletionTimestamp?: string; labels?: Record; annotations?: Record; };