Страница /reports трансформирована в Центр статусов задач:
Backend:
- GET /api/reports/summary — агрегированные счётчики тип×статус (5 корзин)
- GET/PUT /api/settings/reports — глобальные настройки отчётов
- _filter_tasks_by_rbac() — row-level фильтрация по роли
- normalize_task_report: LLM-валидация с ошибками → FAILED/PARTIAL
- get_summary(): 5 корзин pending/running/awaiting_input/success/failed
Frontend:
- TaskCenterModel.svelte.ts (400 строк) — Screen Model
- SummaryPanel — сводная панель с цветовым кодированием и active filter
- ReportCard — humanized labels, duration, task_id, failed border
- FilterBar — search + sort + time range с label'ами
- Pagination — showingText, уникальные id для select
- Quick views: «Упавшие», «В работе», «Успешные»
- TaskDrawer: scroll-to-error, footer скрыт для terminal, «Н/Д» fix
Тесты: 48 backend + 38 frontend (2521 всего)
Build: ✅ Console errors: 0
16 KiB
#region ResearchDoc [C:4] [TYPE ADR] [SEMANTICS research, architecture, decision, task-status-center, reports] @BRIEF Phase 0 research — resolves all material unknowns for the Task Status Center feature: API design, WebSocket integration, Screen Model decomposition, RBAC row-level filtering, debounce strategy, type sync, and ADR continuity. @RATIONALE Every architectural unknown must be resolved before Phase 1 contract generation. Without explicit decisions, agents default to their pre-trained heuristics, which may conflict with superset-tools conventions. @REJECTED Deferring decisions to Phase 1 was rejected — contracts need stable targets.
R1: Summary API Design
Decision: New endpoint GET /api/reports/summary returning TaskSummary — aggregated counts by (task_type × status) with an optional user_id filter applied server-side per RBAC.
Rationale:
- The existing
list_reportsendpoint loads all tasks and computes everything in-memory. Adding summary computation there would make the endpoint do double duty: paginated list AND aggregation — two different response shapes, two different caching strategies. - A separate endpoint is RESTful —
summaryis a sub-resource ofreports. - Allows frontend to fetch summary independently (lightweight, cacheable) without pulling the full paginated list.
Rejected:
- Combining summary into list response header — violates separation of concerns, bloats the list response, and forces every paginated fetch to recompute summary.
- WebSocket-only summary — initial page load needs REST reliability. WebSocket is for live updates AFTER initial load. REST gives the guaranteed-first-paint contract (SC-001: ≤1 second).
Impact: New backend route, new Pydantic schema TaskSummary, new frontend TypeScript DTO, new API client function getReportsSummary().
R2: WebSocket Integration Pattern
Decision: Reuse existing /ws/task-events WebSocket endpoint. Subscribe in the frontend TaskCenterModel, buffer incoming {type: "task_status", task_id, task} events in a 100ms debounce window, then apply incremental updates to the in-memory task list and let $derived recompute the summary.
Rationale:
- The
EventBus.broadcast_status()already fans out to global subscribers via_task_event_subscribers. Every task status transition (PENDING→RUNNING, RUNNING→SUCCESS, RUNNING→FAILED) is already broadcast to/ws/task-eventswith no code changes needed. - Creating a custom WebSocket endpoint for reports would duplicate the same data stream. Maintenance burden of two parallel streams is unjustified.
- The event format (
{type: "task_status", task_id, task}) carries the fulltaskdict — enough to determineplugin_id(for task type) andstatus(for report status) without an extra API call.
Rejected:
- Polling with
setInterval— was the old pattern, rejected because spec calls for real-time updates (SC-002: ≤2 seconds WebSocket-to-DOM latency). Polling introduces unnecessary load and lag. - Server-Sent Events (SSE) — rejected because WebSocket infrastructure already exists in the codebase. Adding SSE would create a parallel real-time mechanism.
- Per-task WebSocket subscription (
/ws/logs/{task_id}) — rejected because reports page needs ALL task status changes, not just one task.
Event handling strategy:
- On initial page load: fetch summary via REST + list via REST.
- Connect to
/ws/task-events. - Buffer incoming events for 100ms (debounce window).
- After 100ms: apply all buffered events to the in-memory
tasksmap:- New task → insert at top of list.
- Status change → update task in-place.
- Task completion → update status, mark finished_at.
$derivedrecomputes summary from updated task list.- If WebSocket disconnects → indicator + auto-reconnect with exponential backoff (1s, 2s, 4s, 8s, max 30s). During disconnect, data remains visible with "stale" indicator.
Impact: No backend changes for WebSocket. Frontend TaskCenterModel gains WebSocket subscription logic.
R3: Screen Model Architecture
Decision: Create TaskCenterModel (frontend/src/lib/models/TaskCenterModel.svelte.ts) as a C4 Screen Model. Single model for initial implementation (expected ~250-300 lines, ~15-20 atoms + methods). If it crosses 400 lines or 40 methods, decompose per ADR-0010 into TaskCenterSummaryModel + TaskCenterFilterModel + TaskCenterListModel.
Rationale:
- The spec has 3 user stories (summary, filtering, drill-down) — well within single-model scope.
- ADR-0010 decomposition gate is 400 lines / 40 methods. A model handling summary + list + filters is estimated at ~250 lines initially.
- Premature decomposition adds indirection (
m.filters.taskTypevsm.taskType) without proportional benefit.
Model structure:
// @STATE atoms:
tasks: TaskReport[] // Full task list
summary: TaskSummary | null // Aggregated counts
filters: TaskFilter // Current filter state
screenState: ScreenState // idle | loading | empty | disconnected | error
wsConnected: boolean
selectedTaskId: string | null
// @DERIVED:
filteredTasks: TaskReport[] // Derived from tasks + filters
activeTasksFirst: TaskReport[] // Sort: active before completed
// @ACTION methods:
loadInitialData()
applyFilter(partial: Partial<TaskFilter>)
clearFilters()
selectTask(taskId: string)
refreshSummary()
connectWebSocket()
disconnectWebSocket()
Rejected:
- Keeping all logic in
+page.svelte— violates ADR-0006 Model-first pattern. The page file becomes a thin render layer. - Submodels from day one — premature decomposition when the model is <300 lines. Creates unnecessary file sprawl.
- Global store (
.svelte.tsmodule-level) — the task center state is page-scoped, not cross-route. Global store would keep stale data in memory after navigation.
Impact: New file frontend/src/lib/models/TaskCenterModel.svelte.ts. Page +page.svelte becomes a thin render layer (~60 lines).
R4: RBAC Row-Level Filtering
Decision: Implement user-aware task filtering in reports_service.py. The list_reports() and new get_summary() methods will filter task_manager.get_all_tasks() based on the authenticated user's role BEFORE normalization:
| Role | Visible tasks |
|---|---|
admin |
All tasks (all users + system) |
analyst |
Own tasks (task.user_id == current_user.id) + system tasks (task.user_id is None) |
viewer |
Only own tasks (task.user_id == current_user.id) |
Rationale:
- The spec (TSC-FR-009) explicitly requires RBAC on task visibility. Currently
has_permission("tasks", "READ")only gates access — it doesn't filter rows. - The
TaskPydantic model already has auser_idfield (str | None). No schema changes needed. - Role determined from JWT token (already available via
Depends(get_current_user)).
Rejected:
- Filtering at TaskGraph level — the graph is an in-memory task registry and should not have RBAC awareness. Separation of concerns: graph manages tasks, service enforces access.
- Adding
owner_idcolumn to TaskRecord — unnecessary DB migration. The in-memoryTask.user_idalready carries this data. Persisted tasks already have this info. - ABAC (Attribute-Based Access Control) — rejected by ADR-0005. RBAC is sufficient for this scope.
Impact: ReportsService gains a new parameter current_user: User. Filtering happens in _load_normalized_reports() before normalization.
R5: Filter State Persistence
Decision: Store filter state in URL query parameters (?type=migration,backup&status=running,failed&search=dashboard&time_from=...&time_to=...). On page load, parse from URL. On filter change, update URL via history.replaceState() (NOT pushState).
Rationale:
- SvelteKit convention — URL is the source of truth for page state.
- Survives page refresh (browser restores URL).
- Survives browser back/forward navigation.
- Enables URL sharing (operator can send link to filtered view).
replaceStateprevents back-button pollution from every keystroke in the search box.
Rejected:
- LocalStorage persistence — doesn't survive URL sharing. State hidden from URL bar.
- SessionStorage — lost on tab close, no sharing.
pushStatefor every filter change — would create dozens of history entries from a single search session. Back button becomes unusable.
Implementation:
- SvelteKit
+page.tsload function readsurl.searchParamsand returns initialTaskFilter. TaskCenterModelconstructor accepts initial filter from load function.$effectwatches filter changes and callsreplaceStatewith new query params.- Debounce URL update to 300ms (avoid flooding browser history API).
Impact: New +page.ts load function. Model gains syncToUrl() action.
R6: WebSocket Event Debounce Strategy
Decision: Buffer incoming events in a 100ms window using setTimeout accumulation. After each 100ms window, apply all buffered events to the in-memory task list atomically, then let $derived recompute the filtered list and summary.
Rationale:
- During mass task completion (edge case: batch translation completes 50 tasks simultaneously),
broadcast_statusfires 50 times on the global event bus. Without debounce, the UI would thrash: 50 DOM updates in a single frame. - 100ms window is below human perception threshold (200ms) while above browser frame budget (16ms). UI feels instant.
- Svelte 5
$derivedis microtask-scheduled — after applying all buffered events synchronously, the derived values update once.
Rejected:
- Throttle (fixed interval) — can miss the last event in a burst. If events arrive at t=90ms and throttle fires at t=100ms, with a 200ms throttle, the next fire is at t=290ms — 190ms stale.
requestAnimationFramefor each event — each event triggers a separate DOM update. During burst, this starves the render pipeline.- No debounce — spec SC-004 requires 30fps at 100+ concurrent tasks. Without debounce, this is impossible.
Impact: Model adds _eventBuffer: TaskStatusEvent[] and _flushTimeout: number | null.
R7: Frontend-Backend Type Synchronization
Decision: Create TypeScript interfaces in frontend/src/types/reports.ts matching the backend Pydantic schemas with @DATA_CONTRACT cross-stack annotations. Add CI enforcement that backend schema changes trigger frontend type review.
Rationale:
- Currently the reports API client uses generic
<T>— no TypeScript compile-time protection against field mismatches. - When backend
TaskReportgains a new field, frontend code usingreport.newFieldwon't cause a TypeScript error — it will silently beundefined. @DATA_CONTRACTon both backend PydanticTaskReportand frontendinterface TaskReportcreates a bidirectional contract reference that survives HCA 128× cross-stack amnesia.
Type mapping:
| Backend (Pydantic) | Frontend (TypeScript) |
|---|---|
TaskType (str enum) |
type TaskType = 'llm_verification' | 'backup' | 'migration' | 'documentation' | 'clean_release' | 'unknown' |
ReportStatus (str enum) |
type ReportStatus = 'success' | 'failed' | 'in_progress' | 'partial' |
TaskReport |
interface TaskReport { report_id, task_id, task_type, status, ... } |
ReportQuery |
interface ReportQuery { page, page_size, task_types, ... } |
ReportCollection |
interface ReportCollection { items, total, page, ... } |
ReportDetailView |
interface ReportDetailView { report, timeline, ... } |
NEW TaskSummary |
interface TaskSummary { by_type_and_status, total_tasks, ... } |
Rejected:
- OpenAPI code generation — adds build step complexity. The schema set is small (<10 interfaces). Manual sync with CI review is proportionate.
- Runtime validation (Zod) — unnecessary. Backend Pydantic already validates on response. Frontend gets typed JSON. Adding Zod doubles the schema surface for no gain.
Impact: New file frontend/src/types/reports.ts. Updated reports.ts API client to use typed responses.
R8: Task Type List Synchronization
Decision: Frontend reportTypeProfiles.ts must be synchronized with backend type_profiles.py. Add entries for all backend TaskType enum values that are missing: clean_release, and all task types from the spec (translate, validation, git, storage, debug, search, mapper, maintenance).
Rationale:
- Spec lists 13 task types: translate, migration, backup, validation, documentation, git, storage, debug, search, mapper, maintenance, clean_release, unknown.
- Backend
TaskTypeenum has 6: LLM_VERIFICATION, BACKUP, MIGRATION, DOCUMENTATION, CLEAN_RELEASE, UNKNOWN. - Frontend
reportTypeProfiles.tshas 5: llm_verification, backup, migration, documentation, unknown. Missing: clean_release. - The
PLUGIN_TO_TASK_TYPEmapping in the backend maps plugin IDs toTaskType — the spec's broader list maps to these 6 canonical types. The frontend should mirror the 6 canonical types.
Decision: Frontend profiles match backend TaskType enum exactly (6 types + UNKNOWN fallback). Plugin-level types (translate, validation, git, etc.) are sub-types resolved by the backend — frontend only sees canonical types.
Impact: Update reportTypeProfiles.ts to add clean_release profile.
R9: Active-Tasks-First Sorting
Decision: Implement two-level sort: first by is_active (desc), then by user-selected sort field. Active = status IN (PENDING, RUNNING, AWAITING_INPUT). This satisfies TSC-FR-010 without a dedicated API parameter.
Rationale:
- The spec requires active tasks always shown first regardless of sort order.
- This is a UI concern, not a data concern. Implement in the Model's
$derivedrather than adding a backend parameter. - Two-level sort:
is_active DESC, sort_field ASC/DESC.
Rejected:
- Backend sort parameter
active_first=true— adds complexity to query parsing without real benefit. The full task list is already loaded in-memory. - Separate API endpoint for active tasks — fragments the data model unnecessarily.
Impact: TaskCenterModel.filteredTasks $derived expression gains two-level sort.
R10: Existing WebSocket /ws/task-events Authentication
Decision: No changes needed. The existing endpoint authenticates via query parameter ?token=JWT. Frontend getTaskEventsWsUrl() helper already constructs this URL from localStorage.auth_token. The JWT contains user_id and roles — the backend _authenticate_websocket() already validates this.
Rationale:
- Existing infrastructure works. No code changes.
- The WebSocket endpoint does NOT currently filter events by user — it broadcasts ALL task status changes to ALL connected clients. This is acceptable because:
- Task status metadata (id, plugin_id, status, timestamps) is not sensitive.
- The REST API enforces RBAC on detail (logs, results, error messages).
- If future requirements demand it, filtering can be added at the
EventBussubscriber level.
Impact: Zero backend changes for WebSocket.
Summary of Decisions
| ID | Decision | Backend Impact | Frontend Impact |
|---|---|---|---|
| R1 | New GET /api/reports/summary |
New route, schema, service method | New API client, TypeScript DTO |
| R2 | Reuse /ws/task-events |
None | Model gains WS subscription |
| R3 | Single TaskCenterModel |
None | New model file |
| R4 | RBAC row-level filtering | Service method change | None |
| R5 | URL query param persistence | None | +page.ts + model method |
| R6 | 100ms debounce window | None | Model event buffering |
| R7 | TypeScript DTOs + @DATA_CONTRACT |
Add @DATA_CONTRACT annotations |
New types/reports.ts |
| R8 | Frontend profiles match backend TaskType |
None | Add clean_release profile |
| R9 | Two-level sort in $derived |
None | Model derived expression |
| R10 | No WS auth changes | None | None |
#endregion ResearchDoc