Страница /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
475 lines
22 KiB
Markdown
475 lines
22 KiB
Markdown
#region ModuleContracts [C:5] [TYPE ADR] [SEMANTICS contracts, modules, grace, task-status-center, backend, frontend]
|
||
@BRIEF GRACE-Poly v2.6 module contracts for the Task Status Center feature — backend API, service, and frontend Model/Component contracts with complexity anchors, decision memory, and cross-stack relations.
|
||
@RATIONALE Contracts are the executable specification bridging spec.md ↔ code. Without them, agents optimize for token likelihood rather than spec compliance.
|
||
@RELATION DEPENDS_ON -> [ReportModels]
|
||
@RELATION DEPENDS_ON -> [TaskCenterModel]
|
||
@RELATION DEPENDS_ON -> [TaskDrawer]
|
||
|
||
## 1. Complexity Inventory
|
||
|
||
| Module | Language | Complexity | Justification |
|
||
|--------|----------|:----------:|---------------|
|
||
| `Reports.Summary` (api route) | Python | C4 | Multi-step: RBAC check, task loading, normalization, aggregation. Side effects: DB read. |
|
||
| `Reports.SummaryService` (service) | Python | C4 | Stateful: accesses TaskManager singleton, applies RBAC filtering. |
|
||
| `Reports.List` (api route, enhanced) | Python | C4 | Enhanced with RBAC row-filtering. Existing C3→C4. |
|
||
| `TaskCenterModel` (model) | TypeScript/Svelte | C4 | Stateful: 15+ atoms, WebSocket lifecycle, debounce, URL sync. Side effects: WS connect/disconnect, REST calls. |
|
||
| `SummaryPanel` (component) | Svelte 5 | C4 | WebSocket-reactive derived display. UX states: idle, loading, empty, disconnected. Side effects: click → filter. |
|
||
| `FilterBar` (component) | Svelte 5 | C3 | Multi-select inputs, search, time range. No side effects beyond model mutation. |
|
||
| `TaskList` (component) | Svelte 5 | C4 | Pagination, sort, click→drawer. UX states: loading, empty, error. |
|
||
| `TaskCenterReportsPage` (page) | Svelte 5 | C4 | Page-level orchestration: Model init, layout, UX state machine. |
|
||
| `ReportsTypes` (types) | TypeScript | C2 | DTO definitions only. |
|
||
|
||
---
|
||
|
||
## 2. Backend Contracts
|
||
|
||
### 2.1 Reports.SummaryRouter [C:4] [TYPE Module]
|
||
|
||
```python
|
||
# backend/src/api/routes/reports.py (ADD to existing)
|
||
|
||
# #region Reports.SummaryRouter [C:4] [TYPE Module] [SEMANTICS api, reports, summary, task-status-center]
|
||
# @ingroup Reports
|
||
# @BRIEF Task summary endpoint — aggregated counts by type × status with RBAC filtering.
|
||
# @RELATION CALLS -> [Reports.SummaryService]
|
||
# @RELATION DEPENDS_ON -> [TaskSummary]
|
||
# @RELATION DEPENDS_ON -> [Auth.Dependencies]
|
||
# @RATIONALE Separate endpoint from list_reports (R1) — summary is a lightweight aggregation suitable for independent caching.
|
||
# @REJECTED Combining summary into list_reports header — rejected because it bloats paginated list responses and doubles server work.
|
||
# @REJECTED WebSocket-only summary delivery — rejected because initial page load needs guaranteed REST delivery (SC-001).
|
||
|
||
@router.get("/summary", response_model=TaskSummary)
|
||
async def get_task_summary(
|
||
current_user: User = Depends(get_current_user),
|
||
task_manager: TaskManager = Depends(get_task_manager),
|
||
clean_release_repo=Depends(get_clean_release_repository),
|
||
) -> TaskSummary:
|
||
"""
|
||
Get aggregated task summary by type and status.
|
||
|
||
Permission: tasks:READ (checked via get_current_user).
|
||
RBAC filtering: admin sees all, analyst sees own+system, viewer sees own only.
|
||
"""
|
||
...
|
||
# #endregion Reports.SummaryRouter
|
||
```
|
||
|
||
### 2.2 Reports.SummaryService [C:4] [TYPE Module]
|
||
|
||
```python
|
||
# backend/src/services/reports/report_service.py (ADD method)
|
||
|
||
# #region Reports.SummaryService [C:4] [TYPE Module] [SEMANTICS service, summary, aggregation, rbac, task-status-center]
|
||
# @ingroup Reports
|
||
# @BRIEF Compute aggregated task summary with RBAC-aware filtering.
|
||
# @RELATION CALLS -> [TaskManager.get_all_tasks]
|
||
# @RELATION CALLS -> [Reports.Normalizer]
|
||
# @PRE task_manager is initialized and has tasks loaded.
|
||
# @PRE current_user is an authenticated User with tasks:READ permission.
|
||
# @POST Returns TaskSummary with counts filtered by user's role.
|
||
# @SIDE_EFFECT None (read-only aggregation).
|
||
# @RATIONALE RBAC filtering happens here (R4) — service layer, not API layer, keeps business logic testable.
|
||
# @REJECTED Filtering at TaskGraph level — rejected because graph is a data structure, not an authorization boundary.
|
||
|
||
def get_summary(self, current_user: User) -> TaskSummary:
|
||
"""
|
||
Compute aggregated task counts by (task_type × status).
|
||
|
||
RBAC rules:
|
||
- admin: all tasks
|
||
- analyst: own tasks + tasks with user_id=None (system)
|
||
- viewer: own tasks only
|
||
"""
|
||
...
|
||
# #endregion Reports.SummaryService
|
||
```
|
||
|
||
### 2.3 Reports.ListEnhanced [C:4] [TYPE Module]
|
||
|
||
```python
|
||
# backend/src/api/routes/reports.py (MODIFY existing)
|
||
|
||
# #region Reports.ListEnhanced [C:4] [TYPE Module] [SEMANTICS api, reports, list, rbac, task-status-center]
|
||
# @ingroup Reports
|
||
# @BRIEF Enhanced report list endpoint with RBAC row-level filtering.
|
||
# @PRE current_user is authenticated.
|
||
# @POST Returns ReportCollection filtered by user role.
|
||
# @RATIONALE Enhancement of existing C3 endpoint to C4 — adds RBAC filtering per TSC-FR-009.
|
||
# @REJECTED Separate admin/user endpoints — rejected because single endpoint with role-based filtering avoids route duplication.
|
||
|
||
@router.get("", response_model=ReportCollection)
|
||
async def list_reports(
|
||
# ... existing params ...
|
||
current_user: User = Depends(get_current_user),
|
||
# ...
|
||
) -> ReportCollection:
|
||
# Unchanged signature, enhanced implementation:
|
||
# - task_filter applies RBAC before normalization
|
||
# - sort_by now accepts "created_at"
|
||
...
|
||
# #endregion Reports.ListEnhanced
|
||
```
|
||
|
||
### 2.4 RBAC Task Filter [C:3] [TYPE Function]
|
||
|
||
```python
|
||
# backend/src/services/reports/report_service.py (ADD helper)
|
||
|
||
# #region Reports.RbacTaskFilter [C:3] [TYPE Function] [SEMANTICS service, rbac, filter, task-status-center]
|
||
# @ingroup Reports
|
||
# @BRIEF Filter tasks by user role for RBAC compliance.
|
||
# @PRE current_user is an authenticated User object.
|
||
# @POST Returns list of tasks visible to the user.
|
||
# @RATIONALE Extracted as a pure function for testability — no side effects, no service dependencies.
|
||
# @REJECTED Inline filtering in list_reports — rejected because it mixes authorization with query logic, making unit testing harder.
|
||
|
||
def _filter_tasks_by_rbac(tasks: list[Task], current_user: User) -> list[Task]:
|
||
"""
|
||
Filter tasks based on user role.
|
||
|
||
Rules (from TSC-FR-009):
|
||
- admin → all tasks
|
||
- analyst → own tasks + system tasks (user_id=None)
|
||
- viewer → own tasks only
|
||
"""
|
||
if any(role.name == "Admin" or getattr(role, "is_admin", False) for role in current_user.roles):
|
||
return tasks
|
||
if any(role.name == "analyst" for role in current_user.roles if not getattr(role, "is_admin", False)):
|
||
return [t for t in tasks if t.user_id == current_user.id or t.user_id is None]
|
||
# viewer
|
||
return [t for t in tasks if t.user_id == current_user.id]
|
||
# #endregion Reports.RbacTaskFilter
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Frontend Contracts
|
||
|
||
### 3.1 TaskCenterModel [C:4] [TYPE Model]
|
||
|
||
```typescript
|
||
// frontend/src/lib/models/TaskCenterModel.svelte.ts (NEW FILE)
|
||
|
||
// #region TaskCenterModel [C:4] [TYPE Model] [SEMANTICS model, task-center, summary, filtering, websocket, task-status-center]
|
||
// @BRIEF Screen Model for the Task Status Center — aggregates summary, task list, filtering, WebSocket subscription, and Task Drawer integration.
|
||
// @LAYER Store
|
||
// @PRE Auth token in localStorage, user authenticated via root +layout.svelte.
|
||
// @INVARIANT Active tasks (pending, running, awaiting_input) are always sorted before completed tasks.
|
||
// @INVARIANT visibleSummary excludes types in disabledTypes (admin) AND not in visibleTypes (operator).
|
||
// @INVARIANT DECOMPOSITION GATE: 400 lines, 40 methods (ADR-0010).
|
||
// @STATE tasks, summary, filters, screenState, wsConnected, selectedTaskId, page, pageSize, disabledTypes, visibleTypes, lastUpdated
|
||
// @ACTION loadInitialData, applyFilter, clearFilters, selectTask, refreshSummary, connectWebSocket, disconnectWebSocket, destroy, toggleTypeVisibility
|
||
// @RELATION CALLS -> [ReportsApi]
|
||
// @RELATION CALLS -> [ApiModule.getTaskEventsWsUrl]
|
||
// @RELATION BINDS_TO -> [TaskDrawerStore]
|
||
// @RELATION BINDS_TO -> [SummaryPanel]
|
||
// @RELATION BINDS_TO -> [FilterBar]
|
||
// @RELATION BINDS_TO -> [TaskList]
|
||
// @DATA_CONTRACT Input: TaskCenterFilter → Output: filteredTasks: TaskReport[], visibleSummary: TaskSummary
|
||
// @POST All public methods that mutate state trigger $derived recomputation.
|
||
// @POST WebSocket connection lifecycle is managed: connect on mount, disconnect on destroy.
|
||
// @SIDE_EFFECT Connects to /ws/task-events WebSocket. Calls fetch API. Updates browser URL via replaceState.
|
||
// @SIDE_EFFECT Opens TaskDrawer via taskDrawerStore.openDrawerForTask(). Reads/writes localStorage for visibleTypes.
|
||
// @RATIONALE Single model (R3) chosen because the 3 user stories are tightly coupled — summary, filtering, and list share the same task dataset. Premature decomposition would add indirection without benefit.
|
||
// @REJECTED Submodels from day one — rejected because estimated model size is ~250 lines, well below ADR-0010 400-line threshold.
|
||
// @REJECTED Global store — rejected because Task Center state is page-scoped, not cross-route.
|
||
// @REJECTED Polling-based refresh — rejected because spec requires real-time WebSocket updates (SC-002).
|
||
|
||
export class TaskCenterModel {
|
||
// === ATOMS ===
|
||
tasks: TaskReport[] = $state([]);
|
||
summary: TaskSummary | null = $state(null);
|
||
filters: TaskCenterFilter = $state({ ...DEFAULT_FILTERS });
|
||
screenState: ScreenState = $state('loading');
|
||
wsConnected: boolean = $state(false);
|
||
selectedTaskId: string | null = $state(null);
|
||
page: number = $state(1);
|
||
pageSize: number = $state(20);
|
||
disabledTypes: TaskType[] = $state([]); // admin-disabled types (from backend)
|
||
visibleTypes: TaskType[] = $state(loadVisibleTypes()); // operator preference (localStorage)
|
||
lastUpdated: Date | null = $state(null);
|
||
|
||
// === DERIVED ===
|
||
get filteredTasks(): TaskReport[] { ... } // filter + sort (active first)
|
||
get visibleSummary(): TaskSummary | null { ... } // exclude disabledTypes AND not in visibleTypes
|
||
get isEmpty(): boolean { ... }
|
||
get isFilteredEmpty(): boolean { ... }
|
||
get hasActiveFilters(): boolean { ... }
|
||
get totalPages(): number { ... }
|
||
get hasNextPage(): boolean { ... }
|
||
|
||
// === LIFECYCLE ===
|
||
async loadInitialData(initialFilters?: Partial<TaskCenterFilter>): Promise<void> { ... }
|
||
connectWebSocket(): void { ... }
|
||
disconnectWebSocket(): void { ... }
|
||
destroy(): void { ... }
|
||
|
||
// === ACTIONS ===
|
||
applyFilter(partial: Partial<TaskCenterFilter>): void { ... }
|
||
clearFilters(): void { ... }
|
||
selectTask(taskId: string): void { ... }
|
||
async refreshSummary(): Promise<void> { ... }
|
||
toggleTypeVisibility(type: TaskType): void { ... } // localStorage persist
|
||
|
||
// === PRIVATE METHODS ===
|
||
private _onWsMessage(event: MessageEvent): void { ... }
|
||
private _flushEventBuffer(): void { ... } // 100ms debounce
|
||
private _scheduleReconnect(): void { ... } // exponential backoff
|
||
private _syncFiltersToUrl(): void { ... } // replaceState, 300ms debounce
|
||
private _buildQueryString(): string { ... }
|
||
}
|
||
// #endregion TaskCenterModel
|
||
```
|
||
|
||
### 3.2 SummaryPanel [C:4] [TYPE Component]
|
||
|
||
```svelte
|
||
<!-- frontend/src/lib/components/reports/SummaryPanel.svelte (NEW FILE) -->
|
||
|
||
<!-- #region SummaryPanel [C:4] [TYPE Component] [SEMANTICS component, summary, dashboard, websocket-reactive, task-status-center] -->
|
||
<!-- @BRIEF Summary dashboard panel — grid of clickable cards showing task counts by type × status. -->
|
||
<!-- @LAYER UI Component -->
|
||
<!-- @UX_STATE idle: cards rendered normally, clickable -->
|
||
<!-- @UX_STATE loading: skeleton placeholders (4 cards with pulse animation) -->
|
||
<!-- @UX_STATE empty: all cards show zero counts -->
|
||
<!-- @UX_STATE disconnected: last known counts visible, opacity reduced, "disconnected" badge -->
|
||
<!-- @UX_STATE reconnecting: last known counts visible, yellow pulsing indicator -->
|
||
<!-- @UX_FEEDBACK Count change: number animates (CSS transition). Card click: emits filter event. -->
|
||
<!-- @UX_RECOVERY Disconnected: auto-reconnect indicator. Click → manual reconnect button. -->
|
||
<!-- @UX_REACTIVITY Props: summary: TaskSummary | null, screenState: ScreenState. Derived: by_type grid. -->
|
||
<!-- @RELATION BINDS_TO -> [TaskCenterModel] -->
|
||
<!-- @PRE summary is non-null when screenState is 'ready' or 'disconnected'. -->
|
||
<!-- @POST Clicking a card calls onFilterByTypeAndStatus(taskType, status). -->
|
||
<!-- @RATIONALE Cards are clickable per spec US2: "click on summary card → filter list by that type×status". -->
|
||
<script lang="ts">
|
||
import type { TaskSummary, TaskType, ReportStatus, ScreenState } from '$types/reports';
|
||
import { Card } from '$lib/ui';
|
||
|
||
interface Props {
|
||
summary: TaskSummary | null;
|
||
screenState: ScreenState;
|
||
onFilterByTypeAndStatus: (taskType: TaskType, status: ReportStatus) => void;
|
||
}
|
||
|
||
let { summary, screenState, onFilterByTypeAndStatus }: Props = $props();
|
||
</script>
|
||
|
||
{#if screenState === 'loading'}
|
||
<!-- skeleton -->
|
||
{:else if summary}
|
||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-3">
|
||
{#each summary.by_type as typeSummary}
|
||
{#each STATUS_KEYS as status}
|
||
<Card padding="sm" onclick={() => onFilterByTypeAndStatus(typeSummary.task_type, status)}>
|
||
<!-- icon + count + status badge -->
|
||
</Card>
|
||
{/each}
|
||
{/each}
|
||
</div>
|
||
{/if}
|
||
<!-- #endregion SummaryPanel -->
|
||
```
|
||
|
||
### 3.3 TaskCenterReportsPage [C:4] [TYPE Page]
|
||
|
||
```svelte
|
||
<!-- frontend/src/routes/reports/+page.svelte (REWRITE from existing) -->
|
||
|
||
<!-- #region TaskCenterReportsPage [C:4] [TYPE Page] [SEMANTICS page, reports, task-center, layout, task-status-center] -->
|
||
<!-- @BRIEF Task Status Center page — redesign of /reports with summary dashboard, real-time updates, and enhanced filtering. -->
|
||
<!-- @LAYER Page -->
|
||
<!-- @UX_STATE loading: full-page skeleton -->
|
||
<!-- @UX_STATE empty: "No tasks" empty state with navigation hints -->
|
||
<!-- @UX_STATE ready: summary panel + filter bar + task list -->
|
||
<!-- @UX_STATE disconnected: data visible with stale indicator + reconnect banner -->
|
||
<!-- @UX_STATE error: error banner + retry button -->
|
||
<!-- @UX_REACTIVITY Model: TaskCenterModel instance. Page is thin render layer. -->
|
||
<!-- @RELATION BINDS_TO -> [TaskCenterModel] -->
|
||
<!-- @RELATION BINDS_TO -> [SummaryPanel] -->
|
||
<!-- @RELATION BINDS_TO -> [FilterBar] -->
|
||
<!-- @RELATION BINDS_TO -> [TaskList] -->
|
||
<!-- @RELATION CALLS -> [TaskDrawer] -->
|
||
<!-- @PRE User is authenticated (enforced by root +layout.svelte). -->
|
||
<!-- @POST Page renders summary, filters, and task list. WebSocket connects automatically. -->
|
||
<!-- @SIDE_EFFECT Initializes TaskCenterModel on mount. Connects WebSocket. Destroys on unmount. -->
|
||
<!-- @RATIONALE Thin render layer per ADR-0006 model-first pattern — all state and logic lives in TaskCenterModel. -->
|
||
<script lang="ts">
|
||
import { TaskCenterModel } from '$lib/models/TaskCenterModel.svelte.ts';
|
||
import SummaryPanel from '$lib/components/reports/SummaryPanel.svelte';
|
||
import FilterBar from '$lib/components/reports/FilterBar.svelte';
|
||
import TaskList from '$lib/components/reports/TaskList.svelte';
|
||
import { PageHeader, Button } from '$lib/ui';
|
||
import { onMount } from 'svelte';
|
||
|
||
let { data } = $props();
|
||
const m = new TaskCenterModel();
|
||
|
||
onMount(() => {
|
||
m.loadInitialData(data.initialFilters);
|
||
});
|
||
|
||
// $effect for cleanup when component unmounts
|
||
$effect(() => {
|
||
return () => m.destroy();
|
||
});
|
||
</script>
|
||
|
||
<PageHeader title="Центр статусов">
|
||
<Button variant="ghost" onclick={() => m.refreshSummary()}>Обновить</Button>
|
||
</PageHeader>
|
||
|
||
{#if m.screenState === 'disconnected'}
|
||
<div class="bg-warning-light border border-warning-ring text-warning p-3 rounded-lg mb-4">
|
||
Соединение потеряно. Попытка переподключения...
|
||
<Button variant="secondary" onclick={() => m.connectWebSocket()}>Подключиться сейчас</Button>
|
||
</div>
|
||
{/if}
|
||
|
||
<SummaryPanel
|
||
summary={m.summary}
|
||
screenState={m.screenState}
|
||
onFilterByTypeAndStatus={(type, status) => m.applyFilter({ task_types: [type], statuses: [status] })}
|
||
/>
|
||
|
||
<FilterBar filters={m.filters} onApplyFilter={m.applyFilter} onClearFilters={m.clearFilters} />
|
||
|
||
<TaskList
|
||
tasks={m.filteredTasks}
|
||
screenState={m.screenState}
|
||
selectedTaskId={m.selectedTaskId}
|
||
page={m.page}
|
||
onSelectTask={m.selectTask}
|
||
onPageChange={(p) => { m.page = p; }}
|
||
/>
|
||
|
||
<!-- #endregion TaskCenterReportsPage -->
|
||
```
|
||
|
||
### 3.4 ReportsApiClient [C:3] [TYPE Module]
|
||
|
||
```typescript
|
||
// frontend/src/lib/api/reports.ts (ENHANCE existing)
|
||
|
||
// #region ReportsApiClient [C:3] [TYPE Module] [SEMANTICS api, client, reports, typed, task-status-center]
|
||
// @BRIEF Typed API client for reports endpoints including the new summary endpoint.
|
||
// @LAYER API Client
|
||
// @RELATION CALLS -> [ApiModule.fetchApi]
|
||
// @RELATION CALLS -> [ApiModule.getTaskEventsWsUrl]
|
||
// @DATA_CONTRACT Input: ReportQuery → Output: ReportCollection
|
||
// @DATA_CONTRACT Input: (none) → Output: TaskSummary
|
||
// @PRE Auth token exists in localStorage.
|
||
// @POST All responses are typed (replaces generic <T>).
|
||
|
||
// EXISTING — add types
|
||
import type { TaskReport, ReportCollection, ReportDetailView, ReportQuery } from '$types/reports';
|
||
|
||
// NEW
|
||
import type { TaskSummary } from '$types/reports';
|
||
|
||
/** Fetch aggregated task summary. */
|
||
export async function getReportsSummary(): Promise<TaskSummary> {
|
||
const response = await api.fetchApi('/reports/summary');
|
||
return response.json();
|
||
}
|
||
|
||
// EXISTING — enhance with types
|
||
export async function getReports(options: Partial<ReportQuery>): Promise<ReportCollection> {
|
||
const query = buildReportQueryString(options);
|
||
const response = await api.fetchApi(`/reports${query}`);
|
||
return response.json();
|
||
}
|
||
|
||
export async function getReportDetail(reportId: string): Promise<ReportDetailView> {
|
||
const response = await api.fetchApi(`/reports/${reportId}`);
|
||
return response.json();
|
||
}
|
||
// #endregion ReportsApiClient
|
||
```
|
||
|
||
### 3.5 FilterBar [C:3] [TYPE Component]
|
||
|
||
```svelte
|
||
<!-- frontend/src/lib/components/reports/FilterBar.svelte (NEW FILE — replaces inline filters in +page.svelte) -->
|
||
|
||
<!-- #region FilterBar [C:3] [TYPE Component] [SEMANTICS component, filter, search, multi-select, task-status-center] -->
|
||
<!-- @BRIEF Horizontal filter bar: task type multi-select, status multi-select, text search, time range, sort controls. -->
|
||
<!-- @LAYER UI Component -->
|
||
<!-- @UX_STATE default: all filters empty, showing all tasks -->
|
||
<!-- @UX_STATE filtered: active filters visible with "clear" button -->
|
||
<!-- @UX_FEEDBACK Filter change: immediate visual indicator on active filter badges -->
|
||
<!-- @UX_REACTIVITY Props: filters: TaskCenterFilter, onApplyFilter(), onClearFilters(). Pure presentation. -->
|
||
<!-- @RELATION BINDS_TO -> [TaskCenterModel] -->
|
||
<!-- @PRE filters is a valid TaskCenterFilter object. -->
|
||
<!-- @POST Calls onApplyFilter with partial filter updates. -->
|
||
<script lang="ts">
|
||
import { Button, Select, Input } from '$lib/ui';
|
||
import type { TaskCenterFilter, TaskType, ReportStatus } from '$types/reports';
|
||
|
||
// ... filter UI: multi-select dropdowns, search input, time range select, sort controls
|
||
</script>
|
||
<!-- #endregion FilterBar -->
|
||
```
|
||
|
||
### 3.6 TaskList [C:4] [TYPE Component]
|
||
|
||
```svelte
|
||
<!-- frontend/src/lib/components/reports/TaskList.svelte (MODIFY existing — enhance with pagination, sort, active-first) -->
|
||
|
||
<!-- #region TaskList [C:4] [TYPE Component] [SEMANTICS component, list, task, pagination, sort, task-status-center] -->
|
||
<!-- @BRIEF Paginated task list with active-tasks-first sorting, status badges, and click-to-drawer. -->
|
||
<!-- @LAYER UI Component -->
|
||
<!-- @UX_STATE loading: skeleton rows -->
|
||
<!-- @UX_STATE empty: "No tasks" empty state with CTA buttons (inbox icon) -->
|
||
<!-- @UX_STATE filtered_empty: "No tasks matching filters" + clear filters button -->
|
||
<!-- @UX_STATE ready: task rows rendered -->
|
||
<!-- @UX_FEEDBACK Row click: opens TaskDrawer. Status change: animated status badge transition. -->
|
||
<!-- @UX_RECOVERY Empty: "Clear filters" button. -->
|
||
<!-- @RELATION BINDS_TO -> [TaskCenterModel] -->
|
||
<!-- @RELATION CALLS -> [TaskDrawer] -->
|
||
<!-- @PRE tasks is an array of TaskReport (may be empty). -->
|
||
<!-- @POST Clicking a task calls onSelectTask(taskId) which opens TaskDrawer. -->
|
||
<!-- #endregion TaskList -->
|
||
```
|
||
|
||
### 3.7 ReportsTypeProfiles [C:2] [TYPE Module]
|
||
|
||
```typescript
|
||
// frontend/src/lib/components/reports/reportTypeProfiles.ts (MODIFY existing — add clean_release)
|
||
|
||
// #region ReportsTypeProfiles [C:2] [TYPE Module] [SEMANTICS profile, task-type, visual, mapping, task-status-center]
|
||
// @BRIEF Visual profiles for canonical task types — synchronized with backend type_profiles.py.
|
||
// @RATIONALE R8: Frontend profiles must match backend TaskType enum. Added clean_release entry.
|
||
// @RELATION DEPENDS_ON -> [Backend.type_profiles] via @DATA_CONTRACT
|
||
|
||
export function getReportTypeProfile(taskType: string): TypeProfile {
|
||
const profiles: Record<string, TypeProfile> = {
|
||
llm_verification: { key: 'llm_verification', label: 'LLM Verification', variant: 'info', icon: 'sparkles', fallback: false },
|
||
backup: { key: 'backup', label: 'Backup', variant: 'default', icon: 'archive', fallback: false },
|
||
migration: { key: 'migration', label: 'Migration', variant: 'default', icon: 'shuffle', fallback: false },
|
||
documentation: { key: 'documentation', label: 'Documentation', variant: 'default', icon: 'file-text', fallback: false },
|
||
clean_release: { key: 'clean_release', label: 'Clean Release', variant: 'success', icon: 'shield-check', fallback: false }, // ← NEW
|
||
};
|
||
return profiles[taskType] ?? profiles.unknown;
|
||
}
|
||
// #endregion ReportsTypeProfiles
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Contract Traceability Matrix
|
||
|
||
| Spec FR | Contract(s) | Type | Complexity |
|
||
|---------|-------------|------|:----------:|
|
||
| TSC-FR-001 | `SummaryPanel`, `Reports.SummaryRouter`, `Reports.SummaryService` | Component, API, Service | C4, C4, C4 |
|
||
| TSC-FR-002 | `TaskCenterModel._onWsMessage`, `SummaryPanel` | Model, Component | C4, C4 |
|
||
| TSC-FR-003 | `TaskList`, `Reports.ListEnhanced` | Component, API | C4, C4 |
|
||
| TSC-FR-004 | `FilterBar`, `TaskCenterModel.applyFilter` | Component, Model | C3, C4 |
|
||
| TSC-FR-005 | `TaskList` (row template), `TaskReport` | Component, Model | C4, C3 |
|
||
| TSC-FR-006 | `TaskCenterModel.selectTask`, `TaskDrawer` | Model, Component | C4, existing |
|
||
| TSC-FR-007 | `TaskList` (awaiting_input indicator) | Component | C4 |
|
||
| TSC-FR-008 | `TaskCenterModel` (reconnect logic), `TaskCenterReportsPage` | Model, Page | C4, C4 |
|
||
| TSC-FR-009 | `Reports.RbacTaskFilter`, `Reports.SummaryService` | Function, Service | C3, C4 |
|
||
| TSC-FR-010 | `TaskCenterModel.filteredTasks` ($derived) | Model | C4 |
|
||
|
||
#endregion ModuleContracts
|