feat(FN-3549): add mailbox approvals inbox and decision flow

- Add dashboard approvals APIs and route handlers for listing requests, fetching detail, and submitting approve/deny decisions
- Emit approval SSE events and subscribe mailbox refresh logic to keep approval lists and badges live
- Extend Mailbox view with an Approvals tab, pending/history filters, request detail history, and approve/deny actions
- Update mailbox styles, approval route tests, and docs for the new approvals workflow

Fusion-Task-Id: FN-3549
This commit is contained in:
Fusion
2026-05-09 05:38:34 -07:00
committed by gsxdsm
parent 9e6574c777
commit f3626489ed
9 changed files with 770 additions and 113 deletions

View File

@@ -215,6 +215,18 @@ export type MessageSseEventType =
| "message:read"
| "message:deleted";
export type ApprovalSseEventType = "approval:requested" | "approval:updated" | "approval:decided";
type ApprovalSseListener = (event: ApprovalSseEventType, payload: unknown, projectId?: string) => void;
const approvalSseListeners = new Set<ApprovalSseListener>();
export function emitApprovalSseEvent(event: ApprovalSseEventType, payload: unknown, projectId?: string): void {
for (const listener of approvalSseListeners) {
listener(event, payload, projectId);
}
}
/**
* Normalized plugin lifecycle payload emitted via SSE.
* This is the stable contract the UI can reconcile.
@@ -556,6 +568,11 @@ export function createSSE(
send(`event: message:deleted\ndata: ${JSON.stringify({ id: messageId })}\n\n`);
};
const onApprovalEvent: ApprovalSseListener = (event, payload, eventProjectId) => {
if (projectId && eventProjectId && eventProjectId !== projectId) return;
send(`event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`);
};
// --- Chat store event handlers ---
const onChatSessionCreated = (session: unknown) => {
send(`event: chat:session:created\ndata: ${JSON.stringify(session)}\n\n`);
@@ -671,6 +688,7 @@ export function createSSE(
messageStore.off("message:read", onMessageRead);
messageStore.off("message:deleted", onMessageDeleted);
}
approvalSseListeners.delete(onApprovalEvent);
if (chatStore) {
chatStore.off("chat:session:created", onChatSessionCreated);
chatStore.off("chat:session:updated", onChatSessionUpdated);
@@ -799,6 +817,8 @@ export function createSSE(
// Sent as a named event so the client's EventSource can detect it
// (SSE comments starting with ":" are silently consumed and never
// fire event listeners in the browser).
approvalSseListeners.add(onApprovalEvent);
registerManagedConnection({
id: connectionId,
clientId,