Skip to content
Merged
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
6 changes: 6 additions & 0 deletions app/api/pandora/shadow-context-packs/[id]/archive/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { NextResponse, type NextRequest } from "next/server";
import { assertNoClientUserIdOverride, resolvePandoraServerSession } from "@/lib/auth/pandora-server-session-resolver";
import { createSupabaseServerClient } from "@/lib/supabase/server";
import { updateShadowContextPackStatus, type ShadowContextPackDbClient } from "@/lib/services/pandora-shadow-context-pack-service";
export const dynamic = "force-dynamic";
export async function POST(request: NextRequest, context: { params: Promise<{ id: string }> }) { const body = await request.json().catch(() => ({})); const rejected = await assertNoClientUserIdOverride(request, body); if (rejected) return NextResponse.json({ ok: false, blockers: rejected.blockers }, { status: 400 }); const session = await resolvePandoraServerSession({ request }); if (!session.ok) return NextResponse.json({ ok: false, blockers: session.blockers }, { status: 401 }); try { const { id } = await context.params; const supabase = await createSupabaseServerClient(); const pack = await updateShadowContextPackStatus(supabase as unknown as ShadowContextPackDbClient, { userId: session.session.userId, shadowPackId: id, status: "archived", reason: typeof body.reason === "string" ? body.reason : undefined }); return NextResponse.json({ ok: true, pack, no_core_memory_mutation_performed: true, no_promotion_performed: true }); } catch (e) { return NextResponse.json({ ok: false, error: e instanceof Error ? e.message : "Shadow pack update failed" }, { status: 400 }); } }
6 changes: 6 additions & 0 deletions app/api/pandora/shadow-context-packs/[id]/reject/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { NextResponse, type NextRequest } from "next/server";
import { assertNoClientUserIdOverride, resolvePandoraServerSession } from "@/lib/auth/pandora-server-session-resolver";
import { createSupabaseServerClient } from "@/lib/supabase/server";
import { updateShadowContextPackStatus, type ShadowContextPackDbClient } from "@/lib/services/pandora-shadow-context-pack-service";
export const dynamic = "force-dynamic";
export async function POST(request: NextRequest, context: { params: Promise<{ id: string }> }) { const body = await request.json().catch(() => ({})); const rejected = await assertNoClientUserIdOverride(request, body); if (rejected) return NextResponse.json({ ok: false, blockers: rejected.blockers }, { status: 400 }); const session = await resolvePandoraServerSession({ request }); if (!session.ok) return NextResponse.json({ ok: false, blockers: session.blockers }, { status: 401 }); try { const { id } = await context.params; const supabase = await createSupabaseServerClient(); const pack = await updateShadowContextPackStatus(supabase as unknown as ShadowContextPackDbClient, { userId: session.session.userId, shadowPackId: id, status: "rejected", reason: typeof body.reason === "string" ? body.reason : undefined }); return NextResponse.json({ ok: true, pack, no_core_memory_mutation_performed: true, no_promotion_performed: true }); } catch (e) { return NextResponse.json({ ok: false, error: e instanceof Error ? e.message : "Shadow pack update failed" }, { status: 400 }); } }
6 changes: 6 additions & 0 deletions app/api/pandora/shadow-context-packs/[id]/review/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { NextResponse, type NextRequest } from "next/server";
import { assertNoClientUserIdOverride, resolvePandoraServerSession } from "@/lib/auth/pandora-server-session-resolver";
import { createSupabaseServerClient } from "@/lib/supabase/server";
import { updateShadowContextPackStatus, type ShadowContextPackDbClient } from "@/lib/services/pandora-shadow-context-pack-service";
export const dynamic = "force-dynamic";
export async function POST(request: NextRequest, context: { params: Promise<{ id: string }> }) { const body = await request.json().catch(() => ({})); const rejected = await assertNoClientUserIdOverride(request, body); if (rejected) return NextResponse.json({ ok: false, blockers: rejected.blockers }, { status: 400 }); const session = await resolvePandoraServerSession({ request }); if (!session.ok) return NextResponse.json({ ok: false, blockers: session.blockers }, { status: 401 }); try { const { id } = await context.params; const supabase = await createSupabaseServerClient(); const pack = await updateShadowContextPackStatus(supabase as unknown as ShadowContextPackDbClient, { userId: session.session.userId, shadowPackId: id, status: "reviewed", reason: typeof body.reason === "string" ? body.reason : undefined }); return NextResponse.json({ ok: true, pack, no_core_memory_mutation_performed: true, no_promotion_performed: true }); } catch (e) { return NextResponse.json({ ok: false, error: e instanceof Error ? e.message : "Shadow pack update failed" }, { status: 400 }); } }
6 changes: 6 additions & 0 deletions app/api/pandora/shadow-context-packs/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { NextResponse, type NextRequest } from "next/server";
import { assertNoClientUserIdOverride, resolvePandoraServerSession } from "@/lib/auth/pandora-server-session-resolver";
import { createSupabaseServerClient } from "@/lib/supabase/server";
import { getShadowContextPack, type ShadowContextPackDbClient } from "@/lib/services/pandora-shadow-context-pack-service";
export const dynamic = "force-dynamic";
export async function GET(request: NextRequest, context: { params: Promise<{ id: string }> }) { const rejected = await assertNoClientUserIdOverride(request); if (rejected) return NextResponse.json({ ok: false, blockers: rejected.blockers }, { status: 400 }); const session = await resolvePandoraServerSession({ request }); if (!session.ok) return NextResponse.json({ ok: false, blockers: session.blockers }, { status: 401 }); try { const { id } = await context.params; const supabase = await createSupabaseServerClient(); const pack = await getShadowContextPack(supabase as unknown as ShadowContextPackDbClient, { userId: session.session.userId, shadowPackId: id }); return NextResponse.json({ ok: true, pack }); } catch (e) { return NextResponse.json({ ok: false, error: e instanceof Error ? e.message : "Not found" }, { status: 404 }); } }
6 changes: 6 additions & 0 deletions app/api/pandora/shadow-context-packs/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { NextResponse, type NextRequest } from "next/server";
import { assertNoClientUserIdOverride, resolvePandoraServerSession } from "@/lib/auth/pandora-server-session-resolver";
import { createSupabaseServerClient } from "@/lib/supabase/server";
import { listShadowContextPacks, type ShadowContextPackDbClient } from "@/lib/services/pandora-shadow-context-pack-service";
export const dynamic = "force-dynamic";
export async function GET(request: NextRequest) { const rejected = await assertNoClientUserIdOverride(request); if (rejected) return NextResponse.json({ ok: false, blockers: rejected.blockers }, { status: 400 }); const session = await resolvePandoraServerSession({ request }); if (!session.ok) return NextResponse.json({ ok: false, blockers: session.blockers }, { status: 401 }); const url = new URL(request.url); const supabase = await createSupabaseServerClient(); const packs = await listShadowContextPacks(supabase as unknown as ShadowContextPackDbClient, { userId: session.session.userId, namespace: url.searchParams.get("namespace") ?? undefined, status: url.searchParams.get("status") ?? undefined, limit: 25 }); return NextResponse.json({ ok: true, packs }); }
2 changes: 1 addition & 1 deletion components/pandora/OperatorActionComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ export function OperatorActionComposer() {
const [actionType, setActionType] = useState("verify_namespace_invariants");
const [namespace, setNamespace] = useState("real_life");
async function prepare() { await fetch("/api/pandora/operator-actions", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action_type: actionType, namespace, mode: "dry_run", payload: { source: "operator_action_center" } }) }); window.location.reload(); }
return <div className="pd-composer"><div className="pd-mini-grid"><label className="pd-mini"><span>Action type</span><select value={actionType} onChange={(e) => setActionType(e.target.value)}><option value="verify_namespace_invariants">verify_namespace_invariants</option><option value="verify_pack_supersession">verify_pack_supersession</option><option value="check_retrieval_eval_status">check_retrieval_eval_status</option><option value="refresh_dashboard_snapshot">refresh_dashboard_snapshot</option><option value="prepare_distill_smoke_plan">prepare_distill_smoke_plan</option></select></label><label className="pd-mini"><span>Namespace</span><select value={namespace} onChange={(e) => setNamespace(e.target.value)}><option value="real_life">real_life</option><option value="au">au</option></select></label></div><button className="button-link button-link--primary" type="button" onClick={prepare}>Prepare dry-run</button><p className="pd-muted">Only dry-run or queued-only proposals are available. No core memory mutation is available from this card.</p></div>;
return <div className="pd-composer"><div className="pd-mini-grid"><label className="pd-mini"><span>Action type</span><select value={actionType} onChange={(e) => setActionType(e.target.value)}><option value="verify_namespace_invariants">verify_namespace_invariants</option><option value="verify_pack_supersession">verify_pack_supersession</option><option value="check_retrieval_eval_status">check_retrieval_eval_status</option><option value="refresh_dashboard_snapshot">refresh_dashboard_snapshot</option><option value="prepare_distill_smoke_plan">prepare_distill_smoke_plan</option><option value="prepare_shadow_context_pack">prepare_shadow_context_pack</option></select></label><label className="pd-mini"><span>Namespace</span><select value={namespace} onChange={(e) => setNamespace(e.target.value)}><option value="real_life">real_life</option><option value="au">au</option></select></label></div><button className="button-link button-link--primary" type="button" onClick={prepare}>Prepare dry-run</button><p className="pd-muted">For prepare_shadow_context_pack: Creates shadow candidate only after approval/execution. No promotion. Only dry-run or queued-only proposals are available. No core memory mutation is available from this card.</p></div>;
}
2 changes: 2 additions & 0 deletions components/pandora/PandoraDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { StatCard } from "./StatCard";
import { TopBar } from "./TopBar";
import { VerificationConsoleCard } from "./VerificationConsoleCard";
import { OperatorActionCenterCard } from "./OperatorActionCenterCard";
import { ShadowContextPackLabCard } from "./ShadowContextPackLabCard";
import { WorkQueueCard } from "./WorkQueueCard";
import type { PandoraDashboardData, StatItem } from "./types";
import { useState } from "react";
Expand Down Expand Up @@ -45,6 +46,7 @@ export function PandoraDashboard({ dashboardData }: { dashboardData: PandoraDash
</section>
<VerificationConsoleCard verification={dashboardData.verification} />
<OperatorActionCenterCard data={dashboardData.operatorActions} />
<ShadowContextPackLabCard data={dashboardData.shadowContextPackLab} />
<div className="pd-dashboard-grid">
<div className="pd-dashboard-col pd-dashboard-col-wide">
<MemorySpacesCard spaces={dashboardData.memorySpaces} />
Expand Down
4 changes: 4 additions & 0 deletions components/pandora/ShadowContextPackControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"use client";
import type { ShadowContextPackSummary } from "./types";
async function post(id: string, verb: "review" | "reject" | "archive") { await fetch(`/api/pandora/shadow-context-packs/${id}/${verb}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) }); window.location.reload(); }
export function ShadowContextPackControls({ pack }: { pack: ShadowContextPackSummary }) { return <div className="pd-action-controls"><button className="button-link button-link--primary" type="button" onClick={() => post(pack.id, "review")}>Mark reviewed</button><button className="button-link" type="button" onClick={() => post(pack.id, "reject")}>Reject</button><button className="button-link" type="button" onClick={() => post(pack.id, "archive")}>Archive</button></div>; }
4 changes: 4 additions & 0 deletions components/pandora/ShadowContextPackDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { ShadowContextPackSummary } from "./types";
import { ShadowContextPackEvidence } from "./ShadowContextPackEvidence";
import { ShadowContextPackControls } from "./ShadowContextPackControls";
export function ShadowContextPackDetail({ pack }: { pack: ShadowContextPackSummary }) { const payload = pack.candidate_payload; return <article className="pd-action-card"><div className="pd-action-head"><div><h4>{pack.title}</h4><p>{pack.summary}</p></div><span className="pd-pill pd-pill-amber">{pack.status}</span></div><div className="pd-mini-grid"><div className="pd-mini"><strong>{pack.namespace}</strong><span>namespace</span></div><div className="pd-mini"><strong>{String(payload.source_event_count ?? 0)}</strong><span>source events</span></div><div className="pd-mini"><strong>{String(payload.active_master_pack_id ?? "none")}</strong><span>active master pack</span></div><div className="pd-mini"><strong>{String(payload.latest_event_at ?? "none")}</strong><span>latest event</span></div></div><ShadowContextPackEvidence pack={pack} /><details><summary>Candidate payload</summary><pre>{JSON.stringify(payload, null, 2)}</pre></details><ShadowContextPackControls pack={pack} /></article>; }
2 changes: 2 additions & 0 deletions components/pandora/ShadowContextPackEvidence.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import type { ShadowContextPackSummary } from "./types";
export function ShadowContextPackEvidence({ pack }: { pack: ShadowContextPackSummary }) { return <div className="pd-warning-list"><p><strong>Evidence summary:</strong> {String(pack.candidate_payload.evidence_summary ?? pack.summary)}</p>{pack.warnings.map((w) => <p key={w}>⚠ {w}</p>)}</div>; }
4 changes: 4 additions & 0 deletions components/pandora/ShadowContextPackLabCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { ShadowContextPackLabData } from "./types";
import { ShadowContextPackList } from "./ShadowContextPackList";
const empty: ShadowContextPackLabData = { packs: [], warnings: [], countsByStatus: { draft: 0, ready_for_review: 0, reviewed: 0, rejected: 0, archived: 0 } };
export function ShadowContextPackLabCard({ data }: { data?: ShadowContextPackLabData }) { const lab = data ?? empty; return <section className="pd-card"><div className="pd-section-head"><div><p className="pd-label">Shadow Context Pack Lab</p><h3>Staged candidates for operator review</h3><p>Shadow only — no production memory mutation</p></div><span className="pd-pill pd-pill-slate">Promotion unavailable</span></div><div className="pd-mini-grid">{Object.entries(lab.countsByStatus).map(([status, count]) => <div className="pd-mini" key={status}><strong>{count}</strong><span>{status}</span></div>)}</div><ShadowContextPackList packs={lab.packs} />{lab.warnings.length ? <div className="pd-warning-list">{lab.warnings.map((w) => <p key={w}>⚠ {w}</p>)}</div> : null}</section>; }
3 changes: 3 additions & 0 deletions components/pandora/ShadowContextPackList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { ShadowContextPackSummary } from "./types";
import { ShadowContextPackDetail } from "./ShadowContextPackDetail";
export function ShadowContextPackList({ packs }: { packs: ShadowContextPackSummary[] }) { if (!packs.length) return <p className="pd-muted">No shadow context-pack candidates staged yet.</p>; return <div className="pd-action-list">{packs.map((pack) => <ShadowContextPackDetail key={pack.id} pack={pack} />)}</div>; }
2 changes: 1 addition & 1 deletion components/pandora/mock-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ export const timelineEvents: TimelineEventData[] = [{ id: "fixture", color: "sla
export const coreSystems: SystemRow[] = [{ label: "Fixture", value: "No live data", state: "idle" }];
export const gatedSystems: SystemRow[] = [{ label: "Semantic retrieval", value: "Gated", state: "gated" }];
const verification = { generatedAt: "No live data", status: "not_run" as const, namespaces: [], packSupersession: { status: "not_run" as const, namespaces: [], warnings: ["Mock only"] }, retrievalEval: { status: "not_run" as const, source: "fixture", latestRunId: null, latestRunAt: null, resultLabel: "Not run", realResultAvailable: false, warnings: ["Mock only"] }, auditEvidence: [], smokeEvidence: { status: "not_run" as const, latest: null, warnings: ["Mock only"] }, invariantStatus: { exactlyOneActiveMasterPerNamespace: "not_run" as const, noCrossNamespacePackMixing: "not_run" as const, noDuplicateActiveMaster: "not_run" as const, retrievalEvalHasNoFabricatedScore: "pass" as const, smokeEvidence: "not_run" as const }, warnings: ["Mock only"] };
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." } }, verification, operatorActions: { actions: [], warnings: [], countsByStatus: { proposed: 0, dry_ran: 0, approved: 0, executing: 0, completed: 0, blocked: 0, failed: 0, cancelled: 0 } } };
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." } }, verification, shadowContextPackLab: { packs: [], warnings: [], countsByStatus: { draft: 0, ready_for_review: 0, reviewed: 0, rejected: 0, archived: 0 } }, operatorActions: { actions: [], warnings: [], countsByStatus: { proposed: 0, dry_ran: 0, approved: 0, executing: 0, completed: 0, blocked: 0, failed: 0, cancelled: 0 } } };

8 changes: 7 additions & 1 deletion components/pandora/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,13 @@ export type PandoraVerificationData = {
};


export type ShadowContextPackStatus = "draft" | "ready_for_review" | "reviewed" | "rejected" | "archived";
export type ShadowContextPackEvent = { id: string; shadow_pack_id: string; user_id: string; event_type: string; message: string; metadata: Record<string, unknown>; created_at: string };
export type ShadowContextPackSummary = { id: string; request_id: string; operator_action_id?: string | null; namespace: PandoraNamespace; pack_type: string; status: ShadowContextPackStatus; title: string; summary: string; source_window: Record<string, unknown>; candidate_payload: Record<string, unknown>; evidence: Record<string, unknown>; warnings: string[]; created_at: string; updated_at: string; reviewed_at?: string | null; rejected_at?: string | null; archived_at?: string | null; event_preview?: ShadowContextPackEvent[] };
export type ShadowContextPackLabData = { packs: ShadowContextPackSummary[]; warnings: string[]; countsByStatus: Record<ShadowContextPackStatus, number> };

export type OperatorActionStatus = "proposed" | "dry_ran" | "approved" | "executing" | "completed" | "blocked" | "failed" | "cancelled";
export type OperatorActionType = "verify_namespace_invariants" | "verify_pack_supersession" | "check_retrieval_eval_status" | "refresh_dashboard_snapshot" | "prepare_distill_smoke_plan";
export type OperatorActionType = "verify_namespace_invariants" | "verify_pack_supersession" | "check_retrieval_eval_status" | "refresh_dashboard_snapshot" | "prepare_distill_smoke_plan" | "prepare_shadow_context_pack";
export type OperatorActionMode = "dry_run" | "queued_only";

export type OperatorActionEventSummary = { id: string; action_id: string; user_id: string; event_type: string; message: string; metadata: Record<string, unknown>; created_at: string; };
Expand Down Expand Up @@ -202,4 +207,5 @@ export type PandoraDashboardData = {
};
verification: PandoraVerificationData;
operatorActions: OperatorActionCenterData;
shadowContextPackLab: ShadowContextPackLabData;
};
Loading
Loading