Skip to content
Open
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/promotion-requests/[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 { archivePromotionRequest, type PromotionRequestDbClient } from "@/lib/services/pandora-promotion-request-service";
export const dynamic = "force-dynamic";
export async function POST(request: NextRequest, context: { params: Promise<{ id: string }> }) { let body: unknown; try { body = await request.json(); } catch { body = {}; } 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 }); const input = body && typeof body === "object" ? body as Record<string, unknown> : {}; try { const { id } = await context.params; const supabase = await createSupabaseServerClient(); const promotionRequest = await archivePromotionRequest(supabase as unknown as PromotionRequestDbClient, { userId: session.session.userId, promotionRequestId: id, reason: typeof input.reason === "string" ? input.reason : undefined }); return NextResponse.json({ ok:true, promotionRequest, no_promotion_performed:true }); } catch(e) { return NextResponse.json({ ok:false, error: e instanceof Error ? e.message : "Unable to archive" }, { status: 400 }); } }
6 changes: 6 additions & 0 deletions app/api/pandora/promotion-requests/[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 { updatePromotionRequestReview, type PromotionRequestDbClient } from "@/lib/services/pandora-promotion-request-service";
export const dynamic = "force-dynamic";
export async function POST(request: NextRequest, context: { params: Promise<{ id: string }> }) { let body: unknown; try { body = await request.json(); } catch { body = {}; } 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 }); const input = body && typeof body === "object" ? body as Record<string, unknown> : {}; try { const { id } = await context.params; const supabase = await createSupabaseServerClient(); const promotionRequest = await updatePromotionRequestReview(supabase as unknown as PromotionRequestDbClient, { userId: session.session.userId, promotionRequestId: id, reviewerNotes: String(input.reviewer_notes ?? input.reviewerNotes ?? ""), reviewerDecision: String(input.reviewer_decision ?? input.reviewerDecision ?? "") }); return NextResponse.json({ ok:true, promotionRequest, no_promotion_performed:true }); } catch(e) { return NextResponse.json({ ok:false, error: e instanceof Error ? e.message : "Unable to review" }, { status: 400 }); } }
6 changes: 6 additions & 0 deletions app/api/pandora/promotion-requests/[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 { getPromotionRequest, type PromotionRequestDbClient } from "@/lib/services/pandora-promotion-request-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 promotionRequest = await getPromotionRequest(supabase as unknown as PromotionRequestDbClient, { userId: session.session.userId, promotionRequestId: id }); return NextResponse.json({ ok:true, promotionRequest }); } 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/promotion-requests/[id]/submit/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 { submitPromotionRequest, type PromotionRequestDbClient } from "@/lib/services/pandora-promotion-request-service";
export const dynamic = "force-dynamic";
export async function POST(request: NextRequest, context: { params: Promise<{ id: string }> }) { let body: unknown; try { body = await request.json(); } catch { body = {}; } 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 promotionRequest = await submitPromotionRequest(supabase as unknown as PromotionRequestDbClient, { userId: session.session.userId, promotionRequestId: id }); return NextResponse.json({ ok:true, promotionRequest, no_promotion_performed:true }); } catch(e) { return NextResponse.json({ ok:false, error: e instanceof Error ? e.message : "Unable to submit" }, { status: 400 }); } }
6 changes: 6 additions & 0 deletions app/api/pandora/promotion-requests/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 { listPromotionRequests, type PromotionRequestDbClient } from "@/lib/services/pandora-promotion-request-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 { searchParams } = new URL(request.url); const supabase = await createSupabaseServerClient(); const requests = await listPromotionRequests(supabase as unknown as PromotionRequestDbClient, { userId: session.session.userId, namespace: searchParams.get("namespace") ?? undefined, status: searchParams.get("status") ?? undefined, limit: 25 }); return NextResponse.json({ ok:true, requests }); }
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 { createOrRefreshPromotionRequest, type PromotionRequestDbClient } from "@/lib/services/pandora-promotion-request-service";
export const dynamic = "force-dynamic";
export async function POST(request: NextRequest, context: { params: Promise<{ id: string }> }) { let body: unknown; try { body = await request.json(); } catch { body = {}; } 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 promotionRequest = await createOrRefreshPromotionRequest(supabase as unknown as PromotionRequestDbClient, { userId: session.session.userId, preflightId: id }); return NextResponse.json({ ok:true, promotionRequest, promotion_execution_available:false, no_core_memory_mutation_performed:true, no_promotion_performed:true }); } catch(e) { return NextResponse.json({ ok:false, error: e instanceof Error ? e.message : "Unable to create promotion request" }, { status: 400 }); } }
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><option value="prepare_shadow_context_pack">prepare_shadow_context_pack</option><option value="prepare_shadow_pack_preflight">prepare_shadow_pack_preflight</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. For prepare_shadow_pack_preflight, payload.shadow_pack_id is required by the API/service; use a direct action payload only when the target shadow pack is known. No promotion. 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><option value="prepare_shadow_pack_preflight">prepare_shadow_pack_preflight</option><option value="prepare_promotion_request">prepare_promotion_request</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. For prepare_shadow_pack_preflight, payload.shadow_pack_id is required by the API/service. For prepare_promotion_request, payload.preflight_id is required by the API/service; use a direct action payload only when the target shadow pack is known. 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 @@ -14,6 +14,7 @@ import { VerificationConsoleCard } from "./VerificationConsoleCard";
import { OperatorActionCenterCard } from "./OperatorActionCenterCard";
import { ShadowContextPackLabCard } from "./ShadowContextPackLabCard";
import { ShadowPackPreflightCard } from "./ShadowPackPreflightCard";
import { PromotionRequestBoardCard } from "./PromotionRequestBoardCard";
import { WorkQueueCard } from "./WorkQueueCard";
import type { PandoraDashboardData, StatItem } from "./types";
import { useState } from "react";
Expand Down Expand Up @@ -49,6 +50,7 @@ export function PandoraDashboard({ dashboardData }: { dashboardData: PandoraDash
<OperatorActionCenterCard data={dashboardData.operatorActions} />
<ShadowContextPackLabCard data={dashboardData.shadowContextPackLab} />
<ShadowPackPreflightCard data={dashboardData.shadowPackPreflights} />
<PromotionRequestBoardCard data={dashboardData.promotionRequests} />
<div className="pd-dashboard-grid">
<div className="pd-dashboard-col pd-dashboard-col-wide">
<MemorySpacesCard spaces={dashboardData.memorySpaces} />
Expand Down
2 changes: 2 additions & 0 deletions components/pandora/PromotionPlanViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import type { PromotionPlanSummary } from "./types";
export function PromotionPlanViewer({ plan }: { plan: PromotionPlanSummary }) { return <div className="pd-mini"><strong>Promotion plan</strong><p>Operation: {plan.intended_operation}</p><p>Execution available: {String(plan.execution_available)}</p><ol>{(plan.steps ?? []).map((s)=><li key={s}>{s}</li>)}</ol><p className="pd-muted">Required checks: {(plan.required_checks ?? []).join(", ") || "none"}</p>{plan.blockers?.length ? <div className="pd-warning-list">{plan.blockers.map((b)=><p key={b}>⛔ {b}</p>)}</div> : null}</div>; }
4 changes: 4 additions & 0 deletions components/pandora/PromotionRequestBoardCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { PromotionRequestBoardData } from "./types";
import { PromotionRequestList } from "./PromotionRequestList";
const empty: PromotionRequestBoardData = { requests: [], warnings: [], countsByStatus: { draft:0, submitted:0, approved:0, blocked:0, archived:0 }, riskDistribution: { low:0, medium:0, high:0, blocked:0 } };
export function PromotionRequestBoardCard({ data }: { data?: PromotionRequestBoardData }) { const d=data??empty; return <section className="pd-card"><div className="pd-section-head"><div><p className="pd-label">Promotion Request Board</p><h3>Human-approved promotion planning</h3><p>Promotion request only — execution unavailable</p></div><span className="pd-pill pd-pill-amber">No promotion execution</span></div><div className="pd-mini-grid">{Object.entries(d.countsByStatus).map(([s,c])=><div className="pd-mini" key={s}><strong>{c}</strong><span>{s}</span></div>)}</div><div className="pd-mini-grid">{Object.entries(d.riskDistribution).map(([s,c])=><div className="pd-mini" key={s}><strong>{c}</strong><span>{s} risk</span></div>)}</div><PromotionRequestList requests={d.requests}/>{d.warnings.length ? <div className="pd-warning-list">{d.warnings.map((w)=><p key={w}>⚠ {w}</p>)}</div> : null}</section>; }
4 changes: 4 additions & 0 deletions components/pandora/PromotionRequestControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"use client";
import type { PromotionRequestSummary } from "./types";
async function post(url:string, body?:Record<string,unknown>) { await fetch(url,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(body??{})}); window.location.reload(); }
export function PromotionRequestControls({ request }: { request: PromotionRequestSummary }) { return <div className="pd-actions"><button className="button-link" type="button" onClick={()=>post(`/api/pandora/promotion-requests/${request.id}/submit`)}>Submit request</button><button className="button-link" type="button" onClick={()=>post(`/api/pandora/promotion-requests/${request.id}/review`,{reviewer_decision:"needs_changes",reviewer_notes:request.reviewer_notes})}>Mark needs changes</button><button className="button-link" type="button" onClick={()=>post(`/api/pandora/promotion-requests/${request.id}/review`,{reviewer_decision:"approved",reviewer_notes:request.reviewer_notes})}>Approve request (does NOT execute promotion)</button><button className="button-link" type="button" onClick={()=>post(`/api/pandora/promotion-requests/${request.id}/review`,{reviewer_decision:"blocked",reviewer_notes:request.reviewer_notes})}>Block request</button><button className="button-link" type="button" onClick={()=>post(`/api/pandora/promotion-requests/${request.id}/archive`,{reason:"Archived from board"})}>Archive</button></div>; }
2 changes: 2 additions & 0 deletions components/pandora/PromotionRequestCreateButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"use client";
export function PromotionRequestCreateButton({ preflightId, disabled }: { preflightId: string; disabled?: boolean }) { async function create(){ await fetch(`/api/pandora/shadow-pack-preflights/${preflightId}/promotion-request`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({})}); window.location.reload(); } return <button className="button-link" type="button" disabled={disabled} onClick={create}>Create/refresh promotion request</button>; }
7 changes: 7 additions & 0 deletions components/pandora/PromotionRequestList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { PromotionRequestSummary } from "./types";
import { PromotionPlanViewer } from "./PromotionPlanViewer";
import { RollbackPlanViewer } from "./RollbackPlanViewer";
import { PromotionRequestRiskSnapshot } from "./PromotionRequestRiskSnapshot";
import { PromotionRequestReviewForm } from "./PromotionRequestReviewForm";
import { PromotionRequestControls } from "./PromotionRequestControls";
export function PromotionRequestList({ requests }: { requests: PromotionRequestSummary[] }) { if(!requests.length) return <p className="pd-muted">No promotion requests yet. Create one from an approved preflight.</p>; return <div className="pd-action-list">{requests.map((r)=><article className="pd-action-item" key={r.id}><div><p className="pd-label">{r.namespace} • {r.status}</p><h4>{r.title}</h4><p>{r.summary}</p><p className="pd-muted">Shadow {r.shadow_pack_id} • Preflight {r.preflight_id} • Active master {r.active_master_pack_id ?? "none"}</p><p>Reviewer decision: {r.reviewer_decision ?? "not reviewed"}</p></div><PromotionRequestRiskSnapshot risk={r.risk_snapshot}/><div className="pd-mini"><strong>Diff snapshot</strong><p>Added {r.diff_snapshot.added_keys?.length ?? 0}, removed {r.diff_snapshot.removed_keys?.length ?? 0}, changed {r.diff_snapshot.changed_keys?.length ?? 0}</p><p>Has active master: {String(r.diff_snapshot.has_active_master)}</p></div><PromotionPlanViewer plan={r.promotion_plan}/><RollbackPlanViewer plan={r.rollback_plan}/>{r.warnings.length ? <div className="pd-warning-list">{r.warnings.map((w)=><p key={w}>⚠ {w}</p>)}</div> : null}<p className="pd-muted">Reviewer notes: {r.reviewer_notes || "none"}</p><PromotionRequestReviewForm request={r}/><PromotionRequestControls request={r}/></article>)}</div>; }
4 changes: 4 additions & 0 deletions components/pandora/PromotionRequestReviewForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"use client";
import { useState } from "react";
import type { PromotionRequestSummary } from "./types";
export function PromotionRequestReviewForm({ request }: { request: PromotionRequestSummary }) { const [notes,setNotes]=useState(request.reviewer_notes); const [decision,setDecision]=useState<string>(request.reviewer_decision ?? "needs_changes"); async function save(){ await fetch(`/api/pandora/promotion-requests/${request.id}/review`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({reviewer_notes:notes,reviewer_decision:decision})}); window.location.reload(); } return <div className="pd-composer"><label className="pd-mini"><span>Reviewer notes</span><textarea value={notes} onChange={(e)=>setNotes(e.target.value)} /></label><label className="pd-mini"><span>Decision</span><select value={decision} onChange={(e)=>setDecision(e.target.value)}><option value="needs_changes">needs_changes</option><option value="approved">approved (no execution)</option><option value="blocked">blocked</option></select></label><button className="button-link" type="button" onClick={save}>Save review</button></div>; }
Loading
Loading