feat(proj): init
This commit is contained in:
@@ -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 просит z15–18 вместо 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).
|
||||
@@ -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.
|
||||
@@ -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` — это не трогали.
|
||||
@@ -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.
|
||||
|
||||
Чтение строк по диапазону, агрегация по дню уже в памяти (неделя × токены — мало строк).
|
||||
Reference in New Issue
Block a user