> Архив первоначального проекта. С 2026-10-01 Rio удалён: приложение использует FastAPI, Jinja2 и HTML/CSS/JavaScript. Актуальное описание — в README.md. # План реализации учебного сайта для парсинга > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans for implementation in this session, or superpowers:subagent-driven-development if the user chooses delegation. Steps use checkbox syntax for tracking. **Goal:** Создать универсальный учебный сайт с кабинетом преподавателя на Rio, импортом данных, HTML и динамическими упражнениями, готовый к запуску через Docker Compose. **Architecture:** Один Python-сервис объединяет Rio и FastAPI с Jinja2. Импорт, выборка, публикации и SQLite общие для обоих режимов; публичные запросы читают неизменяемые версии. **Tech Stack:** Python, rio-ui, FastAPI, Jinja2, SQLite через sqlite3, Argon2 для пароля, pytest, requests, BeautifulSoup, Playwright, Docker Compose. Точные версии закрепляются после проверки совместимости в задаче 1. **Spec:** [Утверждённая спецификация](design-spec.md). ## Рабочее место и ограничения Новый проект создаётся в `outputs/parser-lab/` относительно текущего рабочего каталога. Все пути ниже относятся к этому корню. Старый проект `P:\Coding\Python\NetologyWebParserSite\HockeyTeamParse` служит источником хоккейного примера и не перезаписывается. Документы копируются в `docs/` нового проекта. Временные результаты проверок находятся в `work/` текущего рабочего каталога. При наличии Git этапы фиксируются отдельными коммитами; отсутствие Git не блокирует сборку и передачу проекта. - Один аккаунт преподавателя; без регистрации и аккаунтов студентов. - Четыре представления; оба режима для каждого включённого представления. - Файл до 25 МиБ, до 50 000 записей, глубина JSON до 20 уровней; пределы настраиваемы. - Размеры страниц 10, 25, 50, 100; исходное значение 25. Предпросмотр первых 20 записей. - Unicode, пропуски, дубликаты, порядок и исходные типы сохраняются; CSV остаётся строковым. - Русский интерфейс, светлая тема, адаптивность, локальные ресурсы. - Неизменяемые публикации, отсутствие доступа к черновикам и скрытым полям извне. - Один контейнер, порт 8080, постоянный каталог `/data`, HTTP и WebSocket за reverse proxy. - Нет автоматического деплоя на внешний сервер, обучения моделей или публичного API выгрузки. ## Особое внимание при проверке 1. Заголовки JSON с точками, слешами и тильдами не должны путаться с вложенными путями — задачи 2 и 4. 2. Значения null, false, 0 и пустая строка не должны схлопываться в отсутствие поля — задачи 2 и 5. 3. Замена файла и авария публикации не должны повреждать доступный студентам снимок — задачи 3 и 7. 4. Прямой запрос страницы деталей или callback Rio не должен обходить авторизацию либо раскрывать скрытое поле — задачи 6, 8 и 10. 5. Две вкладки преподавателя не должны незаметно перезаписывать чужие изменения — задача 3; сохранять черновик с ожидаемой ревизией и показывать конфликт. ## Карта файлов | Файлы | Ответственность | |---|---| | `pyproject.toml`, `requirements.lock`, `requirements-dev.lock` | Зависимости приложения и проверок | | `src/parser_lab/app.py`, `config.py`, `__main__.py` | Сборка сервиса, конфигурация, запуск | | `src/parser_lab/domain.py`, `paths.py` | Общие типы и JSON Pointer | | `src/parser_lab/importing/csv_reader.py`, `json_reader.py`, `service.py` | Разбор файлов и диагностика | | `src/parser_lab/storage/database.py`, `repository.py`, `migrations/001.sql` | База, миграции, транзакции | | `src/parser_lab/views.py`, `query.py`, `publishing.py` | Конфигурация представлений, выборка, публикация | | `src/parser_lab/auth.py`, `cli.py` | Сессии, вход, создание хеша пароля | | `src/parser_lab/web/routes.py`, `templates/`, `static/` | HTML, стили и ресурсы | | `src/parser_lab/rio_ui/admin/`, `rio_ui/public/`, `rio_ui/theme.py` | Кабинет, динамические страницы, тема | | `tests/unit/`, `tests/integration/`, `tests/browser/` | Проверки поведения | | `examples/data/`, `examples/parsers/` | Данные и решения для преподавателя | | `Dockerfile`, `compose.yaml`, `.env.example`, `.dockerignore`, `README.md` | Развёртывание и использование | Каждая задача включает запуск своей проверки до реализации, подтверждение ожидаемого сбоя, реализацию, повторную проверку и фиксацию результата. Ошибки настройки тестового окружения не считать ожидаемым провалом поведения. ## Задача 1 Совместимый каркас сервиса **Files:** создать `pyproject.toml`, оба lock-файла, `src/parser_lab/{__init__,app,config,__main__}.py`, `tests/integration/test_app.py`. **Interfaces:** `Settings.from_env() -> Settings`; `create_app(settings: Settings) -> FastAPI`. Settings содержит каталог данных, лимиты импорта, имя администратора, хеш пароля, секрет сессий, secure-cookie и доверенные origin. В тестах настройки передаются явно. - [ ] Проверить доступные Python и Docker, прочитать API установленной для проекта Rio, выбрать поддерживаемую Python и закрепить совместимые версии. Не обновлять глобальное окружение пользователя. - [ ] Добавить тест `test_html_route_coexists_with_rio`: `client.get('/healthz').status_code == 200`, HTML-проба содержит `' ImportResult`; `resolve_pointer(record: Record, pointer: str) -> ResolvedValue(present, value)`. - [ ] Добавить проверки: CSV `'id;answer\n001;\n'` даёт `{'id': '001', 'answer': ''}`; JSON сохраняет `False`, `0`, `None`; `resolve_pointer({'a/b': {'~c': 0}}, '/a~1b/~0c').value == 0`; отсутствующий путь имеет `present == False`. - [ ] Добавить параметризованные случаи: UTF-8 BOM, Windows-1251, многострочные CSV-ячейки, плохая длина строки, повторные заголовки и JSON-ключи, JSONL с ошибкой на строке 3, пустые строки JSONL, одиночный объект, вложенный массив, NaN/Infinity, глубина 21, лимиты размера и количества, массив скаляров. - [ ] Выполнить `python -m pytest tests/unit/test_importing.py tests/unit/test_paths.py -q`, проверить ожидаемые провалы. - [ ] Реализовать чтение с `csv` и `json`, строгой обработкой повторяющихся ключей, ограничением размера до разбора и контролем глубины/числа записей. Не угадывать формат по пользовательскому имени без проверки содержимого. JSONL читает построчно; результаты не сохраняются частично. - [ ] Повторить тесты. Проверить объединение полей по всем записям, явный выбор вложенного массива и различение пустого набора от ошибки. ## Задача 3 Хранение черновиков и исходных файлов **Files:** создать `storage/{__init__,database,repository}.py`, `storage/migrations/001.sql`, `tests/integration/test_repository.py`. **Interfaces:** `Repository.create_draft(title: str, source: bytes, imported: ImportResult) -> Draft`; `get_draft(dataset_id: str) -> Draft`; `save_draft(draft: Draft, expected_revision: int) -> Draft`; `replace_import(dataset_id: str, source: bytes, imported: ImportResult, expected_revision: int) -> Draft`. `Draft` содержит id, slug, revision, записи, источник, представления, название и описание. Ошибки: `NotFound`, `RevisionConflict`. - [ ] Добавить тесты сохранения и повторного открытия SQLite: `reopened.records == imported.records`; повторное сохранение со старой revision вызывает `RevisionConflict`; авария транзакции не меняет старую запись. Проверить, что исходное имя `'../../outside.json'` не влияет на фактический путь хранения. - [ ] Запустить `python -m pytest tests/integration/test_repository.py -q` до реализации. - [ ] Реализовать таблицы datasets, draft_records, sources, views, publications, published_records, sessions, schema_version. Записи хранятся JSON вместе с ordinal и внутренним id. Все SQL параметризованы; опубликованный снимок не зависит от изменяемого черновика. - [ ] Реализовать файлы под UUID, временную запись с последующим переносом в пределах `/data`, транзакционную привязку к базе и очистку несвязанных временных файлов. Замена импорта сбрасывает подтверждение настройки полей. - [ ] Повторить тесты и проверить идемпотентный запуск миграции и откат при ошибке записи файла. ## Задача 4 Настройки представлений **Files:** создать `views.py`, `tests/unit/test_views.py`; дополнить общие модели в `domain.py`. **Interfaces:** `ViewConfig(kind, columns, roles, questionnaire_mode, questions_pointer, page_size, search_pointer, category_pointer)`; `FieldConfig(pointer, label)`; `validate_view(config: ViewConfig, records: list[Record]) -> list[ImportIssue]`; `project_record(record: Record, config: ViewConfig) -> DisplayRecord`. `DisplayRecord` содержит только назначенные значения и сведения об их наличии/типе. - [ ] Добавить проверки для всех четырёх видов: произвольные подписи колонок; текст сообщения обязателен в конфигурации, автор — нет; анкета из колонок и массива question/answer; отсутствующее значение в отдельной записи допустимо и помечается. Путь, которого нет ни в одной записи, не допускается как обязательная роль. - [ ] Проверить страницу 25 по умолчанию и отклонение 24; выбор только текстового поля для поиска; категориальное поле должно быть скалярным. Проверить, что `secret` отсутствует во всём сериализованном DisplayRecord, когда оно не выбрано. - [ ] Запустить `python -m pytest tests/unit/test_views.py -q` до реализации. - [ ] Реализовать явные конфигурации без эвристического назначения смысла полям. JSON Pointer отвечает за чтение и экранирование имён; структура анкеты не смешивается с универсальным табличным рендером. - [ ] Повторить тесты, включая слеши и тильды в именах полей, смешанные записи и многострочные ответы. ## Задача 5 Общая выборка и HTML **Files:** создать `query.py`, `web/{__init__,routes}.py`, `web/templates/{base,list,detail,table,cards,messages,questionnaire}.html`, `web/static/site.css`, `tests/unit/test_query.py`, `tests/integration/test_html.py`. **Interfaces:** `Query(page=1, search='', category=None)`; `select_records(records: list[StoredRecord], config: ViewConfig, query: Query) -> PageResult`; `PageResult(items, total, page, page_size, has_next)`. `StoredRecord(id, ordinal, data)` используется обоими режимами. `register_html_routes(app: FastAPI, repository: Repository) -> None` после задачи 7 читает Publication; до этого HTML тестируется на явной фикстуре снимка. - [ ] Добавить тесты выборки: 51 запись и размер 25 дают страницы 25/25/1, ids не пересекаются; поиск регистронезависимый, фильтр точный и сохраняет различие числа 0 и строки `'0'`; порядок стабилен. - [ ] Добавить тест HTML: `BeautifulSoup(response.text, 'html.parser').select('tbody tr')` содержит нужные значения без JavaScript. Вредоносное значение `` остаётся текстом, исполняемый тег не появляется. Проверить отсутствие скрытых полей на списке и деталях. - [ ] Запустить `python -m pytest tests/unit/test_query.py tests/integration/test_html.py -q` до реализации. - [ ] Реализовать общий отбор, Jinja2 с autoescape, реальные ссылки пагинации и деталей. CSS-контракт: `.record`, `.field`, `data-record-id`, `data-field`, `data-value-state`; состояния `missing`, `null`, `value`. Ссылки сохраняют фильтры и версию, значения URL кодируются. - [ ] Определить детали по `/html/{slug}/v/{version}/{view}/records/{id}`. Некорректная страница — 400, отсутствующая — 404. Для фильтра без совпадений страница 1 возвращает 200 с пустым состоянием; следующие — 404. - [ ] Повторить тесты и проверить ручным HTTP-запросом, что содержимое и ссылки входят в исходный ответ. ## Задача 6 Авторизация преподавателя **Files:** создать `auth.py`, `cli.py`, `tests/integration/test_auth.py`; дополнить `app.py`, `config.py`. **Interfaces:** `AuthService.login(username: str, password: str, client_key: str) -> SessionToken`; `require_admin(token: str) -> AdminSession`; `logout(token: str) -> None`; CLI `python -m parser_lab.cli hash-password` читает пароль скрытым вводом. - [ ] Написать тесты: неверный пароль не создаёт сессию; приватный маршрут без сессии отклонён; выход отзывает сессию; изменяющий POST без CSRF отклонён; внешний Origin при WebSocket/callback отклонён; cookie HttpOnly/SameSite и Secure соответствуют настройке. - [ ] Зафиксировать предел входа: 5 неудачных попыток на client_key за 5 минут, далее 429 до истечения окна; срок сессии 12 часов. Добавить тест управляемым временем, без реального ожидания. Не доверять произвольному X-Forwarded-For. - [ ] Запустить `python -m pytest tests/integration/test_auth.py -q` до реализации. - [ ] Реализовать Argon2, случайный токен с хранением только его хеша, проверку срока сессии в каждом защищённом действии. HTML-формы и Rio-сессии используют одну авторизацию; редирект страницы сам по себе не считается защитой callback. - [ ] Повторить тесты, проверить отсутствие паролей в логах и безопасный отказ запуска без обязательных настроек. Изменение учётных данных отзывает прежние сессии. ## Задача 7 Неизменяемые публикации **Files:** создать `publishing.py`, `tests/integration/test_publications.py`; дополнить repository и HTML routes. **Interfaces:** `PublishingService.publish(dataset_id: str, expected_revision: int) -> Publication`; `get_publication(slug: str, version: int) -> Publication`; `unpublish(slug: str, version: int) -> None`. `Publication` содержит slug, version, title, description, views и снимок records. Неопубликованная версия вызывает `NotFound`. - [ ] Добавить тест: после публикации A и изменения черновика на B `get_publication(slug, 1).records` остаются A; повторная публикация создаёт version 2. Проверить откат при сбое копирования, запрет пустого набора и конфликт revision. - [ ] Запустить `python -m pytest tests/integration/test_publications.py -q` до реализации. - [ ] Реализовать атомарную публикацию всех включённых валидных представлений и отдельное снятие версии. Если одно включённое представление невалидно, публикация целиком отклоняется с указанием причины. - [ ] Закрепить slug после первой публикации, чтобы переименование заголовка не ломало ссылки. Разрешить ASCII lowercase, цифры и дефисы; уникальность проверяет база. - [ ] Подключить HTML к реальным снимкам и повторить тесты: снятая версия и неизвестный id возвращают 404; прежние ссылки остаются прежними после редактирования. ## Задача 8 Кабинет Rio **Files:** создать `rio_ui/admin/{__init__,login,catalog,import_wizard,view_editor,preview,publications}.py`, `rio_ui/theme.py`, `tests/browser/test_admin.py`. **Interfaces:** компоненты `LoginPage`, `CatalogPage`, `ImportWizard`, `ViewEditor`, `PreviewPage`, `PublicationsPage` получают сервисы через подтверждённый механизм Rio attachment. Состояние несохранённых изменений принадлежит конкретной сессии, постоянные данные — Repository. Вызовы методов изменения требуют `require_admin` и expected_revision. - [ ] Создать браузерные сценарии входа, импорта CSV, выбора полей, чернового предпросмотра, публикации и выхода; до реализации сценарии не находят соответствующие экраны. - [ ] Реализовать каталог и мастер с шагами «Файл», «Проверка», «Отображение», «Предпросмотр». Внешние названия не содержат JSON Pointer; выбор путей показан деревом. Ошибка импорта сохраняет выбранные настройки и объясняет причину. - [ ] Реализовать оба предпросмотра за авторизацией, смену представлений, предупреждение о несохранённых изменениях и сообщение о конфликте двух вкладок. Дублирующие операции отключаются до завершения запроса. - [ ] При работе FileInfo прочитать загруженные байты один раз и хранить до подтверждения/отмены; валидация повторяется на сервере, а не только фильтром расширений. Предпросмотр показывает 20 записей, число всех записей и предупреждения. - [ ] Выполнить `python -m pytest tests/browser/test_admin.py -q` с браузером Playwright и отдельной временной базой. Проверить ручной вход, восстановление после ошибки и невозможность действия после logout в другой вкладке. ## Задача 9 Динамические упражнения Rio **Files:** создать `rio_ui/public/{__init__,listing,detail,renderers}.py`, `tests/browser/test_interactive.py`. **Interfaces:** `InteractivePage` загружает Publication по slug/version и использует `select_records` и `project_record`; `RecordPage` показывает только выбранные значения. Состояние страницы хранит фильтры, next_page, generation и флаг loading. - [ ] Создать сценарий: 51 запись, размер 25; после двух нажатий «Загрузить ещё» получены 51 уникальных id, кнопки больше нет. Проверить каждое из четырёх представлений. - [ ] Проверить быстрые повторные клики, смену фильтра во время загрузки и снятие публикации при открытой странице. Устаревший ответ фильтра не должен добавляться к новому списку; снятые данные не выдаются следующими запросами. - [ ] Запустить `python -m pytest tests/browser/test_interactive.py -q` до реализации. - [ ] Реализовать дозагрузку, состояния и детали, используя подтверждённые Rio API. Стабильные доступные подписи и роли обязательны; способы установки дополнительных DOM-атрибутов сначала проверить по версии Rio. Не строить тесты на случайных сгенерированных классах. - [ ] Повторить сценарии; проверить прямое открытие вложенного URL, обновление браузера и отсутствие скрытых значений в передаваемых данных. ## Задача 10 Дизайн и проверка пользовательского сценария **Files:** доработать `rio_ui/theme.py`, Rio-компоненты, `web/static/site.css`, HTML-шаблоны; создать `tests/browser/test_layout.py`, `tests/integration/test_visibility.py`. - [ ] Реализовать общие цветовые и типографические параметры, боковую навигацию, компактную навигацию, карточки статусов и локальные ресурсы. Не добавлять новые функции ради оформления. - [ ] Проверить 1440×900 и 390×844: основные действия доступны; длинный текст переносится; горизонтально прокручивается таблица, а не вся страница; заголовки и ответы не перекрываются. Сохранить скриншоты в рабочую папку и визуально просмотреть. - [ ] Проверить клавиатурный фокус, подписи полей, ошибки рядом с полями и состояния пустых результатов. Для простых косметических правок использовать визуальную проверку, не тестировать каждую константу стиля. - [ ] Добавить интеграционные тесты скрытых полей на списках, деталях, HTML предпросмотре без входа и прямых служебных запросах. Проверить пользовательские `