Files
ss-tools/specs/034-task-status-center/research.md
busya 33ee976c48 feat(reports): Task Status Center — unified /reports dashboard
Страница /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
2026-07-02 18:53:58 +03:00

16 KiB
Raw Blame History

#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_reports endpoint 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 — summary is a sub-resource of reports.
  • 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-events with 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 full task dict — enough to determine plugin_id (for task type) and status (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:

  1. On initial page load: fetch summary via REST + list via REST.
  2. Connect to /ws/task-events.
  3. Buffer incoming events for 100ms (debounce window).
  4. After 100ms: apply all buffered events to the in-memory tasks map:
    • New task → insert at top of list.
    • Status change → update task in-place.
    • Task completion → update status, mark finished_at.
  5. $derived recomputes summary from updated task list.
  6. 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.taskType vs m.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.ts module-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 Task Pydantic model already has a user_id field (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_id column to TaskRecord — unnecessary DB migration. The in-memory Task.user_id already 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).
  • replaceState prevents 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.
  • pushState for every filter change — would create dozens of history entries from a single search session. Back button becomes unusable.

Implementation:

  • SvelteKit +page.ts load function reads url.searchParams and returns initial TaskFilter.
  • TaskCenterModel constructor accepts initial filter from load function.
  • $effect watches filter changes and calls replaceState with 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_status fires 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 $derived is 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.
  • requestAnimationFrame for 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 TaskReport gains a new field, frontend code using report.newField won't cause a TypeScript error — it will silently be undefined.
  • @DATA_CONTRACT on both backend Pydantic TaskReport and frontend interface TaskReport creates 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 TaskType enum has 6: LLM_VERIFICATION, BACKUP, MIGRATION, DOCUMENTATION, CLEAN_RELEASE, UNKNOWN.
  • Frontend reportTypeProfiles.ts has 5: llm_verification, backup, migration, documentation, unknown. Missing: clean_release.
  • The PLUGIN_TO_TASK_TYPE mapping 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 $derived rather 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:
    1. Task status metadata (id, plugin_id, status, timestamps) is not sensitive.
    2. The REST API enforces RBAC on detail (logs, results, error messages).
    3. If future requirements demand it, filtering can be added at the EventBus subscriber 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