diff --git a/app/admin/memory/proposals/[id]/page.tsx b/app/admin/memory/proposals/[id]/page.tsx new file mode 100644 index 0000000..3a99a37 --- /dev/null +++ b/app/admin/memory/proposals/[id]/page.tsx @@ -0,0 +1,11 @@ +import { AppShell } from "@/components/layout/app-shell"; +import { PageHeader } from "@/components/ui/page-header"; +import { SectionCard } from "@/components/ui/section-card"; +import { resolvePandoraRuntimeSafetyConfig } from "@/lib/config/pandora-runtime-safety-config"; +import { resolvePandoraServerSession } from "@/lib/auth/pandora-server-session-resolver"; +import { createSupabaseServerClient } from "@/lib/supabase/server"; +import { getMemoryProposal } from "@/lib/services/memory-proposal-service"; +import { approveProposalAction, persistProposalAction, rejectProposalAction, reviseProposalAction } from "../actions"; +export const dynamic="force-dynamic"; +type Props={params:Promise<{id:string}>}; +export default async function Page({params}:Props){const {id}=await params; const runtime=resolvePandoraRuntimeSafetyConfig(); const session=await resolvePandoraServerSession(); const supabase=await createSupabaseServerClient() as never; const result=await getMemoryProposal(supabase,id,session,runtime); const p=result.ok?result.data:null; const gate=runtime.config.approvedReviewPersistenceEnabled; return

Approval is audited.

Public writes remain disabled.

Model calls, embeddings, retrieval, GPT Actions, and MCP remain disabled.

Reviewed write gate enabled: {String(gate)}

{!p?

No proposal rows are exposed.

:<>
Proposal id
{p.id}
Namespace
{p.namespace}
Memory text
{p.memory_text}
Source
{p.source_type} {p.source_ref}
Proposed by
{p.proposed_by}
Reviewed by
{p.reviewed_by??"—"}
Reviewed at
{p.reviewed_at??"—"}
Persisted memory id
{p.persisted_memory_id??"—"}

Use /admin/memory/audit with namespace {p.namespace} to inspect append-only audit proof.

}
} diff --git a/app/admin/memory/proposals/actions.ts b/app/admin/memory/proposals/actions.ts new file mode 100644 index 0000000..3f91d9b --- /dev/null +++ b/app/admin/memory/proposals/actions.ts @@ -0,0 +1,25 @@ +"use server"; + +import { redirect } from "next/navigation"; +import { revalidatePath } from "next/cache"; +import { resolvePandoraRuntimeSafetyConfig } from "@/lib/config/pandora-runtime-safety-config"; +import { resolvePandoraServerSession } from "@/lib/auth/pandora-server-session-resolver"; +import { createSupabaseServerClient } from "@/lib/supabase/server"; +import { approveMemoryProposal, createMemoryProposal, persistApprovedMemoryProposal, rejectMemoryProposal, requestMemoryProposalRevision } from "@/lib/services/memory-proposal-service"; + +function text(formData: FormData, key: string) { const value = formData.get(key); return typeof value === "string" && value.trim() ? value.trim() : undefined; } +function num(formData: FormData, key: string) { const value = text(formData, key); return value ? Number(value) : undefined; } +async function deps() { return { supabase: await createSupabaseServerClient() as never, session: await resolvePandoraServerSession(), runtime: resolvePandoraRuntimeSafetyConfig() }; } +function fail(message: string): never { throw new Error(message); } + +export async function createProposalAction(formData: FormData) { + const { supabase, session, runtime } = await deps(); + const result = await createMemoryProposal(supabase, { namespace: (text(formData, "namespace") ?? "real_life") as "real_life" | "au", title: text(formData, "title"), memory_text: text(formData, "memory_text") ?? "", memory_type: text(formData, "memory_type"), source_type: text(formData, "source_type"), source_ref: text(formData, "source_ref"), confidence: num(formData, "confidence") }, session, runtime); + if (!result.ok) fail(result.error); + revalidatePath("/admin/memory/proposals"); + redirect(`/admin/memory/proposals/${result.data.id}`); +} +export async function approveProposalAction(formData: FormData) { const { supabase, session, runtime } = await deps(); const id = text(formData, "id") ?? fail("id required"); const r = await approveMemoryProposal(supabase, id, "server-action", session, runtime); if (!r.ok) fail(r.error); revalidatePath(`/admin/memory/proposals/${id}`); } +export async function rejectProposalAction(formData: FormData) { const { supabase, session, runtime } = await deps(); const id = text(formData, "id") ?? fail("id required"); const r = await rejectMemoryProposal(supabase, id, text(formData, "reason") ?? "Rejected by operator.", "server-action", session, runtime); if (!r.ok) fail(r.error); revalidatePath(`/admin/memory/proposals/${id}`); } +export async function reviseProposalAction(formData: FormData) { const { supabase, session, runtime } = await deps(); const id = text(formData, "id") ?? fail("id required"); const r = await requestMemoryProposalRevision(supabase, id, text(formData, "note") ?? "Revision requested by operator.", "server-action", session, runtime); if (!r.ok) fail(r.error); revalidatePath(`/admin/memory/proposals/${id}`); } +export async function persistProposalAction(formData: FormData) { const { supabase, session, runtime } = await deps(); const id = text(formData, "id") ?? fail("id required"); const r = await persistApprovedMemoryProposal(supabase, id, "server-action", session, runtime); if (!r.ok) fail(r.error); revalidatePath(`/admin/memory/proposals/${id}`); revalidatePath("/admin/memory/browser"); } diff --git a/app/admin/memory/proposals/new/page.tsx b/app/admin/memory/proposals/new/page.tsx new file mode 100644 index 0000000..7c2c696 --- /dev/null +++ b/app/admin/memory/proposals/new/page.tsx @@ -0,0 +1,2 @@ +import { AppShell } from "@/components/layout/app-shell";import { PageHeader } from "@/components/ui/page-header";import { SectionCard } from "@/components/ui/section-card";import { resolvePandoraRuntimeSafetyConfig } from "@/lib/config/pandora-runtime-safety-config";import { createProposalAction } from "../actions"; +export const dynamic="force-dynamic";export default function Page(){const runtime=resolvePandoraRuntimeSafetyConfig();return