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):