diff --git a/app/app/compliance/page.tsx b/app/app/compliance/page.tsx index a1834cd3c..57d929b3f 100644 --- a/app/app/compliance/page.tsx +++ b/app/app/compliance/page.tsx @@ -1,4 +1,6 @@ import { Suspense } from 'react'; +import Link from 'next/link'; +import { ClipboardCheck, ArrowRight } from 'lucide-react'; import { ObligationsTable } from '@/components/compliance/ObligationsTable'; import { CompliancePageHero } from '@/components/compliance/CompliancePageHero'; import { SkeletonCard } from '@/components/ui/skeleton'; @@ -8,6 +10,31 @@ export default function ComplianceIndexPage() {
+ {/* + * Audit Sprint 7a (2026-05-24): surface the manual-attestation + * workflow (Sprint 6c PR #173) from the compliance landing. + * Per-industry sidebar navs (lib/navigation/industry-sidebar.ts) + * are 9 separate arrays and not worth churning for a single + * sub-link; the landing page is the natural hub. + */} + +
+ +
+

+ Manual attestations +

+

+ Controls whose evaluator requires a human sign-off. +

+
+
+ + +
}> diff --git a/scripts/check-db-test-verify.mjs b/scripts/check-db-test-verify.mjs index c8217abd5..1881e9726 100644 --- a/scripts/check-db-test-verify.mjs +++ b/scripts/check-db-test-verify.mjs @@ -305,19 +305,114 @@ async function verifyAuthenticatedFormsRoundTrip(admin, anon) { } } +async function deleteAndLog(admin, table, query) { + const { error } = await query; + if (error) { + // Audit Sprint 7a (2026-05-24): previously cleanup errors were + // ignored. Live DB had 95 leftover "FormaOS DB Verify" orgs + // because a failing delete left rows behind silently. Log so the + // operator can investigate without grepping prod for orphans. + console.warn(`[cleanupProbe] ${table} delete failed: ${error.message}`); + } +} + async function cleanupProbe(admin) { if (cleanup.formId) { - await admin.from('org_form_submissions').delete().eq('form_id', cleanup.formId); - await admin.from('org_forms').delete().eq('id', cleanup.formId); + await deleteAndLog( + admin, + 'org_form_submissions', + admin.from('org_form_submissions').delete().eq('form_id', cleanup.formId), + ); + await deleteAndLog( + admin, + 'org_forms', + admin.from('org_forms').delete().eq('id', cleanup.formId), + ); } if (cleanup.orgId) { - await admin.from('org_members').delete().eq('organization_id', cleanup.orgId); - await admin.from('organizations').delete().eq('id', cleanup.orgId); - await admin.from('orgs').delete().eq('id', cleanup.orgId); + await deleteAndLog( + admin, + 'org_members', + admin.from('org_members').delete().eq('organization_id', cleanup.orgId), + ); + await deleteAndLog( + admin, + 'organizations', + admin.from('organizations').delete().eq('id', cleanup.orgId), + ); + await deleteAndLog( + admin, + 'orgs', + admin.from('orgs').delete().eq('id', cleanup.orgId), + ); } if (cleanup.userId) { - await admin.auth.admin.deleteUser(cleanup.userId); + const { error } = await admin.auth.admin.deleteUser(cleanup.userId); + if (error) { + console.warn(`[cleanupProbe] auth user delete failed: ${error.message}`); + } + } +} + +/** + * Audit Sprint 7a (2026-05-24): sweep any "FormaOS DB Verify " + * orgs older than 1 hour. The script has been accumulating probe + * orgs whenever cleanupProbe silently failed (no error logging + * pre-this-fix). Run at the START of each invocation so a healthy + * env doesn't accumulate orphans even if the new error logging + * surfaces a future bug after-the-fact. + * + * Scoped narrowly: + * - name LIKE 'FormaOS DB Verify%' so we don't touch real orgs + * - created_at < now() - 1 hour so we never delete a probe that's + * currently running in a parallel invocation + */ +async function sweepOldProbes(admin) { + const cutoff = new Date(Date.now() - 60 * 60 * 1000).toISOString(); + const { data: orphans, error: listError } = await admin + .from('organizations') + .select('id, name, created_at') + .like('name', 'FormaOS DB Verify%') + .lt('created_at', cutoff) + .limit(500); + + if (listError) { + console.warn(`[sweepOldProbes] list failed: ${listError.message}`); + return; } + + const rows = orphans ?? []; + if (rows.length === 0) return; + + console.log(`[sweepOldProbes] removing ${rows.length} stale probe org(s)`); + const ids = rows.map((r) => r.id); + + // Order matters — FK-respecting teardown. + await deleteAndLog( + admin, + 'org_form_submissions', + admin.from('org_form_submissions').delete().in('org_id', ids), + ); + await deleteAndLog( + admin, + 'org_forms', + admin.from('org_forms').delete().in('org_id', ids), + ); + await deleteAndLog( + admin, + 'org_members', + admin.from('org_members').delete().in('organization_id', ids), + ); + await deleteAndLog( + admin, + 'organizations', + admin.from('organizations').delete().in('id', ids), + ); + await deleteAndLog( + admin, + 'orgs', + admin.from('orgs').delete().in('id', ids), + ); } async function main() { @@ -331,6 +426,11 @@ async function main() { const { admin, anon } = createSupabaseClients(); + // Audit Sprint 7a: clean up any stale probe orgs from prior runs + // before doing the current check. Bounded to "FormaOS DB Verify%" + // older than 1h so a parallel invocation isn't disturbed. + await sweepOldProbes(admin); + try { for (const table of requiredTables) { await verifyTable(admin, table);