From 3015527b7552a2afa133b62c99737e5880fa85d6 Mon Sep 17 00:00:00 2001 From: besfeng23 <136943241+besfeng23@users.noreply.github.com> Date: Sat, 4 Jul 2026 03:42:13 +0800 Subject: [PATCH] feat: restore live Pandora dashboard --- components/pandora/AdaptiveProfileCard.tsx | 17 +++-- components/pandora/AskPandoraHero.tsx | 15 ++-- components/pandora/DiagnosticsCard.tsx | 7 +- components/pandora/MemorySpacesCard.tsx | 6 +- components/pandora/PandoraDashboard.tsx | 42 ++++++----- components/pandora/RecentEventsTimeline.tsx | 7 +- components/pandora/WorkQueueCard.tsx | 10 +-- components/pandora/mock-data.ts | 29 +++----- lib/services/pandora-dashboard-service.ts | 77 +++++++++++++------- tests/unit/pandora-dashboard-guard.test.ts | 26 +++++++ tests/unit/pandora-dashboard-service.test.ts | 37 +++++++++- tests/unit/pandora-dashboard-ui.test.tsx | 33 +++++++++ 12 files changed, 209 insertions(+), 97 deletions(-) create mode 100644 tests/unit/pandora-dashboard-guard.test.ts create mode 100644 tests/unit/pandora-dashboard-ui.test.tsx diff --git a/components/pandora/AdaptiveProfileCard.tsx b/components/pandora/AdaptiveProfileCard.tsx index 8e7db45..7861f25 100644 --- a/components/pandora/AdaptiveProfileCard.tsx +++ b/components/pandora/AdaptiveProfileCard.tsx @@ -1,10 +1,11 @@ import { MoreHorizontal, RefreshCw } from "lucide-react"; -import { profileSnapshot } from "./mock-data"; +import type { ProfileSnapshot } from "./types"; function ConfidenceRing({ value, label }: { value: number; label: string }) { + const safeValue = Math.max(0, Math.min(100, Number.isFinite(value) ? value : 0)); const radius = 42; const circumference = 2 * Math.PI * radius; - const offset = circumference * (1 - value / 100); + const offset = circumference * (1 - safeValue / 100); return (
@@ -17,19 +18,19 @@ function ConfidenceRing({ value, label }: { value: number; label: string }) { ); } -export function AdaptiveProfileCard({ loading }: { loading: boolean }) { +export function AdaptiveProfileCard({ profile, loading = false }: { profile: ProfileSnapshot; loading?: boolean }) { return (
-

Adaptive Profile

{profileSnapshot.name}

- {profileSnapshot.status} +

Adaptive Profile

{profile.name}

+ {profile.status}
{loading ?
: <>
- -
{profileSnapshot.summary}

{profileSnapshot.lastRefreshed}

+ +
{profile.summary}

{profile.lastRefreshed}

-
{profileSnapshot.traits.map((trait) => {trait})}
+
{profile.traits.map((trait) => {trait})}
diff --git a/components/pandora/AskPandoraHero.tsx b/components/pandora/AskPandoraHero.tsx index 0f43a97..a10f8cb 100644 --- a/components/pandora/AskPandoraHero.tsx +++ b/components/pandora/AskPandoraHero.tsx @@ -1,18 +1,17 @@ -import { profileSnapshot } from "./mock-data"; - -export function AskPandoraHero() { +export function AskPandoraHero({ hero, evidence, warnings }: { hero: { title: string; description: string; primaryAction: string; secondaryAction: string }; evidence: string; warnings: string[] }) { return (

Ask Pandora

-

Pandora dashboard is an authenticated mock shell.

-

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.

+

{hero.title}

+

{hero.description}

- - + +
-
{profileSnapshot.evidence}
+
{evidence}
+ {warnings.length ?
Loader warnings

{warnings.join(" ")}

: null}
); } diff --git a/components/pandora/DiagnosticsCard.tsx b/components/pandora/DiagnosticsCard.tsx index de4d0cd..c6e2533 100644 --- a/components/pandora/DiagnosticsCard.tsx +++ b/components/pandora/DiagnosticsCard.tsx @@ -1,8 +1,7 @@ -import { coreSystems, gatedSystems } from "./mock-data"; -import type { SystemRow } from "./types"; +import type { PandoraDashboardData, SystemRow } from "./types"; function SystemStatusRow({ row }: { row: SystemRow }) { return
{row.label}{row.value}
; } -export function DiagnosticsCard({ loading }: { loading: boolean }) { - return

Diagnostics

Authenticated mock shell only.

{loading ?
: <>
{coreSystems.map((row) => )}
{gatedSystems.map((row) => )}
Action Envelope

Pending backend proof. Do not treat this UI as live engine evidence.

}
; +export function DiagnosticsCard({ diagnostics, loading = false }: { diagnostics: PandoraDashboardData["diagnostics"]; loading?: boolean }) { + return

Diagnostics

Live RLS Data

{loading ?
: <>
{diagnostics.coreSystems.map((row) => )}
{diagnostics.gatedSystems.map((row) => )}
{diagnostics.envelope.title}

{diagnostics.envelope.description}

}
; } diff --git a/components/pandora/MemorySpacesCard.tsx b/components/pandora/MemorySpacesCard.tsx index cafc478..13368bc 100644 --- a/components/pandora/MemorySpacesCard.tsx +++ b/components/pandora/MemorySpacesCard.tsx @@ -1,7 +1,7 @@ import { LockKeyhole } from "lucide-react"; -import { memorySpaces } from "./mock-data"; +import type { MemorySpace } from "./types"; import { COLORS, cn } from "./theme"; -export function MemorySpacesCard() { - return

Memory Spaces

Namespace separation enforced

{memorySpaces.map((space) => { const color = COLORS[space.color]; return ; })}
; +export function MemorySpacesCard({ spaces }: { spaces: MemorySpace[] }) { + return

Memory Spaces

Namespace separation enforced

{spaces.map((space) => { const color = COLORS[space.color]; return ; })}
; } diff --git a/components/pandora/PandoraDashboard.tsx b/components/pandora/PandoraDashboard.tsx index 05b6fd2..0b7b8b6 100644 --- a/components/pandora/PandoraDashboard.tsx +++ b/components/pandora/PandoraDashboard.tsx @@ -1,16 +1,24 @@ "use client"; -import { Package } from "lucide-react"; +import { Archive, ClipboardList, GitBranch, History, Package, ShieldCheck } from "lucide-react"; +import { AdaptiveProfileCard } from "./AdaptiveProfileCard"; +import { AskPandoraHero } from "./AskPandoraHero"; +import { DiagnosticsCard } from "./DiagnosticsCard"; +import { MemorySpacesCard } from "./MemorySpacesCard"; import { MobileBottomNav } from "./MobileBottomNav"; +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"; +const statIcons = [History, Package, ShieldCheck, GitBranch, Archive, ClipboardList]; + export function PandoraDashboard({ dashboardData }: { dashboardData: PandoraDashboardData }) { const [activeNav, setActiveNav] = useState("Dashboard"); - const stats: StatItem[] = dashboardData.stats.map((stat) => ({ ...stat, icon: Package })); + const stats: StatItem[] = dashboardData.stats.map((stat, index) => ({ ...stat, icon: statIcons[index] ?? Package })); return (
@@ -29,26 +37,24 @@ export function PandoraDashboard({ dashboardData }: { dashboardData: PandoraDash Semantic Gated
-
-
-

Live Truth Boundary

-

{dashboardData.hero.title}

-

{dashboardData.hero.description}

-
-
{dashboardData.evidence}
-
+
{stats.map((stat) => )}
-
-

Live Snapshot

Server scoped state

-
-
{dashboardData.memorySpaces.length}namespaces
-
{dashboardData.workQueue.openLoops}open loops
-
{dashboardData.workQueue.needsReview}needs review
+
+
+ +
-
{dashboardData.diagnostics.envelope.title}

{dashboardData.diagnostics.envelope.description}

-
+
+ + +
+
+ +

Operator Boundary

{dashboardData.operatorLabel}

{dashboardData.memorySpaces.length}namespaces
{dashboardData.warnings.length}warnings
{dashboardData.live ? "Live" : "No live data"}loader
+
+
diff --git a/components/pandora/RecentEventsTimeline.tsx b/components/pandora/RecentEventsTimeline.tsx index 74b3566..99af19e 100644 --- a/components/pandora/RecentEventsTimeline.tsx +++ b/components/pandora/RecentEventsTimeline.tsx @@ -1,6 +1,7 @@ -import { timelineEvents } from "./mock-data"; +import { BadgeCheck } from "lucide-react"; +import type { TimelineEventData } from "./types"; import { COLORS, cn } from "./theme"; -export function RecentEventsTimeline() { - return

Recent Memory Events

Checkpoint timeline

    {timelineEvents.map((event) => { const Icon = event.icon; const color = COLORS[event.color]; return
  1. {event.title}

    {event.desc}

  2. ; })}
; +export function RecentEventsTimeline({ events }: { events: TimelineEventData[] }) { + return

Recent Memory Events

Checkpoint timeline

    {events.length ? events.map((event) => { const color = COLORS[event.color]; return
  1. {event.title}

    {event.desc}

  2. ; }) :
  3. No live events returned

    No timeline rows are fabricated.

  4. }
; } diff --git a/components/pandora/WorkQueueCard.tsx b/components/pandora/WorkQueueCard.tsx index 3fda637..1f61370 100644 --- a/components/pandora/WorkQueueCard.tsx +++ b/components/pandora/WorkQueueCard.tsx @@ -6,10 +6,10 @@ function MiniMetric({ label, value }: { label: string; value: number }) { return export function WorkQueueCard({ queue }: { queue: WorkQueueData }) { const total = Object.values(queue).reduce((sum, value) => sum + value, 0); const items = [ - { label: "Open Loops", value: queue.openLoops, desc: "Backend pending", icon: AlertCircle }, - { label: "Needs Review", value: queue.needsReview, desc: "Backend pending", icon: ListChecks }, - { label: "Pack Supersession", value: queue.packSupersessionNeeded, desc: "Backend pending", icon: GitMerge }, - { label: "People Map Design", value: queue.peopleMapDesignNeeded, desc: "Backend pending", icon: UserRoundSearch }, + { label: "Open Loops", value: queue.openLoops, desc: "Live open-loop rows needing operator follow-up", icon: AlertCircle }, + { label: "Needs Review", value: queue.needsReview, desc: "Live capture/review rows awaiting review", icon: ListChecks }, + { label: "Pack Supersession", value: queue.packSupersessionNeeded, desc: "Duplicate active master packs requiring attention", icon: GitMerge }, + { label: "People Map Design", value: queue.peopleMapDesignNeeded, desc: "No fabricated people-map work is created", icon: UserRoundSearch }, ]; - return

Work Queue

{total} actionable items

Mock only
{items.map((item) => { const Icon = item.icon; return
; })}
; + return

Work Queue

{total} actionable items

Live RLS Data
{items.map((item) => { const Icon = item.icon; return
; })}
; } diff --git a/components/pandora/mock-data.ts b/components/pandora/mock-data.ts index 528bcd4..aa0bab0 100644 --- a/components/pandora/mock-data.ts +++ b/components/pandora/mock-data.ts @@ -1,30 +1,21 @@ -import { BadgeCheck, Package } from "lucide-react"; -import type { MemorySpace, StatItem, SystemRow, WorkQueueData } from "./types"; +import { Package } from "lucide-react"; +import type { MemorySpace, PandoraDashboardData, StatItem, SystemRow, TimelineEventData, 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 profileSnapshot = { name: "Fixture", status: "Mock only", confidencePercent: 0, confidenceLabel: "No live data", summary: "No live data fixture profile.", lastRefreshed: "No live data", traits: ["Fixture", "Mock only"], evidence: "No live data" }; export const mockStats: StatItem[] = [ - { id: "one", title: "One", value: "0", subtitle: "Fixture", icon: Package, color: "slate", sparklineData: [0] }, + { id: "fixture", title: "Fixture", value: "No live data", subtitle: "Mock only", icon: Package, color: "slate", sparklineData: [0, 0] }, ]; export const memorySpaces: MemorySpace[] = [ - { 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" }, + { id: "real_life", label: "real_life", type: "Fixture", description: "Mock only: no live data.", memories: 0, people: 0, projects: 0, status: "Degraded", color: "emerald" }, + { id: "au", label: "au", type: "Fixture", description: "Mock only: no live data.", 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 = [ - { id: "one", icon: BadgeCheck, color: "slate", title: "One", time: "Now", desc: "Fixture" }, -] as const; - -export const coreSystems: SystemRow[] = [ - { label: "One", value: "OK", state: "healthy" }, -]; - -export const gatedSystems: SystemRow[] = [ - { label: "Two", value: "OK", state: "gated" }, -]; - +export const timelineEvents: TimelineEventData[] = [{ id: "fixture", color: "slate", title: "Fixture", time: "No live data", desc: "Mock only", namespace: "real_life" }]; +export const coreSystems: SystemRow[] = [{ label: "Fixture", value: "No live data", state: "idle" }]; +export const gatedSystems: SystemRow[] = [{ label: "Semantic retrieval", value: "Gated", state: "gated" }]; +export const fixtureDashboardData: PandoraDashboardData = { generatedAt: "No live data", operatorLabel: "Fixture", live: false, warnings: ["Mock only"], hero: { title: "Fixture dashboard", description: "Mock only: no live data.", primaryAction: "No live data", secondaryAction: "Semantic gated" }, evidence: "No live data", stats: [{ id: "fixture", title: "Fixture", value: "No live data", subtitle: "Mock only", color: "slate", sparklineData: [0, 0] }], memorySpaces, workQueue, profileSnapshot, timelineEvents, diagnostics: { coreSystems, gatedSystems, envelope: { title: "Fixture", description: "Mock only: no live data." } } }; export const navItems = ["Dashboard", "Memory Feed", "Context Packs", "Adaptive Profiles", "Open Loops", "People", "Projects", "Retrieval Tests", "Settings"] as const; export const mobileNavItems = ["Dashboard", "Memory Feed", "Queue", "Profiles", "More"]; diff --git a/lib/services/pandora-dashboard-service.ts b/lib/services/pandora-dashboard-service.ts index 222098d..d8cbb12 100644 --- a/lib/services/pandora-dashboard-service.ts +++ b/lib/services/pandora-dashboard-service.ts @@ -4,60 +4,83 @@ import type { PandoraDashboardData } from "@/components/pandora/types"; export type PandoraDashboardDbClient = { from: (table: string) => any }; type Namespace = "real_life" | "au"; type Row = Record; +type NamespaceRead = { namespace: Namespace; events: Row[]; packs: Row[]; profiles: Row[]; loops: Row[]; candidates: Row[]; review: Row[]; pruning: Row[] }; const namespaces: Namespace[] = ["real_life", "au"]; -async function rows(client: PandoraDashboardDbClient, table: string, userId: string, namespace: Namespace, limit = 100): Promise { - 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 ?? []; +async function rows(client: PandoraDashboardDbClient, table: string, userId: string, namespace: Namespace, warnings: string[], limit = 100): Promise { + try { + const result = await client.from(table).select("*").eq("user_id", userId).eq("namespace", namespace).order("created_at", { ascending: false }).limit(limit); + if (result.error) { warnings.push(`${table}/${namespace} read unavailable; showing empty state.`); return []; } + return Array.isArray(result.data) ? result.data : []; + } catch { + warnings.push(`${table}/${namespace} read unavailable; showing empty state.`); + return []; + } } -function countStatus(list: Row[], values: string[]) { - return list.filter((row) => values.includes(String(row.status ?? ""))).length; +function countStatus(list: Row[], values: string[]) { return list.filter((row) => values.includes(String(row.status ?? ""))).length; } +function activeMasters(item: NamespaceRead) { return item.packs.filter((pack) => pack.pack_type === "master" && pack.status === "active"); } +function safeArrayLength(value: unknown) { return Array.isArray(value) ? value.length : 0; } +function formatConfidence(value: unknown) { + const n = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : NaN; + if (!Number.isFinite(n)) return { percent: 0, label: "N/A" }; + const normalized = n <= 1 ? n * 100 : n; + const percent = Math.round(Math.max(0, Math.min(100, normalized))); + return { percent, label: `${percent}%` }; +} +function eventSummary(event: Row) { + const summary = typeof event.extracted_summary === "string" && event.extracted_summary.trim() ? event.extracted_summary.trim() : null; + const raw = typeof event.raw_text === "string" && event.raw_text.trim() ? event.raw_text.trim() : null; + return summary ?? raw ?? "No summary returned for this live row."; } export async function loadPandoraDashboardData(client: PandoraDashboardDbClient, input: { userId: string; operatorLabel?: string }): Promise { + const warnings: string[] = []; 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), + events: await rows(client, "memory_events", input.userId, namespace, warnings, 500), + packs: await rows(client, "memory_context_packs", input.userId, namespace, warnings, 50), + profiles: await rows(client, "memory_profiles", input.userId, namespace, warnings, 20), + loops: await rows(client, "memory_open_loops", input.userId, namespace, warnings, 100), + candidates: await rows(client, "memory_capture_candidates", input.userId, namespace, warnings, 100), + review: await rows(client, "memory_review_queue_items", input.userId, namespace, warnings, 100), + pruning: await rows(client, "memory_pruning_candidates", input.userId, namespace, warnings, 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 mastersByNamespace = data.map((item) => ({ namespace: item.namespace, masters: activeMasters(item) })); + const activeMasterCount = mastersByNamespace.reduce((sum, item) => sum + item.masters.length, 0); + const duplicates = mastersByNamespace.reduce((sum, item) => sum + Math.max(0, item.masters.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 pruningReview = data.reduce((sum, item) => sum + countStatus(item.pruning, ["open", "needs_review"]), 0); const profile = data.flatMap((item) => item.profiles).find((row) => row.status === "active" || row.status == null); + const confidence = formatConfidence(profile?.confidence); 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.", + live: warnings.length === 0, + warnings, + hero: { title: "Pandora dashboard is reading live memory state.", description: "This route renders authenticated Supabase data for memory state while semantic retrieval, embeddings, model calls, GPT Actions, and MCP remain gated.", primaryAction: "Context pack data live", secondaryAction: "Retrieval eval Gated" }, + evidence: warnings.length ? "Live dashboard read completed with safe empty states for unavailable tables." : "Live dashboard read complete from server-derived session scope.", 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: "packs", title: "Active Master Packs", value: String(activeMasterCount), subtitle: duplicates ? `${duplicates} duplicate active master pack(s)` : "Invariant OK", color: duplicates ? "amber" : "emerald", sparklineData: mastersByNamespace.map((item) => item.masters.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: "retrieval", title: "Retrieval Eval", value: "Gated", subtitle: "Semantic gated; 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" }; + const masters = activeMasters(item); + const master = masters[0]; + const description = master ? `Active master context pack: ${String(master.title ?? master.id ?? "untitled")}` : "No active master context pack returned for this namespace."; + return { id: item.namespace, label: item.namespace, type: item.namespace === "real_life" ? "Primary Space" : "Isolated AU Space", description, memories: item.events.length, people: safeArrayLength(master?.people_map), projects: safeArrayLength(master?.active_projects), 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." } }, + profileSnapshot: { name: profile?.title ?? "No active profile", status: profile ? "Live read" : "No live data", confidencePercent: confidence.percent, confidenceLabel: confidence.label, 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: String(event.id ?? `${event.namespace}-${event.created_at ?? "event"}`), title: `${event.namespace} • ${event.status ?? "unknown"}`, time: event.created_at ?? "Live read", desc: eventSummary(event), namespace: event.namespace === "au" ? "au" : "real_life", color: event.namespace === "au" ? "purple" : "emerald" })), + diagnostics: { coreSystems: [{ label: "Route exposure", value: "Auth gated", state: "healthy" }, { label: "Displayed data", value: warnings.length ? "Partial live reads" : "Live reads", state: warnings.length ? "attention" : "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: warnings.length ? "Unavailable reads were converted to warnings and empty UI state." : "Live loader completed from authenticated Supabase reads." } }, }; } diff --git a/tests/unit/pandora-dashboard-guard.test.ts b/tests/unit/pandora-dashboard-guard.test.ts new file mode 100644 index 0000000..b11921d --- /dev/null +++ b/tests/unit/pandora-dashboard-guard.test.ts @@ -0,0 +1,26 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const productionFiles = ["app/pandora/page.tsx", "components/pandora/PandoraDashboard.tsx"]; + +describe("Pandora production dashboard guards", () => { + it("does not import mock data from the route or dashboard shell", () => { + for (const file of productionFiles) expect(readFileSync(file, "utf8")).not.toMatch(/mock-data/); + }); + + it("does not accept user_id query params or props for dashboard loading", () => { + const page = readFileSync("app/pandora/page.tsx", "utf8"); + expect(page).not.toMatch(/searchParams/); + expect(page).not.toMatch(/searchParams.*user_id|searchParams.*userId|query.*user_id|query.*userId/s); + expect(page).toContain("session.session.userId"); + }); + + it("does not ship fake accuracy claims in production dashboard code", () => { + const files = ["app/pandora/page.tsx", "components/pandora/PandoraDashboard.tsx", "components/pandora/StatCard.tsx", "components/pandora/DiagnosticsCard.tsx", "lib/services/pandora-dashboard-service.ts"]; + for (const file of files) { + const source = readFileSync(file, "utf8"); + expect(source).not.toContain("94.3"); + expect(source).not.toMatch(/retrieval accuracy/i); + } + }); +}); diff --git a/tests/unit/pandora-dashboard-service.test.ts b/tests/unit/pandora-dashboard-service.test.ts index ccca8f9..b560f0b 100644 --- a/tests/unit/pandora-dashboard-service.test.ts +++ b/tests/unit/pandora-dashboard-service.test.ts @@ -4,9 +4,10 @@ import { loadPandoraDashboardData } from "@/lib/services/pandora-dashboard-servi type Row = Record; -function makeClient(store: Record) { +function makeClient(store: Record, failTables: string[] = []) { return { from(table: string) { + if (failTables.includes(table)) throw new Error("table missing"); const eqs: Record = {}; let limitN: number | undefined; let orderBy: string | undefined; @@ -58,6 +59,8 @@ describe("loadPandoraDashboardData", () => { expect(data.operatorLabel).toBe("operator@example.com"); expect(data.memorySpaces.find((space) => space.id === "real_life")?.memories).toBe(1); expect(data.memorySpaces.find((space) => space.id === "au")?.memories).toBe(1); + expect(data.memorySpaces.find((space) => space.id === "real_life")?.description).toContain("real master"); + expect(data.memorySpaces.find((space) => space.id === "au")?.description).toContain("au master"); expect(JSON.stringify(data)).not.toContain("other user"); expect(JSON.stringify(data)).not.toContain(OTHER); }); @@ -70,11 +73,41 @@ describe("loadPandoraDashboardData", () => { expect(JSON.stringify(data)).not.toContain("retrieval accuracy"); }); - it("surfaces active master duplicates as a supersession queue item", async () => { + it("surfaces active master duplicates as diagnostics and work queue attention", async () => { const store = baseStore(); store.memory_context_packs.push({ id: "rl-dup", user_id: USER, namespace: "real_life", pack_type: "master", status: "active", title: "duplicate", created_at: "2026-07-03T12:30:00Z" }); const data = await loadPandoraDashboardData(makeClient(store), { userId: USER }); expect(data.workQueue.packSupersessionNeeded).toBe(1); expect(data.diagnostics.coreSystems.find((row) => row.label === "Master-pack invariant")?.state).toBe("attention"); + expect(data.stats.find((stat) => stat.id === "packs")?.subtitle).toContain("duplicate"); + }); + + it("returns warnings and safe empty dashboard when table reads fail", async () => { + const data = await loadPandoraDashboardData(makeClient({}, ["memory_events", "memory_context_packs", "memory_profiles", "memory_open_loops", "memory_capture_candidates", "memory_review_queue_items", "memory_pruning_candidates"]), { userId: USER }); + expect(data.warnings.length).toBeGreaterThan(0); + expect(data.memorySpaces.find((space) => space.id === "real_life")?.memories).toBe(0); + expect(data.timelineEvents).toEqual([]); + expect(data.diagnostics.coreSystems.find((row) => row.label === "Displayed data")?.state).toBe("attention"); + }); + + it.each([ + [null, "N/A", 0], + [0.77, "77%", 77], + [77, "77%", 77], + ["invalid", "N/A", 0], + ])("formats profile confidence safely for %s", async (confidence, label, percent) => { + const store = baseStore(); + store.memory_profiles[0].confidence = confidence; + const data = await loadPandoraDashboardData(makeClient(store), { userId: USER }); + expect(data.profileSnapshot.confidenceLabel).toBe(label); + expect(data.profileSnapshot.confidencePercent).toBe(percent); + }); + + it("handles null timeline summaries safely", async () => { + const store = baseStore(); + store.memory_events[0].raw_text = null; + store.memory_events[0].extracted_summary = null; + const data = await loadPandoraDashboardData(makeClient(store), { userId: USER }); + expect(data.timelineEvents.find((event) => event.id === "rl-1")?.desc).toBe("No summary returned for this live row."); }); }); diff --git a/tests/unit/pandora-dashboard-ui.test.tsx b/tests/unit/pandora-dashboard-ui.test.tsx new file mode 100644 index 0000000..dd66fdb --- /dev/null +++ b/tests/unit/pandora-dashboard-ui.test.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; +import { PandoraDashboard } from "@/components/pandora/PandoraDashboard"; +import type { PandoraDashboardData } from "@/components/pandora/types"; + +const data: PandoraDashboardData = { + generatedAt: "2026-07-03T00:00:00Z", + operatorLabel: "operator@example.com", + live: true, + warnings: ["memory_profiles/au read unavailable; showing empty state."], + hero: { title: "Live title", description: "Live description", primaryAction: "Context pack data live", secondaryAction: "Retrieval eval Gated" }, + evidence: "Live evidence", + stats: [{ id: "events", title: "Memory Events", value: "2", subtitle: "Authenticated rows", color: "indigo", sparklineData: [1, 1] }, { id: "retrieval", title: "Retrieval Eval", value: "Gated", subtitle: "Semantic gated; no accuracy claim", color: "amber", sparklineData: [0] }], + memorySpaces: [{ id: "real_life", label: "real_life", type: "Primary Space", description: "Active master context pack: real", memories: 1, people: 2, projects: 3, status: "Active", color: "emerald" }, { id: "au", label: "au", type: "Isolated AU Space", description: "Active master context pack: au", memories: 1, people: 0, projects: 0, status: "Active", color: "purple" }], + workQueue: { needsReview: 4, openLoops: 5, stalePacks: 0, failedTests: 0, profileRefreshDue: 0, packSupersessionNeeded: 1, peopleMapDesignNeeded: 0 }, + profileSnapshot: { name: "Operator", status: "Live read", confidencePercent: 77, confidenceLabel: "77%", summary: "Live profile", lastRefreshed: "now", traits: ["Authenticated"], evidence: "profile row" }, + timelineEvents: [{ id: "event", title: "real_life • captured", time: "now", desc: "Live event summary", namespace: "real_life", color: "emerald" }], + diagnostics: { coreSystems: [{ label: "Displayed data", value: "Live reads", state: "healthy" }], gatedSystems: [{ label: "Semantic retrieval", value: "Gated Off", state: "gated" }], envelope: { title: "Dashboard Truth Envelope", description: "Live loader completed" } }, +}; + +describe("PandoraDashboard", () => { + it("renders live values passed by props with gated semantic copy and warnings", () => { + const html = renderToStaticMarkup(); + expect(html).toContain("Live RLS Data"); + expect(html).toContain("Semantic Gated"); + expect(html).toContain("Live title"); + expect(html).toContain("Live event summary"); + expect(html).toContain("operator@example.com"); + expect(html).toContain("memory_profiles/au read unavailable"); + expect(html).toContain("Gated Off"); + }); +});