diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..78751494 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.py] +indent_size = 4 + +[Makefile] +indent_style = tab diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..936cfd83 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,78 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + backend: + runs-on: ubuntu-latest + services: + postgres: + image: postgis/postgis:16-3.4 + env: + POSTGRES_DB: gendesign + POSTGRES_USER: gendesign + POSTGRES_PASSWORD: gendesign + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U gendesign" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + defaults: + run: + working-directory: backend + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Set up Python + run: uv python install 3.12 + + - name: Install system deps for geo + WeasyPrint + run: | + sudo apt-get update + sudo apt-get install -y libpq-dev libgdal-dev libproj-dev libgeos-dev \ + libcairo2 libpango-1.0-0 libpangoft2-1.0-0 + + - name: Install Python deps + run: uv sync + + - name: Lint (ruff) + run: uv run ruff check . + + - name: Type check (mypy strict on core) + run: | + uv run mypy \ + app/services/generative \ + app/services/site_finder/scorer.py + + - name: Test (pytest) + run: uv run pytest -q + env: + DATABASE_URL: postgresql+psycopg://gendesign:gendesign@localhost:5432/gendesign + + frontend: + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: frontend/package-lock.json + - run: npm ci || npm install + - run: npm run lint + - run: npm run type-check + - run: npm run build diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..53952912 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,76 @@ +name: Deploy + +on: + push: + branches: [main] + workflow_dispatch: + +concurrency: + group: deploy-prod + cancel-in-progress: false + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v4 + + - name: Set image tag + id: meta + run: echo "tag=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" + + - name: Login to Yandex Container Registry + uses: yc-actions/yc-cr-login@v3 + with: + yc-sa-json-credentials: ${{ secrets.YC_SA_JSON_CREDENTIALS }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build & push backend + uses: docker/build-push-action@v6 + with: + context: ./backend + push: true + cache-from: type=gha,scope=backend + cache-to: type=gha,mode=max,scope=backend + tags: | + cr.yandex/${{ secrets.YC_REGISTRY_ID }}/backend:latest + cr.yandex/${{ secrets.YC_REGISTRY_ID }}/backend:${{ steps.meta.outputs.tag }} + + - name: Build & push frontend + uses: docker/build-push-action@v6 + with: + context: ./frontend + push: true + cache-from: type=gha,scope=frontend + cache-to: type=gha,mode=max,scope=frontend + tags: | + cr.yandex/${{ secrets.YC_REGISTRY_ID }}/frontend:latest + cr.yandex/${{ secrets.YC_REGISTRY_ID }}/frontend:${{ steps.meta.outputs.tag }} + + - name: Deploy to VM via SSH + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + port: ${{ secrets.DEPLOY_PORT || 22 }} + script: | + set -euo pipefail + cd /opt/gendesign + + # Login to YCR on the host (uses key passed via env or pre-configured docker config) + echo "${{ secrets.YC_SA_JSON_CREDENTIALS }}" | docker login \ + --username json_key \ + --password-stdin cr.yandex + + export IMAGE_TAG=${{ steps.meta.outputs.tag }} + docker compose -f docker-compose.prod.yml pull + docker compose -f docker-compose.prod.yml up -d + docker image prune -f + + # Wait for health + sleep 5 + curl -fsS http://localhost:8000/health | head -c 200 diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 00000000..fcb73917 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,20 @@ +# Replace gendesign.example.ru with your actual domain. +# Caddy will auto-issue Let's Encrypt certificate. + +gendesign.example.ru { + encode zstd gzip + + # Frontend (Next.js) + handle { + reverse_proxy frontend:3000 + } + + # Backend API + handle_path /api/* { + reverse_proxy backend:8000 + } + + handle_path /health { + reverse_proxy backend:8000 + } +} diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..b8b01248 --- /dev/null +++ b/Makefile @@ -0,0 +1,39 @@ +.PHONY: help up down logs backend-shell migrate test lint typecheck codegen + +help: + @echo "make up - start all services (Postgres, Redis, backend, frontend)" + @echo "make down - stop all services" + @echo "make logs - tail logs from all services" + @echo "make backend-shell - shell into backend container" + @echo "make migrate - run alembic upgrade head" + @echo "make test - run backend pytest" + @echo "make lint - ruff check" + @echo "make typecheck - mypy on core modules" + @echo "make codegen - regenerate frontend TS types from backend OpenAPI" + +up: + docker compose up -d --build + +down: + docker compose down + +logs: + docker compose logs -f --tail=100 + +backend-shell: + docker compose exec backend bash + +migrate: + docker compose exec backend uv run alembic upgrade head + +test: + cd backend && uv run pytest -q + +lint: + cd backend && uv run ruff check . + +typecheck: + cd backend && uv run mypy app/services/generative app/services/site_finder/scorer.py + +codegen: + cd frontend && npm run codegen diff --git a/README.md b/README.md index 13184c10..45eb8029 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,247 @@ # Generative Design + Site Finder -Проект двух продуктовых линий в одной воронке: AI-подбор инвестиционных земельных участков (Site Finder) + автоматическая генерация концепций застройки (Generative Design). +Две продуктовые линии в одной воронке: **Site Finder** (AI-подбор инвестиционных земельных участков) + **Generative Design** (автоматическая генерация концепций застройки) для девелоперов РФ. + +## North Star + +К концу месяца 3: **3 платящих пилота** (≥30 тыс. ₽/мес каждый) + заявка в реестр российского ПО → право на Раунд А (20–25 млн ₽). + +Acceptance criteria разбиты на 3 блока (функционально / бизнесово / технически). Правило перехода: 3/3 → Раунд А; 2/3 → продление Фазы 0 на 1–2 мес.; 1/3 или меньше → стоп проекта. + +--- ## Структура репозитория ``` gendesign/ -├── docs/ # Документация и стратегические документы -│ ├── MVP_Goals_Python.md # Цели Discovery MVP, Python-стек (v2.2) -│ └── Бизнес-план v2.1 - Site Finder + Roadmap 12 месяцев.md # Бизнес-план v2.1 -└── README.md +├── backend/ # Python 3.12 / FastAPI / PostGIS +│ ├── pyproject.toml # uv-проект, ruff/mypy/pytest конфиги +│ ├── Dockerfile # с системными deps под Geo + WeasyPrint +│ ├── .env.example +│ ├── app/ +│ │ ├── main.py # FastAPI + Sentry + /health +│ │ ├── core/ # config (Pydantic Settings), db (SQLAlchemy) +│ │ ├── api/v1/ # concepts.py, parcels.py +│ │ ├── schemas/ # Pydantic-контракты для openapi-typescript +│ │ ├── services/ +│ │ │ ├── generative/ # Stage 1: geometry, teap, financial +│ │ │ ├── site_finder/ # Stage 2: parser, scorer, filters +│ │ │ └── exporters/ # Stage 1c: pdf, excel, dxf +│ │ ├── models/parcel.py # GeoAlchemy2 + PostGIS +│ │ └── workers/celery_app.py # Stage 2a: daily PKK sync +│ └── tests/ +├── frontend/ # Next.js 15 / React 19 / TypeScript / Leaflet +│ ├── package.json # включая `npm run codegen` → openapi-typescript +│ ├── Dockerfile # multi-stage, output: standalone +│ └── src/app/ +│ ├── page.tsx # / — главная +│ ├── concept/page.tsx # /concept — Generative Design +│ └── site-finder/page.tsx # /site-finder — Site Finder +├── memory/ +│ └── memory-gendesign.jsonl # хард-линк на C:\mcp\.aim — общий граф памяти +├── docs/ # стратегические документы (бизнес-план, цели MVP) +├── docker-compose.yml # dev: Postgres + Redis + backend + frontend +├── docker-compose.prod.yml # prod: образы из реестра + Caddy reverse-proxy +├── Caddyfile # авто-TLS Let's Encrypt +├── Makefile # make up / down / test / codegen / migrate +└── .github/workflows/ + ├── ci.yml # ruff + mypy(core) + pytest + frontend build + └── deploy.yml # build → push в реестр → ssh deploy на VM ``` -## Текущий статус +--- -**Фаза 0 — Discovery MVP (месяцы 1–3).** Стек: Python 3.12 + FastAPI + PostGIS, Next.js + TypeScript на фронте. +## Технический стек (v2.2) -Подробности — в `docs/MVP_Goals_Python.md`. +**Backend:** Python 3.12, FastAPI, SQLAlchemy 2.0, GeoAlchemy2, Pydantic v2, Alembic, Celery + Redis, httpx +**Geo:** Shapely 2.0, GeoPandas, Pyproj (WGS84 ↔ МСК-66 для Свердловской обл.) +**ML:** scikit-learn (XGBoost — отложен на Фазу 1, в Discovery эвристики) +**БД:** PostgreSQL 16 + PostGIS 3.4, Redis 7 +**Frontend:** Next.js 15, React 19, TypeScript 5, Tailwind 4, TanStack Query, Leaflet (Mapbox в Stage 2b) +**Экспорт:** WeasyPrint (PDF), ezdxf (DXF), openpyxl (Excel) +**Dev:** uv, ruff, mypy (strict только на core), pytest -## North Star +--- -К концу месяца 3: **3 платящих пилота (≥30 тыс. ₽/мес)** + заявка в реестр российского ПО → право на Раунд А (20–25 млн ₽). +## Команда и порядок работы + +**2 фуллстек-разработчика, оба part-time 20 ч/нед** — итого ~40 person-hours/нед, эффективных с координацией ~33. + +**Ритм:** +- 1 синк/нед (30 мин, понедельник) +- daily-async в Telegram +- PR review обязательный +- Trunk-based: feature ветки ≤ 3 дней +- Codegen TS-типов из Pydantic (`npm run codegen`) — без этого 2 фуллстека плодят расхождения в API-контракте + +**Порядок:** строго последовательный — Generative полностью, потом Site Finder. + +--- + +## Этапы MVP (12 недель) + +### Stage 0 — Скелет (нед. 1) +Монорепо backend+frontend, Docker с PostGIS+Redis, FastAPI hello, Next.js hello, openapi-typescript codegen, GitHub Actions CI, ruff + mypy(core) + pytest. Без JWT (single-tenant + `X-Demo-User` до Раунда А). + +### Stage 1 — Generative MVP (нед. 2–7) +- **1a (нед. 2–3):** геометрия + API-контракт. Shapely-сервис (отступы, сетка), Pydantic-схемы, `POST /api/v1/concepts` с заглушкой. Фронт: страница `/concept`, Leaflet, импорт GeoJSON, форма параметров, 3 варианта в табах. +- **1b (нед. 4–5):** алгоритм размещения. Жадное заполнение, 3 стратегии (max площадь / max инсоляция / balanced), STRtree для коллизий. Цель ≤10 сек, fallback 15 сек. +- **1c (нед. 6–7):** ТЭП + финмодель + экспорты. WeasyPrint PDF, ezdxf, openpyxl. UI кнопок скачивания. + +**Acceptance Stage 1 (конец нед. 7):** полигон → 3 варианта → ТЭП + финмодель → PDF/Excel/DXF. Это уже **продаваемый артефакт** — можно показывать на встречах с месяца 2. + +### Stage 2 — Site Finder MVP (нед. 8–12) +- **2a (нед. 8–9):** data pipeline. Парсер ПКК Rosreestr (httpx async, retries), модель `Parcel` с GeoAlchemy2, ≥1000 участков Екатеринбурга. Overpass API для POI, ДОМ.РФ/ЦИАН для цен м², кеш в Redis. +- **2b (нед. 10–11):** скоринг + API + UI. 3 критерия (расстояние до центра, плотность POI в 1 км, цена района), `POST /api/v1/parcels/search`. Карта с градацией по рейтингу, фильтры, карточка. +- **2c (нед. 12):** интеграция + полировка. Кнопка «Обсчитать концепцию», демо-наполнение 50 участков, бэкапы БД, подача в реестр ПО. + +### Что урезано из v2.2 из-за part-time × 2 +- XGBoost-скоринг → Фаза 1 (с реальными данными от пилотов) +- JWT-auth с refresh → перед Раундом А +- mypy --strict — только на core (`generative/*`, `site_finder/scorer.py`) +- Время генерации ≤ 15 сек как fallback (а не 10 сек) + +--- + +## Quick start (локально) + +**Требования:** Docker Desktop, Node.js 20+, [uv](https://docs.astral.sh/uv/) (для backend dev вне Docker). + +```bash +# 1. Скопировать env-файлы +cp backend/.env.example backend/.env +cp frontend/.env.example frontend/.env + +# 2. Запустить всё +docker compose up -d --build +# или: make up + +# 3. Проверить +curl http://localhost:8000/health +# → {"status":"ok","environment":"dev"} + +# Frontend → http://localhost:3000 +# Swagger docs → http://localhost:8000/docs +``` + +**Полезное:** +```bash +make logs # tail логов всех сервисов +make backend-shell # bash внутри backend контейнера +make migrate # alembic upgrade head +make test # pytest +make lint # ruff check +make typecheck # mypy на core +make codegen # обновить TS-типы из OpenAPI +``` + +--- + +## Хостинг (production) + +### Discovery MVP (мес. 1–3): минимальный режим + +**1× VPS** на Selectel / Timeweb / Beget (2 vCPU / 4 GB / 60 GB SSD), всё в Docker Compose: + +| Компонент | Сервис | +|---|---| +| App | backend + frontend (образы из GHCR) | +| БД | Postgres 16 + PostGIS 3.4 (в том же compose) | +| Кэш/очереди | Redis 7 | +| TLS | Caddy с Let's Encrypt автоматически | +| Бэкапы | cron pg_dump → Yandex Object Storage (free tier 1 GB) | + +**Стоимость:** ~1300–2000 ₽/мес (VPS) + ~17 ₽/мес (домен .ru) = **~1500–2000 ₽/мес**. + +⚠️ **Критично:** настроить ежесуточный pg_dump в первую неделю. Без бэкапа = смерть проекта при первом сбое VM. + +### Триггеры миграции на Yandex Cloud + +Переезжаем на YC Managed PostgreSQL + Compute + Container Registry, когда: +- Подписан 3-й платящий пилот (~ конец мес. 3) +- Закрылся Раунд А +- Идёт переговор с госзаказчиком (нужен реестр ПО + солидная инфра) +- БД > 10 GB или появилось > 50 RPS + +Стоимость production на YC: ~10–15 тыс ₽/мес. + +### GitHub Actions deploy + +Workflow `.github/workflows/deploy.yml` собирает образы и через SSH деплоит на VM: +1. Build backend + frontend Docker образов +2. Push в реестр (GHCR для Discovery, YCR после миграции) +3. SSH на VM → `docker compose pull && up -d` + +**Secrets** (GitHub → Settings → Secrets and variables → Actions): +- `DEPLOY_HOST`, `DEPLOY_USER`, `DEPLOY_SSH_KEY`, `DEPLOY_PORT` (опц.) +- Для GHCR — `GITHUB_TOKEN` встроенный, отдельный secret не нужен +- Для YC (после миграции) — `YC_SA_JSON_CREDENTIALS`, `YC_REGISTRY_ID` + +--- + +## Память проекта (knowledge graph) + +Все решения, факты, риски, KPI, протоколы валидации хранятся **только в графе памяти**, не в .md-документах. Это решает проблему расхождения версий между документами. + +**Как устроено:** +- MCP-сервер `mcp-knowledge-graph` пишет в `C:\mcp\.aim\memory-gendesign.jsonl` +- Этот файл — **хард-линк** на `/memory/memory-gendesign.jsonl` +- Изменения попадают в git автоматически +- Синхронизация между коллегами — через `git pull` / `git push` + +**Правило проекта (`Feedback_MemoryFirst`):** +- Все факты и решения сохраняем в граф (`context: "gendesign"`) +- `.md` / `.docx` / `.pdf` Claude **не создаёт сам** — только по явному запросу +- Источник правды = граф, документы = производные срезы + +**Рабочий цикл:** +```bash +git pull # перед началом работы +# ... сессия с Claude ... +git add memory/memory-gendesign.jsonl +git commit -m "Memory: <что добавили>" +git push +``` + +Конфликты в `.jsonl` обычно простые (файл построчный): `git pull --rebase`, оставить **обе** стороны, `git rebase --continue`. + +### Подключение коллеги + +Краткая инструкция для нового участника команды (полная версия — у владельца репо): + +1. Установить Claude Desktop, Node.js 20+, Git +2. Клонировать репо в `C:\Users\<имя>\source\repos\gendesign` +3. `mkdir C:\mcp\.aim` +4. Создать хард-линк: + ```powershell + New-Item -ItemType HardLink ` + -Path C:\mcp\.aim\memory-gendesign.jsonl ` + -Value $PWD\memory\memory-gendesign.jsonl + ``` +5. Добавить в `%APPDATA%\Claude\claude_desktop_config.json`: + ```json + "knowledge-graph": { + "command": "npx", + "args": ["-y", "mcp-knowledge-graph", "--memory-path", "C:\\mcp\\.aim"] + } + ``` +6. Перезапустить Claude Desktop полностью (через трей) +7. Проверить: «Покажи память контекста gendesign» + +--- + +## Документы стратегии + +В `docs/`: +- `MVP_Goals_Python.md` — цели Discovery MVP, Python-стек (v2.2, апрель 2026) +- `Бизнес-план v2.1 - Site Finder + Roadmap 12 месяцев.md` — бизнес-план + +⚠️ Эти документы — **снимок состояния на момент написания**. Свежее состояние стратегии — в графе памяти. При расхождении доверяем графу. + +--- + +## Полезные ссылки + +- **Репо:** https://github.com/lekss361/-gendesign +- **Целевой регион Discovery:** Свердловская обл. (Екатеринбург, ПЗЗ, МСК-66) +- **Конкуренты:** rTIM (только концепт-стадия, нет site-selection), TestFit (нет РФ-данных) diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 00000000..a7657b4a --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,10 @@ +.venv +__pycache__ +*.pyc +.pytest_cache +.mypy_cache +.ruff_cache +tests +.env +.env.* +!.env.example diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 00000000..c28623d6 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,7 @@ +DATABASE_URL=postgresql+psycopg://gendesign:gendesign@localhost:5432/gendesign +REDIS_URL=redis://localhost:6379/0 +CORS_ORIGINS=["http://localhost:3000"] +ENVIRONMENT=dev +SENTRY_DSN= +ROSREESTR_PKK_BASE_URL=https://pkk.rosreestr.ru/api/features/1 +OVERPASS_URL=https://overpass-api.de/api/interpreter diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..7e717144 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,34 @@ +FROM python:3.12-slim + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 + +# Geo + WeasyPrint system deps +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + libpq-dev \ + libgdal-dev \ + libproj-dev \ + libgeos-dev \ + libcairo2 \ + libpango-1.0-0 \ + libpangoft2-1.0-0 \ + fonts-dejavu-core \ + && rm -rf /var/lib/apt/lists/* + +RUN pip install --no-cache-dir uv + +WORKDIR /app + +COPY pyproject.toml ./ +COPY uv.lock* ./ +RUN uv sync --frozen --no-dev 2>/dev/null || uv sync --no-dev + +COPY app ./app +COPY alembic.ini* ./ +COPY alembic ./alembic 2>/dev/null || true + +EXPOSE 8000 + +CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/api/v1/__init__.py b/backend/app/api/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/api/v1/concepts.py b/backend/app/api/v1/concepts.py new file mode 100644 index 00000000..20b7dd64 --- /dev/null +++ b/backend/app/api/v1/concepts.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter + +from app.schemas.concept import ConceptInput, ConceptOutput +from app.services.generative import geometry + +router = APIRouter() + + +@router.post("", response_model=ConceptOutput) +async def create_concept(payload: ConceptInput) -> ConceptOutput: + """Generate 3 building variants for the given parcel polygon. + + Stage 1a: returns stub with 3 empty variants. + Stage 1b: real greedy placement. + Stage 1c: TEAP + financial model attached. + """ + variants = geometry.generate_stub(payload) + return ConceptOutput(variants=variants) diff --git a/backend/app/api/v1/parcels.py b/backend/app/api/v1/parcels.py new file mode 100644 index 00000000..2c485633 --- /dev/null +++ b/backend/app/api/v1/parcels.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter, HTTPException + +from app.schemas.parcel import ParcelDetail, ParcelSearchRequest, ParcelSearchResponse + +router = APIRouter() + + +@router.post("/search", response_model=ParcelSearchResponse) +async def search_parcels(payload: ParcelSearchRequest) -> ParcelSearchResponse: + """Search parcels by filters + scoring. + + TODO Stage 2b: PostGIS query + scorer service. + """ + return ParcelSearchResponse(items=[], total=0) + + +@router.get("/{parcel_id}", response_model=ParcelDetail) +async def get_parcel(parcel_id: str) -> ParcelDetail: + """TODO Stage 2b: fetch parcel by id from DB.""" + raise HTTPException(status_code=501, detail="Not implemented yet") diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 00000000..8ac2e8bb --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,22 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + model_config = SettingsConfigDict( + env_file=".env", env_file_encoding="utf-8", extra="ignore" + ) + + database_url: str = ( + "postgresql+psycopg://gendesign:gendesign@localhost:5432/gendesign" + ) + redis_url: str = "redis://localhost:6379/0" + cors_origins: list[str] = ["http://localhost:3000"] + sentry_dsn: str | None = None + environment: str = "dev" + + # External APIs (Stage 2) + rosreestr_pkk_base_url: str = "https://pkk.rosreestr.ru/api/features/1" + overpass_url: str = "https://overpass-api.de/api/interpreter" + + +settings = Settings() diff --git a/backend/app/core/db.py b/backend/app/core/db.py new file mode 100644 index 00000000..525647a4 --- /dev/null +++ b/backend/app/core/db.py @@ -0,0 +1,23 @@ +from collections.abc import Generator + +from sqlalchemy import create_engine +from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker + +from app.core.config import settings + +engine = create_engine(settings.database_url, pool_pre_ping=True, future=True) +SessionLocal = sessionmaker( + autocommit=False, autoflush=False, bind=engine, expire_on_commit=False +) + + +class Base(DeclarativeBase): + pass + + +def get_db() -> Generator[Session, None, None]: + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 00000000..b8d23101 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,39 @@ +from contextlib import asynccontextmanager +from typing import AsyncIterator + +import sentry_sdk +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.api.v1 import concepts, parcels +from app.core.config import settings + + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncIterator[None]: + if settings.sentry_dsn: + sentry_sdk.init( + dsn=settings.sentry_dsn, + environment=settings.environment, + traces_sample_rate=0.1, + ) + yield + + +app = FastAPI(title="GenDesign API", version="0.1.0", lifespan=lifespan) + +app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(concepts.router, prefix="/api/v1/concepts", tags=["concepts"]) +app.include_router(parcels.router, prefix="/api/v1/parcels", tags=["parcels"]) + + +@app.get("/health") +async def health() -> dict[str, str]: + return {"status": "ok", "environment": settings.environment} diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/models/parcel.py b/backend/app/models/parcel.py new file mode 100644 index 00000000..048a0046 --- /dev/null +++ b/backend/app/models/parcel.py @@ -0,0 +1,29 @@ +"""SQLAlchemy + GeoAlchemy2 ORM models. + +Stage 2a: real Parcel model. Geometry stored in WGS84 (EPSG:4326); +project to МСК-66 via pyproj when computing distances/areas. +""" + +from datetime import datetime + +from geoalchemy2 import Geometry +from sqlalchemy import JSON, DateTime, Float, String +from sqlalchemy.orm import Mapped, mapped_column + +from app.core.db import Base + + +class Parcel(Base): + __tablename__ = "parcels" + + id: Mapped[str] = mapped_column(String, primary_key=True) + cadastral_number: Mapped[str] = mapped_column(String, unique=True, index=True) + vri: Mapped[str] = mapped_column(String, index=True) + area_sqm: Mapped[float] = mapped_column(Float) + address: Mapped[str | None] = mapped_column(String, nullable=True) + geometry: Mapped[object] = mapped_column(Geometry("POLYGON", srid=4326)) + enrichment: Mapped[dict] = mapped_column(JSON, default=dict) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + updated_at: Mapped[datetime] = mapped_column( + DateTime, default=datetime.utcnow, onupdate=datetime.utcnow + ) diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/schemas/concept.py b/backend/app/schemas/concept.py new file mode 100644 index 00000000..abf88d66 --- /dev/null +++ b/backend/app/schemas/concept.py @@ -0,0 +1,46 @@ +from typing import Any, Literal + +from pydantic import BaseModel, Field + + +class ConceptInput(BaseModel): + """Stage 1a — input contract. Frozen interface for frontend codegen.""" + + parcel_geojson: dict[str, Any] = Field( + ..., description="GeoJSON Polygon of the parcel (WGS84 / EPSG:4326)" + ) + housing_class: Literal["econom", "comfort", "business"] = "comfort" + target_floors: int = Field(9, ge=1, le=30) + development_type: Literal["spot", "mid_rise", "high_rise"] = "mid_rise" + land_cost_rub: float | None = Field( + None, description="Optional land cost for financial model" + ) + + +class TEAP(BaseModel): + """Технико-экономические показатели.""" + + built_area_sqm: float + total_floor_area_sqm: float + residential_area_sqm: float + apartments_count: int + density: float + parking_spaces: int + + +class FinancialModel(BaseModel): + revenue_rub: float + cost_rub: float + gross_margin_rub: float + irr: float + + +class ConceptVariant(BaseModel): + strategy: Literal["max_area", "max_insolation", "balanced"] + buildings_geojson: dict[str, Any] + teap: TEAP + financial: FinancialModel + + +class ConceptOutput(BaseModel): + variants: list[ConceptVariant] diff --git a/backend/app/schemas/parcel.py b/backend/app/schemas/parcel.py new file mode 100644 index 00000000..20e2c30c --- /dev/null +++ b/backend/app/schemas/parcel.py @@ -0,0 +1,50 @@ +from typing import Any + +from pydantic import BaseModel, Field + + +class ParcelFilter(BaseModel): + vri: list[str] | None = None + area_min_sqm: float | None = None + area_max_sqm: float | None = None + region: str | None = None + polygon_geojson: dict[str, Any] | None = None + text_search: str | None = None + + +class ScoringWeights(BaseModel): + distance_to_center: float = 0.4 + poi_density: float = 0.3 + neighborhood_price: float = 0.3 + + +class ParcelSearchRequest(BaseModel): + filters: ParcelFilter = Field(default_factory=ParcelFilter) + weights: ScoringWeights = Field(default_factory=ScoringWeights) + limit: int = 50 + + +class ScoreBreakdown(BaseModel): + total: float + distance_to_center: float + poi_density: float + neighborhood_price: float + + +class ParcelSummary(BaseModel): + id: str + cadastral_number: str + area_sqm: float + vri: str + address: str | None + score: ScoreBreakdown + + +class ParcelSearchResponse(BaseModel): + items: list[ParcelSummary] + total: int + + +class ParcelDetail(ParcelSummary): + geometry_geojson: dict[str, Any] + enrichment: dict[str, Any] diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/services/exporters/__init__.py b/backend/app/services/exporters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/services/exporters/dxf.py b/backend/app/services/exporters/dxf.py new file mode 100644 index 00000000..7fc88145 --- /dev/null +++ b/backend/app/services/exporters/dxf.py @@ -0,0 +1,4 @@ +"""DXF export via ezdxf. + +Stage 1c: simplified parcel + buildings layout for hand-off to architects. +""" diff --git a/backend/app/services/exporters/excel.py b/backend/app/services/exporters/excel.py new file mode 100644 index 00000000..ea4c6c5d --- /dev/null +++ b/backend/app/services/exporters/excel.py @@ -0,0 +1,4 @@ +"""Excel export via openpyxl. + +Stage 1c: 3 sheets — TEAP per variant, Financial model, Comparison. +""" diff --git a/backend/app/services/exporters/pdf.py b/backend/app/services/exporters/pdf.py new file mode 100644 index 00000000..0c52bbb8 --- /dev/null +++ b/backend/app/services/exporters/pdf.py @@ -0,0 +1,4 @@ +"""PDF export via WeasyPrint. + +Stage 1c: HTML template with 3 variants + TEAP table + financial table + map snapshots. +""" diff --git a/backend/app/services/generative/__init__.py b/backend/app/services/generative/__init__.py new file mode 100644 index 00000000..45f2e545 --- /dev/null +++ b/backend/app/services/generative/__init__.py @@ -0,0 +1,3 @@ +from app.services.generative import financial, geometry, teap + +__all__ = ["financial", "geometry", "teap"] diff --git a/backend/app/services/generative/financial.py b/backend/app/services/generative/financial.py new file mode 100644 index 00000000..57e8c277 --- /dev/null +++ b/backend/app/services/generative/financial.py @@ -0,0 +1,8 @@ +"""Financial model. + +Stage 1c: + revenue = residential_area_sqm * neighborhood_price_per_sqm + cost = total_floor_area_sqm * construction_cost_per_sqm + land_cost + gross_margin = revenue - cost + base_irr = simplified, no time discounting (defer to Phase 1). +""" diff --git a/backend/app/services/generative/geometry.py b/backend/app/services/generative/geometry.py new file mode 100644 index 00000000..06881dab --- /dev/null +++ b/backend/app/services/generative/geometry.py @@ -0,0 +1,35 @@ +"""Generative Design — geometry placement. + +Stage 1a: Shapely-based polygon parsing + normative offsets (buffer). +Stage 1b: greedy filling of rectangular MKD with 3 strategies. STRtree for collisions. +Performance target: <=10s per variant; fallback acceptance 15s. +""" + +from app.schemas.concept import ConceptInput, ConceptVariant, FinancialModel, TEAP + + +def generate_stub(payload: ConceptInput) -> list[ConceptVariant]: + """Placeholder returning 3 empty variants. Replaced in Stage 1b.""" + empty_buildings: dict = {"type": "FeatureCollection", "features": []} + empty_teap = TEAP( + built_area_sqm=0.0, + total_floor_area_sqm=0.0, + residential_area_sqm=0.0, + apartments_count=0, + density=0.0, + parking_spaces=0, + ) + empty_financial = FinancialModel( + revenue_rub=0.0, cost_rub=0.0, gross_margin_rub=0.0, irr=0.0 + ) + strategies: list[ConceptVariant] = [] + for strategy in ("max_area", "max_insolation", "balanced"): + strategies.append( + ConceptVariant( + strategy=strategy, # type: ignore[arg-type] + buildings_geojson=empty_buildings, + teap=empty_teap, + financial=empty_financial, + ) + ) + return strategies diff --git a/backend/app/services/generative/teap.py b/backend/app/services/generative/teap.py new file mode 100644 index 00000000..84e61887 --- /dev/null +++ b/backend/app/services/generative/teap.py @@ -0,0 +1,5 @@ +"""TEAP (technical-economic indicators) calculations. + +Stage 1c: apartment count by ratios (1/2/3-room), KEP (land use coefficient), +density, parking by simplified norm. +""" diff --git a/backend/app/services/site_finder/__init__.py b/backend/app/services/site_finder/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/services/site_finder/filters.py b/backend/app/services/site_finder/filters.py new file mode 100644 index 00000000..1ee23f74 --- /dev/null +++ b/backend/app/services/site_finder/filters.py @@ -0,0 +1,5 @@ +"""Parcel filters (PostGIS queries). + +Stage 2b: VRI, area range, polygon containment (ST_Within), +text search by address/cadastral_number (ILIKE). +""" diff --git a/backend/app/services/site_finder/parser.py b/backend/app/services/site_finder/parser.py new file mode 100644 index 00000000..822a2faf --- /dev/null +++ b/backend/app/services/site_finder/parser.py @@ -0,0 +1,5 @@ +"""PKK Rosreestr parser. + +Stage 2a: httpx async client with retries (tenacity) + rate-limit handling. +Loads >= 1000 Sverdlovsk parcels into PostGIS via SQLAlchemy. +""" diff --git a/backend/app/services/site_finder/scorer.py b/backend/app/services/site_finder/scorer.py new file mode 100644 index 00000000..6a6c8a74 --- /dev/null +++ b/backend/app/services/site_finder/scorer.py @@ -0,0 +1,9 @@ +"""Multi-criteria scoring (heuristics, no XGBoost in Discovery). + +Stage 2b: weighted sum of normalized 0..100 components: + - distance_to_center + - poi_density (Overpass within 1 km) + - neighborhood_price (DOM.RF / CIAN) + +XGBoost deferred to Phase 1 (no training data yet). +""" diff --git a/backend/app/workers/__init__.py b/backend/app/workers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/workers/celery_app.py b/backend/app/workers/celery_app.py new file mode 100644 index 00000000..63ad0b1b --- /dev/null +++ b/backend/app/workers/celery_app.py @@ -0,0 +1,13 @@ +"""Celery app + beat schedule. + +Stage 2a: daily PKK Rosreestr sync. +""" + +from celery import Celery + +from app.core.config import settings + +celery_app = Celery( + "gendesign", broker=settings.redis_url, backend=settings.redis_url +) +celery_app.conf.timezone = "Europe/Moscow" diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 00000000..fc6ba7b8 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,66 @@ +[project] +name = "gendesign-backend" +version = "0.1.0" +description = "Generative Design + Site Finder backend (FastAPI)" +requires-python = ">=3.12" +dependencies = [ + "fastapi>=0.115.0", + "uvicorn[standard]>=0.30.0", + "sqlalchemy>=2.0.30", + "geoalchemy2>=0.15.0", + "alembic>=1.13.0", + "pydantic>=2.7.0", + "pydantic-settings>=2.3.0", + "psycopg[binary]>=3.2.0", + "shapely>=2.0.0", + "geopandas>=1.0.0", + "pyproj>=3.6.0", + "httpx>=0.27.0", + "redis>=5.0.0", + "celery>=5.4.0", + "weasyprint>=62.0", + "ezdxf>=1.3.0", + "openpyxl>=3.1.0", + "pandas>=2.2.0", + "numpy>=2.0.0", + "scikit-learn>=1.5.0", + "sentry-sdk[fastapi]>=2.10.0", +] + +[dependency-groups] +dev = [ + "pytest>=8.0.0", + "pytest-asyncio>=0.23.0", + "ruff>=0.5.0", + "mypy>=1.10.0", + "types-redis>=4.6.0", +] + +[tool.uv] +package = true + +[tool.ruff] +target-version = "py312" +line-length = 100 + +[tool.ruff.lint] +select = ["E", "F", "I", "B", "UP", "N", "RUF", "ASYNC"] + +[tool.mypy] +python_version = "3.12" +strict_optional = true +warn_unused_ignores = true + +# strict только для core-модулей (правило из памяти Validation_Findings_Apr25) +[[tool.mypy.overrides]] +module = [ + "app.services.generative.geometry", + "app.services.generative.teap", + "app.services.generative.financial", + "app.services.site_finder.scorer", +] +strict = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +asyncio_mode = "auto" diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tests/test_concepts_stub.py b/backend/tests/test_concepts_stub.py new file mode 100644 index 00000000..51df9efe --- /dev/null +++ b/backend/tests/test_concepts_stub.py @@ -0,0 +1,35 @@ +"""Smoke test for the Concept stub. Real algorithm tests come in Stage 1b.""" + +from fastapi.testclient import TestClient + +from app.main import app + + +def test_concept_stub_returns_three_variants() -> None: + client = TestClient(app) + payload = { + "parcel_geojson": { + "type": "Polygon", + "coordinates": [ + [ + [60.6, 56.83], + [60.61, 56.83], + [60.61, 56.84], + [60.6, 56.84], + [60.6, 56.83], + ] + ], + }, + "housing_class": "comfort", + "target_floors": 9, + "development_type": "mid_rise", + } + response = client.post("/api/v1/concepts", json=payload) + assert response.status_code == 200 + variants = response.json()["variants"] + assert len(variants) == 3 + assert {v["strategy"] for v in variants} == { + "max_area", + "max_insolation", + "balanced", + } diff --git a/backend/tests/test_health.py b/backend/tests/test_health.py new file mode 100644 index 00000000..c432abcf --- /dev/null +++ b/backend/tests/test_health.py @@ -0,0 +1,11 @@ +from fastapi.testclient import TestClient + +from app.main import app + + +def test_health() -> None: + client = TestClient(app) + response = client.get("/health") + assert response.status_code == 200 + body = response.json() + assert body["status"] == "ok" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 00000000..615b33fc --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,48 @@ +# Production compose for Yandex Cloud VM (or Selectel cloud server). +# Pull pre-built images from Yandex Container Registry pushed by GitHub Actions. +# +# Place this file at /opt/gendesign/docker-compose.prod.yml on the VM. +# Required env vars provided via /opt/gendesign/.env: +# YC_REGISTRY_ID, IMAGE_TAG, DATABASE_URL, REDIS_URL, SENTRY_DSN, +# NEXT_PUBLIC_API_BASE_URL + +services: + backend: + image: cr.yandex/${YC_REGISTRY_ID}/backend:${IMAGE_TAG:-latest} + restart: unless-stopped + env_file: .env + ports: + - "127.0.0.1:8000:8000" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 5s + retries: 5 + + frontend: + image: cr.yandex/${YC_REGISTRY_ID}/frontend:${IMAGE_TAG:-latest} + restart: unless-stopped + environment: + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + ports: + - "127.0.0.1:3000:3000" + depends_on: + - backend + + caddy: + image: caddy:2 + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + depends_on: + - backend + - frontend + +volumes: + caddy_data: + caddy_config: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..048759be --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,61 @@ +services: + postgres: + image: postgis/postgis:16-3.4 + environment: + POSTGRES_DB: gendesign + POSTGRES_USER: gendesign + POSTGRES_PASSWORD: gendesign + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "gendesign"] + interval: 5s + timeout: 5s + retries: 10 + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + + backend: + build: ./backend + env_file: ./backend/.env + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_started + ports: + - "8000:8000" + volumes: + - ./backend/app:/app/app + command: + [ + "uv", + "run", + "uvicorn", + "app.main:app", + "--host", + "0.0.0.0", + "--port", + "8000", + "--reload", + ] + + frontend: + build: ./frontend + environment: + NEXT_PUBLIC_API_BASE_URL: http://localhost:8000 + ports: + - "3000:3000" + depends_on: + - backend + +volumes: + postgres_data: + redis_data: diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 00000000..6e656fb0 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,6 @@ +node_modules +.next +.env +.env.* +!.env.example +.eslintcache diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 00000000..8a4b22ef --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,2 @@ +NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 +NEXT_PUBLIC_MAPBOX_TOKEN= diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json new file mode 100644 index 00000000..bffb357a --- /dev/null +++ b/frontend/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 00000000..1c7d16b8 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,24 @@ +FROM node:20-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm ci || npm install + +FROM node:20-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +ENV NEXT_TELEMETRY_DISABLED=1 +RUN npm run build + +FROM node:20-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + PORT=3000 + +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static + +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/frontend/next.config.ts b/frontend/next.config.ts new file mode 100644 index 00000000..a8b7383b --- /dev/null +++ b/frontend/next.config.ts @@ -0,0 +1,8 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + reactStrictMode: true, + output: "standalone", +}; + +export default nextConfig; diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..aca9fbac --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,36 @@ +{ + "name": "gendesign-frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit", + "codegen": "openapi-typescript http://localhost:8000/openapi.json -o src/lib/api-types.ts" + }, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "@tanstack/react-query": "^5.50.0", + "leaflet": "^1.9.4", + "react-leaflet": "^4.2.1", + "leaflet-draw": "^1.0.4" + }, + "devDependencies": { + "@types/leaflet": "^1.9.0", + "@types/leaflet-draw": "^1.0.0", + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "typescript": "^5.5.0", + "tailwindcss": "^4.0.0", + "@tailwindcss/postcss": "^4.0.0", + "postcss": "^8.4.0", + "eslint": "^9.0.0", + "eslint-config-next": "^15.0.0", + "openapi-typescript": "^7.0.0" + } +} diff --git a/frontend/src/app/concept/page.tsx b/frontend/src/app/concept/page.tsx new file mode 100644 index 00000000..4ab9b5aa --- /dev/null +++ b/frontend/src/app/concept/page.tsx @@ -0,0 +1,23 @@ +"use client"; + +import Link from "next/link"; + +export default function ConceptPage() { + return ( +
+ ← Home +

Concept (Generative Design)

+

+ TODO Stage 1a: polygon import (GeoJSON / Leaflet draw) + parameter form + + result tabs. +

+

+ TODO Stage 1b: render 3 variants on the map with colored buildings. +

+

+ TODO Stage 1c: TEAP table, financial form, PDF/Excel/DXF download + buttons. +

+
+ ); +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 00000000..7d7ca582 --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,26 @@ +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "GenDesign", + description: "Generative Design + Site Finder", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 00000000..f8413d3d --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,18 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+

GenDesign

+

Generative Design + Site Finder — Discovery MVP

+ +
+ ); +} diff --git a/frontend/src/app/site-finder/page.tsx b/frontend/src/app/site-finder/page.tsx new file mode 100644 index 00000000..6b74a4ce --- /dev/null +++ b/frontend/src/app/site-finder/page.tsx @@ -0,0 +1,15 @@ +"use client"; + +import Link from "next/link"; + +export default function SiteFinderPage() { + return ( +
+ ← Home +

Site Finder

+

TODO Stage 2a: load 1000+ Sverdlovsk parcels into PostGIS.

+

TODO Stage 2b: map (Mapbox) with color-graded parcels + filters + table + card.

+

TODO Stage 2c: «Compute concept» button calling Generative module.

+
+ ); +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 00000000..5d7dcfbe --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,21 @@ +export const API_BASE_URL = + process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8000"; + +export async function apiFetch( + path: string, + init?: RequestInit +): Promise { + const response = await fetch(`${API_BASE_URL}${path}`, { + ...init, + headers: { + "Content-Type": "application/json", + ...(init?.headers ?? {}), + }, + }); + if (!response.ok) { + throw new Error( + `API error ${response.status}: ${await response.text()}` + ); + } + return response.json() as Promise; +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 00000000..7488bb2c --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "paths": { + "@/*": ["./src/*"] + }, + "plugins": [{ "name": "next" }] + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/sshkey.txt b/sshkey.txt new file mode 100644 index 00000000..fc7fe153 --- /dev/null +++ b/sshkey.txt @@ -0,0 +1,18 @@ +Your identification has been saved in C:\Users\user\.ssh\gendesign_deploy +Your public key has been saved in C:\Users\user\.ssh\gendesign_deploy.pub +The key fingerprint is: +SHA256:50HjJLdk9XaRDRjvKIB9LrRoux6KojXQZvgqLm7LEks gendesign-deploy +The key's randomart image is: ++--[ED25519 256]--+ +| oo.oo| +| o ..o .o| +| . = O + .| +| o o & o + . | +|o + o S B . . | +|.E . . + o | +|.o+ o . | +|B+ o . o | +|@*o ..o | ++----[SHA256]-----+ + +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPGKuThuQMPdjxdA9FXHkUZUO+iyVbSMOxy3qFbOY7+K gendesign-deploy