diff --git a/README.md b/README.md index 9c74d52f..0da5e271 100644 --- a/README.md +++ b/README.md @@ -10,25 +10,27 @@ Acceptance criteria разбиты на 3 блока (функционально --- -## Текущий статус (25 апреля 2026) +## Текущий статус (26 апреля 2026) -**Инфраструктура: live.** Полный цикл автодеплоя из GitHub Actions работает. +**Инфраструктура: live.** Полный цикл автодеплоя из GitHub Actions работает. 26.04 прод полностью пересоздан (`down -v` + redeploy) — БД свежая, backend в multi-stage образе под non-root пользователем, healthcheck-и работают, ловит ошибки в Sentry. | Что | Где | Статус | |---|---|---| | Production VPS | Beget Cloud, Москва, 2 vCPU / 4 GB / 40 GB NVMe | ✅ работает | | IP сервера | `46.173.16.127` | — | | Postgres + PostGIS 3.4 | Docker, в составе compose | ✅ Up (healthy) | -| Redis 7 | Docker | ✅ Up | -| Backend FastAPI | Docker (образ из GHCR) | ✅ Up, `/health` отвечает | -| Frontend Next.js 15 | Docker (образ из GHCR) | ✅ Up | -| Caddy reverse proxy | Docker, ports 80/443 | ✅ Up (TLS ждёт домена) | +| Redis 7 | Docker | ✅ Up (healthy, redis-cli ping) | +| Backend FastAPI | Docker, multi-stage, non-root, 1.44 GB | ✅ Up (healthy, curl /health) | +| Frontend Next.js 15 | Docker, multi-stage, npm ci | ✅ Up | +| Caddy reverse proxy | Docker, ports 80/443 | ✅ Up (TLS ждёт DNS-пропагации) | | GitHub Actions deploy | `.github/workflows/deploy.yml` | ✅ build → push GHCR → ssh deploy | | GHCR пакеты | `ghcr.io/lekss361/gendesign-{backend,frontend}` | приватные, сервер авторизуется через PAT | | UFW firewall | 22 / 80 / 443 | ✅ активен | | SSH hardening | root login off, password auth off | ✅ | -| Домен | — | ⏸ не подключён | -| Бэкапы pg_dump | — | ⏸ не настроены | +| **Домен** | `gendsgn.ru` (Beget, до 26.04.2027) | ⏳ DNS пропагируется к публичным резолверам | +| **Caddyfile с `gendsgn.ru`** | в `main` (commit 68e95aa) | ⏸ ждёт DNS чтобы накатить на VM (Let's Encrypt rate-limit) | +| **Sentry** | проект `gendesign-backend` (org `qqq`, free tier) | ✅ DSN на VM, тестовое событие прошло | +| Бэкапы pg_dump | `ops/backup.sh` готов | ⏸ S3-бакет не настроен (Selectel/Yandex) | **Решение по плану разработки: pending.** Антон (второй разработчик) сообщил о доступе к ДДУ-данным. Решается в понедельник 27.04.2026 — выбор между: - **Plan A (Analytics First)** — Stage 1 переезжает на «3 плитки» аналитики (что строить / по какой цене / за какое время), Generative и Site Finder сдвигаются; @@ -69,14 +71,22 @@ gendesign/ ├── memory/ │ └── memory-gendesign.jsonl # хард-линк на C:\mcp\.aim — общий граф памяти ├── docs/ # стратегические документы (бизнес-план, цели MVP) -├── .env.example # шаблон prod env (POSTGRES_*, IMAGE_TAG, NEXT_PUBLIC_API_BASE_URL) +├── ops/ # эксплуатация +│ ├── backup.sh # pg_dump cron + опциональный S3 upload +│ └── restore.sh # восстановление из дампа (RESTORE_CONFIRM=yes) +├── backend/ +│ ├── alembic.ini, alembic/ # миграции БД (versions/ пуст до Stage 2a) +│ └── db/init/ # init-скрипты, монтятся в /docker-entrypoint-initdb.d +│ └── 99_drop_unused_extensions.sql # дроп TIGER/topology/fuzzystrmatch на свежем volume +├── .env.example # шаблон prod env (POSTGRES_*, IMAGE_TAG) ├── docker-compose.yml # dev: Postgres + Redis + backend + frontend ├── docker-compose.prod.yml # prod: образы из GHCR + Caddy + Postgres+Redis -├── Caddyfile # авто-TLS Let's Encrypt -├── Makefile # make up / down / test / codegen / migrate +├── Caddyfile # авто-TLS Let's Encrypt для gendsgn.ru +├── Makefile # make up / migrate / migration / test / codegen / pre-commit-install +├── .pre-commit-config.yaml # ruff + prettier + housekeeping hooks └── .github/workflows/ ├── ci.yml # ruff + mypy(core) + pytest + frontend build - └── deploy.yml # build → push в GHCR → ssh deploy на VM + └── deploy.yml # build → push в GHCR → ssh: git fetch + reset --hard + caddy reload ``` --- @@ -89,7 +99,7 @@ gendesign/ **БД:** PostgreSQL 16 + PostGIS 3.4, Redis 7 **Frontend:** Next.js 15, React 19, TypeScript 5, Tailwind 4, TanStack Query, Leaflet (Mapbox в Stage 2b), react-leaflet 5 **Экспорт:** WeasyPrint (PDF), ezdxf (DXF), openpyxl (Excel) -**Dev:** uv, ruff, mypy (strict только на core), pytest +**Dev:** uv (lockfile зафиксирован), ruff (lint+format), mypy (strict только на core), pytest, pre-commit (hooks: ruff, prettier, detect-private-key) --- @@ -196,11 +206,16 @@ make codegen # обновить TS-типы из OpenAPI ### Что осталось до «infra-ready» -- [ ] Купить домен .ru (REG.RU / РуЦентр), направить A-запись на `46.173.16.127` -- [ ] Обновить `Caddyfile` с реальным доменом → авто-TLS Let's Encrypt -- [ ] Задать `NEXT_PUBLIC_API_BASE_URL=https://<домен>` в `/opt/gendesign/.env` либо оставить пустым (frontend ходит через Caddy `/api/*` → backend) -- [ ] Настроить ежесуточный `pg_dump` → Object Storage (Yandex free tier 1 GB или Selectel S3) -- [ ] Sentry DSN в `backend/.env` +- [x] ~~Купить домен~~ — `gendsgn.ru` зарегистрирован в Beget 26.04.2026 (199₽/420₽). DNS на NS Beget, A-записи прописаны +- [ ] Дождаться пропагации DNS к публичным резолверам (30 мин – 4 ч), потом запушить **deploy.yml** с `git fetch + reset --hard + caddy reload` → Caddy сам выпустит Let's Encrypt cert для `gendsgn.ru` +- [x] ~~Sentry DSN~~ — проект `gendesign-backend` создан в org `qqq`, DSN в `/opt/gendesign/backend/.env`, тестовое событие прошло +- [ ] Настроить ежесуточный `pg_dump` → Object Storage. Скрипт `ops/backup.sh` готов, осталось создать Selectel/Yandex bucket и положить креды в `/etc/default/gendesign-backup` на VM +- [ ] При следующем wipe `down -v` — TIGER/topology/fuzzystrmatch автоматом дропнутся (init-скрипт `backend/db/init/99_drop_unused_extensions.sql`) + +### Sensitive операционные грабли + +- **`docker compose restart` НЕ перечитывает `env_file:`** — после правки `backend/.env` всегда `docker compose -f docker-compose.prod.yml up -d --force-recreate --no-deps backend` +- **`deploy.yml` использует `git reset --hard origin/main`** — любые ручные правки в `/opt/gendesign` будут стёрты при следующем деплое. `.env` файлы переживут (в `.gitignore`) --- @@ -296,23 +311,34 @@ git push ⚠️ Эти документы — **снимок состояния на момент написания**. Свежее состояние стратегии — в графе памяти. При расхождении доверяем графу. -Релевантные сущности графа на 25.04.2026: +Релевантные сущности графа на 26.04.2026: + +**Стратегия / план:** - `Project_GenDesign` — общая шапка - `TechStack_v2.2` — Python + Next.js стек - `NorthStar_DiscoveryMVP` — 3 пилота + реестр ПО → Раунд А - `Stages_MVP_v1` — базовая разбивка по этапам - `Team_Setup` — 2 фуллстека part-time - `WorkSplit_Generative_Stage1` — кто что делает в Generative -- `PlanA_AnalyticsFirst_Apr25` — pending pivot решение +- `PlanA_AnalyticsFirst_Apr25` — pending pivot решение (ждём 27.04 — Антон по ДДУ) - `SalesTracking_Strategy_Apr25` — шахматки застройщиков - `DataSources_Pricing_Apr25` — источники цен и площадей +- `Validation_Findings_Apr25` — валидация бизнес-плана + +**Инфраструктура:** - `Hosting_Pricing_Apr25` — сравнение провайдеров - `Hosting_Decision_Apr25` — решение по Yandex Cloud (для будущей миграции) - `Runbook_VPS_Setup_Beget` — пошаговая настройка production VM - `SSH_Setup_Apr25` — локальная настройка SSH +- `DockerOps_Apr26` — multi-stage Dockerfile, npm ci, healthcheck fix, same-origin API +- `Production_Wipe_Apr26` — clean re-deploy с нуля (event) +- `Domain_Setup_Apr26` — gendsgn.ru, Beget, DNS, ESIA-правило +- `Sentry_Setup_Apr26` — проект, DSN, тестовое событие +- `OpsBundle_Apr26` — Alembic + TIGER cleanup + pre-commit + pg_dump + +**Процесс / правила:** - `Onboarding_Colleague_KG` — подключение нового участника к графу - `Feedback_MemoryFirst` — правило: память first, docs только по запросу -- `Validation_Findings_Apr25` — валидация бизнес-плана --- diff --git a/memory/memory-gendesign.jsonl b/memory/memory-gendesign.jsonl index 3fe966d2..69d5f165 100644 --- a/memory/memory-gendesign.jsonl +++ b/memory/memory-gendesign.jsonl @@ -32,3 +32,16 @@ {"type":"relation","from":"PlanA_AnalyticsFirst_Apr25","to":"SalesTracking_Strategy_Apr25","relationType":"depends_on"} {"type":"relation","from":"PlanA_AnalyticsFirst_Apr25","to":"DataSources_Pricing_Apr25","relationType":"depends_on"} {"type":"relation","from":"PlanA_AnalyticsFirst_Apr25","to":"Project_GenDesign","relationType":"reshapes"} +{"type": "entity", "name": "Domain_Setup_Apr26", "entityType": "infrastructure", "observations": ["Дата: 26.04.2026", "Домен: gendsgn.ru, регистрация в Beget, срок до 26.04.2027", "Стоимость: 199 ₽ первый год, 420 ₽/год продление. Сравнение из браузера 26.04.2026: Reg.ru 169₽ первый год + ~890₽ продление; Timeweb 200/399; nic.ru 747₽ регистрация (премиум)", "Решение: Beget — потому что (1) хостинг VPS уже там, единая админка и единая платежка для бухгалтерии; (2) продление 420₽ — лучшее на рынке; (3) free DNS включён", "DNS-серверы: ns1.beget.com, ns2.beget.com (плюс ns1.beget.pro, ns2.beget.pro как backup)", "A-записи прописаны 26.04.2026: @ (apex) и www → 46.173.16.127. Beget NS отдают корректно (проверено nslookup gendsgn.ru ns1.beget.com → 46.173.16.127). Публичные резолверы (8.8.8.8, 1.1.1.1) на 26.04 ещё NXDOMAIN — пропагация .ru registry → public DNS, ожидаемо 30 мин - 4 часа", "Caddyfile обновлён под gendsgn.ru: основной site block с auto-TLS Let's Encrypt, www → 301 redirect на apex, :80 fallback для IP-доступа сохранён. Закоммичен 68e95aa, запушен в main. На VM не применён (старый deploy.yml не делает git pull)", "deploy.yml будет дополнен `git fetch + reset --hard origin/main` + `caddy reload --config /etc/caddy/Caddyfile` после `up -d`. Закоммитим и запушим когда DNS пропагируется. Это финальный шаг для HTTPS", "Важное правило отрасли: с 1 сентября 2026 владелец .ru-домена обязан иметь подтверждённую учётку ЕСИА (Госуслуги) — иначе домен не продлят. Для ИП/ООО — через ЭДО/СБИС. Касается всех регистраторов одинаково. К моменту продления (26.04.2027) ООО уже должно быть зарегистрировано — учёт без проблем"]} +{"type": "entity", "name": "Sentry_Setup_Apr26", "entityType": "infrastructure", "observations": ["Дата: 26.04.2026", "Sentry организация: qqq (qqq-5y.sentry.io), free tier (5К ошибок + 10К транзакций / мес)", "Sentry проект: gendesign-backend, платформа FastAPI, регион Frankfurt (ingest.de.sentry.io)", "DSN сохранён в /opt/gendesign/backend/.env как SENTRY_DSN. На локальной машине — не нужен (Sentry init в backend/app/main.py условный — отключён если DSN пустой)", "Конфиг init: sentry_sdk.init(dsn=settings.sentry_dsn, environment=settings.environment, traces_sample_rate=0.1) — 10% запросов трейсятся для performance", "Алерты: «high priority» по умолчанию + email на alexandro361@gmail.com", "Тестовое событие отправлено и получено 26.04.2026 — event_id 67f05bdd59234a608a828fa81f72ded5. Sentry интеграция работает на проде", "Важная операционная заметка: docker compose restart НЕ перечитывает env_file. После правки backend/.env нужно `docker compose -f docker-compose.prod.yml up -d --force-recreate --no-deps backend`. Применимо к любым env-изменениям, не только Sentry", "DSN — не критичный секрет (в frontend SDK всегда зашит публично в JS-бандл). Держим в .env, не в репе. Ротация при необходимости через Sentry → Settings → Client Keys (DSN) → New Key"]} +{"type": "entity", "name": "OpsBundle_Apr26", "entityType": "infrastructure", "observations": ["Дата: 26.04.2026", "Подготовлен пакет инфраструктурных правок (в working tree, не закоммичено): Alembic baseline + TIGER cleanup + pre-commit + pg_dump backups + Makefile targets", "Alembic: создан backend/alembic.ini + backend/alembic/env.py (импорт Base из app.core.db, моделей через app.models.*) + script.py.mako + versions/.gitkeep. Первая реальная миграция — в Stage 2a, когда Parcel-модель будет финализирована. Сейчас versions/ намеренно пуст", "TIGER cleanup: backend/db/init/99_drop_unused_extensions.sql дропает postgis_tiger_geocoder + postgis_topology + fuzzystrmatch на свежем postgres-volume (через bind-mount ./backend/db/init:/docker-entrypoint-initdb.d:ro в обоих compose-файлах). Срабатывает только при первой инициализации пустого volume", "Pre-commit: .pre-commit-config.yaml в корне (trailing-whitespace, end-of-file-fixer, ruff lint+format на backend/, prettier на frontend/, detect-private-key, check-yaml/toml). Pre-commit добавлен в dev-deps backend/pyproject.toml. Локально — `python -m pre_commit install` или `make pre-commit-install`", "pg_dump: ops/backup.sh — cron-скрипт с опциональной выгрузкой в S3. Ретенция 7 дней локально. ops/restore.sh с защитой RESTORE_CONFIRM=yes", "Makefile: добавлены `make migration NAME=...` и `make pre-commit-install`. `make migrate` использует `alembic` напрямую (uv нет в production-образе)", "backend/Dockerfile: COPY alembic.ini + COPY alembic в обе стадии (builder и runner)", "S3 для бэкапов: пока не настроено. Когда настроим Selectel или Yandex Object Storage, env-конфиг — /etc/default/gendesign-backup на VM, cron — `30 3 * * * /usr/local/bin/gendesign-backup`"]} +{"type": "entity", "name": "Production_Wipe_Apr26", "entityType": "event", "observations": ["Дата: 26.04.2026", "Прод полностью пересоздан с нуля для чистого состояния. Не было критичных данных — БД содержала только postgis-defaults", "Команды на VM: `docker compose down -v` → `docker rmi` GHCR-образов и базовых (caddy:2, postgis/postgis:16-3.4, redis:7-alpine) → `git pull --ff-only` (Caddyfile, compose) → `docker system prune -af --volumes`", "Re-deploy через GitHub Actions Deploy → все 5 контейнеров поднялись с image-тегом 08a63f6 (главный коммит на момент wipe)", "Состояние после re-deploy: postgres healthy, redis healthy (новый healthcheck), backend healthy (фикс curl-healthcheck в multi-stage Dockerfile), frontend up, caddy up. PostGIS 3.4 USE_GEOS=1 USE_PROJ=1", "TIGER-мусор вернулся (как ожидалось) при свежей инициализации postgis-image. Будет автоматически удаляться при следующих wipe-ах когда применим OpsBundle_Apr26 (init-скрипт 99_drop_unused_extensions.sql)"]} +{"type": "entity", "name": "DockerOps_Apr26", "entityType": "infrastructure", "observations": ["Дата: 26.04.2026", "Серия Docker-фиксов задеплоена в main: e22aeb9 (multi-stage + npm ci + healthcheck + same-origin API), d0909e6 (ruff fixes), 08a63f6 (mypy fixes), 68e95aa (real domain Caddyfile)", "backend/Dockerfile: переписан на multi-stage (builder с build-essential + libgdal-dev + uv sync; runner только с runtime-либами libpq5, libgdal36, libproj25, libgeos-c1t64, libcairo2, libpango*, curl). Финальный образ 1.44 GB вместо ~2 GB. Запускается под non-root пользователем `app` (uid 1000)", "backend curl healthcheck в docker-compose.prod.yml теперь работает (до этого был баг — curl не был установлен в slim-образе)", "frontend/Dockerfile: переключён на `npm ci --legacy-peer-deps` вместо `npm install`. Запускается под `node` (uid 1000). package-lock.json закоммичен", "docker-compose.prod.yml + dev: убран dead NEXT_PUBLIC_API_BASE_URL (Next.js bakes NEXT_PUBLIC_* на build-time, runtime override no-op). Frontend ходит на same-origin /api/* — в проде через Caddy, в dev через Next.js rewrites (BACKEND_URL env)", "Caddyfile: исправлен баг — handle_path /api/* стрипал префикс /api, FastAPI router на /api/v1/* возвращал 404. Заменено на handle /api/*", "backend/uv.lock сгенерирован (через docker run python:3.12-slim + pip install uv && uv lock). Дальнейшие билды детерминированы: `uv sync --frozen --no-dev`", "Ruff config: ignore=['RUF001','RUF002','RUF003'] — проект русскоязычный, кириллица в комментариях/docstring (МСК-66 etc.) — норма, иначе постоянно ругается"]} +{"type": "relation", "from": "Domain_Setup_Apr26", "to": "Project_GenDesign", "relationType": "infrastructure_for"} +{"type": "relation", "from": "Domain_Setup_Apr26", "to": "Hosting_Decision_Apr25", "relationType": "extends"} +{"type": "relation", "from": "Sentry_Setup_Apr26", "to": "Project_GenDesign", "relationType": "monitors"} +{"type": "relation", "from": "Sentry_Setup_Apr26", "to": "Phase0_DiscoveryMVP", "relationType": "infrastructure_for"} +{"type": "relation", "from": "OpsBundle_Apr26", "to": "Project_GenDesign", "relationType": "infrastructure_for"} +{"type": "relation", "from": "OpsBundle_Apr26", "to": "Stages_MVP_v1", "relationType": "prepares_for"} +{"type": "relation", "from": "Production_Wipe_Apr26", "to": "Runbook_VPS_Setup_Beget", "relationType": "refreshed_state_of"} +{"type": "relation", "from": "DockerOps_Apr26", "to": "Runbook_VPS_Setup_Beget", "relationType": "extends"}