tasks
This commit is contained in:
256
specs/030-dataset-lifecycle-workspace/contracts/modules.md
Normal file
256
specs/030-dataset-lifecycle-workspace/contracts/modules.md
Normal file
@@ -0,0 +1,256 @@
|
||||
# Contracts: Dataset Lifecycle Workspace
|
||||
|
||||
> Generated for speckit feature 030. All IDs scoped to this feature. Complexity per GRACE-Poly v2.6. Allowed predicates: DEPENDS_ON, CALLS, INHERITS, IMPLEMENTS, DISPATCHES, BINDS_TO.
|
||||
|
||||
---
|
||||
|
||||
## Backend Contracts
|
||||
|
||||
### #region MetricItem [C:1] [TYPE DataClass]
|
||||
@BRIEF Pydantic DTO for a dataset metric — carries Superset metric metadata.
|
||||
@RELATION DEPENDS_ON -> [DatasetDetailResponse]
|
||||
**File:** `backend/src/api/routes/datasets.py`
|
||||
|
||||
```python
|
||||
class MetricItem(BaseModel):
|
||||
id: int
|
||||
metric_name: str
|
||||
expression: str | None = None
|
||||
verbose_name: str | None = None
|
||||
description: str | None = None
|
||||
metric_type: str | None = None
|
||||
```
|
||||
|
||||
### #region StatsCounts [C:1] [TYPE DataClass]
|
||||
@BRIEF Aggregate statistics for the Stats Bar — computed from full dataset list.
|
||||
**File:** `backend/src/api/routes/datasets.py`
|
||||
|
||||
```python
|
||||
class StatsCounts(BaseModel):
|
||||
total: int
|
||||
unmapped_count: int
|
||||
mapped_count: int
|
||||
linked_count: int
|
||||
```
|
||||
|
||||
### #region ColumnDescriptionUpdate [C:1] [TYPE DataClass]
|
||||
@BRIEF Request DTO for inline-edit of a column description.
|
||||
**File:** `backend/src/api/routes/datasets.py`
|
||||
|
||||
```python
|
||||
class ColumnDescriptionUpdate(BaseModel):
|
||||
description: str
|
||||
```
|
||||
|
||||
### #region MetricDescriptionUpdate [C:1] [TYPE DataClass]
|
||||
@BRIEF Request DTO for inline-edit of a metric description.
|
||||
**File:** `backend/src/api/routes/datasets.py`
|
||||
|
||||
```python
|
||||
class MetricDescriptionUpdate(BaseModel):
|
||||
description: str
|
||||
```
|
||||
|
||||
### #region DatasetDetailResponse [C:2] [TYPE DataClass] — MODIFIED
|
||||
@BRIEF Detailed DTO for a dataset including columns, linked dashboards, and metrics.
|
||||
@RELATION DEPENDS_ON -> [MetricItem]
|
||||
**File:** `backend/src/api/routes/datasets.py`
|
||||
**Modification:** Add `metrics: list[MetricItem]` and `metric_count: int`.
|
||||
|
||||
### #region DatasetsResponse [C:2] [TYPE DataClass] — MODIFIED
|
||||
@BRIEF Paginated response DTO for dataset listings with stats.
|
||||
@RELATION DEPENDS_ON -> [StatsCounts]
|
||||
**File:** `backend/src/api/routes/datasets.py`
|
||||
**Modification:** Add `stats: StatsCounts`.
|
||||
|
||||
### #region update_column_description [C:4] [TYPE Endpoint]
|
||||
@BRIEF Save description for a single dataset column. Internal: GET full dataset from Superset, modify one column's description, PUT back.
|
||||
@PRE: dataset_id and column_id must exist in the target environment.
|
||||
@POST: Column description in Superset is updated. Response confirms success.
|
||||
@SIDE_EFFECT: Mutates dataset metadata in upstream Superset instance via PUT.
|
||||
@RELATION CALLS -> [SupersetClient.get_dataset]
|
||||
@RELATION CALLS -> [SupersetClient.update_dataset]
|
||||
@RATIONALE Must perform GET→modify→PUT because Superset has no PATCH for individual columns — only full object PUT with `override_columns=false`.
|
||||
@REJECTED Direct PUT from frontend — rejected because frontend would need to handle full Superset payload structure.
|
||||
**File:** `backend/src/api/routes/datasets.py`
|
||||
**Route:** `PUT /api/datasets/{dataset_id}/columns/{column_id}/description`
|
||||
**Method:** `PUT`
|
||||
**Request:** `ColumnDescriptionUpdate`
|
||||
**Response:** `{success: true, description: "new text"}`
|
||||
**RBAC:** `has_permission("plugin:migration", "READ")`
|
||||
|
||||
### #region update_metric_description [C:4] [TYPE Endpoint]
|
||||
@BRIEF Save description for a single dataset metric. Mirror of update_column_description for metrics.
|
||||
@PRE: dataset_id and metric_id must exist in the target environment.
|
||||
@POST: Metric description in Superset is updated.
|
||||
@SIDE_EFFECT: Mutates dataset metadata in upstream Superset instance via PUT.
|
||||
@RELATION CALLS -> [SupersetClient.get_dataset]
|
||||
@RELATION CALLS -> [SupersetClient.update_dataset]
|
||||
**File:** `backend/src/api/routes/datasets.py`
|
||||
**Route:** `PUT /api/datasets/{dataset_id}/metrics/{metric_id}/description`
|
||||
**Request:** `MetricDescriptionUpdate`
|
||||
**Response:** `{success: true, description: "new text"}`
|
||||
**RBAC:** `has_permission("plugin:migration", "READ")`
|
||||
|
||||
### #region get_datasets [C:4] [TYPE Endpoint] — MODIFIED
|
||||
@BRIEF Fetch paginated dataset list with enhanced metadata, mapped fields, and stats counts.
|
||||
@PRE: env_id must be valid. page >= 1, page_size in [1,100].
|
||||
@POST: Returns paginated datasets plus StatsCounts for the Stats Bar.
|
||||
@SIDE_EFFECT: Computes aggregate counts from full dataset list (in-memory, no DB write).
|
||||
@RELATION CALLS -> [resource_service.get_datasets_with_status]
|
||||
@RATIONALE Stats counts returned in the same response as datasets to avoid an extra API call for the Stats Bar (FR-022).
|
||||
**File:** `backend/src/api/routes/datasets.py`
|
||||
**Modification:** Compute `stats` dict from full dataset list before pagination slicing, add to DatasetsResponse.
|
||||
|
||||
### #region get_dataset_detail [C:3] [TYPE Endpoint] — MODIFIED
|
||||
@BRIEF Get detailed dataset information including columns, linked dashboards, and metrics.
|
||||
@RELATION CALLS -> [SupersetClient.get_dataset_detail]
|
||||
**File:** `backend/src/api/routes/datasets.py`
|
||||
**Modification:** Extract `dataset.get("metrics", [])` from Superset response, map to `MetricItem` list.
|
||||
|
||||
### #region SupersetClient.get_dataset_detail [C:3] [TYPE Function] — MODIFIED
|
||||
@BRIEF Extract dataset detail from Superset API including metrics.
|
||||
@RELATION CALLS -> [SupersetClient.get_dataset]
|
||||
**File:** `backend/src/core/superset_client/_datasets.py`
|
||||
**Modification:** Add metrics extraction loop after columns loop. Add `metrics` and `metric_count` to result dict.
|
||||
|
||||
---
|
||||
|
||||
## Frontend Contracts
|
||||
|
||||
### #region StatsBar [C:3] [TYPE Component] [SEMANTICS ui,dataset,stats,filter]
|
||||
@BRIEF Displays 4 aggregate metric tiles (All, Without mapping, With description, Linked) and emits filter selection.
|
||||
@LAYER UI
|
||||
@RELATION BINDS_TO -> [environmentContextStore]
|
||||
@UX_STATE Loading -> 4 skeleton tiles pulsing.
|
||||
@UX_STATE Loaded -> 4 tiles with numbers, active filter highlighted.
|
||||
@UX_STATE Filtered -> Selected tile has accent border; list reflects filter.
|
||||
@UX_STATE Empty -> All tiles show 0.
|
||||
@UX_REACTIVITY Props -> `stats: StatsCounts`, `activeFilter: string|null`.
|
||||
@UX_REACTIVITY Events -> `onfilter(filterKey)` dispatched on tile click.
|
||||
**File:** `frontend/src/routes/datasets/StatsBar.svelte`
|
||||
|
||||
### #region DatasetList [C:4] [TYPE Component] [SEMANTICS ui,dataset,card,list,pagination]
|
||||
@BRIEF Renders paginated dataset cards with mapping progress bars, checkboxes, and quick actions.
|
||||
@LAYER UI
|
||||
@RELATION BINDS_TO -> [environmentContextStore]
|
||||
@RELATION BINDS_TO -> [taskDrawerStore]
|
||||
@UX_STATE Loading -> Skeleton cards.
|
||||
@UX_STATE Loaded -> Cards with progress bars.
|
||||
@UX_STATE Selecting -> Checkboxes visible, checked cards highlighted.
|
||||
@UX_STATE Empty -> "No datasets found" after search/filter.
|
||||
@UX_FEEDBACK Click card → dispatches dataset selection for detail panel.
|
||||
@UX_FEEDBACK Click checkbox → updates selection set.
|
||||
@UX_FEEDBACK Click quick action → opens Map Columns or Generate Docs modal.
|
||||
@UX_REACTIVITY Props -> `datasets: DatasetCard[]`, `selectedIds: Set`, `activeFilter: string|null`.
|
||||
@UX_REACTIVITY Events -> `onselect(datasetId)`, `onaction(dataset, action)`.
|
||||
**File:** `frontend/src/routes/datasets/DatasetList.svelte`
|
||||
|
||||
### #region DatasetPreview [C:3] [TYPE Component] [SEMANTICS ui,dataset,detail,columns,metrics]
|
||||
@BRIEF Right panel showing dataset detail: header info, columns table, metrics table, linked dashboards.
|
||||
@LAYER UI
|
||||
@RELATION DEPENDS_ON -> [ColumnsTable]
|
||||
@RELATION DEPENDS_ON -> [MetricsTable]
|
||||
@UX_STATE NoSelection -> "Select a dataset from the list" placeholder.
|
||||
@UX_STATE Loading -> Skeleton for header + 2 tables.
|
||||
@UX_STATE Loaded -> Full detail with columns and metrics rendered.
|
||||
@UX_STATE Error -> Error banner with retry.
|
||||
@UX_REACTIVITY Props -> `dataset: DatasetDetailResponse | null`.
|
||||
@UX_TEST: NoSelection -> {click: card, expected: Loading -> Loaded}.
|
||||
**File:** `frontend/src/routes/datasets/DatasetPreview.svelte`
|
||||
|
||||
### #region ColumnsTable [C:4] [TYPE Component] [SEMANTICS ui,dataset,columns,inline-edit]
|
||||
@BRIEF Table of dataset columns with type chips, description, and inline-edit capability.
|
||||
@LAYER UI
|
||||
@RELATION CALLS -> [api_module]
|
||||
@UX_STATE Display -> All rows in read mode.
|
||||
@UX_STATE Editing -> One row in edit mode (textarea), others dimmed.
|
||||
@UX_STATE Saving -> Spinner on save button.
|
||||
@UX_STATE Error -> Red border on textarea + error message.
|
||||
@UX_FEEDBACK verbose_name → shown bold; missing → column_name in italic as fallback.
|
||||
@UX_FEEDBACK No description → "✏️ Add description" action prompt.
|
||||
@UX_REACTIVITY Props -> `columns: ColumnRow[]`, `datasetId: number`.
|
||||
@UX_REACTIVITY LocalState -> `isEditingRowId: number|null`.
|
||||
@RATIONALE Inline-edit uses optimised state per row to minimise re-renders across the table.
|
||||
@REJECTED Modal-per-column editing — rejected as too many clicks for repetitive workflow.
|
||||
**File:** `frontend/src/routes/datasets/ColumnsTable.svelte`
|
||||
|
||||
### #region MetricsTable [C:4] [TYPE Component] [SEMANTICS ui,dataset,metrics,inline-edit]
|
||||
@BRIEF Table of dataset metrics with expression, description, and inline-edit capability.
|
||||
@LAYER UI
|
||||
@RELATION CALLS -> [api_module]
|
||||
@UX_STATE Display -> All rows in read mode with expression shown under name.
|
||||
@UX_STATE Editing -> One row in edit mode.
|
||||
@UX_STATE Saving -> Spinner on save button.
|
||||
@UX_FEEDBACK verbose_name → shown bold; missing → metric_name in italic as fallback.
|
||||
@UX_FEEDBACK No description → shows expression as hint + "✏️ Add description" prompt.
|
||||
@UX_REACTIVITY Props -> `metrics: MetricRow[]`, `datasetId: number`.
|
||||
**File:** `frontend/src/routes/datasets/MetricsTable.svelte`
|
||||
|
||||
### #region DatasetHub [C:5] [TYPE Page] — REWRITTEN
|
||||
@BRIEF Dataset lifecycle workspace page — split-view with Stats Bar, card list, detail panel, and bulk actions.
|
||||
@LAYER Page
|
||||
@RELATION DEPENDS_ON -> [StatsBar]
|
||||
@RELATION DEPENDS_ON -> [DatasetList]
|
||||
@RELATION DEPENDS_ON -> [DatasetPreview]
|
||||
@RELATION BINDS_TO -> [environmentContextStore]
|
||||
@RELATION BINDS_TO -> [taskDrawerStore]
|
||||
@PRE: Environment context is initialized before dataset loading.
|
||||
@POST: Page renders split-view layout; dataset detail updates without full page reload.
|
||||
@SIDE_EFFECT: Fetches datasets from API; subscribes to WebSocket `dataset.updated` events; opens modals for bulk actions.
|
||||
@INVARIANT: Always shows datasets for the active environment from context store.
|
||||
@DATA_CONTRACT: Receives DatasetsResponse (list + stats) and DatasetDetailResponse (detail panel). Sends ColumnDescriptionUpdate/MetricDescriptionUpdate on save.
|
||||
@RATIONALE Split-view replaces separate list + detail pages to reduce navigation clicks and make the workspace feel integrated.
|
||||
@REJECTED Multi-page navigation (list → detail separate routes) — rejected because it forces full page reload between selecting datasets, breaking flow.
|
||||
**File:** `frontend/src/routes/datasets/+page.svelte`
|
||||
|
||||
---
|
||||
|
||||
## Complexity Summary
|
||||
|
||||
| Contract | Type | C | File |
|
||||
|----------|------|---|------|
|
||||
| MetricItem | DataClass | 1 | datasets.py |
|
||||
| StatsCounts | DataClass | 1 | datasets.py |
|
||||
| ColumnDescriptionUpdate | DataClass | 1 | datasets.py |
|
||||
| MetricDescriptionUpdate | DataClass | 1 | datasets.py |
|
||||
| DatasetDetailResponse (mod) | DataClass | 2 | datasets.py |
|
||||
| DatasetsResponse (mod) | DataClass | 2 | datasets.py |
|
||||
| get_dataset_detail (mod) | Endpoint | 3 | datasets.py |
|
||||
| SupersetClient.get_dataset_detail (mod) | Function | 3 | _datasets.py |
|
||||
| StatsBar | Component | 3 | StatsBar.svelte |
|
||||
| DatasetPreview | Component | 3 | DatasetPreview.svelte |
|
||||
| update_column_description | Endpoint | 4 | datasets.py |
|
||||
| update_metric_description | Endpoint | 4 | datasets.py |
|
||||
| get_datasets (mod) | Endpoint | 4 | datasets.py |
|
||||
| DatasetList | Component | 4 | DatasetList.svelte |
|
||||
| ColumnsTable | Component | 4 | ColumnsTable.svelte |
|
||||
| MetricsTable | Component | 4 | MetricsTable.svelte |
|
||||
| DatasetHub (rewrite) | Page | 5 | +page.svelte |
|
||||
|
||||
---
|
||||
|
||||
## ADR Continuity
|
||||
|
||||
| ADR | Status | This Feature |
|
||||
|-----|--------|-------------|
|
||||
| ADR-0001 (module layout) | ACTIVE | ✅ Follows `backend/src/api/routes/`, `frontend/src/routes/` placement |
|
||||
| ADR-0002 (semantic protocol) | ACTIVE | ✅ All contracts use GRACE anchors with C1-C5 complexity |
|
||||
| ADR-0003 (orchestrator pattern) | ACTIVE | ✅ Backend proxies Superset API, no direct frontend-to-Superset calls |
|
||||
| ADR-0004 (plugin architecture) | ACTIVE | ✅ Mapping/docs tasks use existing plugin system (no new plugin) |
|
||||
| ADR-0005 (auth RBAC) | ACTIVE | ✅ New endpoints gated by existing permission checks |
|
||||
| ADR-0006 (frontend architecture) | ACTIVE | ✅ Svelte 5 runes, Tailwind, static SPA, vitest |
|
||||
| ADR-0007 (rejected fromStore) | ACTIVE | ✅ Uses `$state`, avoids deprecated patterns |
|
||||
|
||||
---
|
||||
|
||||
## Rejected Paths
|
||||
|
||||
| Path | Reason | Guardrail |
|
||||
|------|--------|-----------|
|
||||
| PATCH endpoint on Superset directly | Superset v1 API has no PATCH for columns/metrics | `override_columns=false` with full-object PUT only |
|
||||
| Direct frontend-to-Superset PUT | Violates orchestrator pattern (ADR-0003), exposes auth | All mutations go through ss-tools backend |
|
||||
| Modal-per-row editing | Too many clicks for repetitive column description workflow | Inline edit via textarea toggle |
|
||||
| Multi-page list→detail navigation | Full page reload between dataset selections breaks flow | Split-view within single SvelteKit route |
|
||||
| Polling for Stats Bar refresh | Unnecessary network overhead, user chose WebSocket | WebSocket `dataset.updated` event |
|
||||
| `override_columns=true` | Risk of deleting unmapped columns on PUT | Always `override_columns=false` (FR-023) |
|
||||
182
specs/030-dataset-lifecycle-workspace/data-model.md
Normal file
182
specs/030-dataset-lifecycle-workspace/data-model.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# Data Model: Dataset Lifecycle Workspace
|
||||
|
||||
## Entity Overview
|
||||
|
||||
```
|
||||
GET /api/datasets GET /api/datasets/{id}
|
||||
─────────────────────────────────────────────────────────────
|
||||
DatasetsResponse DatasetDetailResponse
|
||||
├── datasets: DatasetItem[] ├── ... (existing fields)
|
||||
│ ├── id ├── metrics: MetricItem[]
|
||||
│ ├── table_name │ ├── id
|
||||
│ ├── schema │ ├── metric_name
|
||||
│ ├── database │ ├── expression
|
||||
│ ├── mapped_fields │ ├── verbose_name
|
||||
│ └── last_task │ ├── description
|
||||
├── stats: StatsCounts │ └── metric_type
|
||||
│ ├── total └── metric_count
|
||||
│ ├── unmapped_count
|
||||
│ ├── mapped_count
|
||||
│ └── linked_count
|
||||
├── total, page, page_size,
|
||||
│ total_pages (existing)
|
||||
└── [unchanged pagination]
|
||||
|
||||
PUT /api/datasets/{id}/columns/{col_id}/description
|
||||
PUT /api/datasets/{id}/metrics/{metric_id}/description
|
||||
─────────────────────────────────────────────────────────────
|
||||
Request: ColumnDescriptionUpdate | MetricDescriptionUpdate
|
||||
{ description: string }
|
||||
|
||||
Response: { success: true, description: string }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backend Pydantic Schemas (in `backend/src/api/routes/datasets.py`)
|
||||
|
||||
### MetricItem (new)
|
||||
```python
|
||||
class MetricItem(BaseModel):
|
||||
id: int
|
||||
metric_name: str
|
||||
expression: str | None = None
|
||||
verbose_name: str | None = None
|
||||
description: str | None = None
|
||||
metric_type: str | None = None
|
||||
```
|
||||
|
||||
### StatsCounts (new)
|
||||
```python
|
||||
class StatsCounts(BaseModel):
|
||||
total: int # all datasets
|
||||
unmapped_count: int # mapped_fields.mapped == 0
|
||||
mapped_count: int # mapped_fields.mapped > 0
|
||||
linked_count: int # linked_dashboard_count > 0
|
||||
```
|
||||
|
||||
### DatasetsResponse (modified)
|
||||
```python
|
||||
class DatasetsResponse(BaseModel):
|
||||
datasets: list[DatasetItem]
|
||||
stats: StatsCounts # NEW
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
total_pages: int
|
||||
```
|
||||
|
||||
### DatasetDetailResponse (modified)
|
||||
```python
|
||||
class DatasetDetailResponse(BaseModel):
|
||||
# ... existing fields unchanged ...
|
||||
metrics: list[MetricItem] # NEW
|
||||
metric_count: int # NEW
|
||||
```
|
||||
|
||||
### ColumnDescriptionUpdate (new)
|
||||
```python
|
||||
class ColumnDescriptionUpdate(BaseModel):
|
||||
description: str
|
||||
```
|
||||
|
||||
### MetricDescriptionUpdate (new)
|
||||
```python
|
||||
class MetricDescriptionUpdate(BaseModel):
|
||||
description: str
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend Types (TypeScript / Svelte inferred)
|
||||
|
||||
```typescript
|
||||
interface StatsCounts {
|
||||
total: number;
|
||||
unmapped_count: number;
|
||||
mapped_count: number;
|
||||
linked_count: number;
|
||||
}
|
||||
|
||||
interface MetricItem {
|
||||
id: number;
|
||||
metric_name: string;
|
||||
expression?: string;
|
||||
verbose_name?: string;
|
||||
description?: string;
|
||||
metric_type?: string;
|
||||
}
|
||||
|
||||
// UI-only state types (not in API)
|
||||
type ColumnRow = {
|
||||
column_name: string;
|
||||
verbose_name?: string;
|
||||
type?: string;
|
||||
description?: string;
|
||||
is_dttm: boolean;
|
||||
is_active: boolean;
|
||||
id: number;
|
||||
isEditing?: boolean; // UI state for inline edit
|
||||
};
|
||||
|
||||
type MetricRow = {
|
||||
metric_name: string;
|
||||
expression?: string;
|
||||
verbose_name?: string;
|
||||
description?: string;
|
||||
metric_type?: string;
|
||||
id: number;
|
||||
isEditing?: boolean; // UI state for inline edit
|
||||
};
|
||||
|
||||
type StatsBarFilter = 'all' | 'unmapped' | 'mapped' | 'linked' | null;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WebSocket Message
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "dataset.updated",
|
||||
"payload": {
|
||||
"env_id": "ss-dev",
|
||||
"dataset_ids": ["242377ce-bdbd-42df-85da-ca1340a72fbb"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State Transitions
|
||||
|
||||
### Stats Bar Tile States
|
||||
```
|
||||
Idle → (click tile) → Active (highlighted, list filtered)
|
||||
Active → (click same tile) → Idle (filter cleared)
|
||||
Active → (click different tile) → Active (new tile, list re-filtered)
|
||||
```
|
||||
|
||||
### Inline Edit States
|
||||
```
|
||||
Display → (click ✏️) → Editing (textarea visible)
|
||||
Editing → (Save + API success) → Display (new description shown)
|
||||
Editing → (Save + API error) → Editing (error message, textarea stays)
|
||||
Editing → (Cancel) → Display (original description restored)
|
||||
```
|
||||
|
||||
### Detail Panel States
|
||||
```
|
||||
NoSelection → (click card) → Loading → Loaded
|
||||
Loaded → (click different card) → Loading → Loaded
|
||||
Loaded → (deselect / bulk mode) → NoSelection
|
||||
Loading → (API error) → Error → (Retry) → Loading
|
||||
```
|
||||
|
||||
### Bulk Selection States
|
||||
```
|
||||
None → (check 1+ cards) → Selecting (bottom panel hidden for 1)
|
||||
Selecting → (check 2+ cards) → BulkActive (bottom panel visible)
|
||||
BulkActive → (uncheck all) → None
|
||||
BulkActive → (click action) → Modal → (complete) → None
|
||||
```
|
||||
97
specs/030-dataset-lifecycle-workspace/plan.md
Normal file
97
specs/030-dataset-lifecycle-workspace/plan.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Implementation Plan: Dataset Lifecycle Workspace
|
||||
|
||||
**Branch**: `030-dataset-lifecycle-workspace` | **Date**: 2026-05-19 | **Spec**: [spec.md](./spec.md)
|
||||
**Input**: Feature specification from `specs/030-dataset-lifecycle-workspace/spec.md`
|
||||
|
||||
## Summary
|
||||
|
||||
Переработка страницы датасетов (`/datasets`) из табличного вида в полноценное split-view рабочее пространство с: (1) Stats Bar с 4 агрегирующими метриками и клиентской фильтрацией, (2) split-view с карточками датасетов слева и детальной панелью справа, (3) inline-редактированием описаний колонок и метрик через новые ss-tools API эндпоинты, (4) WebSocket-автообновлением при завершении фоновых задач, (5) отображением метрик из Superset API. Изменения затрагивают бэкенд (3 новых DTO, 2 новых эндпоинта, модификация 2 существующих) и фронтенд (5 новых компонентов, переписанный +page.svelte).
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: Python 3.9+ (backend), JavaScript/TypeScript — Svelte 5 runes (frontend)
|
||||
**Primary Dependencies**: FastAPI 0.104+, Pydantic v2, SQLAlchemy (backend); SvelteKit 2.x, Svelte 5.x, Vite 7.x, Tailwind CSS 3.x (frontend)
|
||||
**Storage**: PostgreSQL 16 (ss-tools own DB); no schema changes in this feature
|
||||
**Testing**: pytest (backend), vitest + @testing-library/svelte 5.x (frontend)
|
||||
**Target Platform**: Linux server (Docker), modern browsers
|
||||
**Project Type**: web application (FastAPI REST + WebSocket backend, SvelteKit SPA frontend)
|
||||
**Performance Goals**: Split-view dataset switch <500ms, inline-edit save <1s, initial Stats Bar load <200ms
|
||||
**Constraints**: <400 lines per module (INV_7), RBAC on all new endpoints, Tailwind-only styling, Svelte 5 runes only
|
||||
**Scale/Scope**: 33 datasets (current), single environment (dev), 5 new frontend components, 2 new API endpoints
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
| Principle | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| Library-First | ✅ N/A | No new library extracted |
|
||||
| Semantic Protocol (ADR-0002) | ✅ PASS | All C4/C5 contracts use belief_scope/reason/reflect markers. All components use @UX_STATE. @RATIONALE/@REJECTED on key decisions. |
|
||||
| Orchestrator Pattern (ADR-0003) | ✅ PASS | Backend proxies Superset API; no direct frontend-to-Superset calls |
|
||||
| Plugin Architecture (ADR-0004) | ✅ PASS | No new plugin; mapping/docs reuse existing MapperPlugin/DocumentationPlugin |
|
||||
| Auth RBAC (ADR-0005) | ✅ PASS | New endpoints gated: `has_permission("plugin:migration","READ")` |
|
||||
| Frontend Svelte 5 Runes (ADR-0006) | ✅ PASS | All components use $state/$derived/$props/$effect |
|
||||
| Anti-Erosion (INV_7) | ✅ PASS | New components <400 lines each; no monolithic page |
|
||||
|
||||
**No blocking conflicts detected.**
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
```
|
||||
specs/030-dataset-lifecycle-workspace/
|
||||
├── plan.md # This file
|
||||
├── research.md # 10 research decisions
|
||||
├── data-model.md # Pydantic schemas, frontend types, state machines
|
||||
├── quickstart.md # Verification commands
|
||||
├── contracts/
|
||||
│ └── modules.md # 16 contracts with complexity classes
|
||||
├── spec.md # Feature specification (7 user stories, 24 FR)
|
||||
├── ux_reference.md # UX interaction reference
|
||||
└── checklists/
|
||||
└── requirements.md # 44 checklist items
|
||||
```
|
||||
|
||||
### Source Code Changes
|
||||
|
||||
```text
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── api/routes/
|
||||
│ │ └── datasets.py # +MetricItem, +StatsCounts, +2 endpoints, MOD DatasetsResponse
|
||||
│ └── core/
|
||||
│ ├── superset_client/
|
||||
│ │ └── _datasets.py # MOD get_dataset_detail (metrics extraction)
|
||||
│ └── task_manager.py # MOD on_task_complete (WebSocket event)
|
||||
└── tests/
|
||||
└── __tests__/
|
||||
└── test_datasets.py # +tests for new endpoints and metrics
|
||||
|
||||
frontend/
|
||||
├── src/
|
||||
│ ├── routes/datasets/
|
||||
│ │ ├── +page.svelte # REWRITE (split-view orchestrator, ~200 lines)
|
||||
│ │ ├── StatsBar.svelte # NEW (4 metric tiles, ~80 lines)
|
||||
│ │ ├── DatasetList.svelte # NEW (card grid, pagination, ~250 lines)
|
||||
│ │ ├── DatasetPreview.svelte # NEW (detail panel, ~150 lines)
|
||||
│ │ ├── ColumnsTable.svelte # NEW (inline-edit columns, ~180 lines)
|
||||
│ │ └── MetricsTable.svelte # NEW (inline-edit metrics, ~150 lines)
|
||||
│ └── lib/i18n/locales/
|
||||
│ ├── en/datasets.json # +11 keys
|
||||
│ └── ru/datasets.json # +11 keys
|
||||
└── tests/
|
||||
└── routes/datasets/
|
||||
└── dataset_workspace.test.js # +component tests for new components
|
||||
```
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
No violations. All components adhere to C1-C5 tier rules per ADR-0002.
|
||||
|
||||
| Contract | C | Justification |
|
||||
|----------|---|--------------|
|
||||
| MetricItem, StatsCounts, *Update DTOs | 1 | Pure data classes, anchors only |
|
||||
| DatasetDetailResponse, DatasetsResponse (mod) | 2 | DTOs with @PURPOSE |
|
||||
| get_dataset_detail (mod), StatsBar, DatasetPreview | 3 | Multi-step logic with @RELATION |
|
||||
| update_*_description endpoints, get_datasets (mod), DatasetList, ColumnsTable, MetricsTable | 4 | Stateful operations with @PRE/@POST/@SIDE_EFFECT |
|
||||
| DatasetHub (+page.svelte rewrite) | 5 | Orchestrator with @DATA_CONTRACT, @INVARIANT, decision memory |
|
||||
100
specs/030-dataset-lifecycle-workspace/quickstart.md
Normal file
100
specs/030-dataset-lifecycle-workspace/quickstart.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Quickstart: Dataset Lifecycle Workspace
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Backend virtualenv: `cd backend && source .venv/bin/activate`
|
||||
- Frontend deps: `cd frontend && npm install`
|
||||
- Superset environment configured (dev via http://localhost:8100)
|
||||
- RBAC: user must have `plugin:migration:READ` permission
|
||||
|
||||
## Backend Verification
|
||||
|
||||
```bash
|
||||
# Run backend tests
|
||||
cd backend && source .venv/bin/activate && python -m pytest -v
|
||||
|
||||
# Lint
|
||||
python -m ruff check .
|
||||
|
||||
# Run dev server
|
||||
uvicorn src.main:app --reload --port 8000
|
||||
```
|
||||
|
||||
## Frontend Verification
|
||||
|
||||
```bash
|
||||
# Run frontend tests
|
||||
cd frontend && npm run test
|
||||
|
||||
# Lint
|
||||
cd frontend && npm run lint
|
||||
|
||||
# Build check
|
||||
cd frontend && npm run build
|
||||
|
||||
# Dev server (requires backend on :8000)
|
||||
cd frontend && npm run dev
|
||||
```
|
||||
|
||||
## Feature-Specific Verification
|
||||
|
||||
### Backend API Tests
|
||||
|
||||
```bash
|
||||
# Test metrics in dataset detail response
|
||||
curl -s http://localhost:8100/api/datasets/32?env_id=dev | jq '.metrics, .metric_count'
|
||||
|
||||
# Test stats counts in dataset list
|
||||
curl -s 'http://localhost:8100/api/datasets?env_id=dev&page=1&page_size=5' | jq '.stats'
|
||||
|
||||
# Test column description inline-edit
|
||||
curl -s -X PUT http://localhost:8100/api/datasets/32/columns/851/description \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"description":"Updated via API"}'
|
||||
|
||||
# Test metric description inline-edit
|
||||
curl -s -X PUT http://localhost:8100/api/datasets/32/metrics/70/description \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"description":"Count of all records"}'
|
||||
```
|
||||
|
||||
### Frontend Component Tests
|
||||
|
||||
```bash
|
||||
# Run dataset-specific component tests
|
||||
cd frontend && npx vitest run src/routes/datasets/ --reporter=verbose
|
||||
```
|
||||
|
||||
### Browser Validation
|
||||
|
||||
1. Open `http://localhost:8100/datasets`
|
||||
2. Verify Stats Bar shows 4 tiles with counts
|
||||
3. Click each tile — list filters correctly
|
||||
4. Click a dataset card — detail panel loads with columns + metrics
|
||||
5. Click ✏️ on a column — textarea appears, type description, save
|
||||
6. Verify description persists after save
|
||||
7. Check 2+ checkboxes — bottom panel appears
|
||||
8. Click "Map Columns" — modal opens
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
# Visit http://localhost:8100/datasets
|
||||
```
|
||||
|
||||
## Key Files Changed/Created
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `backend/src/api/routes/datasets.py` | +MetricItem, +StatsCounts, +2 endpoints, MOD DatasetsResponse/DatasetDetailResponse |
|
||||
| `backend/src/core/superset_client/_datasets.py` | +metrics extraction in get_dataset_detail |
|
||||
| `backend/src/core/task_manager.py` | +dataset.updated WebSocket event on task complete |
|
||||
| `frontend/src/routes/datasets/+page.svelte` | REWRITE (split-view orchestrator) |
|
||||
| `frontend/src/routes/datasets/StatsBar.svelte` | NEW |
|
||||
| `frontend/src/routes/datasets/DatasetList.svelte` | NEW |
|
||||
| `frontend/src/routes/datasets/DatasetPreview.svelte` | NEW |
|
||||
| `frontend/src/routes/datasets/ColumnsTable.svelte` | NEW |
|
||||
| `frontend/src/routes/datasets/MetricsTable.svelte` | NEW |
|
||||
| `frontend/src/lib/i18n/locales/en/datasets.json` | +11 keys |
|
||||
| `frontend/src/lib/i18n/locales/ru/datasets.json` | +11 keys |
|
||||
168
specs/030-dataset-lifecycle-workspace/research.md
Normal file
168
specs/030-dataset-lifecycle-workspace/research.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Research: Dataset Lifecycle Workspace (030)
|
||||
|
||||
## R1 — Backend: Metrics in DatasetDetailResponse
|
||||
|
||||
**Decision:** Add `metrics: list[MetricItem]` and `metric_count: int` to `DatasetDetailResponse`. Add `MetricItem` Pydantic DTO in `backend/src/api/routes/datasets.py`. In `SupersetClient.get_dataset_detail` (`backend/src/core/superset_client/_datasets.py`), extract `dataset.get("metrics", [])` from the Superset API response.
|
||||
|
||||
**Rationale:** Superset's `GET /api/v1/dataset/{pk}` already returns `metrics` array with `metric_name`, `expression`, `verbose_name`, `description`, `metric_type`. Our backend currently ignores them. Extracting them is straightforward — no breaking API change, only additive fields.
|
||||
|
||||
**Alternatives Considered:**
|
||||
- A separate `/datasets/{id}/metrics` endpoint → rejected, adds unnecessary API surface for data already present
|
||||
- Client-side fetch from Superset directly → rejected (bypasses auth, breaks SSR parity)
|
||||
|
||||
**Impact:** `DatasetDetailResponse` gains 2 new fields. `MetricItem` DTO ~8 lines. `get_dataset_detail` gains ~15 lines. Zero DB changes. No migration.
|
||||
|
||||
---
|
||||
|
||||
## R2 — Backend: Inline-edit Endpoints
|
||||
|
||||
**Decision:** Two new endpoints on ss-tools backend under existing `APIRouter(prefix="/api/datasets")`:
|
||||
- `PUT /api/datasets/{id}/columns/{col_id}/description` (body: `{"description": "text"}`)
|
||||
- `PUT /api/datasets/{id}/metrics/{metric_id}/description` (body: `{"description": "text"}`)
|
||||
|
||||
Each performs: GET full dataset from Superset via `SupersetClient.get_dataset()` → modify target column/metric description → PUT back via `SupersetClient.update_dataset()` with `?override_columns=false`.
|
||||
|
||||
**Rationale:** Superset has no PATCH for individual columns/metrics — only PUT on the full dataset with `override_columns=false`. A convenience endpoint on ss-tools handles the GET→modify→PUT dance server-side, keeping frontend payloads simple (`{description: "text"}`).
|
||||
|
||||
**Alternatives Considered:**
|
||||
- Frontend does GET→modify→PUT directly → rejected: requires frontend to handle full Superset payload, increases attack surface
|
||||
- Single `PUT /api/datasets/{id}` with `{"columns":[{"id":...,"description":...}]}` → valid but more complex for frontend, reserved for future batch updates
|
||||
- Optimistic save with rollback → rejected: adds complexity without significant UX benefit for description editing
|
||||
|
||||
**Impact:** +2 route handlers in `datasets.py` (~50 lines each). Reuses existing `SupersetClient.get_dataset()` and `SupersetClient.update_dataset()`. RBAC: checked via existing `has_permission("plugin:migration","READ")` or new permission.
|
||||
|
||||
---
|
||||
|
||||
## R3 — Backend: Stats Counts in GET /api/datasets
|
||||
|
||||
**Decision:** `GET /api/datasets` response adds `stats` object with `total`, `unmapped_count`, `mapped_count`, `linked_count`. Computed by the existing `resource_service.get_datasets_with_status()` which already fetches ALL datasets before pagination. Stats are computed from the full (non-paginated) dataset list.
|
||||
|
||||
**Rationale:** The Statistics Bar needs aggregate counts. Since the backend already fetches all datasets to compute `mapped_fields` enrichment, computing stats is O(n) over the already-loaded data — near-zero cost.
|
||||
|
||||
**Alternatives Considered:**
|
||||
- Separate `/api/datasets/stats` endpoint → rejected, adds unnecessary request
|
||||
- Client-side counts (fetch all, count locally) → rejected, pagination would break counts
|
||||
|
||||
**Impact:** `DatasetsResponse` DTO gains `stats: dict`. One additional aggregation loop in the route handler (~5 lines). No new DB queries.
|
||||
|
||||
---
|
||||
|
||||
## R4 — Backend: WebSocket Event `dataset.updated`
|
||||
|
||||
**Decision:** After a mapping or documentation task completes, the task manager emits a WebSocket event `dataset.updated` with payload `{env_id, dataset_ids: []}`. The frontend subscribes to this event channel and automatically re-fetches `GET /api/datasets`.
|
||||
|
||||
**Rationale:** Existing WebSocket infrastructure already streams task logs to the task drawer. Extending with a completion event avoids polling overhead. The `task_manager` already has an `on_task_complete` hook.
|
||||
|
||||
**Alternatives Considered:**
|
||||
- Polling every N seconds → rejected (unnecessary network traffic, 33 datasets trivial but doesn't scale)
|
||||
- Manual refresh only → rejected by user in clarification Q5
|
||||
|
||||
**Impact:** One additional WebSocket message type in task completion handler (~5 lines). Frontend subscription in `+page.svelte` (~10 lines). No new infrastructure.
|
||||
|
||||
---
|
||||
|
||||
## R5 — Frontend: Component Architecture
|
||||
|
||||
**Decision:** Replace current monolithic 974-line `+page.svelte` with composition of 5 leaf components:
|
||||
|
||||
```
|
||||
+page.svelte (layout orchestrator, ~200 lines)
|
||||
├── StatsBar.svelte (4 metric tiles, client-side filtering)
|
||||
├── DatasetList.svelte (card list with pagination, checkbox selection)
|
||||
├── DatasetPreview.svelte (right panel: header + columns + metrics)
|
||||
│ ├── ColumnsTable.svelte (column rows with inline-edit ✏️)
|
||||
│ └── MetricsTable.svelte (metric rows with inline-edit ✏️)
|
||||
└── BulkActionPanel.svelte (reused from current implementation)
|
||||
```
|
||||
|
||||
Each component follows ss-tools Svelte 5 conventions (runes `$state`, `$derived`, `$props`, Tailwind classes).
|
||||
|
||||
**Rationale:** SS-tools fractal limit (INV_7): modules <400 lines, contract node cyclomatic complexity <10. The current 974-line page violates both. Decomposition into <400-line components is required by ADR-0002.
|
||||
|
||||
**Alternatives Considered:**
|
||||
- Keep monolithic + extract inline-edit as helper → rejected (still violates INV_7)
|
||||
- Use SvelteKit layout groups for sub-panels → rejected (over-engineering; this is one page, not multi-route)
|
||||
|
||||
**Impact:** ~5 new `.svelte` files in `frontend/src/routes/datasets/`. Existing `[id]/+page.svelte` remains as fallback but is largely superseded by `DatasetPreview.svelte`.
|
||||
|
||||
---
|
||||
|
||||
## R6 — Frontend: Inline Edit Interaction
|
||||
|
||||
**Decision:** On ✏️ click, the column/metric row enters `isEditing` state. A `<textarea>` replaces the description text. Save button calls `PUT /api/datasets/{id}/columns/{col_id}/description`. On success, row exits editing state and shows new description. On error, textarea stays open with error message. Cancel reverts to original value.
|
||||
|
||||
**Rationale:** Matches standard inline-edit UX pattern. Svelte 5 `$state` makes `isEditing` toggle trivial. Optimistic save rejected for simplicity — descriptions are short text, save latency is negligible.
|
||||
|
||||
**Alternatives Considered:**
|
||||
- Modal with single textarea → rejected (too heavy for one field)
|
||||
- Click-to-edit with blur-to-save → rejected (ambiguous "cancel" path)
|
||||
|
||||
**Impact:** `ColumnsTable.svelte` and `MetricsTable.svelte` each have `isEditingRowId` state + textarea logic. ~30 lines of template logic per table.
|
||||
|
||||
---
|
||||
|
||||
## R7 — Frontend: WebSocket Subscription
|
||||
|
||||
**Decision:** `+page.svelte` subscribes to `dataset.updated` WebSocket events in `onMount`. On receiving event with matching `env_id`, calls `loadDatasets()` to refresh the list and stats. No visual feedback needed — list updates in-place (Svelte reactivity).
|
||||
|
||||
**Rationale:** Pushes state updates without user action. Existing WebSocket connection is reused (no new connection). Svelte reactivity handles re-render.
|
||||
|
||||
**Alternatives Considered:**
|
||||
- SSE / EventSource → rejected (WebSocket already connected for task logs)
|
||||
- Separate WebSocket channel → rejected (overhead)
|
||||
|
||||
**Impact:** ~10 lines in `+page.svelte`. Reuses `wsStore` or equivalent from existing WebSocket infrastructure.
|
||||
|
||||
---
|
||||
|
||||
## R8 — i18n: New Keys
|
||||
|
||||
**Decision:** Add 11 new keys to `frontend/src/lib/i18n/locales/{en,ru}/datasets.json`: `all`, `without_mapping`, `with_description`, `linked_to_dashboards`, `metrics`, `metric_count`, `no_selection`, `save_description`, `add_description`, `columns`, `detail_empty`. No structural changes to i18n system.
|
||||
|
||||
**Rationale:** Existing i18n uses flat JSON key files loaded by locale. Adding keys is additive. No new dependencies.
|
||||
|
||||
**Impact:** +11 lines per locale file. Components reference via `$t.datasets.all`, etc.
|
||||
|
||||
---
|
||||
|
||||
## R9 — Backend: `override_columns=false` Guarantee
|
||||
|
||||
**Decision:** In line-edit endpoint handlers, when calling `SupersetClient.update_dataset()`, always append `?override_columns=false` to the PUT URL. This ensures Superset preserves all columns/metrics not mentioned in the payload.
|
||||
|
||||
**Rationale:** Superset's default is `override_columns=false` (safe), but explicit parameter ensures no accidental override if default changes. Risk: with `override_columns=true`, Superset would DELETE any column not present in the PUT payload — catastrophic.
|
||||
|
||||
**Alternatives Considered:**
|
||||
- Rely on Superset default → rejected (fragile, no explicit guarantee)
|
||||
|
||||
**Impact:** One query parameter addition per PUT call.
|
||||
|
||||
---
|
||||
|
||||
## R10 — Module Placement & File Map
|
||||
|
||||
| Artifact | File Path | Operation |
|
||||
|----------|-----------|-----------|
|
||||
| MetricItem DTO | `backend/src/api/routes/datasets.py` | ADD class |
|
||||
| Stats counts | `backend/src/api/routes/datasets.py` | MODIFY `DatasetsResponse`, `get_datasets()` |
|
||||
| Column description endpoint | `backend/src/api/routes/datasets.py` | ADD route `PUT /columns/{col_id}/description` |
|
||||
| Metric description endpoint | `backend/src/api/routes/datasets.py` | ADD route `PUT /metrics/{metric_id}/description` |
|
||||
| Metrics extraction | `backend/src/core/superset_client/_datasets.py` | MODIFY `get_dataset_detail()` |
|
||||
| WebSocket event | `backend/src/core/task_manager.py` (or equivalent) | MODIFY on_task_complete |
|
||||
| StatsBar | `frontend/src/routes/datasets/StatsBar.svelte` | NEW |
|
||||
| DatasetList | `frontend/src/routes/datasets/DatasetList.svelte` | NEW |
|
||||
| DatasetPreview | `frontend/src/routes/datasets/DatasetPreview.svelte` | NEW |
|
||||
| ColumnsTable | `frontend/src/routes/datasets/ColumnsTable.svelte` | NEW |
|
||||
| MetricsTable | `frontend/src/routes/datasets/MetricsTable.svelte` | NEW |
|
||||
| Page orchestrator | `frontend/src/routes/datasets/+page.svelte` | REWRITE |
|
||||
| i18n EN | `frontend/src/lib/i18n/locales/en/datasets.json` | ADD keys |
|
||||
| i18n RU | `frontend/src/lib/i18n/locales/ru/datasets.json` | ADD keys |
|
||||
| Breadcrumb | `frontend/src/lib/components/layout/Breadcrumbs.svelte` | MODIFY |
|
||||
| Sidebar nav | `frontend/src/lib/components/layout/sidebarNavigation.js` | VERIFY |
|
||||
|
||||
---
|
||||
|
||||
## Unresolved / Deferred
|
||||
|
||||
- **DatasetItem.metric_count** in list view: requires resource_service enrichment. Deferred to implementation — can be computed from full dataset list if metrics data available, else defaults to 0 until backend support is added.
|
||||
- **Accessibility (ARIA)**: Deferred to Polish phase. Components should use semantic HTML and `aria-busy`/`aria-live` where applicable.
|
||||
- **Large-scale (200+ datasets)**: Client-side filtering works for 33 datasets. At 200+, Stats Bar counts remain server-computed; only list filtering would need server-side `?filter=` parameter. This is a future enhancement.
|
||||
169
specs/030-dataset-lifecycle-workspace/tasks.md
Normal file
169
specs/030-dataset-lifecycle-workspace/tasks.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# Tasks: Dataset Lifecycle Workspace
|
||||
|
||||
**Feature**: 030-dataset-lifecycle-workspace
|
||||
**Branch**: `030-dataset-lifecycle-workspace`
|
||||
**Spec**: [spec.md](./spec.md) | **Plan**: [plan.md](./plan.md)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Setup (i18n + Breadcrumb)
|
||||
|
||||
> No user stories yet — infrastructure that all stories depend on.
|
||||
|
||||
- [ ] T001 [P] Add 11 EN i18n keys to `frontend/src/lib/i18n/locales/en/datasets.json`: `all`, `without_mapping`, `with_description`, `linked_to_dashboards`, `metrics`, `metric_count`, `no_selection`, `save_description`, `add_description`, `columns`, `detail_empty`
|
||||
- [ ] T002 [P] Add 11 RU i18n keys to `frontend/src/lib/i18n/locales/ru/datasets.json`: `все`, `без_маппинга`, `с_описанием`, `связаны_с_дашбордами`, `метрики`, `метрик`, `выберите_датасет`, `сохранить_описание`, `добавить_описание`, `колонки`, `пустой_просмотр`
|
||||
- [ ] T003 Verify breadcrumb for `/datasets` shows "Датасеты / Datasets" in `frontend/src/lib/components/layout/Breadcrumbs.svelte` (already fixed, confirm)
|
||||
- [ ] T004 [P] Verify sidebar navigation entries for datasets in `frontend/src/lib/components/layout/sidebarNavigation.js` are correct
|
||||
|
||||
**Verification**: `cd frontend && npm run build` — проверка что билд не падает с новыми i18n ключами.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational — Backend Metrics API (US7)
|
||||
|
||||
> Blocks US4 (detail panel needs metrics). Must complete before any UI work with metrics data.
|
||||
|
||||
- [ ] T005 Add `MetricItem` Pydantic DTO (fields: `id`, `metric_name`, `expression`, `verbose_name`, `description`, `metric_type`) in `backend/src/api/routes/datasets.py` (RATIONALE: metrics exist in Superset API but are currently ignored; REJECTED: separate metrics endpoint adds unnecessary API surface)
|
||||
- [ ] T006 Add `metrics: list[MetricItem]` and `metric_count: int` to `DatasetDetailResponse` in `backend/src/api/routes/datasets.py`
|
||||
- [ ] T007 Extract `dataset.get("metrics", [])` from Superset API response in `SupersetClient.get_dataset_detail()` at `backend/src/core/superset_client/_datasets.py` (add ~15 lines after existing columns loop, map each metric dict to MetricItem-like dict)
|
||||
- [ ] T008 Add `StatsCounts` Pydantic DTO (fields: `total`, `unmapped_count`, `mapped_count`, `linked_count`) in `backend/src/api/routes/datasets.py`
|
||||
- [ ] T009 Add `stats: StatsCounts` to `DatasetsResponse` in `backend/src/api/routes/datasets.py`
|
||||
- [ ] T010 Compute `stats` counts from full (non-paginated) dataset list in `get_datasets()` handler in `backend/src/api/routes/datasets.py` — counts: `total=len(datasets)`, `unmapped=sum(mapped==0)`, `mapped=sum(mapped>0)`, `linked=sum(linked>0)` (RATIONALE: one API call for both list + stats per FR-022; REJECTED: separate /stats endpoint adds unnecessary request)
|
||||
- [ ] T011 Add inline-edit endpoints to `backend/src/api/routes/datasets.py`: `PUT /api/datasets/{dataset_id}/columns/{column_id}/description` (body: `ColumnDescriptionUpdate`, handler: GET full dataset → modify column → PUT to Superset with `?override_columns=false`) (RATIONALE: Superset lacks PATCH for individual columns; REJECTED: frontend direct PUT — bypasses orchestrator, exposes auth)
|
||||
- [ ] T012 Add inline-edit endpoint to `backend/src/api/routes/datasets.py`: `PUT /api/datasets/{dataset_id}/metrics/{metric_id}/description` (body: `MetricDescriptionUpdate`, handler: mirror of T011 for metrics) (RATIONALE: same GET→modify→PUT pattern; REJECTED: merging columns+metrics into one endpoint — different Superset payload structures)
|
||||
- [ ] T013 Add `dataset.updated` WebSocket event emission in task completion handler at `backend/src/core/task_manager.py` — when a mapping or documentation task completes, broadcast `{type: "dataset.updated", payload: {env_id, dataset_ids}}` via existing WebSocket manager (RATIONALE: real-time refresh without polling per FR-024; REJECTED: polling interval — unnecessary network overhead)
|
||||
- [ ] T014 Write pytest tests for new endpoints in `backend/tests/__tests__/test_datasets.py`: test that `GET /api/datasets/{id}` returns `metrics` and `metric_count`; test that `PUT /columns/{col_id}/description` saves and returns updated description; test that `PUT /metrics/{metric_id}/description` saves correctly; test that `GET /api/datasets` returns `stats` object with correct counts
|
||||
|
||||
**Verification**: `cd backend && source .venv/bin/activate && python -m pytest -v backend/tests/__tests__/test_datasets.py`
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Stats Bar + Split-View Layout (US1 + US2 — P1)
|
||||
|
||||
> Core UI structure. All remaining stories depend on this layout being in place.
|
||||
|
||||
- [ ] T015 [US1] Create `StatsBar.svelte` component in `frontend/src/routes/datasets/StatsBar.svelte` — 4 metric tiles (Все, Без маппинга, С описанием, С дашбордами), each showing count from `stats` prop; active tile has highlight border; dispatches `onfilter` event on click; toggles filter on second click; skeleton tiles when loading (RATIONALE: quick diagnostic overview replaces empty space at top of current table page; REJECTED: dropdown filters — Stats Bar gives 1-click access)
|
||||
- [ ] T016 [US2] Rewrite `frontend/src/routes/datasets/+page.svelte` as split-view orchestrator: replace monolithic 974-line table with layout shell using Tailwind grid `grid grid-cols-[40%_60%]` on desktop, `block` on mobile (<768px). Remove old table rendering, keep data loading logic, add `selectedDatasetId` state. Shell renders `<StatsBar>`, `<DatasetList>`, `<DatasetPreview>` components. (RATIONALE: decompose 974-line page into composable components per INV_7 fractal limit; REJECTED: keep monolithic page — violates semantic erosion rules at C5 complexity)
|
||||
- [ ] T017 [US2] Add mobile responsive behavior in `+page.svelte`: at `max-width: 767px`, left panel takes full width; right panel opens as fixed slide-over overlay with backdrop and close/touch-outside-dismiss behavior (RATIONALE: FR-004 mobile requirement; REJECTED: separate mobile route — over-engineering)
|
||||
- [ ] T018 [US2] Wire `environmentContextStore` into new `+page.svelte`: on environment switch from the context store, reset `selectedDatasetId`, reload datasets, update Stats Bar. Preserve existing `initializeEnvironmentContext` + `selectedEnvironmentStore` logic from current implementation.
|
||||
- [ ] T019 [P] [US1] [US2] Write vitest component test for `StatsBar` in `frontend/src/routes/datasets/__tests__/StatsBar.test.js`: test 4 tiles render, counts display, click dispatches filter, skeleton shows when loading
|
||||
- [ ] T020 [P] [US2] Write vitest component test for split-view layout in `frontend/src/routes/datasets/__tests__/split_view.test.js`: test grid layout on desktop, slide-over on mobile
|
||||
|
||||
**Verification**:
|
||||
- `cd frontend && npm run test`
|
||||
- Browser: `http://localhost:8100/datasets` — Stats Bar visible, clicking tiles filters (no-op until cards exist), split-view layout renders
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Dataset Cards (US3 — P2)
|
||||
|
||||
> Cards replace old table rows. Depends on Phase 3 (split-view shell + Stats Bar).
|
||||
|
||||
- [ ] T021 [US3] Create `DatasetList.svelte` component in `frontend/src/routes/datasets/DatasetList.svelte` — card grid with pagination, search input, bulk selection checkboxes. Each card renders: `table_name` (linked), `schema`, `column_count`, `metric_count` (or hidden if 0), progress bar (mapped/total % with green/yellow/blue colors), quick-action buttons (🗺 Map Columns, 📄 Generate Docs). Clicking card body dispatches `onselect(datasetId)`. (RATIONALE: cards give richer visual info than table rows; REJECTED: keep table — doesn't show progress bars well)
|
||||
- [ ] T022 [US3] Implement client-side filtering in `DatasetList.svelte`: receive `activeFilter` prop from parent; filter datasets array in `$derived` by `mapped_fields.mapped === 0` (unmapped), `> 0` (mapped), `linked_dashboard_count > 0` (linked), or no filter (all). Affected datasets propagate as `filteredDatasets` that pagination uses.
|
||||
- [ ] T023 [US3] Preserve existing pagination (server-side) in `DatasetList.svelte`: when no client-side filter active, paginate via API; when client filter active, paginate filtered array locally (page_size items per page). Total count shown from `stats.total` or `filteredDatasets.length`.
|
||||
- [ ] T024 [US3] Migrate existing search input from old `+page.svelte` into `DatasetList.svelte` — debounced search calls `loadDatasets()` from parent via prop callback. Existing `debounce` util reused.
|
||||
- [ ] T025 [US3] Wire quick-action buttons in `DatasetList.svelte`: "🗺 Map Columns" dispatches `onaction(dataset, 'map_columns')`; "📄 Generate Docs" dispatches `onaction(dataset, 'generate_docs')`. Parent `+page.svelte` opens existing modals (reused from current implementation).
|
||||
- [ ] T026 [P] [US3] Write vitest component test for `DatasetList` in `frontend/src/routes/datasets/__tests__/DatasetList.test.js`: test cards render with all fields, progress bar color logic, filter removes non-matching cards, search triggers callback
|
||||
|
||||
**Verification**: `cd frontend && npm run test`
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Detail Panel + Metrics Component (US4 + US7-frontend — P2)
|
||||
|
||||
> Right panel of split-view. Depends on Phase 3 (split-view shell) and Phase 2 (US7 backend metrics).
|
||||
|
||||
- [ ] T027 [US4] Create `DatasetPreview.svelte` component in `frontend/src/routes/datasets/DatasetPreview.svelte` — displays detail for `selectedDatasetId`. States: placeholder "Выберите датасет" (no selection), skeleton (loading), detail view (loaded), error banner (API failure). On `selectedDatasetId` change, fetches `GET /api/datasets/{id}?env_id=...` via `api.getDatasetDetail()`.
|
||||
- [ ] T028 [US4] Build dataset header section in `DatasetPreview.svelte`: `table_name` (bold), `schema` · `column_count` колонок · `metric_count` метрик, linked dashboards (clickable pills), created/changed dates. Linked dashboards navigate to `/dashboards/{slug}` on click. (FR-007)
|
||||
- [ ] T029 [US4] Create `ColumnsTable.svelte` component in `frontend/src/routes/datasets/ColumnsTable.svelte` — table with columns: name (verbose_name bold, else column_name italic), type chip (color-coded: DATE=green, STRING=purple, INT=blue, FLOAT=orange), description (or "✏️ Добавить описание" action prompt), edit button. Rows from `dataset.columns`. (FR-008, FR-010, FR-011)
|
||||
- [ ] T030 [US4] Create `MetricsTable.svelte` component in `frontend/src/routes/datasets/MetricsTable.svelte` — table with columns: metric_name (verbose_name bold, else metric_name italic), expression shown as gray hint under name when no description, description (or "✏️ Добавить описание" prompt), edit button. Rows from `dataset.metrics`. (FR-009, FR-010, FR-011, FR-012)
|
||||
- [ ] T031 [US4] Wire `ColumnsTable` and `MetricsTable` into `DatasetPreview.svelte` as child components. Pass `dataset.columns` and `dataset.metrics` as props respectively.
|
||||
- [ ] T032 [P] [US4] Write vitest component test for `DatasetPreview` in `frontend/src/routes/datasets/__tests__/DatasetPreview.test.js`: test no-selection placeholder, loading skeleton, loaded state with header+columns+metrics, error state
|
||||
- [ ] T033 [P] [US4] Write vitest component test for `ColumnsTable` in `frontend/src/routes/datasets/__tests__/ColumnsTable.test.js`: test verbose_name bold/column_name italic fallback, type chip colors, mapped/unmapped badge, empty state
|
||||
|
||||
**Verification**: `cd frontend && npm run test`
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Inline Edit (US5 — P3)
|
||||
|
||||
> Click-to-edit descriptions. Depends on Phase 4 (columns/metrics visible) + Phase 2 (backend endpoints).
|
||||
|
||||
- [ ] T034 [US5] Implement inline-edit state in `ColumnsTable.svelte`: `$state(isEditingRowId)`. On ✏️ click: set `isEditingRowId`, render `<textarea>` with existing description (or empty), "Сохранить" and "Отмена" buttons. On Save: call `PUT /api/datasets/{id}/columns/{col_id}/description`, on success update local state and exit editing, on error show red border + error text. On Cancel: reset to original value, exit editing. (FR-013, FR-014)
|
||||
- [ ] T035 [US5] Implement inline-edit state in `MetricsTable.svelte`: mirror of T034 but using `PUT /api/datasets/{id}/metrics/{metric_id}/description`. Same save/cancel/error behavior.
|
||||
- [ ] T036 [US5] Handle API errors gracefully in both tables: 400 (bad request) — show validation message; 503 (Superset down) — show "Сервис временно недоступен"; network error — show "Нет соединения". Textarea stays open for retry.
|
||||
- [ ] T037 [P] [US5] Write vitest component test for inline-edit in `frontend/src/routes/datasets/__tests__/inline_edit.test.js`: test click opens textarea with prefill, save calls API and updates description, cancel reverts, save error keeps textarea open
|
||||
|
||||
**Verification**: `cd frontend && npm run test`
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Bulk Actions (US6 — P3)
|
||||
|
||||
> Adapt existing bulk functionality to cards + split-view. Depends on Phase 3 (cards with checkboxes).
|
||||
|
||||
- [ ] T038 [US6] Integrate bulk selection from `DatasetList.svelte` into `+page.svelte` orchestrator: on checkbox change in DatasetList, update `selectedIds: Set` in parent. When `selectedIds.size >= 2`, show `BulkActionPanel` at bottom of page (fixed position, z-50). When `selectedIds.size === 0`, hide panel.
|
||||
- [ ] T039 [US6] Re-implement `BulkActionPanel` in `+page.svelte` (can be inline or extracted component): show selected count "✓ N выбрано", action buttons "🗺 Маппинг колонок" and "📄 Генерация документации", "✕ Снять" to deselect all. Buttons open existing modals (Map Columns modal, Generate Docs modal) — preserve current modal code from old `+page.svelte`. (FR-015, FR-016)
|
||||
- [ ] T040 [US6] Ensure bulk selection works across pages: when pagination changes page, selected IDs persist (already implemented in current `loadDatasets` logic — verify and preserve)
|
||||
- [ ] T041 [P] [US6] Write vitest regression test for bulk actions in `frontend/src/routes/datasets/__tests__/bulk_actions.test.js`: test panel appears at 2+ selected, disappears at 0, map columns modal opens, generate docs modal opens, selected count displays correctly
|
||||
|
||||
**Verification**: `cd frontend && npm run test`
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Polish & Cross-Cutting Verification
|
||||
|
||||
> Final integration, WebSocket connection, edge cases, ADR compliance check.
|
||||
|
||||
- [ ] T042 Add WebSocket subscription in `+page.svelte`: on mount, subscribe to `dataset.updated` events via existing WebSocket store. On receiving event with matching `env_id`, call `loadDatasets()` to refresh list + Stats Bar. Unsubscribe on destroy. (FR-024)
|
||||
- [ ] T043 Handle loading/empty/error states consistently across all components: StatsBar — skeleton tiles / error message; DatasetList — skeleton cards / "Датасеты не найдены" / error banner; DatasetPreview — placeholder / skeleton / error with retry. Use existing Tailwind `animate-pulse` skeleton pattern.
|
||||
- [ ] T044 Handle edge case: 0 columns and 0 metrics — both tables show "Нет данных" instead of trying to render empty table
|
||||
- [ ] T045 Handle edge case: API 503 (Superset unavailable) — show error banner at top of page with retry button, do not crash
|
||||
- [ ] T046 Add belief runtime markers (`belief_scope`, `reason`, `reflect`, `explore`) in `datasets.py` for C4/C5 contracts: both inline-edit endpoints, modified `get_datasets()`, WebSocket event emission. Also in `_datasets.py` modified `get_dataset_detail()`. Follow `molecular-cot-logging` JSON-line format.
|
||||
- [ ] T047 Full-stack verification: `cd backend && source .venv/bin/activate && python -m pytest -v` — all backend tests pass
|
||||
- [ ] T048 Frontend verification: `cd frontend && npm run test` — all vitest tests pass; `npm run lint` — no lint errors; `npm run build` — production build succeeds
|
||||
- [ ] T049 Semantic audit: run `axiom_semantic_validation audit_contracts` on `frontend/src/routes/datasets/` and `backend/src/api/routes/datasets.py` — no missing required metadata, no orphan C4/C5 contracts, no broken anchors
|
||||
- [ ] T050 Rejected-path regression check: verify no task re-introduces `override_columns=true` (T011/T012 must use `false`); verify no direct Superset API call from frontend (all mutations go through ss-tools backend per ADR-0003)
|
||||
- [ ] T051 Browser validation: navigate `http://localhost:8100/datasets`, verify Stats Bar loads with 33/28/5/12 counts, click filter tiles, click card → detail panel loads with columns+metrics, inline-edit a column description and verify save, select 2+ cards → bulk panel appears, click Map Columns → modal opens
|
||||
|
||||
**Verification**: `cd backend && source .venv/bin/activate && python -m pytest -v && cd frontend && npm run test && npm run build`
|
||||
|
||||
---
|
||||
|
||||
## Task Summary
|
||||
|
||||
| Phase | Tasks | Story | Parallel |
|
||||
|-------|-------|-------|----------|
|
||||
| 1. Setup | T001–T004 | — | T001∥T002, T003∥T004 |
|
||||
| 2. Foundation (US7) | T005–T014 | US7 | T005–T010 partial; T011–T013 after T005–T009 |
|
||||
| 3. Stats Bar + Split View (US1+US2) | T015–T020 | US1,US2 | T019∥T020 |
|
||||
| 4. Dataset Cards (US3) | T021–T026 | US3 | T026∥ after T021–T025 |
|
||||
| 5. Detail Panel (US4+US7) | T027–T033 | US4 | T032∥T033 |
|
||||
| 6. Inline Edit (US5) | T034–T037 | US5 | T037∥ after T034–T036 |
|
||||
| 7. Bulk Actions (US6) | T038–T041 | US6 | T041∥ after T038–T040 |
|
||||
| 8. Polish & Verification | T042–T051 | — | T046–T050 parallel after T042–T045 |
|
||||
|
||||
**Total**: 51 tasks across 8 phases.
|
||||
|
||||
### Story Independent Verification
|
||||
|
||||
| Story | How to verify independently |
|
||||
|-------|---------------------------|
|
||||
| US1 | Load `/datasets`, check 4 tiles visible, click → filter applies |
|
||||
| US2 | Desktop: 40/60 split grid visible; mobile: slide-over on card click |
|
||||
| US3 | Card shows all fields + progress bar; filter/search/page work |
|
||||
| US4 | Click card → right panel loads columns table + metrics table |
|
||||
| US5 | Click ✏️ on column → textarea → save → updated description persists |
|
||||
| US6 | Check 2+ cards → bottom panel appears → map/docs buttons work |
|
||||
| US7 | `curl /api/datasets/32` → response has `metrics` and `metric_count` |
|
||||
|
||||
### ADR / Guardrail Coverage
|
||||
|
||||
| ADR | Guarded by tasks |
|
||||
|-----|-----------------|
|
||||
| ADR-0002 (semantic protocol) | T046 (belief markers), T049 (semantic audit) |
|
||||
| ADR-0003 (orchestrator) | T011, T012, T050 (no direct frontend-to-Superset) |
|
||||
| ADR-0006 (Svelte 5 runes) | All T015–T041 (components use runes only) |
|
||||
| FR-023 (override_columns) | T011, T012, T050 (always `false`) |
|
||||
| INV_7 (fractal limit) | T016 (decompose 974→<400 lines) |
|
||||
Reference in New Issue
Block a user