# 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: Два новых superset-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