Robokassa moderation requires the public offer to list every digital good with
its price. Move /offer/ off the static landing container to the render sidecar:
it splices the live catalog price list (§4.4) into the owner-edited
ui/legal/offer_ru.md and renders it with the shared ui/src/lib/offer.ts — one
renderer, no drift, always matching the current catalog with no redeploy.
- backend: /api/v1/internal/offer/pricing (internal, off the edge allow-list)
projects the active catalog into two markdown tables — chip packs priced per
rail (roubles / VK votes / Telegram Stars) and chip-priced values — through
payments.Money so no float reaches the page. Cached in memory: warmed at boot,
marked stale on every catalog mutation, so a served render issues no query.
- renderer: GET /offer/ fetches the tables and substitutes them at the
<#pricing_template#> marker, then renders; offer_ru.md is baked into the image
and marked is bundled from ui. GET /offer -> 301. Only /offer/ is edge-exposed.
- caddy: route /offer/ to the sidecar; drop the now-dead landing /offer/
handlers and the vite emit-offer plugin.
- offer: fill §4.3 (the chip-payment wording) and drop the in-page back link.
- landing footer: a feedback link (the offer's Telegram contact) beside the
offer link.
- docs (ARCHITECTURE, FUNCTIONAL +_ru, renderer README), CI /offer/ probe,
unit + integration + node tests.
The offer is the legal document a purchase accepts, needed before real-money
intake goes live. Convert the source PDF to an editable ui/legal/offer_ru.md and
render it to a standalone static dist/offer/index.html at build (a vite
emit-offer plugin using marked); the landing container serves it at /offer/,
with a bare /offer redirecting in. Add a small centered "Публичная оферта"
footer link on the landing (ru/en) and a CI probe asserting /offer/ serves the
page rather than silently falling through to the landing shell.
- gateway/Dockerfile gains a `landing` target: caddy:2-alpine + the shared
Vite build (identical build args keep the ui stage a single cached build);
the gateway target drops landing.html from the embed.
- The contour caddy routes /app/, /telegram/ and the Connect path to the
gateway; the catch-all — the landing at / and any stray path — goes to the
new landing service, so junk traffic is absorbed by static file serving.
- deploy/landing/Caddyfile mirrors the webui caching (immutable assets,
no-cache shells) and falls back unknown paths to the landing shell.
- The gateway's / now 308-redirects to /app/ (keeps a local no-caddy run
usable); webui placeholder landing.html removed.
- CI deploy probe checks both / (landing) and /app/ (gateway).
Verified: both images build; the landing container serves landing.html at /
(no-cache) with junk-path fallback; the gateway image redirects / to /app/
and carries no landing content.