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]/preflight/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 { createOrRefreshShadowPackPreflight, type ShadowPackPreflightDbClient } from "@/lib/services/pandora-shadow-pack-preflight-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 preflight = await createOrRefreshShadowPackPreflight(supabase as unknown as ShadowPackPreflightDbClient, { userId: session.session.userId, shadowPackId: id, requestId: typeof body.request_id === "string" ? body.request_id : undefined }); return NextResponse.json({ ok:true, preflight, no_core_memory_mutation_performed:true, no_promotion_performed:true }); } catch(e) { return NextResponse.json({ ok:false, error: e instanceof Error ? e.message : "Preflight failed" }, { status: 400 }); } }
6 changes: 6 additions & 0 deletions app/api/pandora/shadow-pack-preflights/[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 { archiveShadowPackPreflight, type ShadowPackPreflightDbClient } from "@/lib/services/pandora-shadow-pack-preflight-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 preflight = await archiveShadowPackPreflight(supabase as unknown as ShadowPackPreflightDbClient, { userId: session.session.userId, preflightId: id, reason: typeof body.reason === "string" ? body.reason : undefined }); return NextResponse.json({ ok:true, preflight, no_core_memory_mutation_performed:true, no_promotion_performed:true }); } catch(e) { return NextResponse.json({ ok:false, error: e instanceof Error ? e.message : "Archive failed" }, { status: 400 }); } }
6 changes: 6 additions & 0 deletions app/api/pandora/shadow-pack-preflights/[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 { updateShadowPackPreflightReview, type ShadowPackPreflightDbClient } from "@/lib/services/pandora-shadow-pack-preflight-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 input = body && typeof body === "object" ? body as Record<string, unknown> : {}; const supabase = await createSupabaseServerClient(); const preflight = await updateShadowPackPreflightReview(supabase as unknown as ShadowPackPreflightDbClient, { userId: session.session.userId, preflightId: id, reviewerNotes: String(input.reviewer_notes ?? ""), reviewerDecision: String(input.reviewer_decision ?? "") }); return NextResponse.json({ ok:true, preflight, no_core_memory_mutation_performed:true, no_promotion_performed:true }); } catch(e) { return NextResponse.json({ ok:false, error: e instanceof Error ? e.message : "Review failed" }, { status: 400 }); } }
6 changes: 6 additions & 0 deletions app/api/pandora/shadow-pack-preflights/[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 { getShadowPackPreflight, type ShadowPackPreflightDbClient } from "@/lib/services/pandora-shadow-pack-preflight-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 preflight = await getShadowPackPreflight(supabase as unknown as ShadowPackPreflightDbClient, { userId: session.session.userId, preflightId: id }); return NextResponse.json({ ok:true, preflight }); } 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-pack-preflights/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 { listShadowPackPreflights, type ShadowPackPreflightDbClient } from "@/lib/services/pandora-shadow-pack-preflight-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 preflights = await listShadowPackPreflights(supabase as unknown as ShadowPackPreflightDbClient, { userId: session.session.userId, namespace: url.searchParams.get("namespace") ?? undefined, status: url.searchParams.get("status") ?? undefined, limit: 25 }); return NextResponse.json({ ok:true, preflights }); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Invalid namespace/status query params return 500 instead of 400.

listShadowPackPreflights runs assertNamespace/assertStatus, which throw on unrecognized values sourced directly from url.searchParams. With no try/catch here, a request like ?namespace=foo surfaces as an unhandled 500 rather than a client-error 400.

🛡️ Suggested handling
-const preflights = await listShadowPackPreflights(supabase as unknown as ShadowPackPreflightDbClient, { userId: session.session.userId, namespace: url.searchParams.get("namespace") ?? undefined, status: url.searchParams.get("status") ?? undefined, limit: 25 }); return NextResponse.json({ ok:true, preflights });
+try { const preflights = await listShadowPackPreflights(supabase as unknown as ShadowPackPreflightDbClient, { userId: session.session.userId, namespace: url.searchParams.get("namespace") ?? undefined, status: url.searchParams.get("status") ?? undefined, limit: 25 }); return NextResponse.json({ ok:true, preflights }); } catch(e) { return NextResponse.json({ ok:false, error: e instanceof Error ? e.message : "Invalid query" }, { status: 400 }); }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 preflights = await listShadowPackPreflights(supabase as unknown as ShadowPackPreflightDbClient, { userId: session.session.userId, namespace: url.searchParams.get("namespace") ?? undefined, status: url.searchParams.get("status") ?? undefined, limit: 25 }); return NextResponse.json({ ok:true, preflights }); }
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();
try {
const preflights = await listShadowPackPreflights(supabase as unknown as ShadowPackPreflightDbClient, {
userId: session.session.userId,
namespace: url.searchParams.get("namespace") ?? undefined,
status: url.searchParams.get("status") ?? undefined,
limit: 25,
});
return NextResponse.json({ ok: true, preflights });
} catch (e) {
return NextResponse.json(
{ ok: false, error: e instanceof Error ? e.message : "Invalid query" },
{ status: 400 },
);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/pandora/shadow-pack-preflights/route.ts` at line 6, The GET handler
in shadow-pack-preflights should convert invalid `namespace` or `status` query
values into a 400 response instead of letting `listShadowPackPreflights` throw
and bubble up as a 500. Wrap the call path after `resolvePandoraServerSession`
in a `try/catch`, catch validation errors from `assertNamespace`/`assertStatus`,
and return `NextResponse.json({ ok:false, blockers: ... }, { status: 400 })` for
bad params while preserving the existing 401/other success paths in `GET`.

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></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>;
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>;
Comment on lines 7 to +8

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

New prepare_shadow_pack_preflight option is unusable — no field to supply the required shadow_pack_id.

The service throws if payload.shadow_pack_id is missing for this action type: if (action.action_type === "prepare_shadow_pack_preflight") { const shadowPackId = typeof action.payload?.shadow_pack_id === "string" ? action.payload.shadow_pack_id : ""; if (!shadowPackId) throw new Error("prepare_shadow_pack_preflight requires payload.shadow_pack_id");

But prepare() always submits payload: { source: "operator_action_center" } (Line 7) with no way to enter a shadow pack id, for every action type including this new one. Selecting prepare_shadow_pack_preflight and clicking "Prepare dry-run" will always fail server-side, and since prepare() doesn't check the fetch response before reloading, the user gets no feedback that it failed.

Add a conditional input for shadow_pack_id when this action type is selected, and include it in the payload.

🛠️ Proposed fix
 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)}>...</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">...</p></div>;
+  const [shadowPackId, setShadowPackId] = useState("");
+  async function prepare() {
+    const payload = actionType === "prepare_shadow_pack_preflight" ? { shadow_pack_id: shadowPackId } : { source: "operator_action_center" };
+    await fetch("/api/pandora/operator-actions", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action_type: actionType, namespace, mode: "dry_run", payload }) });
+    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)}>...</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>{actionType === "prepare_shadow_pack_preflight" && <label className="pd-mini"><span>Shadow pack id</span><input value={shadowPackId} onChange={(e) => setShadowPackId(e.target.value)} /></label>}</div><button className="button-link button-link--primary" type="button" onClick={prepare}>Prepare dry-run</button><p className="pd-muted">...</p></div>;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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></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>;
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>;
const [shadowPackId, setShadowPackId] = useState("");
async function prepare() {
const payload = actionType === "prepare_shadow_pack_preflight" ? { shadow_pack_id: shadowPackId } : { source: "operator_action_center" };
await fetch("/api/pandora/operator-actions", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action_type: actionType, namespace, mode: "dry_run", payload }) });
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>{actionType === "prepare_shadow_pack_preflight" && <label className="pd-mini"><span>Shadow pack id</span><input value={shadowPackId} onChange={(e) => setShadowPackId(e.target.value)} /></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>;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/pandora/OperatorActionComposer.tsx` around lines 7 - 8, The
OperatorActionComposer prepare flow always sends a fixed payload, so
prepare_shadow_pack_preflight cannot supply the required shadow_pack_id. Update
prepare() and the surrounding JSX to conditionally render an input for
shadow_pack_id when actionType is prepare_shadow_pack_preflight, and include
that value in the request payload only for that action. Also make the fetch
handling in OperatorActionComposer check for non-OK responses before
window.location.reload() so failures surface instead of reloading silently.

}
2 changes: 2 additions & 0 deletions components/pandora/PandoraDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { TopBar } from "./TopBar";
import { VerificationConsoleCard } from "./VerificationConsoleCard";
import { OperatorActionCenterCard } from "./OperatorActionCenterCard";
import { ShadowContextPackLabCard } from "./ShadowContextPackLabCard";
import { ShadowPackPreflightCard } from "./ShadowPackPreflightCard";
import { WorkQueueCard } from "./WorkQueueCard";
import type { PandoraDashboardData, StatItem } from "./types";
import { useState } from "react";
Expand Down Expand Up @@ -47,6 +48,7 @@ export function PandoraDashboard({ dashboardData }: { dashboardData: PandoraDash
<VerificationConsoleCard verification={dashboardData.verification} />
<OperatorActionCenterCard data={dashboardData.operatorActions} />
<ShadowContextPackLabCard data={dashboardData.shadowContextPackLab} />
<ShadowPackPreflightCard data={dashboardData.shadowPackPreflights} />
<div className="pd-dashboard-grid">
<div className="pd-dashboard-col pd-dashboard-col-wide">
<MemorySpacesCard spaces={dashboardData.memorySpaces} />
Expand Down
4 changes: 2 additions & 2 deletions components/pandora/ShadowContextPackControls.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +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>; }
async function post(id: string, verb: "review" | "reject" | "archive" | "preflight") { 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, "preflight")}>Create/refresh preflight</button><button className="button-link" 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>; }
3 changes: 3 additions & 0 deletions components/pandora/ShadowPackDiffViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { ShadowPackDiffSummary } from "./types";
function Keys({ label, keys }: { label: string; keys: string[] }) { return <div className="pd-mini"><strong>{keys.length}</strong><span>{label}: {keys.length ? keys.join(", ") : "none"}</span></div>; }
export function ShadowPackDiffViewer({ diff }: { diff: ShadowPackDiffSummary }) { return <div><p className="pd-muted">Active master: {diff.active_master_pack_id ?? "none"} • Shadow: {diff.shadow_pack_id}</p><div className="pd-mini-grid"><Keys label="added" keys={diff.added_keys} /><Keys label="removed" keys={diff.removed_keys} /><Keys label="changed" keys={diff.changed_keys} /><Keys label="unchanged" keys={diff.unchanged_keys} /></div><p className="pd-muted">Deltas — source events: {diff.source_event_count_delta}, open loops: {diff.open_loop_count_delta}, needs review: {diff.needs_review_count_delta}, recent summaries: {diff.top_recent_summary_count}, evidence changed: {diff.evidence_summary_changed ? "yes" : "no"}</p></div>; }
4 changes: 4 additions & 0 deletions components/pandora/ShadowPackPreflightCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { ShadowPackPreflightData } from "./types";
import { ShadowPackPreflightList } from "./ShadowPackPreflightList";
const empty: ShadowPackPreflightData = { preflights: [], warnings: [], countsByStatus: { draft:0, ready_for_review:0, reviewed:0, approved_for_promotion:0, blocked:0, archived:0 }, riskDistribution: { low:0, medium:0, high:0, blocked:0 } };
export function ShadowPackPreflightCard({ data }: { data?: ShadowPackPreflightData }) { const d=data??empty; return <section className="pd-card"><div className="pd-section-head"><div><p className="pd-label">Shadow Pack Preflight</p><h3>Diff + promotion readiness review</h3><p>Preflight only — no production context pack mutation</p></div><span className="pd-pill pd-pill-slate">No promotion route</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><ShadowPackPreflightList preflights={d.preflights}/>{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/ShadowPackPreflightControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"use client";
import type { ShadowPackPreflightSummary } from "./types";
async function archive(id: string) { await fetch(`/api/pandora/shadow-pack-preflights/${id}/archive`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) }); window.location.reload(); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Same silent-failure pattern as the review form.

archive reloads unconditionally without checking res.ok, so a failed archive (e.g., stale session, server error) is indistinguishable from success.

🛠️ Proposed fix
-async function archive(id: string) { await fetch(`/api/pandora/shadow-pack-preflights/${id}/archive`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) }); window.location.reload(); }
+async function archive(id: string) {
+  const res = await fetch(`/api/pandora/shadow-pack-preflights/${id}/archive`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) });
+  if (!res.ok) {
+    const body = await res.json().catch(() => ({}));
+    alert(body.error ?? "Failed to archive preflight.");
+    return;
+  }
+  window.location.reload();
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function archive(id: string) { await fetch(`/api/pandora/shadow-pack-preflights/${id}/archive`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) }); window.location.reload(); }
async function archive(id: string) {
const res = await fetch(`/api/pandora/shadow-pack-preflights/${id}/archive`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) });
if (!res.ok) {
const body = await res.json().catch(() => ({}));
alert(body.error ?? "Failed to archive preflight.");
return;
}
window.location.reload();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/pandora/ShadowPackPreflightControls.tsx` at line 3, The archive
flow in archive should not reload unconditionally after fetch; it currently
hides failures because it never checks the response status. Update archive(id:
string) in ShadowPackPreflightControls to capture the fetch result, verify
res.ok before calling window.location.reload(), and handle non-OK responses the
same way the review form does so a failed archive is surfaced instead of looking
successful.

export function ShadowPackPreflightControls({ preflight }: { preflight: ShadowPackPreflightSummary }) { return <div className="pd-action-controls"><button className="button-link" type="button" onClick={() => archive(preflight.id)}>Archive</button></div>; }
6 changes: 6 additions & 0 deletions components/pandora/ShadowPackPreflightList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { ShadowPackPreflightSummary } from "./types";
import { ShadowPackDiffViewer } from "./ShadowPackDiffViewer";
import { ShadowPackRiskSummary } from "./ShadowPackRiskSummary";
import { ShadowPackPreflightReviewForm } from "./ShadowPackPreflightReviewForm";
import { ShadowPackPreflightControls } from "./ShadowPackPreflightControls";
export function ShadowPackPreflightList({ preflights }: { preflights: ShadowPackPreflightSummary[] }) { if (!preflights.length) return <p className="pd-muted">No shadow pack promotion preflights yet.</p>; return <div className="pd-action-list">{preflights.map((p)=><article className="pd-action-item" key={p.id}><div><p className="pd-label">{p.namespace} • {p.status}</p><h4>Shadow pack {p.shadow_pack_id}</h4><p>Reviewer decision: {p.reviewer_decision ?? "not reviewed"}</p></div><ShadowPackRiskSummary risk={p.risk_summary}/><ShadowPackDiffViewer diff={p.diff_summary}/><p className="pd-muted">Reviewer notes: {p.reviewer_notes || "none"}</p><ShadowPackPreflightReviewForm preflight={p}/><ShadowPackPreflightControls preflight={p}/></article>)}</div>; }
5 changes: 5 additions & 0 deletions components/pandora/ShadowPackPreflightReviewForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"use client";
import { useState } from "react";
import type { ShadowPackPreflightSummary } from "./types";
async function save(id: string, reviewer_notes: string, reviewer_decision: string) { await fetch(`/api/pandora/shadow-pack-preflights/${id}/review`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ reviewer_notes, reviewer_decision }) }); window.location.reload(); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

No error handling around the review submission.

save reloads the page unconditionally after fetch, even on non-2xx responses or network failures, so a failed reviewer decision (e.g. auth expired, validation error) looks identical to success — the operator has no way to know the decision wasn't recorded.

🛠️ Proposed fix
-async function save(id: string, reviewer_notes: string, reviewer_decision: string) { await fetch(`/api/pandora/shadow-pack-preflights/${id}/review`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ reviewer_notes, reviewer_decision }) }); window.location.reload(); }
+async function save(id: string, reviewer_notes: string, reviewer_decision: string) {
+  const res = await fetch(`/api/pandora/shadow-pack-preflights/${id}/review`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ reviewer_notes, reviewer_decision }) });
+  if (!res.ok) {
+    const body = await res.json().catch(() => ({}));
+    alert(body.error ?? "Failed to save reviewer decision.");
+    return;
+  }
+  window.location.reload();
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function save(id: string, reviewer_notes: string, reviewer_decision: string) { await fetch(`/api/pandora/shadow-pack-preflights/${id}/review`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ reviewer_notes, reviewer_decision }) }); window.location.reload(); }
async function save(id: string, reviewer_notes: string, reviewer_decision: string) {
const res = await fetch(`/api/pandora/shadow-pack-preflights/${id}/review`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ reviewer_notes, reviewer_decision }) });
if (!res.ok) {
const body = await res.json().catch(() => ({}));
alert(body.error ?? "Failed to save reviewer decision.");
return;
}
window.location.reload();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/pandora/ShadowPackPreflightReviewForm.tsx` at line 4, The save
function in ShadowPackPreflightReviewForm submits the review and always reloads
the page, so failures are hidden. Update save to check the fetch result from the
/api/pandora/shadow-pack-preflights/${id}/review request, handle non-2xx
responses and network errors explicitly, and only call window.location.reload()
after a confirmed success. Use the save function and its fetch call as the main
place to add error handling and surface a clear failure state to the reviewer.

export function ShadowPackPreflightReviewForm({ preflight }: { preflight: ShadowPackPreflightSummary }) { const [notes, setNotes] = useState(preflight.reviewer_notes ?? ""); return <div className="pd-action-controls"><textarea aria-label="Reviewer notes" value={notes} onChange={(e)=>setNotes(e.target.value)} placeholder="Reviewer notes" /><button className="button-link" onClick={()=>save(preflight.id, notes, "needs_changes")}>Mark needs changes</button><button className="button-link button-link--primary" onClick={()=>save(preflight.id, notes, "approved_for_promotion")}>Approve for future promotion (NOT promotion)</button><button className="button-link" onClick={()=>save(preflight.id, notes, "blocked")}>Block</button></div>; }
3 changes: 3 additions & 0 deletions components/pandora/ShadowPackRiskSummary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { ShadowPackRiskSummary as Risk } from "./types";
const tone = { low: "pd-pill-emerald", medium: "pd-pill-amber", high: "pd-pill-red", blocked: "pd-pill-red" } as const;
export function ShadowPackRiskSummary({ risk }: { risk: Risk }) { return <div><span className={`pd-pill ${tone[risk.status]}`}>{risk.status} risk • {risk.score}/100</span>{risk.blockers.length ? <div className="pd-warning-list">{risk.blockers.map((b) => <p key={b}>⛔ {b}</p>)}</div> : null}{risk.warnings.length ? <div className="pd-warning-list">{risk.warnings.map((w) => <p key={w}>⚠ {w}</p>)}</div> : null}<ul>{risk.reasons.map((r) => <li key={r}>{r}</li>)}</ul></div>; }
Loading
Loading