feat(curator): pending-action API routes + client helpers (C4/5)
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/<id>/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/<id>/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) <noreply@anthropic.com>
This commit is contained in:
@@ -423,6 +423,36 @@ export async function runJournalCurator(convId: number): Promise<CuratorRunResul
|
||||
return apiPost<CuratorRunResult>(`/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<string, unknown>; // the curator's proposed args
|
||||
current_snapshot: Record<string, unknown>; // 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<Record<string, unknown>> {
|
||||
return apiPost<Record<string, unknown>>(`/api/journal/pending/${actionId}/approve`, {});
|
||||
}
|
||||
|
||||
export async function rejectPendingAction(actionId: number): Promise<Record<string, unknown>> {
|
||||
return apiPost<Record<string, unknown>>(`/api/journal/pending/${actionId}/reject`, {});
|
||||
}
|
||||
|
||||
export async function listJournalMoments(params: Record<string, string | number | boolean> = {}): Promise<JournalMoment[]> {
|
||||
const qs = new URLSearchParams();
|
||||
for (const [k, v] of Object.entries(params)) qs.set(k, String(v));
|
||||
|
||||
@@ -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/<int:action_id>/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/<int:action_id>/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/<int:moment_id>")
|
||||
@login_required
|
||||
async def patch_moment(moment_id: int):
|
||||
|
||||
Reference in New Issue
Block a user