Files
ss-tools/specs/034-task-status-center/tasks.md

402 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<TaskSummary>``GET /api/reports/summary` via `api.fetchApi`
Enhance: `getReports()` returns `Promise<ReportCollection>`, `getReportDetail()` returns `Promise<ReportDetailView>` (replace generic `<T>`)
@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<ReportsSettings>``GET /api/settings/reports`
New: `export async function updateReportsSettings(settings: ReportsSettings): Promise<ReportsSettings>``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 `<Card>` 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: `<PageHeader title="Центр статусов">` + refresh `<Button variant="ghost">` (calls `m.refreshSummary()`)
Below: `<SummaryPanel>`, existing `<ReportCard>`-based list (placeholder for US2-enhanced `<TaskList>`), `<ConnectionIndicator>`
Imports and instantiates `TaskCenterModel`, calls `m.loadInitialData(data.initialFilters)` in `onMount`, cleans up via `$effect` return
Model instance: `const m = new TaskCenterModel()`
RATIONALE: thin render layer per ADR-0006 model-first pattern — all state/logic in Model
Uses `<PageHeader>` from `$lib/ui/PageHeader.svelte` (existing), `<Button variant="ghost">` from `$lib/ui/Button.svelte` (existing)
Disconnected banner: `<div class="bg-warning-light border border-warning-ring text-warning p-3 rounded-lg mb-4">` with text "Соединение потеряно. Попытка переподключения..." + `<Button variant="secondary">Подключиться сейчас</Button>`
### 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 `<Icon name="inbox">`)
- `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<TaskCenterFilter>)` — 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 `<Select multiple>` (from `$lib/ui/Select.svelte` existing), status `<Select multiple>`, text `<Input type="search">` (from `$lib/ui/Input.svelte` existing), time range `<Select>`, sort_by `<Select>`, 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``<EmptyState>` 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 `<span>` 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 `<Button variant="secondary">` from `$lib/ui`
Sort: active tasks (PENDING, RUNNING, AWAITING_INPUT) always sorted before completed
Props: `tasks: TaskReport[]`, `screenState: ScreenState`, `selectedTaskId: string|null`, `page: number`, `onSelectTask(taskId)`, `onPageChange(n)`
Uses `<EmptyState>` from `$lib/ui/EmptyState.svelte` (existing)
STATUS 2026-07-05: Completed; enhanced `TaskList.svelte` created and wired into `/reports` page.
- [x] T026 [US2] Wire `SummaryPanel` click → filter in `+page.svelte`
Update `<SummaryPanel onFilterByTypeAndStatus={...}>` callback to call `m.applyFilter({ task_types: [type], statuses: [status] })`
Integrate `<FilterBar>` and enhanced `<TaskList>` into page layout below `<SummaryPanel>`
Page structure: `PageHeader → SummaryPanel → FilterBar → TaskList`
### Verification for User Story 2
- [x] T027 [US2] Write L2 UX test for `FilterBar` in `frontend/src/lib/components/reports/__tests__/FilterBar.test.ts`
@UX_TEST: select_type→calls_onApplyFilter_with_task_types, select_status→calls_onApplyFilter_with_statuses, type_search→calls_onApplyFilter_with_search, click_clear→calls_onClearFilters, active_badge→visible_with_count
STATUS 2026-07-05: Completed; `FilterBar.test.ts` covers type/status/search/clear UX.
- [x] T028 [US2] Run US2 verification — `cd frontend && npm run test -- FilterBar TaskList TaskCenterModel`, `cd frontend && npm run lint`
Confirm: type filter shows only matching tasks, status filter works, search matches summary/task_id, time range filters correctly, active tasks always sorted first, pagination controls work, clear filters restores full list
**Checkpoint**: Full filtering working — summary cards are clickable filters, multi-select dropdowns work, search and time range functional.
---
## Phase 5: User Story 3 — Быстрый переход к логам и результатам задачи (Priority: P3)
**Goal**: Клик по задаче открывает Task Drawer с логами; hover показывает тултип с деталями; задачи AWAITING_INPUT отображаются с индикатором и кнопкой «Ответить».
**Independent Test**: На `/reports` кликнуть по FAILED задаче → открывается Task Drawer с логами, скролл к последней ошибке.
### Implementation for User Story 3
- [x] T029 [US3] Implement `TaskCenterModel.selectTask()` in `frontend/src/lib/models/TaskCenterModel.svelte.ts` — integrate with existing TaskDrawer
@ACTION: `selectTask(taskId)` — sets `selectedTaskId`, calls `taskDrawerStore.openDrawerForTask(taskId)` from `$lib/stores/taskDrawer.svelte.ts` (existing)
@SIDE_EFFECT: opens Task Drawer (existing component) which connects to `/ws/logs/{taskId}` for live log streaming
Import: `import { openDrawerForTask } from '$lib/stores/taskDrawer.svelte.ts'`
- [x] T030 [US3] Enhance `TaskList` row template in `frontend/src/lib/components/reports/TaskList.svelte` — add hover tooltip + click handler + awaiting_input indicator
Click: `onclick={() => onSelectTask(task.report_id)}` — calls model's `selectTask` which opens TaskDrawer
Hover tooltip: `<div class="absolute z-10 bg-surface-card border border-border rounded-lg shadow-lg p-3 text-sm">` showing task_type label, started_at, duration, key param (extract from `source_ref` or `details`). Shown on `mouseenter`, hidden on `mouseleave`. Position: above row.
AWAITING_INPUT indicator: task with status `in_progress` + has `error_context.code === 'AWAITING_INPUT'` → show yellow `<span class="bg-warning-light text-warning rounded-full px-2 py-0.5 text-xs">Ожидает ввода</span>` + small `<Button variant="secondary" size="sm">Ответить</Button>` that opens TaskDrawer with input form
FAILED task visual: destructive left border ring via `border-l-4 border-destructive-ring`
Uses `<Button>` from `$lib/ui` (existing), `<Icon>` from `$lib/ui` (existing)
STATUS 2026-07-05: Completed; `TaskList.svelte` renders hover tooltip, AWAITING_INPUT action, and failed left border.
- [x] T031 [US3] Ensure Task Drawer scrolls to last error for FAILED tasks
In `TaskList` click handler: when task status is `failed`, pass `scrollTo: 'last_error'` hint to `openDrawerForTask`
RATIONALE: spec US3 acceptance criterion #1 — "курсор прокручивается к последней ошибке"
If TaskDrawer doesn't support this yet, add `scrollToHint` param to `openDrawerForTask()` in `taskDrawer.svelte.ts`
### Verification for User Story 3
- [x] T032 [US3] Write L2 UX test for TaskList drill-down in `frontend/src/lib/components/reports/__tests__/TaskList.test.ts` (extend existing)
@UX_TEST: click_failed_task→opens_task_drawer, hover_row→shows_tooltip, awaiting_input_task→shows_indicator_and_button, failed_task→has_destructive_left_border
STATUS 2026-07-05: Completed; `TaskList.test.ts` covers click, hover tooltip, awaiting input, and failed styling.
- [x] T033 [US3] Run US3 verification — `cd frontend && npm run test -- TaskList TaskCenterModel`, manual smoke: click task → TaskDrawer opens, hover → tooltip appears
**Checkpoint**: Full drill-down functional — click any task to open logs, hover for details, awaiting_input tasks interactive.
---
## Phase 6: Polish & Cross-Cutting Concerns
**Purpose**: Regression defense, lint/build gates, semantic index health.
### Regression Tests
- [x] T034 [P] Write rejected-path regression test for RBAC filtering in `backend/tests/services/reports/test_report_service.py`
@TEST_EDGE: viewer_sees_only_own_tasks→other_users_tasks_excluded, analyst_sees_own_and_system→admin_tasks_excluded, no_role_user→empty_list
@TEST_EDGE: task_with_null_user_id→analyst_can_see, task_with_null_user_id→viewer_cannot_see
Verify that REJECTED patterns (inline filtering, graph-level filtering) are NOT implemented — test checks `_filter_tasks_by_rbac()` is used
STATUS 2026-07-05: Completed; `backend/tests/services/test_report_service.py` covers viewer/analyst/admin list filtering and summary filtering.
- [x] T035 [P] Write WebSocket reconnect regression test in `frontend/src/lib/models/__tests__/TaskCenterModel.test.ts`
@TEST_EDGE: ws_close_abnormal→screenState='reconnecting', ws_reconnect_success→screenState='ready', ws_max_retries→screenState='disconnected', ws_close_normal→screenState='disconnected'
Verify exponential backoff: 1s, 2s, 4s, 8s, max 30s (mock timers)
STATUS 2026-07-05: Completed; `TaskCenterModel.test.ts` covers 1s→2s reconnect delays and reconnect success reset.
- [x] T036 Run full backend tests — `cd backend && source .venv/bin/activate && python -m pytest -v`
All tests must pass. No regressions in non-reports modules.
- [x] T037 Run full frontend tests — `cd frontend && npm run test`
All tests pass. No regressions in unrelated components.
### Lint & Build Gates
- [x] T038 Run `cd backend && python -m ruff check .` — zero new violations
- [x] T039 Run `cd frontend && npm run lint` — zero new violations
- [x] T040 Run `cd frontend && npm run build` — production build succeeds (static adapter, SPA mode)
### Admin Settings UI
- [x] T040a Add "Отчёты" section to settings page `frontend/src/routes/settings/+page.svelte` (or sub-route)
Admin-only section with checkboxes for each `TaskType` — controls `disabled_task_types` globally
Uses `getReportsSettings()` + `updateReportsSettings()` from T008c
@UX_STATE: loading→spinner, loaded→checkboxes, saving→button spinner, error→toast
Save button hidden for non-admins (check `settings:WRITE` permission)
RATIONALE: UX group visibility feature — admin can disable task types for all users
STATUS 2026-07-05: Completed; `ReportsSettings.svelte` added as Settings → Отчёты tab and uses reports settings API.
### Semantic Health
- [x] T041 [P] **Attention compliance audit**: verify contracts pass ATTN_1 (first-line density), ATTN_2 (hierarchical IDs `Reports.*`, `TaskCenter*`), ATTN_3 (`@SEMANTICS` keyword consistency — all contracts use `task-status-center`), ATTN_4 (each contract ≤150 lines, each module ≤400 lines)
- [x] T042 [P] **Semantic index rebuild**: run `axiom_semantic_index rebuild rebuild_mode="full"` — confirm 0 parse warnings from feature files
- [x] T043 [P] **Orphan audit**: run `axiom_semantic_context workspace_health` — confirm no new orphan contracts introduced
---
## Dependencies & Execution Order
### Phase Dependencies
```
Phase 1 (Setup) ──► Phase 2 (Foundational) ──┬──► Phase 3 (US1) ──► Phase 4 (US2) ──► Phase 5 (US3)
└──► Phase 6 (Polish — after all stories)
```
### Within User Story Dependencies
- **US1**: T010,T011 (tests) → T012,T013,T014 (backend) ∥ T015 (model) → T016,T017 (components) → T018,T019 (page) → T020,T021,T022 (verify)
- **US2**: T023 (model enhance) ∥ T024 (FilterBar) ∥ T025 (TaskList) → T026 (wire) → T027,T028 (verify)
- **US3**: T029 (model) ∥ T030 (TaskList enhance) ∥ T031 (Drawer scroll) → T032,T033 (verify)
### Parallel Opportunities
| Phase | Can run in parallel |
|-------|---------------------|
| Phase 2 | T003 ∥ T004 ∥ T005 (different files, different stacks) |
| US1 tests | T010 ∥ T011 (different stacks) |
| US1 components | T016 ∥ T017 (different files) |
| US2 components | T023 ∥ T024 ∥ T025 (different files) |
| US3 | T029 ∥ T030 ∥ T031 (different concerns) |
| Polish | T034 ∥ T035 (different stacks), T041 ∥ T042 ∥ T043 |
### MVP Delivery
1. Complete Phase 1 + Phase 2
2. Complete Phase 3 (US1)
3. **STOP and VALIDATE**: Summary dashboard works with real-time WebSocket updates
4. Deploy/demo if ready
---
## Implementation Strategy
### Incremental Delivery
| Delivery | Scope | Value |
|----------|-------|-------|
| MVP | US1 | Summary dashboard with real-time counts |
| +1 | +US2 | Full filtering, clickable summary cards, search |
| +2 | +US3 | Task Drawer integration, tooltips, awaiting_input |
### Notes
- `[P]` — parallelizable (different files, no shared state)
- `[USx]` — user story tag for traceability
- C4+ tasks have inlined contract constraints (`@PRE`, `@POST`, `@TEST_EDGE`, `RATIONALE`/`REJECTED`)
- Model-first frontend: types → model → L1 test → components → L2 UX test
- Never use `writable()` — all new state uses `$state` in `.svelte.ts`
- Never use raw Tailwind colors — use semantic tokens (`bg-surface-card`, `text-text`, `bg-destructive-light`, etc.)
- After every file write, verify anchor pairs with `read_outline` per anti-corruption protocol