From 4048a771d234ec19f3835f8b3099334b3abf7f11 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 23 May 2026 10:09:40 -0400 Subject: [PATCH] feat(curator): pending-action API routes + client helpers (C4/5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The HTTP surface for the review queue. Three endpoints, all under the existing /api/journal blueprint to keep the journal-related routes together: - GET /api/journal/pending — list current user's pending actions. - POST /api/journal/pending//approve — replay the proposed tool call via execute_tool(authority='user'). On success, marks the row 'approved'; on replay error, leaves it pending so the user can retry. - POST /api/journal/pending//reject — marks 'rejected' with no execution. Each route is a thin wrapper around services/pending_actions and delegates user-scoping to the service (which checks user_id on every load — actions are private to the proposer). api/client.ts: - PendingCuratorAction interface mirroring the backend dict shape: id, user_id, conv_id, action_type, target_type/id/label, payload, current_snapshot, status, timestamps. - listPendingActions / approvePendingAction / rejectPendingAction helpers for the upcoming Needs Review panel. C5 next: the panel itself. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/api/client.ts | 30 +++++++++++++++++++++++ src/fabledassistant/routes/journal.py | 35 +++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index b3a0ad4..0d5df9f 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -423,6 +423,36 @@ export async function runJournalCurator(convId: number): Promise(`/api/journal/curator/run/${convId}`, {}); } +// Curator-proposed mutations awaiting user review (Needs Review panel). +// See routes/journal.py pending_actions endpoints. + +export interface PendingCuratorAction { + id: number; + user_id: number; + conv_id: number | null; + action_type: string; // e.g. "update_note", "delete_note", "update_milestone" + target_type: string | null; // "task" | "note" | "milestone" | "project" | "profile" + target_id: number | null; + target_label: string | null; // human-readable title for the card header + payload: Record; // the curator's proposed args + current_snapshot: Record; // target state at proposal time + status: 'pending' | 'approved' | 'rejected'; + created_at: string; + reviewed_at: string | null; +} + +export async function listPendingActions(): Promise<{ pending: PendingCuratorAction[]; count: number }> { + return apiGet<{ pending: PendingCuratorAction[]; count: number }>('/api/journal/pending'); +} + +export async function approvePendingAction(actionId: number): Promise> { + return apiPost>(`/api/journal/pending/${actionId}/approve`, {}); +} + +export async function rejectPendingAction(actionId: number): Promise> { + return apiPost>(`/api/journal/pending/${actionId}/reject`, {}); +} + export async function listJournalMoments(params: Record = {}): Promise { const qs = new URLSearchParams(); for (const [k, v] of Object.entries(params)) qs.set(k, String(v)); diff --git a/src/fabledassistant/routes/journal.py b/src/fabledassistant/routes/journal.py index dd74b73..237db8e 100644 --- a/src/fabledassistant/routes/journal.py +++ b/src/fabledassistant/routes/journal.py @@ -271,6 +271,41 @@ async def list_moments(): return jsonify({"moments": results}) +@journal_bp.get("/pending") +@login_required +async def list_pending_actions(): + """List curator-proposed mutations awaiting the user's review.""" + user_id = get_current_user_id() + from fabledassistant.services.pending_actions import list_pending + pending = await list_pending(user_id) + return jsonify({"pending": pending, "count": len(pending)}) + + +@journal_bp.post("/pending//approve") +@login_required +async def approve_pending_action(action_id: int): + """Approve a proposed action — replays the underlying tool call. + + Returns the tool result on success. If the replay errors (e.g., the + target was deleted in the meantime), the action stays pending so the + user can re-try or reject explicitly. + """ + user_id = get_current_user_id() + from fabledassistant.services.pending_actions import approve + result = await approve(action_id, user_id) + return jsonify(result) + + +@journal_bp.post("/pending//reject") +@login_required +async def reject_pending_action(action_id: int): + """Reject a proposed action — marks rejected without executing anything.""" + user_id = get_current_user_id() + from fabledassistant.services.pending_actions import reject + result = await reject(action_id, user_id) + return jsonify(result) + + @journal_bp.patch("/moments/") @login_required async def patch_moment(moment_id: int):