feat(proj): init

This commit is contained in:
vl.arkhangelskii
2026-09-21 03:53:15 +03:00
commit 3e34f391b3
258 changed files with 20968 additions and 0 deletions
@@ -0,0 +1,25 @@
# 2026-09-20 — Тайловый сервер на C#
Собрано с нуля: ASP.NET Core 8, Clean Architecture.
## Что сделано
- Воркер `MapSyncHostedService`: cron раз в сутки (`0 3 * * *` UTC) + прогон при старте.
- Для каждого extract из `TileServer:Extracts``data/extracts.json`): скачивание `.osm.pbf` с Geofabrik (If-None-Match / 304), сборка OpenMapTiles MBTiles через `planetiler-openmaptiles` (Java 21+).
- По умолчанию один источник: `central-fed-district`. Можно добавлять сколько угодно регионов — воркер проходит все по очереди.
- `GET /api/v1/tiles/{source}/{z}/{x}/{y}.pbf` — MapLibre XYZ, TMS Y-flip, gzip как в MBTiles.
- `GET /api/v1/styles/{name}?source=...` — стили по имени; `PUT` и файлы `data/styles/*.json`.
- Демо: `GET /` MapLibre GL, выбор источника/стиля, статус воркера.
- Сборка тайлов не на чистом C#: генерация схемы OpenMapTiles — Planetiler. C# оркестрирует download/build/serve.
## Как проверялось
- `dotnet build` / 9 unit-тестов — ок.
- Локальный прогон API: демо 200, стили osm-bright/dark/positron, TileJSON, 204 на отсутствующий тайл, PUT второго extract, 404 на неизвестный стиль.
- Браузерных MCP в сессии не было — проверка через HTTP.
## Важно для следующего запуска
- Без Java 21 воркер скачает PBF, но не соберёт тайлы.
- Первая сборка ЦФО — часы и много диска/RAM (`TileServer:JvmMaxHeap`).
- `TileServer:Sync:RunOnStartup` по умолчанию `true`.
@@ -0,0 +1,6 @@
# 2026-09-20 — docker-compose для сервера, порт 51300
- `docker-compose.yml`: `51300:8080`, `restart: unless-stopped`, `./data:/data`, healthcheck `/health/live`, лимит RAM 12g, логи json-file.
- Dockerfile: `curl` в runtime-образе под healthcheck.
- `.dockerignore`: `data/`, PBF, MBTiles, bin/obj не попадают в контекст сборки.
- README: URL деплоя обновлён на порт 51300.
@@ -0,0 +1,8 @@
# 2026-09-20 — Planetiler: Unsupported format building
На сервере compose отработал до сборки тайлов и упал.
- PBF ЦФО скачался (877 MB), JAR Planetiler тоже.
- Падение: `IllegalArgumentException: Unsupported format building` на `/data/tiles/central-fed-district.mbtiles.building`.
- Planetiler смотрит последнее расширение файла. `.building` не формат архива.
- Фикс: staging `*.building.mbtiles`, вывод через `--output`, `--tmpdir` в `/data/tmp`.
@@ -0,0 +1,7 @@
# 2026-09-20 — Planetiler: MapTiler CDN timeout
После фикса `.mbtiles` сборка дошла до скачивания sidecar-данных.
- `water-polygons` с osmdata.openstreetmap.de — ок (929 MB).
- `natural_earth` и `lake_centerlines` с `dev.maptiler.download` — ~25 КБ и Read timed out (30s × 5).
- Фикс: зеркала naciscdn + GitHub osm-lakelines, предзагрузка нашим HttpClient, `--http_timeout=15m`, обрезки < min size удаляются.
@@ -0,0 +1,7 @@
# 2026-09-20 — «зависло» на Natural Earth
Фронт Building / «идёт синхронизация» / «следующий запуск нет» — это не сборка тайлов.
Лог оборвался после HTTP 200 на `naciscdn.org/.../natural_earth_vector.sqlite.zip`. Тело не шло, HttpClient без таймаута ждал вечно. NextScheduledAt не ставился, пока RunOnStartup не закончится.
Фикс: stall 45s, зеркала, skip PBF если размер совпал, статус PreparingSources, next cron сразу при старте.
@@ -0,0 +1,7 @@
# 2026-09-20 — пустая карта: mixed content
Тайлы собрались (1.3GB). Браузер блокировал `http://tile-server.ru/api/v1/tiles/...json` на HTTPS-странице.
Приложение за nginx видит Scheme=http и вшивало http:// в style/TileJSON.
Фикс: root-relative URL (`/api/v1/tiles/...`), ForwardedHeaders, опция PublicBaseUrl, transformRequest на демо как страховка.
@@ -0,0 +1,5 @@
# 2026-09-20 — данные в named volume
`./data:/data` заменён на volume `tile-server-data`. Деплой/`compose up --build` его не трогает. DataProtection keys тоже в `/data/aspnet-keys`.
На сервере с уже собранными тайлами сначала скопировать старый `./data` в volume, иначе контейнер увидит пустой `/data` и начнёт сборку заново.
@@ -0,0 +1,7 @@
# 2026-09-20 — MapLibre: relative tile URL invalid
Тайлы Ready, TileJSON bounds ЦФО ок. Пустая карта: `tiles: ["/api/v1/tiles/...pbf"]`.
Воркер MapLibre делает `new Request(url)` — относительный путь не URL.
Фикс: абсолютный `https://tile-server.ru` (PublicBaseUrl + scheme/host), demo `transformRequest` через `new URL(url, location.origin)`.
@@ -0,0 +1,5 @@
# 2026-09-20 — карта пустеет ближе 1 км
Тайлы Planetiler до z14 (~1 км в Москве). Дальше MapLibre без source.maxzoom просит z15+; 204 = «пустой тайл», не overzoom.
Фикс: в стиле `tiles` + `maxzoom: 14` (без TileJSON url), демо не делает setStyle каждые 15 с, missing tile → 404.
@@ -0,0 +1,5 @@
# 2026-09-20 — maxzoom 15
z14 был дефолт OpenMapTiles, не потолок данных. Planetiler 0.9.3 (jar v3.16) жёстко режет на z15.
Конфиг и extract → 15, `--render_maxzoom=15`. Синк пересобирает, если в MBTiles maxzoom меньше конфига. z16 этим стеком нельзя.
@@ -0,0 +1,5 @@
# 2026-09-20 — серверный overzoom MVT
Клиентский overzoom нельзя требовать от всех. Отдать parent.pbf как child нельзя: MVT привязан к экстенту тайла.
Сервер для z > dataMaxzoom (15) клипает геометрию в координаты ребёнка, gzip, кэш. TileJSON/style maxzoom=18.
@@ -0,0 +1,3 @@
# 2026-09-20 — переход к lat/lon/zoom
В демо-панели форма «Перейти к точке»: широта, долгота, зум, flyTo. Запятая как десятичный разделитель ок.
@@ -0,0 +1,5 @@
# 2026-09-20 — пустые тайлы Москвы
Проверил прод: z6–z14 Москвы 200, здания/дороги на месте. Пусто из‑за maxzoom=18 в стиле: MapLibre просит z1518 вместо overzoom z14, плюс 404 JSON на клетки вне ЦФО.
Фикс: в TileJSON/стиле снова data maxzoom; missing → 204. Серверный overzoom остаётся, если клиент всё же запросит z>14.
@@ -0,0 +1,3 @@
# 2026-09-20 — пересборка demo
Карта на весь экран, HUD поверх: источник/стиль, камера (пресеты Москва/ЦФО, lat lon zoom), live z/lat/lon, hash `#z/lat/lon`. Воркер свёрнут. Поллинг больше не сбрасывает камеру при смене стиля.
@@ -0,0 +1,5 @@
# 2026-09-20 — пустая карта на z14 Москва
Камера верная, фон osm-bright, слоёв нет. Тайл 14/9904/5121 на проде 200/gzip.
Фикс: отдать сырой MVT без Content-Encoding (nginx+gzip ломает воркер MapLibre); убрать bounds из стиля; HUD не писать 0,0 до moveend; `?v=2` чтобы сбросить кэш gzip-ответов.
@@ -0,0 +1,5 @@
# 2026-09-20 — пустая карта после z2
Мир на z0–z1 рисуется, Москва z14 нет. Тайл 14/9904/5121 на проде 456 KB, в Chrome main-thread fetch 200, а воркер MapLibre получает пустые loaded-тайлы.
Причина: один SqliteConnection на все запросы. z0 — один запрос, ок; z2+ — пачка параллельных чтений, SQLite не thread-safe → 204. Фикс: соединение из пула на каждый GetTile. HUD: не блокировать синкиз-за кнопок в форме. Шрифты Noto Sans (Open Sans 404 на demotiles).
+3
View File
@@ -0,0 +1,3 @@
# 2026-09-21 — UI демо
HUD больше не тянется на всю ширину. Стекло-острова: тулбар слева сверху, камера слева снизу, воркер справа снизу. Те же id для demo.js. Статус — пилюля с точкой, шрифты tabular, MapLibre-контролы в том же тёмном стекле.
@@ -0,0 +1,3 @@
# 2026-09-21 — кэш тайлов nginx
204 больше не кэшируем (Cache-Control: no-store). 200 pbf — week immutable. Nginx: отдельный location на `.pbf`, proxy_cache, ключ с `$args` (`?v=`), не gzip в location тайлов.
@@ -0,0 +1,41 @@
# Аккаунты, токены и SPA
Поверх тайлового пайплайна: Яндекс OAuth, персональные стили/URL, учёт запросов, Vite-кабинет на том же origin.
## Данные
- EF Core SQLite, файл `/data/app.db` (`IDataPaths.AppDatabase`).
- Таблицы: `Users`, `ApiTokens` (только SHA-256 hash + prefix `ts_...`), `UserStyles`, `UsageDaily`.
- Миграция `InitialAccounts`, `Database.Migrate()` на старте.
## Auth
- `GET /api/v1/auth/yandex/start` → 302 на oauth.yandex.ru.
- SPA ловит `?code=&state=` на `/`, `POST /api/v1/auth/yandex`.
- Cookie `ts_session` HttpOnly, SameSite=Lax, Secure=SameAsRequest.
- Кабинет: cookie. Выдача тайлов/стилей: `?token=` или `Authorization: Bearer`.
- Секрет Яндекса только в env: `TileServer__Yandex__ClientId|ClientSecret|RedirectUri`. Не коммитить.
## Выдача и usage
- `/u/{slug}/tiles/{source}/{z}/{x}/{y}.pbf?token=`
- `/u/{slug}/tiles/{source}.json?token=`
- `/u/{slug}/styles/{name}?source=&token=`
- Счётчик в `ConcurrentDictionary`, flush каждые 5 с в `UsageDaily`.
- Публичные `/api/v1/tiles` и `/api/v1/styles` без счётчика.
## Стили
- Клон пресета: `POST /api/v1/me/styles`.
- Bind персонального стиля: `tiles[0]` = `/u/{slug}/tiles/{source}/...pbf?token=`.
- Свои имена перекрывают пресеты.
## Фронт и деплой
- `web/`: Vite + React + styled-components, тема HUD (`#e8923d`).
- Docker: stage `node:22``wwwroot/app`. `/` — SPA, `/demo` — старое Razor-демо.
- Compose: `YANDEX_CLIENT_ID` / `YANDEX_CLIENT_SECRET` с хоста.
## Тесты
26 unit-тестов: hash/verify/revoked, slug, bind `/u/{slug}/` + token, батч usage.
@@ -0,0 +1,5 @@
# Фронт в compose на 5250
Сервис `tile-web`: тот же `Dockerfile`, target `frontend` (nginx + Vite `dist`). Порт хоста **5250**.
`/api` и `/u` проксируются на `tile-server:8080`, чтобы кабинет на `:5250` ходил в API с cookie на том же origin. Статика — SPA fallback на `index.html`.
@@ -0,0 +1,12 @@
# Яндекс OAuth как в please_pay_me_bot
400 `Yandex OAuth is not configured` был из-за пустых `${YANDEX_CLIENT_ID:-}` в контейнере.
Как в боте:
- плоские env `YANDEX_CLIENT_ID` / `YANDEX_CLIENT_SECRET` / `YANDEX_REDIRECT_URI` (плюс `TileServer__Yandex__*`);
- секрет только в `.env` на хосте (`.env.example` без секрета);
- SPA берёт `client_id` с `GET /api/v1/auth/providers` и сама открывает `oauth.yandex.ru/authorize`;
- обмен кода: `POST /api/v1/auth/yandex` с тем же `redirectUri`.
На сервере: скопировать `.env.example``.env`, вписать секрет, `docker compose up -d --build`.
@@ -0,0 +1,8 @@
# UI-kit и форма входа как в please_pay_me_bot
Скопирован design system (`design/`, `ui/`, `components/ui`) и правовые страницы.
- Вход: `AuthLayout` + чекбоксы оферты/согласия + кнопка «Войти через Яндекс» (disabled без галочек).
- Документы: `/legal/offer|privacy|consent|cookies`, футер, cookie-баннер.
- Оператор тот же ИП; предмет оферты — тайлы OSM / стили / токены, без ЮKassa и APK.
- Кабинет в `AppShell` того же UI-kit.
+3
View File
@@ -0,0 +1,3 @@
# Локальный .env для проверки Яндекс OAuth
Рядом с compose лежит `.env` (в gitignore) с текущими YANDEX_* — чтобы проверить вход. После проверки сменить секрет в кабинете OAuth.
@@ -0,0 +1,8 @@
# Учётки на PostgreSQL
Кабинет (Users, ApiTokens, UserStyles, UsageDaily) переведён с SQLite `app.db` на Postgres.
- Compose: сервис `db` (`postgres:16-alpine`), хост **5433** → 5432.
- Volume `tile-server-pg` (отдельно от `tile-server-data`).
- Строка: `ConnectionStrings__Accounts` / `ConnectionStrings:Accounts`.
- MBTiles по-прежнему SQLite в `/data/tiles` — это не трогали.
+9
View File
@@ -0,0 +1,9 @@
# Отдельный Vite demo
Публичная проверка тайлов/стилей/sync без кабинета и без Razor `/demo`.
- Проект `web-demo/`: React + MapLibre, тот же HUD (`demo.css`), публичные `/api/v1/sources|styles|tiles|sync`.
- Локально: `npm run dev` на порту **5174**, proxy `/api``127.0.0.1:5088`.
- Compose: сервис `tile-demo` (Dockerfile `target: demo`), хост **5251** → nginx 80, proxy на `tile-server:8080`.
- Gzip выключен на `/api/v1/tiles/` и `/tiles/`.
- Логин/токены не трогаем. Razor `/demo` оставлен как есть.
@@ -0,0 +1,5 @@
# Vite demo без compose
`tile-demo` убран из compose и Dockerfile. Демо только локально: `web-demo/``npm run dev` :5174.
Проверка личного API: slug + токен (`ts_…`) в HUD или `?slug=&token=`. Тогда стили/тайлы идут на `/u/{slug}/…`, не на публичный `/api/v1`. Токен и стиль создаются в кабинете.
@@ -0,0 +1,5 @@
# Vite demo → боевой API
`web-demo` ходит напрямую на `https://tile-server.ru` (CORS `*`). Локальный proxy на 5088 убран.
Переопределение: `VITE_API_ORIGIN`. Личный режим: slug + `ts_…``/u/{slug}/…` на том же хосте.
@@ -0,0 +1,3 @@
# Публичные стили и тайлы закрыты
`GET/PUT /api/v1/styles`, `GET /api/v1/tiles`, алиасы `/tiles/{id}/…` отвечают 401. Карта только через `/u/{slug}/…?token=`. Кабинет `/api/v1/me/styles` не трогал. В `ExtractResponse` публичные tile URL больше не отдаются.
@@ -0,0 +1,5 @@
# 502 на /u/{slug}
Хостовый nginx слал `/u/` на кабинет :5250. `tile-web` проксировал `/u/` на `127.0.0.1:8080` внутри своего контейнера — там нет API → 502.
Нужен `location /u/``127.0.0.1:51300` (как `/api/`). В `web/nginx.conf` fallback тоже на `http://tile-server:8080`.
@@ -0,0 +1,7 @@
# 500 на стилях
`StylesController` закрывал публичный `/api/v1/styles` через `throw UnauthenticatedException`. ExceptionHandlerMiddleware логирует это как unhandled (снаружи 500), затем handler ставит 401.
Кабинет клонирует через `POST /api/v1/me/styles` — этот маршрут не закрыт. Стек указывал на `StylesController.List()` = `GET /api/v1/styles`.
Исправление: публичные tiles/styles отвечают 401 `ProblemDetails` без throw.
@@ -0,0 +1,5 @@
# Кабинет: инструкции и редактор стилей
- Убран пункт «Демо карты».
- Раздел `/guides`: шаги, URL стиля/тайлов, HTML и React, живая карта.
- Редактор `/styles/:name`: слева свойства слоя (цвет, толщина, видимость), по центру MapLibre, справа список слоёв. Пресеты read-only.
@@ -0,0 +1,5 @@
# 500 на /api/v1/me/usage
Npgsql/EF не транслирует `GroupBy(Date).Select(new UsageDayDto(Sum…))`. Запрос падал `InvalidOperationException` → 500.
Чтение строк по диапазону, агрегация по дню уже в памяти (неделя × токены — мало строк).