Skip to content
Open
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
23 changes: 15 additions & 8 deletions apps/web/app/holder/readiness/ReadinessSurface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ProofSplitPane } from '@/components/proof/LanePanel';
import { LiveStateLog, buildStateLog } from '@/components/proof/LiveStateLog';
import { PostureBadge, ProofTierBadge, MetricBadge } from '@/components/proof/TrustLabel';
import type { ReadinessSnapshot, StateLogEntry, LaneSnapshot } from '@/components/proof/trust-types';
import { TrustHeader } from '@/components/trust';

// ─── Demo/fallback state for when no API is wired ─────────────────
// Replace with real API call in production
Expand Down Expand Up @@ -102,22 +103,28 @@ export default function ReadinessSurface() {

<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8 space-y-6">

{/* Posture header */}
{/* Canonical TrustHeader — institutional reading order on the
readiness surface. PREVIEW variant because this surface
renders a public/anonymous exploration view before claim. */}
{snapshot && (
<div className="flex flex-wrap items-center gap-4">
<div>
<h1 className="text-2xl font-extrabold text-slate-900">{snapshot.name}</h1>
<p className="font-mono text-sm text-slate-500 mt-0.5 tracking-wide">NPI {snapshot.npi}</p>
</div>
<div className="flex flex-wrap items-center gap-2 ml-auto">
<>
<TrustHeader
variant="PREVIEW"
object={{ id: snapshot.npi, label: snapshot.name, kind: 'clinician' }}
ownership={{ state: 'UNCLAIMED' }}
checkedAt={new Date(snapshot.generatedAt).toISOString()}
channel={snapshot.lanes.find((l) => l.status === 'verified')?.source ?? 'preview'}
runId={`readiness-${snapshot.generatedAt}`}
/>
<div className="flex flex-wrap items-center gap-2">
<PostureBadge posture={snapshot.posture} size="md" />
<ProofTierBadge tier={snapshot.proofTier} />
{snapshot.score !== null
? <MetricBadge label={`${snapshot.score}% coverage`} type="measured" />
: <MetricBadge label="score unavailable" type="unverified" />
}
</div>
</div>
</>
)}

{/* Live state log */}
Expand Down
23 changes: 23 additions & 0 deletions apps/web/app/passport/[id]/PassportEntityClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Link from 'next/link';
import PassportWallet from '@/components/passport/PassportWallet';
import { Button } from '@/components/ui/button';
import { TrustStateCard } from '@/components/trust/TrustStateCard';
import { TrustHeader } from '@/components/trust';
import { fetchPassportEntity } from '@/lib/api';
import type { PassportData } from '@/lib/trust/passport-contract';
import { KnowledgeInboxPanel } from '@/components/knowledge-inbox/KnowledgeInboxPanel';
Expand Down Expand Up @@ -74,8 +75,30 @@ export default function PassportEntityClient({ entityId }: PassportEntityClientP
// in classification — see lib/knowledge-inbox/classifyInboxItem.ts.
const inboxItems: KnowledgeInboxItem[] = [];

// Canonical TrustHeader — single source for the institutional reading
// order on the passport surface. Adopts Lane B primitives without
// changing the existing PassportWallet rendering below.
// Channel = the first checked launch-spine source; ownership and runId
// are stubbed where data isn't threaded yet (Lane D plumbing).
const channel = passport.sources?.checked?.[0] ?? 'unknown';
const ownershipClaimant = passport.identity.displayName;

return (
<div className="flex flex-col gap-8 pb-16">
<div className="mx-auto max-w-[480px] sm:max-w-[640px] md:max-w-3xl lg:max-w-4xl px-4 w-full">
<TrustHeader
variant="SNAPSHOT"
object={{
id: passport.identity.npi ?? passport.entityId,
label: passport.identity.displayName,
kind: passport.identity.entityType.toLowerCase(),
}}
ownership={{ state: 'UNCLAIMED', claimant: ownershipClaimant }}
checkedAt={passport.lastCheckedAt}
channel={channel}
runId={passport.entityId}
/>
</div>
<PassportWallet passport={passport} />
<div className="mx-auto max-w-[480px] sm:max-w-[640px] md:max-w-3xl lg:max-w-4xl px-4 w-full space-y-8">
<section
Expand Down
27 changes: 26 additions & 1 deletion apps/web/app/review/[entityId]/ConsoleWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
} from '@/components/review/EmployerDecisionConsole';
import type { LaneSnapshot, ReadinessPosture } from '@/components/proof/trust-types';
import { normalizeTrustContainerManifestView } from '@/lib/trust/trust-container-view';
import { TrustHeader } from '@/components/trust';

// ─── Data adapter ─────────────────────────────────────────────────

Expand Down Expand Up @@ -205,5 +206,29 @@ export default function ConsoleWrapper({ entityId }: { entityId: string }) {
);
}

return <EmployerDecisionConsole {...data} onAction={handleAction} />;
// Canonical TrustHeader at the top of the review surface. Channel is
// derived from the first verified lane; runId uses the loopId thread
// that the audit pipeline keys on. Ownership defaults to UNCLAIMED
// because employer-review entry points don't currently thread the
// claim state (Lane D plumbing).
const channel = data.lanes.find((l) => l.status === 'verified')?.source ?? 'review';
const earliestCheck = data.lanes
.map((l) => l.checkedAt)
.filter((t): t is number => typeof t === 'number')
.sort((a, b) => b - a)[0];
const checkedAtIso = earliestCheck ? new Date(earliestCheck).toISOString() : null;

return (
<div className="flex flex-col gap-4">
<TrustHeader
variant="SNAPSHOT"
object={{ id: data.entityId, label: data.name, kind: 'clinician' }}
ownership={{ state: 'UNCLAIMED', claimant: data.name }}
checkedAt={checkedAtIso}
channel={channel}
runId={data.loopId ?? data.entityId}
/>
<EmployerDecisionConsole {...data} onAction={handleAction} />
</div>
);
}
6 changes: 5 additions & 1 deletion apps/web/components/passport/PassportWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { PassportTrustPosture } from '@/components/passport/PassportTrustPosture
import { useTrackEvent } from '@/lib/learning/useTrackEvent';
import { EvidenceDisclosureCard } from '@/components/trust/EvidenceDisclosureCard';
import { PassportSourceCoveragePanel } from '@/components/trust/PassportSourceCoveragePanel';
import { CheckedAtStamp } from '@/components/trust';
import { SharePacketModal } from '@/components/passport/SharePacketModal';
import { TrustStateCard } from '@/components/trust/TrustStateCard';
import { DivergenceSummaryCard } from '@/components/trust/DivergenceSummaryCard';
Expand Down Expand Up @@ -171,7 +172,10 @@ function buildIdentitySection(passport: PassportData): AccordionItem {
<DetailRow label="NPI" value={identity.npi} />
<DetailRow label="Type" value={identity.entityType === 'PERSON' ? 'Individual provider' : 'Organization'} />
<DetailRow label="Source" value="CMS NPPES" />
<DetailRow label="Last check" value={new Date(lastCheckedAt).toLocaleDateString()} />
<div className="flex items-center justify-between py-1">
<span className="text-sm text-muted-foreground">Last check</span>
<CheckedAtStamp checkedAt={lastCheckedAt} label="" format="long" />
</div>
</div>
),
};
Expand Down
32 changes: 25 additions & 7 deletions apps/web/components/proof/LanePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
STATUS_COLORS, STATUS_EXPLANATIONS, KNOWN_LANES,
type LaneSnapshot, type SourceStatus,
} from './trust-types';
import { CheckedAtStamp, RunIdentity } from '@/components/trust';

// ─── Split-Pane Layout ────────────────────────────────────────────

Expand Down Expand Up @@ -144,17 +145,24 @@ function LaneDetail({
<MetaRow label="Lane ID" value={lane.laneId} mono />
<MetaRow label="Freshness" value={def.freshnessWindowLabel} />
{lane.checkedAt && (
<MetaRow
label="Checked at"
value={new Date(lane.checkedAt).toLocaleString()}
mono
/>
<div className="flex items-center justify-between py-1">
<span className="text-xs text-slate-500">Checked at</span>
<CheckedAtStamp
checkedAt={new Date(lane.checkedAt).toISOString()}
label=""
format="long"
className="text-xs"
/>
</div>
)}
{lane.value && (
<MetaRow label="Value" value={lane.value} />
)}
{lane.receiptId && (
<MetaRow label="Receipt ID" value={lane.receiptId} mono />
<div className="flex items-center justify-between py-1">
<span className="text-xs text-slate-500">Receipt ID</span>
<RunIdentity runId={lane.receiptId} label="receipt" className="text-xs" />
</div>
)}
</div>

Expand All @@ -165,7 +173,17 @@ function LaneDetail({
<div className="bg-slate-50 border border-slate-200 rounded-lg px-4 py-3 font-mono text-xs text-slate-600 space-y-1">
<div className="flex justify-between"><span>receipt_id</span><span className="text-slate-400">{lane.receiptId.slice(0, 16)}…</span></div>
<div className="flex justify-between"><span>source</span><span className="text-slate-400">{def.source}</span></div>
{lane.checkedAt && <div className="flex justify-between"><span>checked_at</span><span className="text-slate-400">{lane.checkedAt}</span></div>}
{lane.checkedAt && (
<div className="flex justify-between items-center">
<span>checked_at</span>
<CheckedAtStamp
checkedAt={new Date(lane.checkedAt).toISOString()}
label=""
format="iso"
className="text-slate-400"
/>
</div>
)}
</div>
</div>
) : (
Expand Down
Loading