Skip to content

Рефакторинг backend/frontend: слои, оптимизация конвейера, тесты и CI - #12

Open
Gaudialis55 wants to merge 21 commits into
sputnik-llc:mainfrom
Gaudialis55:refactor
Open

Рефакторинг backend/frontend: слои, оптимизация конвейера, тесты и CI#12
Gaudialis55 wants to merge 21 commits into
sputnik-llc:mainfrom
Gaudialis55:refactor

Conversation

@Gaudialis55

Copy link
Copy Markdown

Что сделано

Рефакторинг без изменения бизнес-логики и публичного контракта API (единственное осознанное отклонение — поведение DELETE, см. ниже). Каждое решение задокументировано, история — маленькими логичными коммитами поверх исходного состояния.

Документы:

  • AUDIT.md — 31 найденная проблема (файл:строка, категория, серьёзность, статус обработки).
  • ARCHITECTURE.md — целевая архитектура, обоснование решений и разбор неочевидной оптимизации (было → стало → эффект).

Бэкенд — послойная архитектура: core (конфигурация на pydantic-settings + единый async-engine с DI, lifespan/dispose), storage (неблокирующий потоковый I/O на anyio), repositories, services (доменные исключения вместо HTTPException), api (тонкие роутеры + DI), tasks, app factory. Устранены дублирование engine, разрозненный os.environ, блокирующий I/O.

Неочевидная оптимизация: линейный конвейер scan → metadata → alert из трёх Celery-задач объединён в одну — 1 обращение к брокеру / 1 сессия / 1 выборка записи по PK вместо 3. Поведение конвейера сохранено 1:1.

БД: индексы под сортировки (created_at) и внешний ключ (alerts.file_id); ON DELETE CASCADE для алертов.

Фронтенд: монолитный page.tsx (~367 строк) разнесён на слои — типы, API-клиент (base URL из env), хуки (useDashboard, useFileUpload), презентационные компоненты, утилиты форматирования.

Тесты и оснастка: pytest (конвейер обработки + контракт API + scan-правила), Vitest (форматтеры/статусы фронта), health-эндпоинт GET /health, линтер и форматтер ruff, CI (GitHub Actions), pre-commit.

Осознанное изменение поведения

DELETE /files/{id} для обработанного файла раньше падал с 500 (ForeignKeyViolationError — FK без каскада), поэтому обработанный файл (у которого всегда есть алерт) удалить было нельзя. Теперь FK пересоздан с ON DELETE CASCADE, удаление возвращает 204 и каскадно убирает алерты — как и предполагает контракт DELETE. 500 не был частью контракта.

Проверка

Проект поднимается с нуля (docker compose -f docker-compose.dev.yml up + docker exec -it backend alembic upgrade head), сквозной сценарий (текст/pdf/exe/файл >10 МБ/пустой) даёт тот же результат, что и baseline; фронт открывается на /test, бэк — на /docs. Тесты зелёные.

Fullstack Candidate added 21 commits July 11, 2026 13:38
Полный перечень найденных проблем (31 пункт) с файл:строка, категорией
и серьёзностью. Ключевые critical: DELETE→500 из-за FK без каскада,
падение сборки frontend (COPY .env.production), тройной конвейер Celery.
Baseline снят запуском проекта и сквозным прогоном сценария.
Послойное разделение (core/storage/repositories/services/api/tasks),
единый конфиг и engine, неблокирующий I/O. Отдельно описана неочевидная
оптимизация (конвейер 3 задачи -> 1: было/стало/эффект) и осознанное
изменение DELETE (500 -> 204 через ON DELETE CASCADE).
- core/config.py: типизированный Settings как единый источник настроек
  вместо голого os.environ (устраняет молчаливый None в DB_URL)
- core/database.py: единственный engine + async_sessionmaker,
  get_session (DI) и session_scope (для воркера)
- service.py/tasks.py/migrations перестают дублировать engine и URL
- брокер Celery берётся из CELERY_BROKER_URL (был рассинхрон с REDIS_URL)

Закрывает AUDIT: A1, C1, C2, M2.
- storage.py: FileStorage поверх anyio (без новой зависимости);
  запись загрузки идёт чанками и не держит файл целиком в памяти,
  чтение/удаление/проверка вынесены в поток и не блокируют event loop
- service/app/tasks больше не трогают pathlib и диск напрямую
- удалён мёртвый get_file_path (его логику дублировал роутер download)

Закрывает AUDIT: I1, I2, I3, M1 (частично A4).
- repositories/ (File, Alert): весь доступ к данным инкапсулирован
- services/ (FileService, AlertService): бизнес-логика поверх репозиториев
  и хранилища; вместо HTTPException — доменные исключения (exceptions.py)
- api/ (deps, files, alerts): тонкие роутеры, сессия и сервисы через DI
- app.py -> create_app(): middleware, роутеры, обработчики исключений
  (коды 400/404 и detail-сообщения сохранены 1:1)
- удалён service.py и мёртвый create_alert

Закрывает AUDIT: A2, A3, A4, A5, A6.
Главная неочевидная оптимизация. Линейный конвейер (без ветвления и
параллелизма) был разбит на 3 Celery-задачи: 3 обращения к брокеру,
3 сессии, 3 выборки одной записи по PK на файл. Теперь это одна задача
process_file поверх ProcessingService: 1 брокер, 1 сессия, 1 выборка
(объект переиспользуется, expire_on_commit=False), переходы статусов
сохранены коммитами. Поведение конвейера 1:1.

Также починен воркер: engine создаётся в том же event loop, что и
выполняет корутины, и пересоздаётся вместе с ним (исключает
'Future attached to a different loop').

Закрывает AUDIT: P1, P2, D3.
Отдельная ревизия добавляет индексы:
- files.created_at и alerts.created_at (обе выборки сортируют по created_at DESC)
- alerts.file_id (Postgres не индексирует FK автоматически)
Модели помечены index=True для согласованности со схемой.

Закрывает AUDIT: D1, D2.
Осознанное изменение поведения (задокументировано в ARCHITECTURE.md).
Раньше DELETE обработанного файла падал с 500 (ForeignKeyViolationError),
т.к. FK не имел каскада, а у файла после конвейера всегда есть алерт.
Теперь удаление файла каскадно удаляет его алерты и возвращает 204 —
как и предполагает контракт DELETE. 500 не был частью контракта.

Закрывает AUDIT: B1.
Оба сервиса используют брокер Redis, но depends_on его не включал —
порядок старта относительно redis был не гарантирован.

Закрывает AUDIT: C5.
- удалены артефакты не по теме: Dockerfile.bun, app.json (Cloud Run),
  public/vercel.svg, boilerplate README.md
- Dockerfile: убран COPY несуществующего .env.production (ломал сборку)
- favicon перенесён в src/app/ (файловая конвенция Next) — теперь
  корректно отдаётся по /test/favicon.ico с учётом basePath

Закрывает AUDIT: B2, B3, F2, F3, F4, F5.
Монолитный page.tsx (~367 строк) разбит на слои:
- domain/types: общие типы FileItem, AlertItem
- lib/api: типизированный API-клиент, базовый URL из NEXT_PUBLIC_API_BASE_URL
  (был захардкожен http://localhost:8000)
- lib/format, lib/status: форматтеры и маппинг статусов в варианты
- hooks/useDashboard, hooks/useFileUpload: загрузка, состояния, мутации
- components/FilesTable, AlertsTable, UploadModal: презентационные компоненты
- page.tsx: тонкий контейнер, собирающий хуки и компоненты
Внешний вид и поведение сохранены.

Закрывает AUDIT: F1, F6.
13 тестов на pytest + httpx (ASGITransport), реальный Postgres:
- ProcessingService: clean/suspicious/большой файл/pdf/mime-mismatch/failed
- API: 201/400/404, list, patch, download, каскадное удаление, скрытие stored_name
dev-зависимости вынесены в отдельную группу (в прод-образ не попадают).
Сохранены исходные команды запуска; добавлены ссылки на AUDIT.md и
ARCHITECTURE.md, описание слоёв бэкенда и инструкция по тестам.
- lifespan в create_app с await engine.dispose() на shutdown —
  корректное закрытие пула соединений (ранее engine не освобождался)
- настройки обёрнуты в @lru_cache get_settings() (тестируемость)
- новый GET /health с пингом БД (200 ok / 503 unavailable) —
  readiness-проба, публичный контракт не затронут

По мотивам замечания ревьюера про жизненный цикл БД и практики PR sputnik-llc#6/sputnik-llc#8.
Логика сканирования вынесена из ProcessingService._scan в чистую
функцию evaluate_scan (scan_rules.py) — тестируется изолированно,
поведение идентично. Добавлен юнит-тест на правила (6 кейсов).
- ruff добавлен в dev-зависимости, настроен в pyproject
  (E/F/I/UP/B, line-length 100; B008 игнорируется в api/ — идиома FastAPI)
- отсортированы импорты, разбиты длинные строки, применён ruff format
- поведение кода не изменено

Инженерная гигиена по мотивам PR sputnik-llc#6/sputnik-llc#7.
Добавлен Vitest + тесты на чистую логику фронтенда:
- lib/format: formatSize (границы B/KB/MB), formatDate
- lib/status: маппинг уровней и статусов в bootstrap-варианты
Скрипт npm test. Сборка next build не затронута (тесты вне бандла).

Закрывает пробел из сравнения с PR sputnik-llc#6/sputnik-llc#5.
- .github/workflows/ci.yml: линт (ruff) + pytest бэкенда (сервис Postgres),
  npm test (Vitest) + next build фронтенда — на каждый push/PR
- .pre-commit-config.yaml: ruff (lint + format) для backend/

Инженерная оснастка по мотивам PR sputnik-llc#6/sputnik-llc#7.
- .env.dev вынесен в .gitignore (локальные секреты не коммитятся)
- добавлен шаблон .env.dev.example (пароль-плейсхолдер)
- в README добавлен шаг: cp .env.dev.example .env.dev перед запуском
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant