# Tasks: Task Status Center **Input**: Design documents from `/specs/034-task-status-center/` **Prerequisites**: plan.md ✅, spec.md ✅, ux_reference.md ✅, research.md ✅, data-model.md ✅, contracts/modules.md ✅ **Tests**: All C4 contracts require tests verifying `@PRE`/`@POST`/`@INVARIANT`. Rejected-path regression tests for RBAC filtering. **Organization**: Tasks grouped by user story — each story independently implementable and testable. ## Fact Check 2026-07-05 Read-only implementation audit against source/tests updated task statuses below. Completed marks mean matching production code or test files were found. Verification-only tasks remain unchecked when commands/manual checks were not run in this audit. **Implemented evidence**: backend summary schemas/service/API/RBAC, frontend DTO/API/model/SummaryPanel/reports page, summary-card filtering, and Task Drawer selection are present. **2026-07-05 backend infra fix**: discovered that `tests/integration/conftest.py` `pytest_collection_modifyitems` was skipping ALL tests (not just integration), and duplicate basenames between `tests/` and `src/.../__tests__/` caused collection errors. Fixed: (1) added `"integration/" in item.nodeid` filter to conftest, (2) added `norecursedirs = ["__tests__"]` to pyproject.toml. **Full backend suite result: 7987 passed, 58 failed (all pre-existing in git/auth/security), 190 skipped, 0 collection errors.** All 75 report-related tests pass. Frontend: 2546/2549 pass (3 pre-existing unrelated failures). --- ## Phase 1: Setup (Shared Infrastructure) **Purpose**: Verify branch, environment, and prerequisites. - [x] T001 Run `git checkout 034-task-status-center` and verify `specs/034-task-status-center/` has all design docs (spec.md, plan.md, research.md, data-model.md, contracts/modules.md, quickstart.md, ux_reference.md) - [ ] T002 Run `cd backend && source .venv/bin/activate && python -m pytest backend/tests/services/reports/ -v` — verify existing reports tests pass before any changes --- ## Phase 2: Foundational (Blocking Prerequisites) **Purpose**: Core infrastructure that ALL user stories depend on — types, RBAC filter, API client, type profiles. **⚠️ CRITICAL**: No user story work until this phase passes verification. ### Data Layer (backend + frontend — parallel) - [x] T003 [P] Add `TaskSummary`, `StatusCounts`, `TaskTypeSummary` Pydantic schemas to `backend/src/models/report.py` Extend existing `ReportModels` region — add schemas from data-model.md §2.1 @POST: `TaskSummary(by_type: list[TaskTypeSummary], total_tasks: int, active_tasks: int)` with all counts ≥0 - [x] T004 [P] Create TypeScript DTOs in `frontend/src/types/reports.ts` Interfaces from data-model.md §3: `TaskType`, `ReportStatus`, `ErrorContext`, `TaskReport`, `ReportQuery`, `ReportCollection`, `ReportDetailView`, `StatusCounts`, `TaskTypeSummary`, `TaskSummary`, `TaskStatusEvent`, `TaskCenterFilter`, `ScreenState` @DATA_CONTRACT: each interface references `backend/src/models/report.py` counterpart via JSDoc `@see` - [x] T005 [P] Add `clean_release` profile to `frontend/src/lib/components/reports/reportTypeProfiles.ts` Add entry: `clean_release: { key: 'clean_release', label: 'Clean Release', variant: 'success', icon: 'shield-check', fallback: false }` RATIONALE: R8 — sync frontend profiles with backend `TaskType` enum (was missing `clean_release`) ### RBAC & API Client (sequential after T003/T004) - [x] T006 Implement `_filter_tasks_by_rbac()` in `backend/src/services/reports/report_service.py` @PRE: current_user is authenticated User, tasks is list of in-memory Task objects @POST: returns tasks filtered by role — admin→all, analyst→own+system(user_id=None), viewer→own only RATIONALE: extracted as pure function for testability (R4), no side effects, no service dependencies REJECTED: inline filtering in list_reports — mixes authorization with query logic REJECTED: filtering at TaskGraph level — graph is data structure, not auth boundary @TEST_EDGE: admin→all_tasks_returned, analyst→own_plus_system, viewer→only_own, no_matching→empty_list - [x] T007 [P] Add `getReportsSummary()` and type existing functions in `frontend/src/lib/api/reports.ts` New: `export async function getReportsSummary(): Promise` → `GET /api/reports/summary` via `api.fetchApi` Enhance: `getReports()` returns `Promise`, `getReportDetail()` returns `Promise` (replace generic ``) @DATA_CONTRACT: ReportQuery → ReportCollection, (none) → TaskSummary, reportId → ReportDetailView import types from `$types/reports` - [x] T008 [P] Add `"created_at"` to allowed `sort_by` values in `backend/src/models/report.py` `ReportQuery._validate_sort_by` validator Change allowed set from `{"updated_at", "status", "task_type"}` to `{"updated_at", "created_at", "status", "task_type"}` - [x] T008a [P] Add `ReportsSettings` Pydantic schema to `backend/src/models/report.py` Schema: `class ReportsSettings(BaseModel): disabled_task_types: list[TaskType] = Field(default_factory=list)` @POST: `ReportsSettings(disabled_task_types: list[TaskType])` — validates task types against enum - [x] T008b Implement `GET /api/settings/reports` and `PUT /api/settings/reports` endpoints in `backend/src/api/routes/settings.py` (or `reports.py`) GET: returns `{ disabled_task_types: [...] }` — any authenticated user with `tasks:READ` PUT: accepts `{ disabled_task_types: [...] }` — admin only (`settings:WRITE` permission), validates types against `TaskType` enum @PRE: GET — authenticated user; PUT — admin role with `settings:WRITE` @POST: GET → `ReportsSettings`; PUT → updated `ReportsSettings` @TEST_EDGE: get_returns_defaults→empty_list, put_by_admin→saved, put_by_non_admin→403, put_invalid_type→422 RATIONALE: UX group visibility feature — admin can disable task types globally for all users - [x] T008c [P] Add `getReportsSettings()` to `frontend/src/lib/api/reports.ts` New: `export async function getReportsSettings(): Promise` → `GET /api/settings/reports` New: `export async function updateReportsSettings(settings: ReportsSettings): Promise` → `PUT /api/settings/reports` import `ReportsSettings` from `$types/reports` ### Foundational Verification - [x] T009 Verify foundational phase — run `cd backend && python -m ruff check .`, `cd frontend && npm run lint`, `cd backend && source .venv/bin/activate && python -m pytest backend/tests/ -k "reports" -v` All existing tests must pass. New schemas must not break existing code. **Checkpoint**: Types, schemas, RBAC filter, and API client ready — user stories can now begin. --- ## Phase 3: User Story 1 — Сводный обзор всех задач (Priority: P1) 🎯 MVP **Goal**: На странице `/reports` появляется сводная панель над списком отчётов с агрегированными счётчиками по статусам (pending, running, awaiting_input, success, failed) для каждого типа задач, обновляемая в реальном времени через WebSocket. **Independent Test**: Открыть `/reports` → сводная панель с карточками тип×статус, счётчики корректны, при изменении статуса задачи через API счётчики обновляются без перезагрузки страницы. ### Tests for User Story 1 (write FIRST, expect FAIL) - [x] T010 [P] [US1] Write L1 model test for `TaskCenterModel` invariants in `frontend/src/lib/models/__tests__/TaskCenterModel.test.ts` @TEST_INVARIANT: active tasks sorted before completed, visibleSummary excludes disabledTypes AND not in visibleTypes @TEST_EDGE: empty_tasks→summary_shows_zeros, ws_disconnect→screenState='disconnected', ws_reconnect→screenState='ready' @TEST_EDGE: toggleTypeVisibility→localStorage_updated, admin_disabled_type→hidden_from_visibleSummary Use vitest, no DOM render (~27ms target). Mock `fetchApi` and `WebSocket`. - [x] T011 [P] [US1] Write contract test for `Reports.SummaryService.get_summary()` in `backend/tests/services/reports/test_report_service.py` @TEST_EDGE: admin_role→all_tasks_counted, analyst_role→own_plus_system_counted, viewer_role→only_own_counted @TEST_EDGE: empty_tasks→TaskSummary(total_tasks=0, active_tasks=0, by_type=[]) @TEST_CONTRACT: `TaskSummary` invariants — all counts ≥0, total == sum of all status counts across all types ### Backend Implementation for User Story 1 - [x] T012 [US1] Implement `ReportsService.get_summary()` method in `backend/src/services/reports/report_service.py` @PRE: task_manager initialized, current_user authenticated with tasks:READ @POST: TaskSummary with counts filtered by user role — aggregate `get_all_tasks()` → normalize → group by (task_type × status) → TaskTypeSummary per type @SIDE_EFFECT: None (read-only aggregation) RATIONALE: RBAC filtering in service layer (R4), separate method for independent caching (R1) REJECTED: combining into list_reports header, WebSocket-only delivery for initial load Use `_filter_tasks_by_rbac(tasks, current_user)` from T006, then `resolve_task_type(task.plugin_id)` for type grouping - [x] T013 [US1] Add `GET /api/reports/summary` endpoint in `backend/src/api/routes/reports.py` @PRE: current_user authenticated via `Depends(get_current_user)` @POST: returns `TaskSummary` — summary counts reflect RBAC (admin→all, analyst→own+system, viewer→own) RATIONALE: separate endpoint from list_reports (R1) — lightweight, independently cacheable REJECTED: combining summary into list_reports header, WebSocket-only summary delivery Route: `@router.get("/summary", response_model=TaskSummary)`, inject `task_manager` + `clean_release_repo` dependencies - [x] T014 [US1] Enhance `ReportsService.list_reports()` in `backend/src/services/reports/report_service.py` with RBAC row-level filtering @PRE: current_user passed to service method @POST: ReportCollection.items filtered by user role (same rules as summary) Add `current_user` parameter. Call `_filter_tasks_by_rbac()` in `_load_normalized_reports()` before normalization. RATIONALE: TSC-FR-009 requires RBAC on both summary AND list — not just gate, but row-level visibility REJECTED: separate admin/user list endpoints — single endpoint with role-based filtering avoids route duplication ### Frontend Model (core — all stories depend on it) - [x] T015 [US1] Implement `TaskCenterModel` in `frontend/src/lib/models/TaskCenterModel.svelte.ts` @STATE: `tasks`, `summary`, `filters`, `screenState`, `wsConnected`, `selectedTaskId`, `page`, `pageSize`, `disabledTypes` (admin-disabled, from backend), `visibleTypes` (operator preference, from localStorage), `lastUpdated` @ACTION: `loadInitialData(initialFilters?)` — parallel fetch summary + list + settings, then connect WS @ACTION: `connectWebSocket()` — subscribe to `getTaskEventsWsUrl()`, buffer events, 100ms debounce flush @ACTION: `disconnectWebSocket()`, `destroy()` — cleanup WS, timers @ACTION: `toggleTypeVisibility(type)` — toggle type in `visibleTypes`, persist to localStorage @INVARIANT: active tasks (PENDING, RUNNING, AWAITING_INPUT) sorted before completed tasks in `$derived filteredTasks` @INVARIANT: `visibleSummary` excludes types in `disabledTypes` (admin) AND not in `visibleTypes` (operator) @INVARIANT: DECOMPOSITION GATE: 400 lines, 40 methods (ADR-0010) @SIDE_EFFECT: connects to `/ws/task-events` WebSocket, calls `ReportsApi`, updates browser URL via `history.replaceState()`, reads/writes `localStorage` for `visibleTypes` @POST: all public state-mutating methods trigger `$derived` recomputation @POST: WS lifecycle managed: connect on `loadInitialData`, disconnect on `destroy` RATIONALE: single model (R3) — 3 stories share the same task dataset; ~400 lines, at ADR-0010 gate REJECTED: submodels from day one, global store (page-scoped state), polling-based refresh Private: `_eventBuffer`, `_flushTimeout`, `_reconnectAttempt`(0), `_reconnectTimer`, `_scheduleReconnect()` with exponential backoff 1s→2s→4s→8s→max 30s ### Frontend Components (US1) - [x] T016 [US1] Implement `SummaryPanel` in `frontend/src/lib/components/reports/SummaryPanel.svelte` @UX_STATE: `loading`→skeleton grid (4 cards with pulse animation), `ready`→grid of clickable cards (2-6 cols responsive), `empty`→all cards show zero counts, `disconnected`→cards visible with reduced opacity + "disconnected" badge @UX_FEEDBACK: count change → CSS `transition: all 200ms` on count numbers, card click → calls `onFilterByTypeAndStatus(taskType, status)` @UX_RECOVERY: disconnected → auto-reconnect indicator, manual reconnect via `onReconnect` prop Props: `summary: TaskSummary|null`, `screenState: ScreenState`, `onFilterByTypeAndStatus: (TaskType, ReportStatus)=>void` Uses `` from `$lib/ui/Card.svelte` (existing). Colors per ux_reference.md: running→token `primary`, pending→`muted`, success→`success`, failed→`destructive`, awaiting_input→`warning`. @POST: clicking card emits `onFilterByTypeAndStatus(taskType, status)` — will be wired to model in Phase 4 (US2) - [x] T017 [US1] Add WebSocket connection indicator to `frontend/src/routes/reports/+page.svelte` (inline in page header) Green dot `●` when `m.wsConnected===true`, yellow pulsing `◉` when reconnecting, red `○` when disconnected Uses semantic tokens: `bg-success` / `bg-warning` / `bg-destructive` in a `w-3 h-3 rounded-full` span Shows tooltip on hover: "Подключено / Переподключение / Соединение потеряно" ### Frontend Page (US1 — thin render layer) - [x] T018 [US1] Create `frontend/src/routes/reports/+page.ts` load function Parse `url.searchParams` into initial `TaskCenterFilter`: `task_types` (comma-sep→array), `statuses` (comma-sep→array), `search`, `time_range`, `sort_by`, `sort_order` Return `{ initialFilters }` for the page component RATIONALE: R5 — filter state survives page refresh via URL query params - [x] T019 [US1] Rewrite `frontend/src/routes/reports/+page.svelte` as thin render layer Layout: `` + refresh `` ### Verification for User Story 1 - [x] T020 [US1] Write L2 UX test for `SummaryPanel` in `frontend/src/lib/components/reports/__tests__/SummaryPanel.test.ts` @UX_TEST: render_with_summary→cards_visible, click_card→calls_onFilterByTypeAndStatus, loading_state→skeleton_visible, zero_counts→cards_show_0, disconnected_state→opacity_reduced - [x] T021 [US1] Run US1 verification — `cd backend && source .venv/bin/activate && python -m pytest backend/tests/services/reports/ backend/tests/api/routes/test_reports.py -v`, `cd frontend && npm run test -- TaskCenterModel SummaryPanel`, `cd frontend && npm run lint` Confirm: summary endpoint returns correct counts, RBAC filtering works per role, model invariants hold, SummaryPanel renders skeletons/empty/ready states - [x] T022 [US1] Semantic audit — manually verify against `ux_reference.md` §3 states: - `loading` → full-page skeleton (pulse animation) - `empty` → icon + "Нет активных задач" + navigation hints (inbox icon from ``) - `disconnected` → data visible with red indicator + reconnect banner - `error` → error banner + "Повторить загрузку" button - `idle` → summary cards + list, green connection indicator **Checkpoint**: Summary dashboard functional — open `/reports`, see aggregated counts, watch them update via WebSocket. --- ## Phase 4: User Story 2 — Детальный список задач с фильтрацией (Priority: P2) **Goal**: Оператор фильтрует список задач по типу, статусу, тексту и времени; кликает по карточке сводки → список фильтруется по тип×статус; активные задачи всегда сверху. **Independent Test**: На `/reports` кликнуть по карточке сводки «Миграция / Выполняется (3)» → список показывает только выполняющиеся миграции, остальные скрыты. ### Frontend Implementation for User Story 2 - [x] T023 [US2] Extend `TaskCenterModel` in `frontend/src/lib/models/TaskCenterModel.svelte.ts` — add filter logic + URL sync @ACTION: `applyFilter(partial: Partial)` — merges partial into `filters`, resets `page` to 1, calls `_syncFiltersToUrl()` @ACTION: `clearFilters()` — resets `filters` to defaults, calls `_syncFiltersToUrl()` Private `_syncFiltersToUrl()`: serializes non-default filters to URL query params via `history.replaceState(null, '', url)`, debounced 300ms `filteredTasks` $derived: apply `task_types[]`, `statuses[]`, `search` (match on summary + task_id + source_ref), `time_range` (1h/24h/7d/30d/all → compute time_from from now) RATIONALE: R5 — URL query params survive refresh, `replaceState` avoids back-button pollution @TEST_EDGE: apply_type_filter→only_matching_types, clearFilters→all_tasks_restored, search_empty_string→no_filtering - [x] T024 [US2] Implement `FilterBar` in `frontend/src/lib/components/reports/FilterBar.svelte` @UX_STATE: `default`→all filters empty, showing all tasks; `filtered`→active filter badges visible with "✕" clear button @UX_FEEDBACK: filter change → immediate visual indicator on active filter badges (badge with count), empty result → "Нет задач, соответствующих фильтрам" + "Сбросить фильтры" button Contains: task type ``, text `` (from `$lib/ui/Input.svelte` existing), time range ``, sort_order toggle button Props: `filters: TaskCenterFilter`, `onApplyFilter(partial)`, `onClearFilters()` Uses semantic tokens: `bg-surface-card border border-border text-text` for the bar container STATUS 2026-07-05: Completed; `FilterBar.svelte` created and wired into `/reports` page. - [x] T025 [US2] Enhance `TaskList` in `frontend/src/lib/components/reports/TaskList.svelte` (modify existing) @UX_STATE: `loading`→10 skeleton rows, `empty`→`` with "Нет задач" (inbox icon + links "Запустить перевод/миграцию/бекап"), `ready`→task rows with pagination @UX_STATE: `filtered_empty`→"Нет задач, соответствующих фильтрам" + "Сбросить фильтры" button @UX_FEEDBACK: status change → animated badge transition (`transition: background-color 300ms`), task completion → brief row highlight (green flash SUCCESS, red flash FAILED), new task → slide-in from top Each row shows: type icon (via `getReportTypeProfile`), task_id/summary, status `` badge (inline Tailwind: `rounded-full px-2.5 py-0.5 text-xs font-medium` with colors: success→`bg-success-light text-success`, failed→`bg-destructive-light text-destructive`, running→`bg-primary/10 text-primary`, pending→`bg-surface-muted text-text-muted`, awaiting_input→`bg-warning-light text-warning`), relative time ("2 мин назад"), duration, source_ref display Pagination: prev/next buttons + "Page X of Y" display. Uses `` that opens TaskDrawer with input form FAILED task visual: destructive left border ring via `border-l-4 border-destructive-ring` Uses `