Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 52 additions & 45 deletions app/app/compliance/page.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,65 @@
import { Suspense } from 'react';
import Link from 'next/link';
import { ClipboardCheck, ArrowRight, Activity } from 'lucide-react';
import { Layers, ShieldCheck } from 'lucide-react';
import { ObligationsTable } from '@/components/compliance/ObligationsTable';
import { CompliancePageHero } from '@/components/compliance/CompliancePageHero';
import { SkeletonCard } from '@/components/ui/skeleton';
import { getCurrentOrgId } from '@/lib/frameworks/org-frameworks';
import { createSupabaseServerClient } from '@/lib/supabase/server';

/**
* First-load density gate: a brand-new org has no obligations, so the full
* register chrome (4-metric hero, quick-link cards, filter bar, empty table,
* pagination) renders as a wall of zero states. Only show it once at least
* one obligation exists. Fail open — any resolution/query error renders the
* full register, never a false "set up" state for a populated org.
*/
async function hasObligations(): Promise<boolean> {
try {
const orgId = await getCurrentOrgId();
const supabase = await createSupabaseServerClient();
const { count, error } = await supabase
.from('org_tasks')
.select('id', { count: 'exact', head: true })
.eq('organization_id', orgId);
if (error) return true;
return (count ?? 0) > 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat enabled frameworks as populated

When an org has just activated a framework, this gate can still return false because framework activation upserts org_frameworks and then starts provisionFrameworkControls(...).catch() without awaiting it in app/api/v1/frameworks/activate/route.ts, so org_tasks may be empty while the framework is already enabled. In that window, or if provisioning fails to create tasks, /app/compliance keeps showing the setup CTA instead of the populated compliance landing/health links, effectively sending users back to browse frameworks they already enabled.

Useful? React with 👍 / 👎.

} catch {
return true;
}
}

export default async function ComplianceIndexPage() {
const populated = await hasObligations();

if (!populated) {
return (
<div className="flex h-full flex-col">
<section className="mx-auto mt-10 w-full max-w-xl rounded-2xl border border-border bg-surface-1 p-8 text-center">
<ShieldCheck className="mx-auto h-8 w-8 text-muted-foreground" />
<h1 className="mt-3 text-lg font-semibold text-foreground">
Set up your obligations register
</h1>
<p className="mx-auto mt-1 max-w-md text-sm text-muted-foreground">
Install a compliance framework and FormaOS generates the
obligations, owners and due dates it tracks here.
</p>
<Link
href="/app/compliance/frameworks"
className="mt-5 inline-flex min-h-[44px] items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 md:min-h-0"
>
<Layers className="h-4 w-4" />
Browse frameworks
</Link>
</section>
</div>
);
}

export default function ComplianceIndexPage() {
return (
<div className="flex flex-col h-full">
<CompliancePageHero />

{/*
* Audit Sprint 7a (2026-05-24): surface the manual-attestation
* workflow (Sprint 6c PR #173) from the compliance landing.
* Audit 2026-05-27 Tier 2.C: also surface the Health dashboard.
*/}
<div className="mx-4 mb-4 grid gap-2 sm:mx-0 md:grid-cols-2">
<Link
href="/app/compliance/health"
className="flex items-center justify-between rounded-lg border border-border bg-card/60 px-4 py-3 transition-colors hover:border-edge-2 hover:bg-card focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
data-testid="compliance-health-link"
>
<div className="flex items-center gap-3">
<Activity className="h-5 w-5 text-muted-foreground" />
<div>
<p className="text-sm font-medium text-card-foreground">
Compliance health
</p>
<p className="text-xs text-muted-foreground">
Cross-framework score, status counts, top outstanding controls.
</p>
</div>
</div>
<ArrowRight className="h-4 w-4 text-muted-foreground" />
</Link>
<Link
href="/app/compliance/attestations"
className="flex items-center justify-between rounded-lg border border-border bg-card/60 px-4 py-3 transition-colors hover:border-edge-2 hover:bg-card focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<div className="flex items-center gap-3">
<ClipboardCheck className="h-5 w-5 text-muted-foreground" />
<div>
<p className="text-sm font-medium text-card-foreground">
Manual attestations
</p>
<p className="text-xs text-muted-foreground">
Controls whose evaluator requires a human sign-off.
</p>
</div>
</div>
<ArrowRight className="h-4 w-4 text-muted-foreground" />
</Link>
</div>

<div className="flex-1 overflow-auto -mx-4 px-4 sm:mx-0 sm:px-0">
<Suspense fallback={<SkeletonCard className="h-96" />}>
<ObligationsTable />
Expand Down
19 changes: 18 additions & 1 deletion components/compliance/CompliancePageHero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import Link from 'next/link';
import { useEffect } from 'react';
import { Layers, ShieldCheck } from 'lucide-react';
import { Activity, ClipboardCheck, Layers, ShieldCheck } from 'lucide-react';
import {
useComplianceStore,
useComplianceSummary,
Expand Down Expand Up @@ -74,6 +74,23 @@ export function CompliancePageHero() {
<Layers className="h-3.5 w-3.5" />
Frameworks
</Link>
{/* Health + Attestations live here (not as standalone cards below)
so the landing page stays two blocks: hero + register. */}
<Link
href="/app/compliance/health"
data-testid="compliance-health-link"
className="inline-flex items-center gap-1.5 rounded-md px-3.5 py-2 text-xs font-semibold text-muted-foreground transition-colors hover:text-foreground"
>
<Activity className="h-3.5 w-3.5" />
Health
</Link>
<Link
href="/app/compliance/attestations"
className="inline-flex items-center gap-1.5 rounded-md px-3.5 py-2 text-xs font-semibold text-muted-foreground transition-colors hover:text-foreground"
>
<ClipboardCheck className="h-3.5 w-3.5" />
Attestations
</Link>
<Link
href="/app/compliance/cross-map"
className="inline-flex items-center gap-1.5 rounded-md px-3.5 py-2 text-xs font-semibold text-muted-foreground transition-colors hover:text-foreground"
Expand Down
Loading