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:
2026-05-23 10:09:40 -04:00
parent 3a316551be
commit 4048a771d2
2 changed files with 65 additions and 0 deletions
+30
View File
@@ -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));