feat: F018 human review queue — 2-panel governance workspace

Replaces the old ad-hoc ReviewQueue page with a full governance-compliant
workflow: filterable task list, detail panel with confidence/risk context,
role-aware approve/reject/escalate/more-data actions, and persistent notes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-17 13:03:34 +02:00
parent 1d0cb8b154
commit 5d3323617c
18 changed files with 1231 additions and 432 deletions
+38 -13
View File
@@ -1,29 +1,51 @@
import { MockupReviewProvider } from '../provider/MockupReviewProvider'
import type { ReviewFilters } from '../provider/IReviewProvider'
import type { ReviewQueueItem } from '../domain/review'
import type { ReviewTask, ReviewTaskStatus, ReviewNote } from '../domain/review'
import type { DashboardReviewTask } from '../domain/dashboard'
import type { ListResponse, ItemResponse } from './types'
const provider = MockupReviewProvider
export const reviewService = {
async getQueue(filters?: ReviewFilters): Promise<ListResponse<ReviewQueueItem>> {
async getQueue(filters?: ReviewFilters): Promise<ListResponse<ReviewTask>> {
const data = await provider.getQueue(filters)
return { data, meta: { total: data.length, page: 1, pageSize: data.length, hasMore: false } }
},
async getById(id: string): Promise<ItemResponse<ReviewQueueItem | null>> {
async getTasks(filters?: ReviewFilters): Promise<ListResponse<ReviewTask>> {
return this.getQueue(filters)
},
async getById(id: string): Promise<ItemResponse<ReviewTask | null>> {
const data = await provider.getById(id)
return { data }
},
async approve(id: string, reviewedBy: string, notes?: string): Promise<ItemResponse<ReviewQueueItem>> {
async getTask(id: string): Promise<ItemResponse<ReviewTask | null>> {
return this.getById(id)
},
async updateStatus(id: string, status: ReviewTaskStatus, userId: string, note?: string): Promise<ItemResponse<ReviewTask>> {
const data = await provider.updateStatus(id, status, userId, note)
return { data }
},
async addNote(id: string, note: Omit<ReviewNote, 'id'>): Promise<ItemResponse<ReviewTask>> {
const data = await provider.addNote(id, note)
return { data }
},
async approve(id: string, reviewedBy: string, notes?: string): Promise<ItemResponse<ReviewTask>> {
const data = await provider.approve(id, reviewedBy, notes)
return { data }
},
async reject(id: string, reviewedBy: string, notes?: string): Promise<ItemResponse<ReviewQueueItem>> {
async reject(id: string, reviewedBy: string, notes?: string): Promise<ItemResponse<ReviewTask>> {
const data = await provider.reject(id, reviewedBy, notes)
return { data }
},
async assign(id: string, assignTo: string): Promise<ItemResponse<ReviewQueueItem>> {
async assign(id: string, assignTo: string): Promise<ItemResponse<ReviewTask>> {
const data = await provider.assign(id, assignTo)
return { data }
},
@@ -35,12 +57,15 @@ export const reviewService = {
async getDashboardTasks(): Promise<DashboardReviewTask[]> {
const items = await provider.getQueue()
return items.slice(0, 8).map(r => ({
id: r.id,
title: `Match ${r.matchId} · Score ${r.matchScore}`,
priority: r.priority,
status: r.status,
type: 'REVIEW',
}))
return items
.filter(r => r.status === 'PENDING' || r.status === 'IN_REVIEW' || r.status === 'ESCALATED')
.slice(0, 8)
.map(r => ({
id: r.id,
title: r.title,
priority: r.priority as 'HIGH' | 'MEDIUM' | 'LOW',
status: r.status,
type: r.entityType,
}))
},
}