#region DataModel [C:4] [TYPE ADR] [SEMANTICS data-model, schema, pydantic, typescript, dto, task-status-center] @BRIEF Data model for the Task Status Center feature — backend Pydantic schemas (new + modified), frontend TypeScript DTOs, and the cross-stack contract bridge. @RATIONALE The data model defines WHAT flows between backend and frontend. Without explicit DTO contracts, type drift between Python and TypeScript is inevitable over long-horizon development. @REJECTED Auto-generating TypeScript from Pydantic was rejected (R7) — the schema set is small enough that manual sync is proportionate and avoids build-step complexity. ## 1. Entity Relationship Summary ``` TaskRecord (SQLAlchemy, persisted) └── Task (Pydantic, in-memory via TaskManager) └── TaskReport (Pydantic, normalized for UI) ├── ReportCollection (paginated list) ├── ReportDetailView (single task detail) └── TaskSummary (aggregated counts) ← NEW TaskSummary └── TaskTypeSummary[] (one per task_type) └── StatusCounts (pending, running, awaiting_input, success, failed) ``` ## 2. New Backend Schemas ### 2.1 TaskSummary (NEW) ```python # backend/src/models/report.py (add to existing module) # #region TaskSummary [C:3] [TYPE Class] [SEMANTICS summary, aggregation, task, status, count] # @ingroup ReportModels # @BRIEF Aggregated task counts by type and status for the summary dashboard. # @DATA_CONTRACT Output: TaskSummary → frontend interface TaskSummary # @RELATION DEPENDS_ON -> [TaskReport] # @TEST_CONTRACT TaskSummaryModel -> { invariants: ["all counts >= 0", "total == sum of all status counts across all types"] } class StatusCounts(BaseModel): """Counts per status for a single task type.""" pending: int = Field(default=0, ge=0) running: int = Field(default=0, ge=0) awaiting_input: int = Field(default=0, ge=0) success: int = Field(default=0, ge=0) failed: int = Field(default=0, ge=0) class TaskTypeSummary(BaseModel): """Summary for one task type.""" task_type: TaskType display_label: str icon_token: str counts: StatusCounts total: int = Field(default=0, ge=0) class TaskSummary(BaseModel): """Full summary response for the summary dashboard.""" by_type: list[TaskTypeSummary] total_tasks: int = Field(default=0, ge=0) active_tasks: int = Field(default=0, ge=0) # pending + running + awaiting_input # #endregion TaskSummary ``` ### 2.2 Enhanced ReportQuery (MODIFIED) The existing `ReportQuery` already supports `task_types[]`, `statuses[]`, `search`, `time_from`, `time_to`, `sort_by`, `sort_order`. No schema changes needed — the filtering logic in the service method will be enhanced to use these parameters more thoroughly. **Change:** `sort_by` validator adds `"created_at"` to allowed values (currently only `updated_at`, `status`, `task_type`). ### 2.3 Enhanced TaskReport (MODIFIED) No new fields needed. The existing `TaskReport` already carries all required fields for the spec: - `report_id` → task identifier in list - `task_type` → filter/summary grouping - `status` → filter/summary grouping + color coding - `started_at` → temporal display - `updated_at` → sort default - `summary` → display text - `error_context` → error display + next actions - `source_ref` → linked resource (dashboard, dataset, config) ### 2.4 No DB Schema Changes No new database tables or columns. The feature works entirely with: - Existing `task_records` table (persisted task metadata) - Existing `task_logs` table (persisted logs for Task Drawer) - In-memory `TaskManager.get_all_tasks()` as the data source ## 3. New Frontend TypeScript DTOs ```typescript // frontend/src/types/reports.ts (NEW FILE) // #region ReportsTypes [C:3] [TYPE Module] [SEMANTICS types, dto, reports, task-status-center] // @BRIEF TypeScript DTOs matching backend Pydantic schemas for the Task Status Center. // @DATA_CONTRACT Input: TypeScript interfaces → Output: typed API responses // @RELATION DEPENDS_ON -> [ReportModels] via @DATA_CONTRACT cross-stack // @RATIONALE Typed API responses prevent field mismatch bugs and enable IDE autocomplete. /** Canonical task types matching backend TaskType enum. */ export type TaskType = | 'llm_verification' | 'backup' | 'migration' | 'documentation' | 'clean_release' | 'unknown'; /** Normalized report status matching backend ReportStatus enum. */ export type ReportStatus = | 'success' | 'failed' | 'in_progress' | 'partial'; /** Error/recovery context for failed tasks. */ export interface ErrorContext { code: string | null; message: string; next_actions: string[]; } /** Single task report — matches backend TaskReport. */ export interface TaskReport { report_id: string; task_id: string; task_type: TaskType; status: ReportStatus; started_at: string | null; updated_at: string; summary: string; details?: Record | null; validation_record?: Record | null; error_context: ErrorContext | null; source_ref?: Record | null; } /** Query parameters for report list filtering — matches backend ReportQuery. */ export interface ReportQuery { page: number; page_size: number; task_types: TaskType[]; statuses: ReportStatus[]; time_from?: string | null; time_to?: string | null; search?: string | null; sort_by: 'updated_at' | 'created_at' | 'status' | 'task_type'; sort_order: 'asc' | 'desc'; } /** Paginated collection — matches backend ReportCollection. */ export interface ReportCollection { items: TaskReport[]; total: number; page: number; page_size: number; has_next: boolean; applied_filters: ReportQuery; } /** Detailed view — matches backend ReportDetailView. */ export interface ReportDetailView { report: TaskReport; timeline: Record[]; diagnostics: Record | null; next_actions: string[]; } // --- NEW for Task Status Center --- /** Status counts for one task type in the summary. */ export interface StatusCounts { pending: number; running: number; awaiting_input: number; success: number; failed: number; } /** Per-type summary entry. */ export interface TaskTypeSummary { task_type: TaskType; display_label: string; icon_token: string; counts: StatusCounts; total: number; } /** Full summary response — matches backend TaskSummary. */ export interface TaskSummary { by_type: TaskTypeSummary[]; total_tasks: number; active_tasks: number; } /** WebSocket task status event from /ws/task-events. */ export interface TaskStatusEvent { type: 'task_status'; task_id: string; task: { id: string; plugin_id: string; status: string; started_at: string | null; finished_at: string | null; user_id: string | null; params: Record | null; result: Record | null; }; } /** Filter state for the Task Center UI (broader than ReportQuery). */ export interface TaskCenterFilter { task_types: TaskType[]; statuses: ReportStatus[]; search: string; time_range: '1h' | '24h' | '7d' | '30d' | 'all'; sort_by: 'updated_at' | 'created_at' | 'status' | 'task_type'; sort_order: 'asc' | 'desc'; } /** UI screen states for the Task Center page. */ export type ScreenState = | 'loading' | 'ready' | 'empty' | 'error' | 'disconnected' | 'reconnecting'; // #endregion ReportsTypes ``` ## 4. Cross-Stack Contract Bridge ``` Backend (Pydantic) Frontend (TypeScript) ───────────────── ───────────────────── TaskType (str Enum) ←──→ TaskType (union type) ReportStatus (str Enum) ←──→ ReportStatus (union type) TaskReport (BaseModel) ←──→ interface TaskReport ReportQuery (BaseModel) ←──→ interface ReportQuery ReportCollection ←──→ interface ReportCollection ReportDetailView ←──→ interface ReportDetailView TaskSummary (NEW) ←──→ interface TaskSummary StatusCounts (NEW) ←──→ interface StatusCounts TaskTypeSummary (NEW) ←──→ interface TaskTypeSummary ``` Each bridge pair carries `@DATA_CONTRACT` annotation referencing the counterpart: - Backend: `@DATA_CONTRACT Output: TaskSummary → frontend interface TaskSummary` - Frontend: `@DATA_CONTRACT Input: TypeScript interface → Output: typed API response` ## 5. Data Flow Diagram ``` ┌─────────────────────────────────────────────────────────────────┐ │ BROWSER │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ TaskCenterModel (.svelte.ts) │ │ │ │ │ │ │ │ loadInitialData() │ │ │ │ ├─ GET /api/reports/summary ──────► TaskSummary │ │ │ │ └─ GET /api/reports?page=1&... ──► ReportCollection │ │ │ │ │ │ │ │ connectWebSocket() │ │ │ │ └─ ws://host/ws/task-events ────► TaskStatusEvent x N │ │ │ │ │ │ │ │ │ └─ 100ms debounce buffer │ │ │ │ └─ apply to tasks[] ──► $derived summary │ │ │ │ │ │ │ │ selectTask(taskId) │ │ │ │ └─ openDrawerForTask(taskId) ──► TaskDrawer + logs WS │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ +page.svelte (thin render layer ~60 lines) │ │ │ │ ├─ SummaryPanel ← m.summary │ │ │ │ ├─ FilterBar ← m.filters, m.applyFilter() │ │ │ │ └─ TaskList ← m.filteredTasks, m.selectTask() │ │ │ └──────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ BACKEND (FastAPI) │ │ │ │ GET /api/reports/summary ───► ReportsService.get_summary() │ │ └─ task_manager.get_all_tasks() │ │ └─ filter by RBAC (role-based) │ │ └─ normalize → aggregate → TaskSummary │ │ │ │ GET /api/reports?... ───► ReportsService.list_reports() │ │ └─ same flow → filter, sort, paginate → ReportCollection │ │ │ │ ws://host/ws/task-events ───► EventBus.broadcast_status() │ │ └─ already broadcasts on every task status transition │ │ └─ NO CODE CHANGES NEEDED │ └─────────────────────────────────────────────────────────────────┘ ``` #endregion DataModel