Skip to content

Commit 9ef48cd

Browse files
authored
Fix Pandora dashboard truth boundary
Gate /pandora behind a server-derived session and downgrade the dashboard to authenticated mock-shell state. Removes live-looking retrieval, memory, profile, diagnostics, and queue claims until backed by real implementation and tests.
1 parent d15f609 commit 9ef48cd

7 files changed

Lines changed: 131 additions & 33 deletions

File tree

app/pandora/page.tsx

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,50 @@
1+
import Link from "next/link";
2+
import { AppShell } from "@/components/layout/app-shell";
3+
import { PageHeader } from "@/components/ui/page-header";
4+
import { SectionCard } from "@/components/ui/section-card";
5+
import { StatusBadge } from "@/components/ui/status-badge";
16
import { PandoraDashboard } from "@/components/pandora/PandoraDashboard";
7+
import { resolvePandoraServerSession } from "@/lib/auth/pandora-server-session-resolver";
28

3-
export default function PandoraPage() {
4-
return <PandoraDashboard />;
9+
export const dynamic = "force-dynamic";
10+
11+
export default async function PandoraPage() {
12+
const session = await resolvePandoraServerSession();
13+
const returnPath = "/pandora";
14+
const loginPath = `/auth/login?next=${encodeURIComponent(returnPath)}`;
15+
16+
if (!session.ok) {
17+
return (
18+
<AppShell>
19+
<div className="page-stack">
20+
<PageHeader
21+
eyebrow="Internal Pandora dashboard"
22+
title="Operator session required."
23+
description="The Pandora dashboard shell is not a public proof page. It stays hidden until a server-derived Supabase session exists, and it must not expose memory namespaces, mock counts, or operational labels to anonymous visitors."
24+
/>
25+
<SectionCard title="Start operator session" description="No memory dashboard content is rendered without authentication.">
26+
<div className="auth-status-panel">
27+
<StatusBadge status="blocked" />
28+
<div>
29+
<h3>Unauthenticated request blocked</h3>
30+
<p>Use a server-visible Supabase session before opening the Pandora dashboard. The dashboard remains mock-only even after login until backed by real routes, schema, RLS, and tests.</p>
31+
<div className="browser-state-grid">
32+
{session.blockers.map((blocker) => (
33+
<span className="browser-state-pill browser-state-pill--blocked" key={blocker.code}>{blocker.message}</span>
34+
))}
35+
</div>
36+
<div className="topbar__actions">
37+
<Link className="button-link button-link--primary" href={loginPath}>Start operator session</Link>
38+
<Link className="button-link" href="/api/session">Check session JSON</Link>
39+
<Link className="button-link" href="/dashboard">Back to foundation dashboard</Link>
40+
</div>
41+
</div>
42+
</div>
43+
</SectionCard>
44+
</div>
45+
</AppShell>
46+
);
47+
}
48+
49+
return <PandoraDashboard operatorLabel={session.session.email ?? session.session.userId} />;
550
}
Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,40 @@
11
import { MoreHorizontal, RefreshCw } from "lucide-react";
2+
import { profileSnapshot } from "./mock-data";
23

3-
function ConfidenceRing({ value }: { value: number }) { const radius = 42; const circumference = 2 * Math.PI * radius; const offset = circumference * (1 - value / 100); return <div className="pd-ring"><svg viewBox="0 0 100 100" aria-label={`${value}% confidence`}><circle cx="50" cy="50" r={radius} className="pd-ring-bg" /><circle cx="50" cy="50" r={radius} className="pd-ring-fg" strokeDasharray={circumference} strokeDashoffset={offset} /></svg><strong>{value}%</strong></div>; }
4+
function ConfidenceRing({ value, label }: { value: number; label: string }) {
5+
const radius = 42;
6+
const circumference = 2 * Math.PI * radius;
7+
const offset = circumference * (1 - value / 100);
8+
9+
return (
10+
<div className="pd-ring">
11+
<svg viewBox="0 0 100 100" aria-label={`Profile confidence ${label}`}>
12+
<circle cx="50" cy="50" r={radius} className="pd-ring-bg" />
13+
<circle cx="50" cy="50" r={radius} className="pd-ring-fg" strokeDasharray={circumference} strokeDashoffset={offset} />
14+
</svg>
15+
<strong>{label}</strong>
16+
</div>
17+
);
18+
}
419

520
export function AdaptiveProfileCard({ loading }: { loading: boolean }) {
6-
return <section className="pd-card"><div className="pd-section-head"><div><p className="pd-label">Adaptive Profile</p><h3>Writer v2</h3></div><span className="pd-pill pd-pill-emerald">Active</span></div>{loading ? <div className="pd-loading" aria-label="Loading adaptive profile" /> : <><div className="pd-profile-main"><ConfidenceRing value={88} /><div><strong>11 memories • 6 preferences • 2 facts</strong><p>Last refreshed 1h ago</p></div></div><div className="pd-traits">{["Analytical", "Methodical", "Private", "Detail-oriented"].map((trait) => <span key={trait}>{trait}</span>)}</div><div className="pd-card-row"><button type="button" className="pd-secondary-btn"><RefreshCw size={16} aria-hidden="true" />Refresh Profile</button><button type="button" className="pd-icon-button" aria-label="More profile options"><MoreHorizontal size={18} aria-hidden="true" /></button></div></>}</section>;
21+
return (
22+
<section className="pd-card">
23+
<div className="pd-section-head">
24+
<div><p className="pd-label">Adaptive Profile</p><h3>{profileSnapshot.name}</h3></div>
25+
<span className="pd-pill pd-pill-slate">{profileSnapshot.status}</span>
26+
</div>
27+
{loading ? <div className="pd-loading" aria-label="Loading adaptive profile shell" /> : <>
28+
<div className="pd-profile-main">
29+
<ConfidenceRing value={profileSnapshot.confidencePercent} label={profileSnapshot.confidenceLabel} />
30+
<div><strong>{profileSnapshot.summary}</strong><p>{profileSnapshot.lastRefreshed}</p></div>
31+
</div>
32+
<div className="pd-traits">{profileSnapshot.traits.map((trait) => <span key={trait}>{trait}</span>)}</div>
33+
<div className="pd-card-row">
34+
<button type="button" className="pd-secondary-btn" disabled><RefreshCw size={16} aria-hidden="true" />Refresh Profile</button>
35+
<button type="button" className="pd-icon-button" aria-label="More profile options" disabled><MoreHorizontal size={18} aria-hidden="true" /></button>
36+
</div>
37+
</>}
38+
</section>
39+
);
740
}
Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1+
import { profileSnapshot } from "./mock-data";
2+
13
export function AskPandoraHero() {
24
return (
35
<section className="pd-hero">
4-
<div><p className="pd-label">Ask Pandora</p><h2>Pandora is stable. The next risk is memory clutter, not stream failure.</h2><p>V0.4 is hardened: payload caps, profile refresh reruns, namespace isolation, and action envelopes are working. The next cleanup target is retrieval evals plus master-pack supersession.</p></div>
5-
<div className="pd-hero-actions"><button type="button" className="pd-primary-btn">Refresh Context Pack</button><button type="button" className="pd-secondary-btn">Run Retrieval Eval</button></div>
6-
<div className="pd-evidence">Based on 11 memories • 6 preferences • 2 facts • Confidence 0.88 • request_id enabled</div>
6+
<div>
7+
<p className="pd-label">Ask Pandora</p>
8+
<h2>Pandora dashboard is an authenticated mock shell.</h2>
9+
<p>This route is for layout review only. It does not present live memory health, retrieval scores, profile state, or queue activity until those claims are backed by implemented routes, database policy, and tests.</p>
10+
</div>
11+
<div className="pd-hero-actions">
12+
<button type="button" className="pd-primary-btn" disabled>Context pack backend pending</button>
13+
<button type="button" className="pd-secondary-btn" disabled>Retrieval eval backend pending</button>
14+
</div>
15+
<div className="pd-evidence">{profileSnapshot.evidence}</div>
716
</section>
817
);
918
}

components/pandora/DiagnosticsCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ import type { SystemRow } from "./types";
44
function SystemStatusRow({ row }: { row: SystemRow }) { return <div className="pd-system-row"><span>{row.label}</span><strong className={`pd-state-${row.state}`}>{row.value}</strong></div>; }
55

66
export function DiagnosticsCard({ loading }: { loading: boolean }) {
7-
return <section className="pd-card"><div className="pd-section-head"><div><p className="pd-label">Diagnostics</p><h3>Core ungated systems operational.</h3></div></div>{loading ? <div className="pd-loading" aria-label="Loading diagnostics" /> : <><div className="pd-system-list">{coreSystems.map((row) => <SystemStatusRow row={row} key={row.label} />)}</div><div className="pd-system-list pd-gated-list">{gatedSystems.map((row) => <SystemStatusRow row={row} key={row.label} />)}</div><div className="pd-envelope"><strong>MCP Envelope</strong><p>Responses now expose ok, request_id, fallback_used, and capped payloads.</p></div><button type="button" className="pd-secondary-btn" disabled title="Backend wiring pending">Run Smoke Test</button></>}</section>;
7+
return <section className="pd-card"><div className="pd-section-head"><div><p className="pd-label">Diagnostics</p><h3>Authenticated mock shell only.</h3></div></div>{loading ? <div className="pd-loading" aria-label="Loading diagnostics" /> : <><div className="pd-system-list">{coreSystems.map((row) => <SystemStatusRow row={row} key={row.label} />)}</div><div className="pd-system-list pd-gated-list">{gatedSystems.map((row) => <SystemStatusRow row={row} key={row.label} />)}</div><div className="pd-envelope"><strong>Action Envelope</strong><p>Pending backend proof. Do not treat this UI as live engine evidence.</p></div><button type="button" className="pd-secondary-btn" disabled title="Backend wiring pending">Run Smoke Test</button></>}</section>;
88
}

components/pandora/PandoraDashboard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { StatCard } from "./StatCard";
1313
import { TopBar } from "./TopBar";
1414
import { WorkQueueCard } from "./WorkQueueCard";
1515

16-
export function PandoraDashboard() {
16+
export function PandoraDashboard({ operatorLabel: _operatorLabel }: { operatorLabel?: string } = {}) {
1717
const [activeNav, setActiveNav] = useState("Dashboard");
1818
const [isSimulatingLoad, setIsSimulatingLoad] = useState(true);
1919

components/pandora/WorkQueueCard.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ function MiniMetric({ label, value }: { label: string; value: number }) { return
66
export function WorkQueueCard({ queue }: { queue: WorkQueueData }) {
77
const total = Object.values(queue).reduce((sum, value) => sum + value, 0);
88
const items = [
9-
{ label: "Open Loops", value: queue.openLoops, desc: "Needs resolution", icon: AlertCircle },
10-
{ label: "Needs Review", value: queue.needsReview, desc: "Operator review pending", icon: ListChecks },
11-
{ label: "Pack Supersession", value: queue.packSupersessionNeeded, desc: "Master-pack cleanup needed", icon: GitMerge },
12-
{ label: "People Map Design", value: queue.peopleMapDesignNeeded, desc: "Stoplist ceiling reached; whitelist needed.", icon: UserRoundSearch },
9+
{ label: "Open Loops", value: queue.openLoops, desc: "Backend pending", icon: AlertCircle },
10+
{ label: "Needs Review", value: queue.needsReview, desc: "Backend pending", icon: ListChecks },
11+
{ label: "Pack Supersession", value: queue.packSupersessionNeeded, desc: "Backend pending", icon: GitMerge },
12+
{ label: "People Map Design", value: queue.peopleMapDesignNeeded, desc: "Backend pending", icon: UserRoundSearch },
1313
];
14-
return <section className="pd-card"><div className="pd-section-head"><div><p className="pd-label">Work Queue</p><h3>{total} actionable items</h3></div><span className="pd-pill pd-pill-amber">Attention</span></div><div className="pd-queue-list">{items.map((item) => { const Icon = item.icon; return <div className="pd-queue-item" key={item.label}><Icon size={18} aria-hidden="true" /><div><strong>{item.label}</strong><p>{item.desc}</p></div><b>{item.value}</b></div>; })}</div><div className="pd-mini-grid"><MiniMetric label="Stale Packs" value={queue.stalePacks} /><MiniMetric label="Refresh Due" value={queue.profileRefreshDue} /><MiniMetric label="Failed Tests" value={queue.failedTests} /></div></section>;
14+
return <section className="pd-card"><div className="pd-section-head"><div><p className="pd-label">Work Queue</p><h3>{total} actionable items</h3></div><span className="pd-pill pd-pill-slate">Mock only</span></div><div className="pd-queue-list">{items.map((item) => { const Icon = item.icon; return <div className="pd-queue-item" key={item.label}><Icon size={18} aria-hidden="true" /><div><strong>{item.label}</strong><p>{item.desc}</p></div><b>{item.value}</b></div>; })}</div><div className="pd-mini-grid"><MiniMetric label="Stale Packs" value={queue.stalePacks} /><MiniMetric label="Refresh Due" value={queue.profileRefreshDue} /><MiniMetric label="Failed Tests" value={queue.failedTests} /></div></section>;
1515
}

components/pandora/mock-data.ts

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,45 @@
11
import { BadgeCheck, Boxes, CircleAlert, Package, RefreshCcw, ShieldCheck, Sparkles, Target, Users } from "lucide-react";
22
import type { MemorySpace, StatItem, SystemRow, TimelineEvent, WorkQueueData } from "./types";
33

4+
export const profileSnapshot = {
5+
name: "Writer v2",
6+
status: "Mock only",
7+
confidencePercent: 0,
8+
confidenceLabel: "N/A",
9+
summary: "No live profile data loaded",
10+
lastRefreshed: "Backend wiring pending",
11+
traits: ["Mock shell", "Layout review", "Auth gated", "No live counts"],
12+
evidence: "Mock UI only • no live memories • no retrieval eval • request_id pending wiring",
13+
} as const;
14+
415
export const mockStats: StatItem[] = [
5-
{ id: "health", title: "Memory Health", value: "Stable", subtitle: "Ungated systems healthy", icon: ShieldCheck, color: "emerald", sparklineData: [68, 70, 72, 74, 74, 76, 78] },
6-
{ id: "retrieval", title: "Retrieval Accuracy", value: "94.3%", subtitle: "vs last 7 days", trend: "↑ 2.4%", icon: Target, color: "indigo", sparklineData: [82, 84, 83, 88, 90, 92, 94] },
7-
{ id: "profiles", title: "Active Profiles", value: "3", subtitle: "2 active • 1 archived", icon: Users, color: "blue", sparklineData: [2, 2, 3, 3, 3, 3, 3] },
8-
{ id: "loops", title: "Open Loops", value: "2", subtitle: "Needs resolution", icon: RefreshCcw, color: "amber", sparklineData: [5, 4, 4, 3, 2, 2, 2] },
9-
{ id: "envelope", title: "Action Envelope", value: "Live", subtitle: "ok/request_id/fallback", icon: Package, color: "purple", sparklineData: [1, 1, 1, 2, 2, 3, 3] },
16+
{ id: "health", title: "Memory Health", value: "Unknown", subtitle: "No live health route wired", icon: ShieldCheck, color: "slate", sparklineData: [0, 0, 0, 0, 0, 0, 0] },
17+
{ id: "retrieval", title: "Retrieval Eval", value: "Gated", subtitle: "No accuracy claim without tests", icon: Target, color: "amber", sparklineData: [0, 0, 0, 0, 0, 0, 0] },
18+
{ id: "profiles", title: "Active Profiles", value: "Mock", subtitle: "No live profile records loaded", icon: Users, color: "blue", sparklineData: [0, 0, 0, 0, 0, 0, 0] },
19+
{ id: "loops", title: "Open Loops", value: "Not wired", subtitle: "Queue UI only", icon: RefreshCcw, color: "amber", sparklineData: [0, 0, 0, 0, 0, 0, 0] },
20+
{ id: "envelope", title: "Action Envelope", value: "Pending proof", subtitle: "Show only after route evidence", icon: Package, color: "purple", sparklineData: [0, 0, 0, 0, 0, 0, 0] },
1021
];
1122

1223
export const memorySpaces: MemorySpace[] = [
13-
{ id: "real_life", label: "real_life", type: "Primary Space", description: "Business, projects, technical state, personal operating context.", memories: 24182, people: 312, projects: 26, status: "Active", color: "emerald" },
14-
{ id: "au", label: "au", type: "Isolated Space", description: "Alternate-universe context, scenarios, canon, and fictionalized work.", memories: 8741, people: 124, projects: 11, status: "Active", color: "purple" },
24+
{ id: "real_life", label: "real_life", type: "Primary Space", description: "Business, projects, technical state, and personal operating context. Counts stay hidden until backed by authenticated reads.", memories: 0, people: 0, projects: 0, status: "Degraded", color: "emerald" },
25+
{ id: "au", label: "au", type: "Isolated Space", description: "Alternate-universe context, scenarios, canon, and fictionalized work. Counts stay hidden until backed by authenticated reads.", memories: 0, people: 0, projects: 0, status: "Degraded", color: "purple" },
1526
];
1627

17-
export const workQueue: WorkQueueData = { needsReview: 4, openLoops: 2, stalePacks: 1, failedTests: 0, profileRefreshDue: 1, packSupersessionNeeded: 1, peopleMapDesignNeeded: 1 };
28+
export const workQueue: WorkQueueData = { needsReview: 0, openLoops: 0, stalePacks: 0, failedTests: 0, profileRefreshDue: 0, packSupersessionNeeded: 0, peopleMapDesignNeeded: 0 };
1829

1930
export const timelineEvents: TimelineEvent[] = [
20-
{ id: "adaptive-v2", icon: BadgeCheck, color: "blue", title: "AU adaptive profile v2 active", time: "1h ago", desc: "Supersession chain verified. Confidence remains 0.88." },
21-
{ id: "envelope", icon: Package, color: "purple", title: "Action envelope deployed", time: "2h ago", desc: "Tool responses now include ok, request_id, and fallback_used." },
22-
{ id: "real-life", icon: Boxes, color: "emerald", title: "real_life pack re-distilled", time: "3h ago", desc: "PLP, Pandora roadmap, and technical context verified without AU bleed." },
23-
{ id: "people-map", icon: CircleAlert, color: "amber", title: "people_map limitation identified", time: "4h ago", desc: "Stoplist reached ceiling. Next real fix is known-people whitelist." },
24-
{ id: "v04", icon: Sparkles, color: "indigo", title: "V0.4 hardening checkpoint", time: "Today", desc: "Payload caps, stream stability, and profile refresh reruns are working." },
31+
{ id: "route-gated", icon: BadgeCheck, color: "emerald", title: "Dashboard route gated", time: "Patch", desc: "Anonymous requests see the operator-session panel, not the dashboard shell." },
32+
{ id: "mock-data", icon: Boxes, color: "slate", title: "Live counts removed", time: "Patch", desc: "The dashboard no longer presents mock memory, people, project, or profile numbers as operational truth." },
33+
{ id: "retrieval-gated", icon: CircleAlert, color: "amber", title: "Retrieval accuracy claim removed", time: "Patch", desc: "Semantic retrieval stays gated until backed by route and test evidence." },
34+
{ id: "actions-pending", icon: Package, color: "purple", title: "Action buttons marked pending", time: "Patch", desc: "Dashboard actions remain disabled until backend wiring exists." },
35+
{ id: "ui-shell", icon: Sparkles, color: "indigo", title: "UI shell preserved", time: "Patch", desc: "The visual layout remains available for authenticated review without pretending to be the engine." },
2536
];
2637

2738
export const coreSystems: SystemRow[] = [
28-
{ label: "Event pipeline", value: "Healthy", state: "healthy" },
29-
{ label: "Profile engine", value: "Healthy", state: "healthy" },
30-
{ label: "Action envelope", value: "Live", state: "healthy" },
31-
{ label: "Namespace isolation", value: "Enforced", state: "healthy" },
39+
{ label: "Route exposure", value: "Auth gated", state: "healthy" },
40+
{ label: "Displayed data", value: "Mock only", state: "gated" },
41+
{ label: "Profile engine", value: "Not wired", state: "gated" },
42+
{ label: "Action envelope", value: "Pending proof", state: "attention" },
3243
];
3344

3445
export const gatedSystems: SystemRow[] = [
@@ -38,5 +49,5 @@ export const gatedSystems: SystemRow[] = [
3849
{ label: "Pruning", value: "Gated Off", state: "gated" },
3950
];
4051

41-
export const navItems = ["Dashboard", "Memory Feed", "Context Packs", "Adaptive Profiles", "Open Loops", "People", "Projects", "Retrieval Tests", "Settings"];
42-
export const mobileNavItems = ["Dashboard", "Feed", "Queue", "Profiles", "More"];
52+
export const navItems = ["Dashboard", "Memory Feed", "Context Packs", "Adaptive Profiles", "Open Loops", "People", "Projects", "Retrieval Tests", "Settings"] as const;
53+
export const mobileNavItems = ["Dashboard", "Feed", "Queue", "Profiles", "More"] as const;

0 commit comments

Comments
 (0)