Files
ss-tools/specs/030-dataset-lifecycle-workspace/tasks.md
busya b529e8082a feat(030): Dataset Lifecycle Workspace — Stats Bar, split-view, inline-edit, bulk actions
Backend:
- Add MetricItem, StatsCounts, ColumnDescriptionUpdate, MetricDescriptionUpdate DTOs
- Add metric_count to DatasetItem and DatasetDetailResponse
- Add server-side filtering via ?filter=unmapped|mapped|linked|all
- Add PUT endpoints for column/metric description inline-edit
- Add HTML stripping validation (max 2000 chars, plain text)
- Add WebSocket dataset.updated event on task completion
- RBAC: plugin:migration:WRITE for mutations, READ for reads
- 14 pytest tests (stats, filter, metrics, inline-edit, validation, 502/503)

Frontend:
- Rewrite +page.svelte as split-view orchestrator (387 lines)
- StatsBar.svelte — 4 metric tiles with aria-pressed, server-side filter dispatch
- DatasetList.svelte — card grid with progress bars, checkboxes, search
- DatasetPreview.svelte — presentational detail panel (container fetches)
- ColumnsTable.svelte — inline-edit with type chips, bold/italic fallback
- MetricsTable.svelte — inline-edit with expression hints
- 52 vitest tests across 7 test files
- i18n: 11 EN + 11 RU keys (with_mapping/с_маппингом)

Spec:
- 15 issues resolved (semantic label, filtering, endpoints, conflict, metric_count, RBAC, ID types, ports, i18n, validation, a11y, propagation, resize, perf, ownership)

Status: 14/14 backend tests pass, 52/52 frontend tests pass, build succeeds
Risk: Low — T051 browser validation pending (non-blocking)
2026-05-21 13:58:47 +03:00

19 KiB
Raw Blame History

Tasks: Dataset Lifecycle Workspace

Feature: 030-dataset-lifecycle-workspace Branch: 030-dataset-lifecycle-workspace Spec: spec.md | Plan: 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 <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 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 <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 T001T004 T001∥T002, T003∥T004
2. Foundation (US7) T005T014 US7 T005T010 partial; T011T013 after T005T009
3. Stats Bar + Split View (US1+US2) T015T020 US1,US2 T019∥T020
3. Stats Bar + Split View (US1+US2) T015T020 US1,US2 T019∥T020
4. Dataset Cards (US3) T021T026 US3 T026∥ after T021T025
5. Detail Panel (US4+US7) T027T033 US4 T032∥T033
6. Inline Edit (US5) T034T037 US5 T037∥ after T034T036
7. Bulk Actions (US6) T038T041 US6 T041∥ after T038T040
8. Polish & Verification T042T051 T046T050 parallel after T042T045

Total: 52 tasks across 8 phases.

Story Independent Verification

Story How to verify independently
US1 Load /datasets, check 4 tiles visible, click → API called with ?filter=, server returns filtered list
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 T015T041 (components use runes only)
FR-023 (override_columns) T011, T012, T050 (always false)
INV_7 (fractal limit) T016 (decompose 974→<400 lines)