# Feature Specification: Dataset Lifecycle Workspace

**Feature Branch**: `030-dataset-lifecycle-workspace`
**Created**: 2026-05-19
**Status**: Draft
**Input**: Feature 029 — Dataset Lifecycle Workspace (variant C)

## Clarifications

### Session 2026-05-19

- Q: Stats Bar фильтрует клиентски или серверно? → A: Серверная фильтрация через query параметр. Бэкенд в ответе `GET /api/datasets` возвращает 4 stats-числа (`total`, `unmapped_count`, `mapped_count`, `linked_count`). При клике на метрику Stats Bar фронтенд отправляет запрос с `?filter=unmapped|mapped|linked|all` и `filter` сбрасывает `page` на 1. Пагинация остаётся серверной; бэкенд фильтрует полный список перед пагинацией. Stats-числа считаются глобально (не зависят от фильтра).
- Q: Каким запросом сохранять inline-edit описания колонок/метрик? → A: Два новых ss-tools эндпоинта: `PUT /api/datasets/{id}/columns/{col_id}/description` и `PUT /api/datasets/{id}/metrics/{metric_id}/description` (body: `{description: "text"}`). Бэкенд выполняет GET датасета из Superset, модифицирует одну колонку/метрику, PUT обратно полный объект. В Superset нет PATCH — только PUT с полным датасетом.
- Q: Как обрабатывать конфликт одновременного редактирования? → A: Last-write-wins. Бэкенд всегда выполняет GET→modify→PUT без проверки версии. Последний сохранивший перезаписывает. Для описаний колонок/метрик это приемлемый риск. ETag/блокировки — out of scope.
- Q: Параметр `override_columns` при PUT в Superset? → A: Всегда `override_columns=false`. Бэкенд получает полный датасет из GET, модифицирует только description нужной колонки, PUT обратно с `override_columns=false`. Superset сохраняет все колонки, не участвовавшие в payload.
- Q: Как Stats Bar узнаёт об изменении counts после асинхронных задач? → A: WebSocket push. При завершении mapping/docs задачи сервер отправляет событие `dataset.updated` через существующий WebSocket. Фронтенд по событию перезапрашивает `GET /api/datasets` и обновляет Stats Bar + карточки автоматически.

## User Scenarios & Testing

### User Story 1 — Stats-Driven Overview & Filtering (Priority: P1)

Пользователь заходит на страницу датасетов и видит 4 ключевые метрики в Stats Bar вверху. Каждая метрика кликабельна как фильтр списка. Метрики дают мгновенный обзор состояния датасетов и позволяют быстро отфильтровать те, которые требуют внимания.

**Why this priority**: Stats bar — первое, что видит пользователь. Он заменяет текущую пустую область над таблицей и даёт моментальный actionable insight. Без него страница неотличима от текущей реализации.

**Independent Test**: Перейти на `/datasets`, проверить что каждая из 4 метрик отображается и клик по ней фильтрует список.

**Acceptance Scenarios**:

1. **Given** пользователь видит страницу датасетов с загруженными данными, **When** страница загружена, **Then** Stats Bar показывает 4 метрики: Все (total), Без маппинга, С маппингом, С дашбордами
2. **Given** Stats Bar отображается, **When** пользователь кликает на "Без маппинга", **Then** список фильтруется до датасетов с `mapped_fields.mapped === 0`, метрика подсвечена
3. **Given** Stats Bar отображается, **When** пользователь кликает на уже активный фильтр, **Then** фильтр сбрасывается и показываются все датасеты
4. **Given** API возвращает ошибку при загрузке, **When** страница рендерится, **Then** Stats Bar показывает 0 для всех метрик или состояние ошибки

---

### User Story 2 — Split-View с карточками и детальной панелью (Priority: P1)

Пользователь видит split-view: слева (40%) — список датасетов в виде карточек с прогресс-баром маппинга, справа (60%) — детальный просмотр выбранного датасета без перезагрузки страницы. На мобильных (< 768px) список занимает весь экран, а детали открываются как slide-over.

**Why this priority**: Split-view — ядро workspace. Без него нет смысла в Stats Bar и inline-редактировании. Это главное UX-изменение относительно текущей таблицы + отдельной страницы деталей.

**Independent Test**: Выбрать датасет в левой панели, убедиться что правая панель показывает его детали без перезагрузки страницы.

**Acceptance Scenarios**:

1. **Given** страница загружена, **When** список датасетов отрендерен, **Then** карточки отображаются в левой панели (40%), правая панель показывает placeholder "Выберите датасет"
2. **Given** список загружен, **When** пользователь кликает на карточку датасета, **Then** правая панель загружает детали выбранного датасета (имя, схема, колонки, метрики, дашборды)
3. **Given** датасет выбран в правой панели, **When** пользователь кликает на другую карточку, **Then** правая панель обновляется без полной перезагрузки страницы
4. **Given** ширина экрана < 768px, **When** пользователь кликает на карточку, **Then** правая панель открывается как slide-over поверх списка
5. **Given** slide-over открыт на мобильном, **When** пользователь кликает "Назад" или вне slide-over, **Then** slide-over закрывается, возвращаясь к списку

---

### User Story 3 — Карточки датасетов с прогресс-баром (Priority: P2)

Каждая карточка в левой панели содержит: имя таблицы, схему, количество колонок, количество метрик, прогресс-бар маппинга (mapped/total в процентах), кнопки быстрых действий (Маппинг, Доки). Прогресс-бар визуально показывает состояние маппинга.

**Why this priority**: Карточки — основной навигационный элемент. Без них split-view бесполезен, но технически карточки зависят от структуры split-view (US2).

**Independent Test**: Проверить что карточка содержит все обязательные поля и прогресс-бар правильно отражает `mapped/total`.

**Acceptance Scenarios**:

1. **Given** список датасетов загружен, **When** карточки отрендерены, **Then** каждая карточка показывает: имя таблицы, схему, кол-во колонок, кол-во метрик, прогресс-бар
2. **Given** датасет имеет `mapped_fields: {total: 10, mapped: 7}`, **When** карточка рендерится, **Then** прогресс-бар заполнен на 70% зелёным/жёлтым/синим в зависимости от процента
3. **Given** датасет без метрик (metric_count = 0), **When** карточка рендерится, **Then** кол-во метрик показывает "0" или скрыто
4. **Given** карточка отображается, **When** пользователь нажимает кнопку "Маппинг", **Then** открывается модалка маппинга колонок для этого датасета

---

### User Story 4 — Детальная панель с колонками и метриками (Priority: P2)

Правая панель показывает: хедер датасета (полное имя, схема, кол-во колонок/метрик, связанные дашборды, дата создания), секцию "Колонки" (таблица с column_name, verbose_name, type, description, кнопка inline-редактирования), секцию "Метрики" (таблица с metric_name, expression, verbose_name, description, кнопка inline-редактирования), связанные дашборды.

**Why this priority**: Детальная панель — вторая половина split-view. Без неё нет рабочего пространства, но технически зависит от US2.

**Independent Test**: Выбрать датасет с метриками и колонками, проверить что обе секции отображаются корректно.

**Acceptance Scenarios**:

1. **Given** датасет выбран, **When** правая панель загружена, **Then** хедер показывает: `table_name`, `schema`, `column_count`, `metric_count`, `linked_dashboard_count`, `created_on`
2. **Given** у колонки есть `verbose_name`, **When** секция колонок рендерится, **Then** `verbose_name` показывается жирным, `column_name` не показывается
3. **Given** у колонки нет `verbose_name`, **When** секция колонок рендерится, **Then** `column_name` показывается курсивом как fallback
4. **Given** у колонки/метрики нет `description`, **When** строка рендерится, **Then** показывается "✏️ Добавить описание" как action prompt
5. **Given** у метрики нет `description`, **When** строка метрики рендерится, **Then** под именем показывается `expression` как подсказка

---

### User Story 5 — Inline-редактирование описаний (Priority: P3)

Пользователь может редактировать description колонок и метрик прямо в детальной панели. Клик по ✏️ на строке открывает textarea, пользователь вводит текст, сохраняет через API. Изменения применяются без перезагрузки страницы.

**Why this priority**: Inline-редактирование — ключевая фича workspace, но она менее критична чем базовая навигация и просмотр. Зависит от US4 (детальная панель) и бэкенд-эндпоинта.

**Independent Test**: Кликнуть ✏️ на колонке, ввести описание, сохранить — проверить что описание обновилось и отображается.

**Acceptance Scenarios**:

1. **Given** строка колонки без описания, **When** пользователь кликает "✏️ Добавить описание", **Then** появляется textarea с кнопками "Сохранить" и "Отмена"
2. **Given** textarea открыта, **When** пользователь вводит текст и нажимает "Сохранить", **Then** описание колонки сохраняется через `PUT /api/datasets/{id}/columns/{col_id}/description`, описание метрики — через `PUT /api/datasets/{id}/metrics/{metric_id}/description`; textarea закрывается, описание отображается
3. **Given** textarea открыта, **When** пользователь нажимает "Отмена", **Then** textarea закрывается без изменений
4. **Given** сохранение не удалось (ошибка API), **When** пользователь нажимает "Сохранить", **Then** показывается сообщение об ошибке, textarea остаётся открытой для повторной попытки
5. **Given** строка колонки с существующим описанием, **When** пользователь кликает ✏️, **Then** textarea открывается с предзаполненным текстом описания

---

### User Story 6 — Bulk-режим с нижней панелью (Priority: P3)

Чекбоксы на карточках позволяют выбрать несколько датасетов. При выборе 2+ датасетов появляется нижняя панель с bulk-действиями: "Маппинг колонок" и "Генерация документации". Модалки bulk-маппинга и генерации доков переиспользуются из текущей реализации.

**Why this priority**: Bulk-режим уже существует в текущей реализации, его нужно адаптировать под карточки в split-view. Менее приоритетно чем базовый просмотр.

**Independent Test**: Выбрать 3 датасета через чекбоксы, проверить что нижняя панель появляется и кнопки работают.

**Acceptance Scenarios**:

1. **Given** карточки отображаются с чекбоксами, **When** пользователь отмечает 2+ датасета, **Then** нижняя панель появляется с кнопками "Маппинг колонок" и "Генерация документации"
2. **Given** нижняя панель видна, **When** пользователь снимает выделение со всех датасетов, **Then** панель исчезает
3. **Given** нижняя панель видна, **When** пользователь нажимает "Маппинг колонок", **Then** открывается модалка маппинга (переиспользованная из текущей реализации)
4. **Given** выбраны датасеты с разных страниц пагинации, **When** нижняя панель отображается, **Then** счётчик показывает общее количество выбранных датасетов

---

### User Story 7 — Отображение метрик в API и UI (Priority: P2)

Бэкенд API возвращает метрики в `DatasetDetailResponse`. Фронтенд отображает их в секции "Метрики" детальной панели.

**Why this priority**: Метрики — обязательная часть детальной панели (US4), но требуют изменений на бэкенде. Без бэкенд-поддержки UI нечего показывать.

**Independent Test**: Вызвать `GET /api/datasets/{id}`, проверить наличие полей `metrics` и `metric_count` в ответе, и что `MetricItem` содержит все обязательные поля.

**Acceptance Scenarios**:

1. **Given** датасет имеет метрики в Superset API, **When** бэкенд получает `GET /api/datasets/{id}`, **Then** ответ содержит `metrics: [...]` и `metric_count`
2. **Given** у метрики есть все поля Superset API, **When** бэкенд трансформирует ответ, **Then** `MetricItem` содержит: `id`, `metric_name`, `expression`, `verbose_name`, `description`, `metric_type`
3. **Given** Superset API возвращает датасет без метрик, **When** бэкенд обрабатывает ответ, **Then** `metrics: []` и `metric_count: 0`

---

### Edge Cases

- Что происходит когда API датасетов возвращает 503 (Superset недоступен)?
- Как обрабатывается случай когда `detail` API для выбранного датасета падает, но список загружен?
- Как ведёт себя split-view при ресайзе окна между десктопным и мобильным layout? → **Решено**: Выбранный датасет сохраняется. При сжатии до <768px открытая детальная панель становится slide-over (открытым). При расширении до ≥768px восстанавливается split-view; если slide-over был закрыт — правая панель показывает последний выбранный датасет или placeholder. (FR-034–FR-036)
- Что показывать в Stats Bar когда данные ещё не загружены (loading state)?
- Как обрабатывать случай когда у датасета 0 колонок и 0 метрик?
- Что происходит при сохранении inline-edit когда несколько пользователей редактируют один датасет (конфликт)? → **Решено**: last-write-wins. Последний сохранивший перезаписывает. ETag/блокировки — out of scope.
- Как Stats Bar обновляет числа при переключении окружения — запрашивает новые counts?

## Requirements

### Functional Requirements

- **FR-001**: Система MUST отображать Stats Bar с 4 метриками (Все, Без маппинга, С маппингом, С дашбордами) вверху страницы `/datasets`
- **FR-002**: Каждая метрика в Stats Bar MUST быть кликабельной и действовать как фильтр списка датасетов. Фильтрация MUST выполняться серверно через query параметр `?filter=unmapped|mapped|linked|all` с серверной пагинацией. Stats-числа считаются глобально.
- **FR-003**: Система MUST отображать split-view layout: левая панель (40%) со списком карточек, правая панель (60%) с деталями выбранного датасета
- **FR-004**: При ширине экрана < 768px правая панель MUST открываться как slide-over поверх списка
- **FR-005**: Карточка датасета MUST содержать: имя таблицы, схему, количество колонок, количество метрик, прогресс-бар маппинга, кнопки быстрых действий
- **FR-006**: Прогресс-бар MUST визуально отображать процент маппинга (mapped/total) с цветовой кодировкой: зелёный (100%), жёлтый (≥50%), синий (<50%)
- **FR-007**: Правая панель MUST показывать хедер датасета (полное имя, схема, кол-во колонок/метрик, связанные дашборды, дата создания)
- **FR-008**: Правая панель MUST содержать секцию "Колонки" с таблицей (column_name, verbose_name, type, description, ✏️ inline edit)
- **FR-009**: Правая панель MUST содержать секцию "Метрики" с таблицей (metric_name, expression, verbose_name, description, ✏️ inline edit)
- **FR-010**: При наличии `verbose_name` система MUST показывать его жирным; при отсутствии — показывать `column_name`/`metric_name` курсивом
- **FR-011**: При отсутствии `description` система MUST показывать "✏️ Добавить описание" как action prompt
- **FR-012**: Для метрик без описания система MUST показывать `expression` как подсказку под именем
- **FR-013**: Inline-редактирование MUST поддерживать открытие textarea с кнопками "Сохранить"/"Отмена", с предзаполнением существующего описания
- **FR-014**: Сохранение описания колонки MUST выполняться через `PUT /api/datasets/{id}/columns/{col_id}/description` (body: `{description: "text"}`). Сохранение описания метрики — `PUT /api/datasets/{id}/metrics/{metric_id}/description`. Бэкенд выполняет GET→modify→PUT в Superset.
- **FR-015**: Карточки MUST содержать чекбоксы для bulk-выбора, при 2+ выбранных MUST появляться нижняя панель с bulk-действиями
- **FR-016**: Bulk-действия "Маппинг колонок" и "Генерация документации" MUST переиспользовать существующие модалки
- **FR-017**: `DatasetDetailResponse` на бэкенде MUST включать `metrics: list[MetricItem]` и `metric_count: int`
- **FR-018**: `MetricItem` Pydantic DTO MUST содержать поля: `id`, `metric_name`, `expression`, `verbose_name`, `description`, `metric_type`
- **FR-019**: `SupersetClient.get_dataset_detail` MUST извлекать `dataset.get("metrics", [])` из ответа Superset API
- **FR-020**: Система MUST поддерживать i18n ключи (EN/RU) для: all, without_mapping, with_mapping, linked_to_dashboards, metrics, metric_count, no_selection, save_description, add_description
- **FR-021**: Breadcrumb для `/datasets` MUST отображать "Датасеты / Datasets" (EN/RU), а не "Маппер колонок"
- **FR-022**: Ответ `GET /api/datasets` MUST включать поля stats: `total` (int), `unmapped_count` (int), `mapped_count` (int), `linked_count` (int) — для отрисовки Stats Bar без дополнительных запросов
- **FR-023**: PUT в Superset при сохранении inline-edit MUST выполняться с `override_columns=false` (query parameter) — предотвращает случайное удаление колонок, не упомянутых в payload
- **FR-024**: По WebSocket-событию `dataset.updated` (отправляемому при завершении mapping/docs задачи) фронтенд MUST перезапрашивать `GET /api/datasets` и обновлять Stats Bar counts + карточки без ручного «Обновить»
- **FR-025**: `DatasetItem` в list response MUST включать `metric_count: int` (количество метрик датасета) для отображения на карточках
- **FR-026**: `GET /api/datasets` MUST поддерживать query параметр `?filter=unmapped|mapped|linked|all` для серверной фильтрации перед пагинацией. При отсутствии параметра поведение эквивалентно `filter=all`.
- **FR-027**: Stats Bar метрики MUST быть кнопками с `aria-pressed`, отражающим активное состояние фильтра
- **FR-028**: Slide-over на мобильных MUST захватывать фокус при открытии и закрываться по Escape
- **FR-029**: Ошибки сохранения inline-edit MUST анонсироваться через `aria-live` регион
- **FR-030**: Skeleton-лоадеры MUST использовать `aria-busy="true"` на время загрузки
- **FR-031**: Клик по чекбоксу карточки MUST только переключать выделение датасета, НЕ открывая детальную панель
- **FR-032**: Клик по quick-action кнопке на карточке MUST открывать соответствующую модалку, НЕ открывая детальную панель
- **FR-033**: Клик по телу карточки (вне чекбокса и quick-action кнопок) MUST выбирать датасет и открывать детальную панель
- **FR-034**: Выбранный датасет MUST сохраняться при ресайзе между десктопным и мобильным layout без потери состояния
- **FR-035**: При ресайзе с десктопа на мобильный: если детальная панель открыта, она MUST перейти в режим slide-over (открытый)
- **FR-036**: При ресайзе с мобильного на десктоп: если slide-over закрыт, правая панель MUST показать placeholder или последний выбранный датасет
- **FR-037**: Сохранение описания MUST принимать plain text, max 2000 символов. Пустая строка разрешена (очищает описание). Пробельные символы в начале/конце сохраняются без обрезки. 404 при отсутствии датасета/колонки/метрики. 502/503 при недоступности Superset.

### Key Entities

- **StatsBarMetric**: Метрика в Stats Bar — label, count, icon, filter_key, is_active
- **DatasetCard**: Карточка в левой панели — id, table_name, schema, column_count, metric_count, mapped_progress (%), quick_actions[]
- **DatasetDetailPanel**: Детальная панель — header_info, columns_table, metrics_table, linked_dashboards
- **MetricItem** (новый): id, metric_name, expression, verbose_name, description, metric_type
- **ColumnRow**: Строка в таблице колонок — column_name, verbose_name, type, description, is_editing (UI state)
- **MetricRow**: Строка в таблице метрик — metric_name, expression, verbose_name, description, is_editing (UI state)
- **BulkActionPanel**: Нижняя панель — selected_count, available_actions[], is_visible

## Success Criteria

### Measurable Outcomes

- **SC-001**: Пользователь может за 1 клик перейти от Stats Bar к отфильтрованному списку датасетов
- **SC-002**: Loading state для переключения между датасетами в split-view отображается в течение 100ms; контент деталей загружается согласно latency API (после получения ответа детали рендерятся без дополнительной задержки)
- **SC-003**: Пользователь может отредактировать и сохранить описание колонки/метрики < 3 кликов
- **SC-004**: Stats Bar корректно обновляет значения при переключении окружения
- **SC-005**: Все существующие bulk-действия (маппинг, генерация доков) продолжают работать в новом UI
- **SC-006**: Страница корректно адаптируется между десктопным split-view и мобильным slide-over без потери функциональности
- **SC-007**: Все новые строки интерфейса имеют переводы на EN и RU
