# 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_mapping`, `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) - [ ] T007a Add `metric_count: int` to `DatasetItem` DTO in `backend/src/api/routes/datasets.py`. Enrich list items with metric count during `resource_service.get_datasets_with_status()` (extract `len(dataset.get("metrics", []))` from Superset dataset metadata). (RATIONALE: FR-025 — cards must display metric count; REJECTED: lazy-loading metric count per card — too many requests, UX jank) - [ ] 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, non-filtered) 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)`. Add server-side filtering: accept `?filter=unmapped|mapped|linked|all`, filter full list before pagination slice. (RATIONALE: one API call for both list + stats per FR-022; server-side filtering ensures all matching datasets returned; REJECTED: separate /stats endpoint adds unnecessary request, client-side filtering incompatible with pagination) - [ ] 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 (parent calls API with `?filter=`); toggles filter on second click (dispatches `null` to clear); skeleton tiles when loading. Tiles use `aria-pressed` reflecting active state. (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 ``, ``, `` 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 triggers API reload with `?filter=`, 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 server-side filtering in `+page.svelte` orchestrator: when `activeFilter` changes to non-null value, call `loadDatasets(filter=activeFilter, page=1)`. Stats Bar tiles receive `activeFilter` prop and highlight accordingly. Backend filters full dataset list before pagination (FR-026). - [ ] T023 [US3] Preserve existing pagination (server-side) in `DatasetList.svelte`: pagination controls call `loadDatasets(page=N)` via parent callback. Total count shown from `stats.total`. When filter is active, `stats.total` still shows global total; pagination shows total matching count from API response `total` field. - [ ] 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` — presentational component receiving `dataset`, `isLoading`, `error` props. States: placeholder "Выберите датасет" (dataset=null, not loading), skeleton (isLoading=true), detail view (dataset data), error banner (error string + onretry callback). Parent `+page.svelte` (DatasetHub) fetches `GET /api/datasets/{id}` on card selection and passes result as props. (RATIONALE: container-fetches pattern for cleaner testing and separation of concerns; REJECTED: self-fetching component — couples data fetching with rendering) - [ ] 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 `