Skip to content
12 changes: 10 additions & 2 deletions app/pandora/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { SectionCard } from "@/components/ui/section-card";
import { StatusBadge } from "@/components/ui/status-badge";
import { PandoraDashboard } from "@/components/pandora/PandoraDashboard";
import { resolvePandoraServerSession } from "@/lib/auth/pandora-server-session-resolver";
import { createSupabaseServerClient } from "@/lib/supabase/server";
import { loadPandoraDashboardData, type PandoraDashboardDbClient } from "@/lib/services/pandora-dashboard-service";

export const dynamic = "force-dynamic";

Expand All @@ -27,7 +29,7 @@ export default async function PandoraPage() {
<StatusBadge status="blocked" />
<div>
<h3>Unauthenticated request blocked</h3>
<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>
<p>Use a server-visible Supabase session before opening the Pandora dashboard. Dashboard reads are server-derived and reject client-supplied user_id values.</p>
<div className="browser-state-grid">
{session.blockers.map((blocker) => (
<span className="browser-state-pill browser-state-pill--blocked" key={blocker.code}>{blocker.message}</span>
Expand All @@ -46,5 +48,11 @@ export default async function PandoraPage() {
);
}

return <PandoraDashboard operatorLabel={session.session.email ?? session.session.userId} />;
const supabase = await createSupabaseServerClient();
const dashboardData = await loadPandoraDashboardData(supabase as unknown as PandoraDashboardDbClient, {
userId: session.session.userId,
operatorLabel: session.session.email ?? session.session.userId,
});

return <PandoraDashboard dashboardData={dashboardData} />;
}
54 changes: 34 additions & 20 deletions components/pandora/PandoraDashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
"use client";

import { useEffect, useState } from "react";
import { AskPandoraHero } from "./AskPandoraHero";
import { AdaptiveProfileCard } from "./AdaptiveProfileCard";
import { DiagnosticsCard } from "./DiagnosticsCard";
import { MemorySpacesCard } from "./MemorySpacesCard";
import { Package } from "lucide-react";
import { MobileBottomNav } from "./MobileBottomNav";
import { mockStats, workQueue } from "./mock-data";
import { RecentEventsTimeline } from "./RecentEventsTimeline";
import { Sidebar } from "./Sidebar";
import { StatCard } from "./StatCard";
import { TopBar } from "./TopBar";
import { WorkQueueCard } from "./WorkQueueCard";
import type { PandoraDashboardData, StatItem } from "./types";
import { useState } from "react";

export function PandoraDashboard({ operatorLabel: _operatorLabel }: { operatorLabel?: string } = {}) {
export function PandoraDashboard({ dashboardData }: { dashboardData: PandoraDashboardData }) {
const [activeNav, setActiveNav] = useState("Dashboard");
const [isSimulatingLoad, setIsSimulatingLoad] = useState(true);

useEffect(() => {
const timer = window.setTimeout(() => setIsSimulatingLoad(false), 900);
return () => window.clearTimeout(timer);
}, []);
const stats: StatItem[] = dashboardData.stats.map((stat) => ({ ...stat, icon: Package }));

return (
<div className="pd-shell">
Expand All @@ -29,12 +19,36 @@ export function PandoraDashboard({ operatorLabel: _operatorLabel }: { operatorLa
<TopBar />
<main className="pd-content">
<div className="pd-header-row">
<div><p className="pd-label">Pandora Dashboard</p><h1>Memory Command Center</h1><p>Stable internal engine. Not productized. Ready for evals and pack supersession.</p></div>
<div className="pd-badges"><span className="pd-pill pd-pill-emerald">V0.4 Stable</span><span className="pd-pill pd-pill-purple">Envelope Live</span><span className="pd-pill pd-pill-slate">Semantic Gated</span></div>
<div>
<p className="pd-label">Pandora Dashboard</p>
<h1>Memory Command Center</h1>
<p>Authenticated operator view backed by server-side Supabase reads for this session.</p>
</div>
<div className="pd-badges">
<span className="pd-pill pd-pill-emerald">Live RLS Data</span>
<span className="pd-pill pd-pill-slate">Semantic Gated</span>
</div>
</div>
<AskPandoraHero />
<section className="pd-stat-grid" aria-label="Pandora status stats">{mockStats.map((stat) => <StatCard stat={stat} key={stat.id} />)}</section>
<section className="pd-dashboard-grid"><div className="pd-column"><WorkQueueCard queue={workQueue} /><MemorySpacesCard /></div><RecentEventsTimeline /><div className="pd-column"><AdaptiveProfileCard loading={isSimulatingLoad} /><DiagnosticsCard loading={isSimulatingLoad} /></div></section>
<section className="pd-hero">
<div>
<p className="pd-label">Live Truth Boundary</p>
<h2>{dashboardData.hero.title}</h2>
<p>{dashboardData.hero.description}</p>
</div>
<div className="pd-evidence">{dashboardData.evidence}</div>
</section>
<section className="pd-stat-grid" aria-label="Pandora status stats">
{stats.map((stat) => <StatCard stat={stat} key={stat.id} />)}
</section>
<section className="pd-card">
<div className="pd-section-head"><div><p className="pd-label">Live Snapshot</p><h3>Server scoped state</h3></div></div>
<div className="pd-mini-grid">
<div className="pd-mini"><strong>{dashboardData.memorySpaces.length}</strong><span>namespaces</span></div>
<div className="pd-mini"><strong>{dashboardData.workQueue.openLoops}</strong><span>open loops</span></div>
<div className="pd-mini"><strong>{dashboardData.workQueue.needsReview}</strong><span>needs review</span></div>
</div>
<div className="pd-envelope"><strong>{dashboardData.diagnostics.envelope.title}</strong><p>{dashboardData.diagnostics.envelope.description}</p></div>
</section>
</main>
</div>
<MobileBottomNav activeNav={activeNav} onNavChange={setActiveNav} />
Expand Down
55 changes: 50 additions & 5 deletions components/pandora/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@ import type { LucideIcon } from "lucide-react";

export type ColorKey = "emerald" | "indigo" | "blue" | "amber" | "purple" | "red" | "slate";

export type StatItem = {
export type DashboardStatData = {
id: string;
title: string;
value: string;
subtitle: string;
trend?: string;
icon: LucideIcon;
color: ColorKey;
sparklineData: number[];
};

export type StatItem = DashboardStatData & {
icon: LucideIcon;
};

export type MemorySpace = {
id: "real_life" | "au";
label: string;
Expand All @@ -25,13 +28,17 @@ export type MemorySpace = {
color: ColorKey;
};

export type TimelineEvent = {
export type TimelineEventData = {
id: string;
icon: LucideIcon;
color: ColorKey;
title: string;
time: string;
desc: string;
namespace: "real_life" | "au";
color: ColorKey;
};

export type TimelineEvent = TimelineEventData & {
icon: LucideIcon;
};

export type WorkQueueData = {
Expand All @@ -49,3 +56,41 @@ export type SystemRow = {
value: string;
state: "healthy" | "gated" | "attention" | "idle";
};

export type ProfileSnapshot = {
name: string;
status: string;
confidencePercent: number;
confidenceLabel: string;
summary: string;
lastRefreshed: string;
traits: string[];
evidence: string;
};

export type PandoraDashboardData = {
generatedAt: string;
operatorLabel: string;
live: boolean;
warnings: string[];
hero: {
title: string;
description: string;
primaryAction: string;
secondaryAction: string;
};
evidence: string;
stats: DashboardStatData[];
memorySpaces: MemorySpace[];
workQueue: WorkQueueData;
profileSnapshot: ProfileSnapshot;
timelineEvents: TimelineEventData[];
diagnostics: {
coreSystems: SystemRow[];
gatedSystems: SystemRow[];
envelope: {
title: string;
description: string;
};
};
};
Loading
Loading