Deep-review PR #2569 (echo-стенд на caddy:2, caddy adapt): комментарий у
/trade-in/api* и @tradein утверждал, что старая Set-форма
(header_up X-Authenticated-User {http.auth.user.id}) пропустила бы клиентский
заголовок насквозь — измерено, что это неверно. Caddy подставляет
неразрешённый плейсхолдер как литеральную строку (ReplaceKnown), т.е. старая
форма была бы fail-closed-but-сломанной (get_role("{http.auth.user.id}") ->
403 для всех), а не bypass. Решение (-Field вместо Set) остаётся правильным
по другой причине: это единственная форма с документированной delete-
семантикой, не зависящая от трактовки пустого/нерезолвленного плейсхолдера.
Также: @uipreview (единственный блок в trade-in-периметре без scrub) теперь
тоже явно удаляет X-Authenticated-User - единообразие периметра, хоть
сейчас и инертно (статичная страница без секрета).
И: комментарий у @saleshare уточнён - снятие Caddy basic_auth с
/trade-in/sale-share не делает страницу публичной, она по-прежнему за
RouteGuard + session-required API (редирект на /login без валидной сессии).
Провалидировано: caddy validate --config Caddyfile --adapter caddyfile
(caddy:2, локальный docker) -> "Valid configuration".
Оба trade-in блока (`handle /trade-in/api/*`, `@tradein`) + связанный
`@saleshare` редирект перенесены ВЫШЕ `import caddy/users.caddy.snippet`
в route{} — Caddy матчит handle-блоки сверху вниз, поэтому они теперь
short-circuit'ят до basic_auth-гейта (тот же механизм, что уже применён
к /health, /preview/*, @uipreview). Site Finder (/, /api/*, /analytics)
остаётся под basic_auth без изменений.
Trade-in уже имеет собственную авторизацию (форма входа + opaque
session-cookie, #2552) поверх RBAC внутри tradein-backend — Caddy
basic_auth для него стал избыточным легаси-слоем.
Security: после переноса плейсхолдер {http.auth.user.id} для этих
блоков никогда не резолвится (basic_auth больше не выполняется на
пути к ним). Вместо `header_up X-Authenticated-User {http.auth.user.id}`
(Set-операция с пустым значением — недокументированно надёжна как
удаление заголовка) используется явное `header_up -X-Authenticated-User`
(документированная delete-семантика `-<field>`). Это исключает проход
клиентского X-Authenticated-User насквозь к tradein-backend, который
в auth_mode="dual" всё ещё принимает legacy trusted-header путь —
без явного удаления внешний запрос с подделанным заголовком мог бы
выдать себя за другого пользователя. X-Internal-Auth-Secret не тронут
(#2213, всегда перезаписывается из env).
Провалидировано: `caddy validate --config Caddyfile --adapter caddyfile`
(caddy:2 образ, локальный docker) → "Valid configuration".
Поиск домов (доля квартир в продаже) вынесен в отдельный продукт, отвязан от
бренда «Мера» (Мера = оценка вторички). Только UI + 1 строка Caddy, бэк не тронут.
- app/sale-share/layout.tsx: своя metadata (title «Поиск домов — gendsgn»).
- components/trade-in/SaleShareHeader.tsx: своя шапка — продукт «Поиск домов»,
без MeraMark и Mera-табов; UserMenu (auth) сохранён.
- app/sale-share/page.tsx: Topbar → SaleShareHeader; убраны «Мера» из
breadcrumb/footer. Функционал/API (/trade-in/api/...) не тронут.
- Caddyfile: gendsgn.ru/sale-share → redir /trade-in/sale-share (короткий адрес;
basePath=/trade-in → true vanity-URL требует отдельного app).
Public CI surface для a11y/lighthouse аудита. /ui-preview/estimate (#803) рендерит mock-фикстуру без бэкенда, но Caddy basic_auth отдавал 401. handle-блок ДО auth-import → публично.
handle_path /trade-in/api/* стрипал /trade-in/api целиком — FastAPI
получал /v1/trade-in/estimate и возвращал 404. FastAPI router на
/api/v1/trade-in/*, нужен strip только префикса basePath /trade-in.
handle /trade-in/* НЕ матчил /trade-in (без слеша) — request попадал в
универсальный handle и проксировался на основной gendesign frontend,
который для unknown path отдавал 200 OK с пустым body.
Заменил на named matcher `@tradein path /trade-in /trade-in/*` — ловит
оба случая. Также убрал redirect-handle /trade-in → /trade-in/ т.к.
trailingSlash=false у Next.js и сам редиректит обратно.
Block был добавлен вручную при Forgejo migration (2026-05-16) но не
закоммичен в repo. Первый Caddyfile-touching deploy (#205 GlitchTip)
сделал git reset --hard origin/main на VPS → блок исчез → TLS handshake
internal error 80 на git.gendsgn.ru (prod down ~16:30).
Routes git.gendsgn.ru → forgejo:3000 (контейнер из forgejo-migration/
docker-compose.yml, shared gendesign_default network, Forgejo default
HTTP port).
VPS уже починен hotfix-ом руками; этот PR делает fix permanent — переживёт
любой git reset --hard от deploy.yml.
Obsidian Self-hosted LiveSync (CouchDB) выведен в отдельный docker-compose
stack и GHA workflow. Любой стек теперь редеплоится независимо.
* docker-compose.obsidian.yml: только couchdb в shared external network
* docker-compose.prod.yml: убрана couchdb-секция; caddy подключён к
shared network для маршрута obsidian.gendsgn.ru → couchdb:5984
* .github/workflows/deploy-obsidian.yml: новый workflow с path-filters
(триггерится только при изменениях obsidian-related файлов)
* .github/workflows/deploy.yml: + path-filters (триггерится только при
main-related файлах) + создание shared network в bootstrap
* docs/obsidian-livesync.md: обновлены инструкции под split-архитектуру
После push:
1. ssh gendesign 'docker network create gendesign_shared' (один раз)
2. Прописать COUCHDB_USER/PASSWORD в backend/.env.runtime
3. Добавить DNS A-record obsidian.gendsgn.ru → IP VPS
4. git push → GHA задеплоит обе части независимо
Преимущества:
- Изменения main не разрывают LiveSync клиентов
- Изменения obsidian не рестартуют main
- Падение obsidian-стека не влияет на основное приложение
- backend/Dockerfile: split builder/runner, runtime libs only, non-root app user, curl for healthcheck, --frozen via new uv.lock
- frontend/Dockerfile: npm ci instead of npm install (deterministic), USER node
- docker-compose.prod.yml: working backend healthcheck (curl now in image), redis healthcheck, drop dead NEXT_PUBLIC_API_BASE_URL env (Next.js bakes NEXT_PUBLIC_* at build time, runtime override is no-op)
- docker-compose.yml: same redis healthcheck, BACKEND_URL for next.config rewrites, drop uv from CMD (not in new image)
- Caddyfile: handle (not handle_path) so /api prefix is preserved into FastAPI router
- next.config.ts: rewrite /api/* and /health to BACKEND_URL in dev (no Caddy locally)
- frontend/src/lib/api.ts: empty default = same-origin relative URLs
- Makefile: drop uv run from migrate target
- Add backend/uv.lock and frontend/package-lock.json for reproducible builds
Verified: docker build succeeds for both, backend container starts, /health responds, curl healthcheck works inside image, container runs as non-root.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>