402 lines
31 KiB
Markdown
402 lines
31 KiB
Markdown
# 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
|