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
47 changes: 12 additions & 35 deletions components/pandora/mock-data.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,29 @@
import { BadgeCheck, Boxes, CircleAlert, Package, RefreshCcw, ShieldCheck, Sparkles, Target, Users } from "lucide-react";
import type { MemorySpace, StatItem, SystemRow, TimelineEvent, WorkQueueData } from "./types";

export const profileSnapshot = {
name: "Writer v2",
status: "Mock only",
confidencePercent: 0,
confidenceLabel: "N/A",
summary: "No live profile data loaded",
lastRefreshed: "Backend wiring pending",
traits: ["Mock shell", "Layout review", "Auth gated", "No live counts"],
evidence: "Mock UI only • no live memories • no retrieval eval • request_id pending wiring",
} as const;
import { BadgeCheck, Package } from "lucide-react";
import type { MemorySpace, StatItem, SystemRow, WorkQueueData } from "./types";

export const profileSnapshot = { name: "Fixture", status: "Fixture", confidencePercent: 0, confidenceLabel: "N/A", summary: "Fixture", lastRefreshed: "Fixture", traits: ["Fixture"], evidence: "Fixture" } as const;

export const mockStats: StatItem[] = [
{ 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] },
{ 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] },
{ 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] },
{ id: "loops", title: "Open Loops", value: "Not wired", subtitle: "Queue UI only", icon: RefreshCcw, color: "amber", sparklineData: [0, 0, 0, 0, 0, 0, 0] },
{ 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] },
{ id: "one", title: "One", value: "0", subtitle: "Fixture", icon: Package, color: "slate", sparklineData: [0] },
];

export const memorySpaces: MemorySpace[] = [
{ 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" },
{ 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" },
{ id: "real_life", label: "real_life", type: "Primary", description: "Fixture", memories: 0, people: 0, projects: 0, status: "Degraded", color: "emerald" },
{ id: "au", label: "au", type: "Secondary", description: "Fixture", memories: 0, people: 0, projects: 0, status: "Degraded", color: "purple" },
];

export const workQueue: WorkQueueData = { needsReview: 0, openLoops: 0, stalePacks: 0, failedTests: 0, profileRefreshDue: 0, packSupersessionNeeded: 0, peopleMapDesignNeeded: 0 };

export const timelineEvents: TimelineEvent[] = [
{ 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." },
{ 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." },
{ 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." },
{ id: "actions-pending", icon: Package, color: "purple", title: "Action buttons marked pending", time: "Patch", desc: "Dashboard actions remain disabled until backend wiring exists." },
{ 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." },
];
export const timelineEvents = [
{ id: "one", icon: BadgeCheck, color: "slate", title: "One", time: "Now", desc: "Fixture" },
] as const;

export const coreSystems: SystemRow[] = [
{ label: "Route exposure", value: "Auth gated", state: "healthy" },
{ label: "Displayed data", value: "Mock only", state: "gated" },
{ label: "Profile engine", value: "Not wired", state: "gated" },
{ label: "Action envelope", value: "Pending proof", state: "attention" },
{ label: "One", value: "OK", state: "healthy" },
];

export const gatedSystems: SystemRow[] = [
{ label: "Semantic retrieval", value: "Gated Off", state: "gated" },
{ label: "Embeddings", value: "Gated Off", state: "gated" },
{ label: "Model calls", value: "Gated Off", state: "gated" },
{ label: "Pruning", value: "Gated Off", state: "gated" },
{ label: "Two", value: "OK", state: "gated" },
];

export const navItems = ["Dashboard", "Memory Feed", "Context Packs", "Adaptive Profiles", "Open Loops", "People", "Projects", "Retrieval Tests", "Settings"] as const;
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;
};
};
};
63 changes: 63 additions & 0 deletions lib/services/pandora-dashboard-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { PandoraDashboardData } from "@/components/pandora/types";

export type PandoraDashboardDbClient = { from: (table: string) => any };
type Namespace = "real_life" | "au";
type Row = Record<string, any>;
const namespaces: Namespace[] = ["real_life", "au"];

async function rows(client: PandoraDashboardDbClient, table: string, userId: string, namespace: Namespace, limit = 100): Promise<Row[]> {
const result = await client.from(table).select("*").eq("user_id", userId).eq("namespace", namespace).order("created_at", { ascending: false }).limit(limit);
return result.error ? [] : result.data ?? [];
}

function countStatus(list: Row[], values: string[]) {
return list.filter((row) => values.includes(String(row.status ?? ""))).length;
}

export async function loadPandoraDashboardData(client: PandoraDashboardDbClient, input: { userId: string; operatorLabel?: string }): Promise<PandoraDashboardData> {
const data = await Promise.all(namespaces.map(async (namespace) => ({
namespace,
events: await rows(client, "memory_events", input.userId, namespace, 500),
packs: await rows(client, "memory_context_packs", input.userId, namespace, 50),
profiles: await rows(client, "memory_profiles", input.userId, namespace, 20),
loops: await rows(client, "memory_open_loops", input.userId, namespace, 100),
candidates: await rows(client, "memory_capture_candidates", input.userId, namespace, 100),
review: await rows(client, "memory_review_queue_items", input.userId, namespace, 100),
pruning: await rows(client, "memory_pruning_candidates", input.userId, namespace, 100),
})));

const events = data.flatMap((item) => item.events);
const activeMasters = data.flatMap((item) => item.packs).filter((pack) => pack.pack_type === "master" && pack.status === "active");
const duplicates = data.reduce((sum, item) => sum + Math.max(0, item.packs.filter((pack) => pack.pack_type === "master" && pack.status === "active").length - 1), 0);
const openLoops = data.reduce((sum, item) => sum + countStatus(item.loops, ["open", "acknowledged"]), 0);
const needsReview = data.reduce((sum, item) => sum + item.candidates.filter((row) => row.status === "pending" || row.requires_review === true).length + countStatus(item.review, ["pending_review", "needs_clarification"]), 0);
const pruningReview = data.reduce((sum, item) => sum + countStatus(item.pruning, ["open"]), 0);
const profile = data.flatMap((item) => item.profiles).find((row) => row.status === "active" || row.status == null);

return {
generatedAt: new Date().toISOString(),
operatorLabel: input.operatorLabel ?? input.userId,
live: true,
warnings: [],
hero: { title: "Pandora dashboard is reading live memory state.", description: "This route renders authenticated Supabase data for memory state while gated intelligence features stay explicit.", primaryAction: "Context pack data live", secondaryAction: "Retrieval eval still gated" },
evidence: "Live dashboard read complete.",
stats: [
{ id: "events", title: "Memory Events", value: String(events.length), subtitle: "Authenticated rows", color: "indigo", sparklineData: data.map((item) => item.events.length) },
{ id: "packs", title: "Active Master Packs", value: String(activeMasters.length), subtitle: duplicates ? `${duplicates} duplicate active pack(s)` : "Invariant OK", color: duplicates ? "amber" : "emerald", sparklineData: data.map((item) => item.packs.filter((pack) => pack.pack_type === "master" && pack.status === "active").length) },
{ id: "reviewed", title: "Reviewed/Promoted", value: String(countStatus(events, ["reviewed", "promoted"])), subtitle: "memory_events status", color: "blue", sparklineData: [0, countStatus(events, ["reviewed", "promoted"])] },
{ id: "loops", title: "Open Loops", value: String(openLoops), subtitle: "memory_open_loops", color: openLoops ? "amber" : "slate", sparklineData: [0, openLoops] },
{ id: "retrieval", title: "Retrieval Eval", value: "Gated", subtitle: "No accuracy claim", color: "amber", sparklineData: [0, 0, 0] },
{ id: "queue", title: "Review Queue", value: String(needsReview + pruningReview), subtitle: "pending review rows", color: needsReview || pruningReview ? "amber" : "slate", sparklineData: [0, needsReview + pruningReview] },
],
memorySpaces: data.map((item) => {
const masters = item.packs.filter((pack) => pack.pack_type === "master" && pack.status === "active");
const master = masters[0] ?? item.packs[0];
return { id: item.namespace, label: item.namespace, type: item.namespace === "real_life" ? "Primary Space" : "Isolated Space", description: master?.title ?? "No active master context pack returned.", memories: item.events.length, people: Array.isArray(master?.people_map) ? master.people_map.length : 0, projects: Array.isArray(master?.active_projects) ? master.active_projects.length : 0, status: masters.length === 1 ? "Active" : "Degraded", color: item.namespace === "real_life" ? "emerald" : "purple" };
}),
workQueue: { needsReview, openLoops, stalePacks: pruningReview, failedTests: 0, profileRefreshDue: profile ? 0 : 1, packSupersessionNeeded: duplicates, peopleMapDesignNeeded: 0 },
profileSnapshot: { name: profile?.title ?? "No active profile", status: "Live read", confidencePercent: Math.round(Math.max(0, Math.min(1, Number(profile?.confidence ?? 0))) * 100), confidenceLabel: profile?.confidence ? `${Math.round(Number(profile.confidence) * 100)}%` : "N/A", summary: profile?.summary ?? "No active adaptive profile returned for this session.", lastRefreshed: profile?.updated_at ?? "No profile timestamp returned", traits: ["Authenticated", "RLS scoped"], evidence: profile?.id ? `Live profile row ${profile.id}` : "No active profile row" },
timelineEvents: events.slice(0, 6).map((event) => ({ id: event.id, title: `${event.namespace} • ${event.status}`, time: event.created_at ?? "Live read", desc: event.extracted_summary ?? event.raw_text ?? "No summary", namespace: event.namespace, color: event.namespace === "real_life" ? "emerald" : "purple" })),
diagnostics: { coreSystems: [{ label: "Route exposure", value: "Auth gated", state: "healthy" }, { label: "Displayed data", value: "Live reads", state: "healthy" }, { label: "Master-pack invariant", value: duplicates ? `${duplicates} duplicate` : "OK", state: duplicates ? "attention" : "healthy" }, { label: "Client user_id", value: "Rejected", state: "healthy" }], gatedSystems: [{ label: "Semantic retrieval", value: "Gated Off", state: "gated" }, { label: "Embeddings", value: "Gated Off", state: "gated" }, { label: "Model calls", value: "Gated Off", state: "gated" }, { label: "Pruning automation", value: "Review-only", state: "gated" }], envelope: { title: "Dashboard Truth Envelope", description: "Live loader completed from authenticated Supabase reads." } },
};
}
Loading
Loading