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}`, {});
|
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[]> {
|
export async function listJournalMoments(params: Record<string, string | number | boolean> = {}): Promise<JournalMoment[]> {
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
for (const [k, v] of Object.entries(params)) qs.set(k, String(v));
|
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})
|
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>")
|
@journal_bp.patch("/moments/<int:moment_id>")
|
||||||
@login_required
|
@login_required
|
||||||
async def patch_moment(moment_id: int):
|
async def patch_moment(moment_id: int):
|
||||||
|
|||||||
Reference in New Issue
Block a user