Stage 17: test-contour verification & defect fixes #19

Merged
developer merged 28 commits from feature/stage-17-contour-verification-fixes into development 2026-06-07 19:20:40 +00:00
7 changed files with 138 additions and 4 deletions
Showing only changes of commit 74683f294f - Show all commits
+3
View File
@@ -285,6 +285,9 @@ jobs:
mkdir -p "$conf" mkdir -p "$conf"
cp -r caddy otelcol prometheus tempo grafana "$conf"/ cp -r caddy otelcol prometheus tempo grafana "$conf"/
export SCRABBLE_CONFIG_DIR="$conf" export SCRABBLE_CONFIG_DIR="$conf"
# App version for the About screen: the git tag if present, else the short SHA
# (the test checkout is shallow/untagged, so this is the SHA here — fine).
export APP_VERSION="$(git -C "$GITHUB_WORKSPACE" describe --tags --always 2>/dev/null || echo dev)"
docker compose --ansi never build --progress plain docker compose --ansi never build --progress plain
docker compose --ansi never up -d --remove-orphans docker compose --ansi never up -d --remove-orphans
# The config-only services bind-mount the reseeded config dir. A plain `up -d` # The config-only services bind-mount the reseeded config dir. A plain `up -d`
+1
View File
@@ -79,6 +79,7 @@ services:
VITE_TELEGRAM_BOT_ID: ${VITE_TELEGRAM_BOT_ID:-} VITE_TELEGRAM_BOT_ID: ${VITE_TELEGRAM_BOT_ID:-}
VITE_TELEGRAM_LINK: ${VITE_TELEGRAM_LINK:-} VITE_TELEGRAM_LINK: ${VITE_TELEGRAM_LINK:-}
VITE_GATEWAY_URL: ${VITE_GATEWAY_URL:-} VITE_GATEWAY_URL: ${VITE_GATEWAY_URL:-}
VITE_APP_VERSION: ${APP_VERSION:-dev}
restart: unless-stopped restart: unless-stopped
depends_on: [backend] depends_on: [backend]
environment: environment:
+4 -1
View File
@@ -17,12 +17,15 @@ WORKDIR /ui
RUN corepack enable && corepack prepare pnpm@11.0.9 --activate RUN corepack enable && corepack prepare pnpm@11.0.9 --activate
# Prod UI build vars (Vite reads VITE_-prefixed env at build; baked into the bundle). # Prod UI build vars (Vite reads VITE_-prefixed env at build; baked into the bundle).
# VITE_APP_VERSION carries `git describe` for the About screen (defaults to "dev").
ARG VITE_TELEGRAM_BOT_ID= ARG VITE_TELEGRAM_BOT_ID=
ARG VITE_TELEGRAM_LINK= ARG VITE_TELEGRAM_LINK=
ARG VITE_GATEWAY_URL= ARG VITE_GATEWAY_URL=
ARG VITE_APP_VERSION=
ENV VITE_TELEGRAM_BOT_ID=$VITE_TELEGRAM_BOT_ID \ ENV VITE_TELEGRAM_BOT_ID=$VITE_TELEGRAM_BOT_ID \
VITE_TELEGRAM_LINK=$VITE_TELEGRAM_LINK \ VITE_TELEGRAM_LINK=$VITE_TELEGRAM_LINK \
VITE_GATEWAY_URL=$VITE_GATEWAY_URL VITE_GATEWAY_URL=$VITE_GATEWAY_URL \
VITE_APP_VERSION=$VITE_APP_VERSION
# Install with the lockfile first (the workspace file carries pnpm's build-script # Install with the lockfile first (the workspace file carries pnpm's build-script
# approval for esbuild), then build. Committed src/gen/ means no codegen here. # approval for esbuild), then build. Committed src/gen/ means no codegen here.
+62
View File
@@ -0,0 +1,62 @@
// Localised "About" / landing copy, shared by the About screen and the public landing
// page (Stage 17). Kept out of the flat i18n catalog because it is structured (a heading,
// a rules link, two bulleted sections) and only used in these two long-form places.
import type { Locale } from './i18n/index.svelte';
export interface AboutContent {
/** Prominent heading: "Scrabble" / "Эрудит (Скрэббл)". */
title: string;
rulesUrl: string;
/** Text before the rules link. */
rulesPrefix: string;
/** The rules link label. */
rulesLink: string;
randomTitle: string;
/** The "respect the opponent's time" note (rendered with a ❗️ prefix). */
randomRespect: string;
random: string[];
friendsTitle: string;
friends: string[];
}
/**
* aboutContent returns the localised About/landing copy. hours is the auto-match move clock
* (backend game.DefaultTurnTimeout), inlined into the random-game time-limit bullet.
*/
export function aboutContent(locale: Locale, hours: number): AboutContent {
if (locale === 'ru') {
return {
title: 'Эрудит (Скрэббл)',
rulesUrl: 'https://ru.wikipedia.org/wiki/Скрэббл',
rulesPrefix: 'Основные ',
rulesLink: 'правила игры',
randomTitle: 'Случайная игра',
randomRespect: 'Уважайте личное время соперника, будьте терпеливы.',
random: [
'В игре двое соперников.',
'Каждому доступна 1 подсказка в новой партии.',
`Лимит времени на ход: ${hours} ч. 00 минут.`,
'Время отсутствия задаётся в профиле и продлевает лимит.',
],
friendsTitle: 'Игра с друзьями',
friends: ['До 4-х участников.', 'Количество подсказок регулируется.', 'Произвольный лимит времени.'],
};
}
return {
title: 'Scrabble',
rulesUrl: 'https://en.wikipedia.org/wiki/Scrabble',
rulesPrefix: 'Basic ',
rulesLink: 'game rules',
randomTitle: 'Random game',
randomRespect: "Respect your opponent's time, be patient.",
random: [
'Two opponents per game.',
'Each player gets 1 hint per new game.',
`Move time limit: ${hours} h 00 min.`,
'An away window set in your profile extends the limit.',
],
friendsTitle: 'Game with friends',
friends: ['Up to 4 players.', 'The number of hints is configurable.', 'A custom time limit.'],
};
}
+59 -3
View File
@@ -1,14 +1,37 @@
<script lang="ts"> <script lang="ts">
import Screen from '../components/Screen.svelte'; import Screen from '../components/Screen.svelte';
import { t } from '../lib/i18n/index.svelte'; import { t } from '../lib/i18n/index.svelte';
import { app } from '../lib/app.svelte';
import { aboutContent } from '../lib/aboutContent';
const version = '0.7.0'; // The auto-match move clock (mirrors backend game.DefaultTurnTimeout = 24h).
const AUTO_MATCH_HOURS = 24;
const version = __APP_VERSION__;
const c = $derived(aboutContent(app.locale, AUTO_MATCH_HOURS));
</script> </script>
<Screen title={t('about.title')} back="/"> <Screen title={t('about.title')} back="/">
<div class="page"> <div class="page">
<h2>{t('app.title')}</h2> <h1>{c.title}</h1>
<p>{t('about.description')}</p> <p>
{c.rulesPrefix}<a href={c.rulesUrl} target="_blank" rel="noopener noreferrer">{c.rulesLink}</a>.
</p>
<section>
<h2>{c.randomTitle}</h2>
<p class="respect">❗️{c.randomRespect}</p>
<ul>
{#each c.random as item (item)}<li>{item}</li>{/each}
</ul>
</section>
<section>
<h2>{c.friendsTitle}</h2>
<ul>
{#each c.friends as item (item)}<li>{item}</li>{/each}
</ul>
</section>
<p class="muted">{t('about.version', { v: version })}</p> <p class="muted">{t('about.version', { v: version })}</p>
</div> </div>
</Screen> </Screen>
@@ -16,8 +39,41 @@
<style> <style>
.page { .page {
padding: var(--pad); padding: var(--pad);
display: flex;
flex-direction: column;
gap: 14px;
}
h1 {
margin: 0;
font-size: 1.5rem;
}
h2 {
margin: 0 0 6px;
font-size: 1.05rem;
color: var(--text-muted);
}
section {
display: flex;
flex-direction: column;
gap: 4px;
}
ul {
margin: 0;
padding-left: 20px;
display: flex;
flex-direction: column;
gap: 4px;
}
a {
color: var(--accent);
}
.respect {
margin: 0;
font-weight: 600;
} }
.muted { .muted {
color: var(--text-muted); color: var(--text-muted);
font-size: 0.9rem;
margin: 0;
} }
</style> </style>
+3
View File
@@ -9,3 +9,6 @@ interface ImportMetaEnv {
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv; readonly env: ImportMetaEnv;
} }
/** App version string, injected by Vite's define from `git describe` at build time. */
declare const __APP_VERSION__: string;
+6
View File
@@ -12,6 +12,12 @@ export default defineConfig(({ mode }) => ({
// Relative asset base so the one build serves under any path — the gateway maps the // Relative asset base so the one build serves under any path — the gateway maps the
// Telegram Mini App to /telegram/ (the hash router is path-agnostic). // Telegram Mini App to /telegram/ (the hash router is path-agnostic).
base: './', base: './',
define: {
// App version shown on the About screen, injected at build time from `git describe`
// via a Docker build-arg (Stage 17). Falls back to "dev" for a plain local/mock build,
// so a missing build-arg never breaks the build.
__APP_VERSION__: JSON.stringify(process.env.VITE_APP_VERSION || 'dev'),
},
plugins: [svelte()], plugins: [svelte()],
server: { server: {
port: 5173, port: 5173,