The tg (bot) VPS came up on Europe/Moscow while the main host is UTC, so host-level
timestamps (journald, file mtimes, cron) sat 3h apart across the fleet. Add a
community.general.timezone task to the common role so every provisioned host is UTC
(idempotent — a no-op on the already-UTC main host). Applied live to the tg host in
the same change; the containerised services run in UTC regardless, so no restart.
The backend recorded 172.19.0.9 — the gateway's own docker connection address — as
the client IP for all users: the account's last-login IP shown in the admin console,
and it never reached the backend access log. The gateway forwarded the client IP as
X-Forwarded-For only on chat/feedback calls; every other backend call (including the
profile fetch that stamps last_login_ip) sent none, so the backend fell back to the
peer address.
Carry the client IP on the request context (WithClientIP, mirroring WithPlatform) and
set it once per request in the Connect edge, so the backend client injects
X-Forwarded-For on every downstream REST call. Also add the resolved client IP to the
backend access log.
Test: WithClientIP rides a non-chat call (Profile) as X-Forwarded-For, and is absent
when no IP is set. Docs updated (ARCHITECTURE gateway↔backend + edge).
The "watch for chips" rewarded payout and its per-day / per-hour caps live in the
shared payments.config row and had no admin surface — they could only be changed by
SQL, contrary to D32 (the rewarded rate is meant to be admin-configurable). Add a
"Rewarded ads" form on the /_gm/catalog page: RewardConfig / SetRewardConfig on the
service (non-negative validated), a setRewardConfig store writer (the singleton
config row), the consoleSetReward handler + POST /_gm/catalog/reward route, and the
form pre-filled from the current config. No migration — the columns already exist.
The reward rate is a config value, not a sellable catalog atom (so a "no-ads forever"
style product is still out of scope by D32/D33).
Test: an integration test sets the config, checks the page pre-fill, and refuses a
negative value.
<section class="panel"><h2>Rewarded ads (watch for chips)</h2>
<p class="note">The chips a player earns per rewarded-video view (VK only), plus the per-day and per-hour caps that bound free chips — <strong>0 payout turns rewarded off</strong>. This is the shared reward config, not a product.</p>
`X-User-ID` (and the session's trusted `X-Platform`, §3) for authenticated
requests; `backend` never re-derives identity or platform from the body. Because every sync call targets the one backend host, the
requests, plus the caller's real IP as **`X-Forwarded-For`** on **every** call
(carried on the request context), so the backend records the real caller — the
account's last-login IP, chat/feedback moderation, the access log — rather than
the gateway's own connection address; `backend` never re-derives identity or platform from the body. Because every sync call targets the one backend host, the
gateway's REST client widens its keep-alive pool well past the stdlib default
of 2 idle connections per host; otherwise the per-request connection churn
exhausts ephemeral ports and burns gateway CPU under load (see
@@ -1496,7 +1499,8 @@ Two contours, two secret/variable prefixes (`TEST_` / `PROD_`):
forwards the domain to `scrabble:80`, so the in-compose caddy serves plain HTTP
(`CADDY_SITE_ADDRESS=:80`). The in-compose caddy **trusts X-Forwarded-For from
private-range upstreams** (`trusted_proxies private_ranges`), so the real client IP —
used for chat-moderation logging and the gateway's per-IP rate limiting — survives the
used for the gateway's per-IP rate limiting and, forwarded on to the backend, the account's
last-login IP, chat/feedback moderation and the access log — survives the
host-caddy hop; in prod (no host caddy) public clients are untrusted and Caddy uses the
real peer, so the single config is correct and spoof-safe in both contours. The
**bot-link mTLS material** (a private CA + gateway/bot leaves, CN=`gateway`) is
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.