From e0a5753f1a5d4812004690c84424dd7908335c80 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Mon, 22 Jun 2026 22:03:35 +0200 Subject: [PATCH 1/4] fix(deploy): force-recreate caddy on its roll so config-only changes apply The prod rolling deploy rolls each service with `compose up -d --no-deps `. For caddy that is a no-op on a config-only release: its image is pinned (caddy:2-alpine, no $TAG), so the compose definition is unchanged between releases, compose treats the container as current and does not recreate it, and admin is off so there is no hot reload. The new bind-mounted Caddyfile is seeded to the host but never loaded -- the v1.2.2 `Alt-Svc: clear` edge fix deployed green yet did not take effect until caddy was restarted by hand. Force a recreate for caddy on its roll (every other service already recreates on its new $TAG image), so a bind-mounted Caddyfile change always applies. Costs a ~1-2s caddy blip per deploy, acceptable for the infrequent manual prod rollout. --- deploy/README.md | 4 +++- deploy/prod-deploy.sh | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/deploy/README.md b/deploy/README.md index 89fa882..f8b4a12 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -150,7 +150,9 @@ Re-run `ansible/` after a host resize — it is idempotent. workflow manually (Gitea → Actions → prod-deploy → run from `master`, input `confirm=deploy`). It builds + pushes the images to the registry, ships the compose/config/certs/env over SSH, deploys the main host with `prod-deploy.sh` (rolling, -health-gated, **auto-rollback to the previous tag**), then the bot host, then probes the +health-gated, **auto-rollback to the previous tag**; caddy is force-recreated on its roll so +a bind-mounted `Caddyfile` change applies — its image is pinned and admin is off, so neither a +new tag nor a hot reload would pick it up), then the bot host, then probes the public site. After `master` is green this workflow is the **only** thing that touches prod — nothing auto-deploys there. It runs four visible jobs: **build → deploy-main → deploy-bot → verify** (the per-service rolling shows in the deploy-main log). diff --git a/deploy/prod-deploy.sh b/deploy/prod-deploy.sh index c2fef68..af87062 100755 --- a/deploy/prod-deploy.sh +++ b/deploy/prod-deploy.sh @@ -74,7 +74,13 @@ health_running() { # health_running : running, not restarting, stable roll() { # roll local svc="$1"; shift echo ">>> rolling $svc -> $TAG" - dc up -d --no-build --no-deps "$svc" || return 1 + # caddy's image is pinned (caddy:2-alpine, no $TAG) and its Caddyfile is bind-mounted, so a + # config-only change leaves the compose definition unchanged: `up -d` treats the container as + # current and does not recreate it, and admin is off so there is no hot reload — the new + # Caddyfile would never load. Force a recreate for caddy so config changes always apply; every + # other service already recreates on its new $TAG image. + local recreate=(); [ "$svc" = caddy ] && recreate=(--force-recreate) + dc up -d --no-build --no-deps "${recreate[@]}" "$svc" || return 1 "$@" || { echo "!!! $svc failed health check"; return 1; } echo "<<< $svc healthy" } From 5a80696fe8d2092b7f0e48d0e7fd26912be2c874 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Tue, 23 Jun 2026 00:09:39 +0200 Subject: [PATCH 2/4] feat(ui): friends list as lobby-style rows with kebab + confirm modals Settings -> Friends previously rendered each friend as a bordered card with two always-visible text buttons (Remove / Block) that fired immediately. Rework the whole screen to the lobby's visual language: one-line rows split by hairline separators across all three sections (friends, incoming requests, blocked). Each friend row gains a right-hand kebab that slides the row open to reveal two icon actions split by a vertical divider -- block (no-entry) and remove (cross) -- mirroring the lobby's slide-to-reveal. Both actions now require a confirmation modal; since the slide moves a short name off-screen, the modal keeps a generic title and shows the friend's name in the body, above the buttons, so a long name cannot stretch the sheet. Incoming keeps its accept/decline buttons and blocked keeps unblock, inline on their rows. Add the friends.actions / friends.blockConfirm / friends.unfriendConfirm keys to both i18n catalogs and document the flow in FUNCTIONAL (+_ru). --- docs/FUNCTIONAL.md | 3 + docs/FUNCTIONAL_ru.md | 3 + ui/src/lib/i18n/en.ts | 3 + ui/src/lib/i18n/ru.ts | 3 + ui/src/screens/Friends.svelte | 218 +++++++++++++++++++++++++++------- 5 files changed, 188 insertions(+), 42 deletions(-) diff --git a/docs/FUNCTIONAL.md b/docs/FUNCTIONAL.md index 38260fe..11e580e 100644 --- a/docs/FUNCTIONAL.md +++ b/docs/FUNCTIONAL.md @@ -213,6 +213,9 @@ block **overrides but does not delete** an existing friendship (so you may block they keep seeing you as one); active games are never interrupted — you can finish them, with the blocked opponent's chat composer hidden (only the log remains). Blocking from a game card mirrors the block in **Settings → Friends**; **unblock** and **unfriend** live there only. +On Settings → Friends each friend is a one-line row whose right-hand kebab (⋮) slides open +**block 🚫** and **remove ✖️** icon actions, and each action is gated by a confirmation +that names the friend (*Block this player?* / *Remove from friends?*). Blocking an **auto-match opponent who is secretly a robot** behaves the same in that game (struck name, hidden composer) and lists the blocked opponent under the name you saw, but is recorded only against that game — the disguise holds, the shared robot is never globally diff --git a/docs/FUNCTIONAL_ru.md b/docs/FUNCTIONAL_ru.md index fa1dcd6..3fc7c05 100644 --- a/docs/FUNCTIONAL_ru.md +++ b/docs/FUNCTIONAL_ru.md @@ -218,6 +218,9 @@ _Вход сейчас только через провайдера, поэто заблокированного соперника «подвал» чата скрыт (остаётся только лог). Блокировка с карточки в партии повторяет блокировку в **Настройках → Друзья**; **разблокировка** и **удаление из друзей** есть только там. +В **Настройках → Друзья** каждый друг — однострочник, чей правый кебаб (⋮) выдвигает +иконки-действия **заблокировать 🚫** и **удалить ✖️**, и каждое действие подтверждается +диалогом с именем друга (*Заблокировать?* / *Удалить из друзей?*). Блокировка **авто-матч соперника, который втайне робот**, в этой партии ведёт себя так же (зачёркнутое имя, скрытый «подвал») и в списке заблокированных показывается под тем именем, которое ты видел, но записывается только для этой партии — маскировка сохраняется, общий diff --git a/ui/src/lib/i18n/en.ts b/ui/src/lib/i18n/en.ts index db0cad9..6f7cb9e 100644 --- a/ui/src/lib/i18n/en.ts +++ b/ui/src/lib/i18n/en.ts @@ -227,6 +227,9 @@ export const en = { 'friends.decline': 'Decline', 'friends.unfriend': 'Remove', 'friends.block': 'Block', + 'friends.actions': 'Actions', + 'friends.blockConfirm': 'Block this player?', + 'friends.unfriendConfirm': 'Remove from friends?', 'friends.add': 'Add a friend', 'friends.addFromGame': 'Add to friends', 'friends.blockFromGame': 'Block player', diff --git a/ui/src/lib/i18n/ru.ts b/ui/src/lib/i18n/ru.ts index 4321efd..bd5b5a6 100644 --- a/ui/src/lib/i18n/ru.ts +++ b/ui/src/lib/i18n/ru.ts @@ -228,6 +228,9 @@ export const ru: Record = { 'friends.decline': 'Отклонить', 'friends.unfriend': 'Удалить', 'friends.block': 'Заблокировать', + 'friends.actions': 'Действия', + 'friends.blockConfirm': 'Заблокировать?', + 'friends.unfriendConfirm': 'Удалить из друзей?', 'friends.add': 'Добавить друга', 'friends.addFromGame': 'В друзья', 'friends.blockFromGame': 'Заблокировать', diff --git a/ui/src/screens/Friends.svelte b/ui/src/screens/Friends.svelte index 4471ef4..831f3d2 100644 --- a/ui/src/screens/Friends.svelte +++ b/ui/src/screens/Friends.svelte @@ -1,5 +1,6 @@