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)
This commit is contained in:
2026-05-21 13:58:47 +03:00
parent 673c0bb217
commit b529e8082a
32 changed files with 2191 additions and 942 deletions

View File

@@ -12,7 +12,7 @@
## 2. The "Happy Path" Narrative
Аналитик открывает страницу датасетов и сразу видит Stats Bar: 142 датасета всего, 38 без маппинга, 89 с описанием, 12 связаны с дашбордами. Он кликает "⚠️ Без маппинга"список слева мгновенно фильтруется до 38 датасетов. Выбирает первый датасет в списке — справа без перезагрузки открывается детальная панель: хедер с именем таблицы и схемой, секция колонок с progress-индикаторами описаний, секция метрик. Видит колонку без описания, кликает "✏️ Добавить описание" — появляется textarea, вводит описание, сохраняет. Описание мгновенно отображается. Затем выделяет 3 датасета через чекбоксы, внизу появляется панель — запускает "Маппинг колонок" для всех трёх одним кликом.
Аналитик открывает страницу датасетов и сразу видит Stats Bar: 142 датасета всего, 38 без маппинга, 89 с маппингом, 12 связаны с дашбордами. Он кликает «⚠️ Без маппинга»Stats Bar подсвечивает метрику, фронтенд отправляет запрос `?filter=unmapped&page=1`, сервер возвращает отфильтрованные 38 датасетов с серверной пагинацией. Выбирает первый датасет в списке — DatasetHub фетчит `GET /api/datasets/{id}`, справа без перезагрузки открывается детальная панель: хедер с именем таблицы и схемой, секция колонок с progress-индикаторами описаний, секция метрик. Видит колонку без описания, кликает «✏️ Добавить описание» — появляется textarea, вводит описание, сохраняет через `PUT /api/datasets/{id}/columns/{col_id}/description`. Описание мгновенно отображается. Затем выделяет 3 датасета через чекбоксы, внизу появляется панель — запускает «Маппинг колонок» для всех трёх одним кликом.
## 3. Interface Mockups
@@ -22,7 +22,7 @@
- **Layout**: Двухколоночный split-view (40/60) на десктопе. При < 768px одноколоночный с slide-over для деталей.
- **Key Elements**:
- **Stats Bar**: 4 метрики в строке вверху, каждая кнопка-фильтр с иконкой, числом и подписью. Активная метрика подсвечена (primary background).
- **Stats Bar**: 4 метрики в строке вверху, каждая кнопка-фильтр (`<button>` с `aria-pressed`) с иконкой, числом и подписью. Активная метрика подсвечена (primary background).
- **Left Panel**: Список карточек датасетов с поиском и пагинацией. Каждая карточка содержит чекбокс.
- **Right Panel**: Детальная панель с хедером, таблицами колонок/метрик, связанными дашбордами.
- **Bulk Action Bar**: Fixed bottom panel, slides up when 2+ datasets selected.
@@ -48,7 +48,7 @@
- **`@UX_RECOVERY`**:
- API error in right panel click "Retry" to reload dataset detail.
- API error in list click "Retry" to reload full list.
- Save conflict show warning "Данные были изменены другим пользователем. Обновить?"
- Save succeeds regardless of concurrent edits (last-write-wins); next detail load shows current value.
- Network offline show offline banner, auto-retry on reconnect.
- **`@UX_REACTIVITY`**:
- `$state` for: `selectedDatasetId`, `activeFilter`, `selectedIds`, `editingCell`
@@ -91,12 +91,18 @@
- Textarea остаётся открытой с введённым текстом (не теряется).
- **Recovery**: Повторное нажатие "Сохранить" либо "Отмена" для выхода без сохранения.
### Scenario D: Конфликт при сохранении (409 Conflict)
### Scenario D: Ресайз окна между десктопом и мобильным
- **User Action**: Сохраняет описание, но данные были изменены другим пользователем.
- **User Action**: Изменяет ширину окна браузера между 768px и <768px.
- **System Response**:
- (UI) Предупреждение: "⚠ Данные были изменены. Обновить страницу?" с кнопками "Обновить" и "Отмена".
- **Recovery**: Кнопка "Обновить" перезагружает детали датасета.
- (UI) Выбранный датасет сохраняется при любом ресайзе.
- При сжатии до <768px: если детальная панель открыта она становится slide-over (открытым). Список занимает 100% ширины.
- При расширении до 768px: split-view восстанавливается. Если slide-over был закрыт правая панель показывает последний выбранный датасет или placeholder «Выберите датасет».
- **Recovery**: Не требуется состояние сохраняется.
- **User Action**: Сохраняет описание.
- **System Response**: Last-write-wins последнее сохранение перезаписывает предыдущее. Конфликт не детектируется (ETag/блокировки out of scope). При успешном сохранении сразу показывается актуальное значение.
- **Recovery**: Не требуется. Если другой пользователь перезаписал описание при следующей загрузке деталей датасета отобразится его версия.
## 5. Tone & Voice