diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index fa5361f..99d0faf 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -1040,8 +1040,9 @@ a dedicated redeem sub-limit or a longer code is the hardening step if abuse app
Single public origin, path-routed. The Vite build has two entries: a lightweight
**landing page** and the game **SPA**. The gateway **embeds** the SPA build
(`go:embed`, baked in by a node stage in `gateway/Dockerfile`) and serves it at
-`/app/` (web) and `/telegram/` (the Telegram Mini App; outside Telegram that path
-redirects to the root — the client-side guard); a stray hit on the gateway's `/`
+`/app/` (web) and `/telegram/` (the Telegram Mini App; on that path without sign-in data
+— no `initData` — the client renders a compact, shareable launch-diagnostic screen instead
+of redirecting away); a stray hit on the gateway's `/`
308-redirects to `/app/`. The **landing** ships in its own static container: the
`landing` target of `gateway/Dockerfile` (caddy:2-alpine + the same Vite build,
`deploy/landing/Caddyfile`) serves it at `/`, so stray public traffic is absorbed by
diff --git a/docs/UI_DESIGN.md b/docs/UI_DESIGN.md
index 3c24066..298f2d2 100644
--- a/docs/UI_DESIGN.md
+++ b/docs/UI_DESIGN.md
@@ -8,7 +8,13 @@ emoji glyphs. Tokens are CSS custom properties (`ui/src/app.css`), light/dark vi
`prefers-color-scheme` or an explicit Settings choice, and **Telegram-themed**:
on a Telegram Mini App launch — the app is served under `/telegram/` and detects the
launch by `Telegram.WebApp.initData` — the SDK's `themeParams` override the tokens at
-runtime; opened outside Telegram, the `/telegram/` path redirects to the site root.
+runtime; on that path without sign-in data (no `initData` — outside Telegram, or a Mini App
+launch that delivered none, as seen on some Android clients) the app renders a compact,
+shareable launch-diagnostic screen (`screens/TelegramLaunchError.svelte`) rather than
+redirecting to the site root. `telegram-web-app.js` is loaded **dynamically with a timeout**,
+only on a Telegram entry — not a render-blocking `
+
{
void bootstrap();
@@ -84,6 +85,11 @@
{#if !routeIsLobby}
{t('common.loading')}
{/if}
+{:else if app.launchError}
+
+
{:else if app.bootError}
@@ -128,7 +134,7 @@
-{#if routeIsLobby && !app.splashDone && !app.blocked && !app.bootError}
+{#if routeIsLobby && !app.splashDone && !app.blocked && !app.bootError && !app.launchError}
{/if}
diff --git a/ui/src/lib/app.svelte.ts b/ui/src/lib/app.svelte.ts
index c96a82f..32a9663 100644
--- a/ui/src/lib/app.svelte.ts
+++ b/ui/src/lib/app.svelte.ts
@@ -12,7 +12,11 @@ import { languageNeedsServerSync } from './language';
import { applyReduceMotion, applyTelegramTheme, applyTheme, type ThemePref } from './theme';
import {
insideTelegram,
+ collectTelegramDiag,
+ type TelegramDiag,
onTelegramPath,
+ hasLaunchFragment,
+ loadTelegramSDK,
telegramColorScheme,
telegramContentSafeAreaTop,
telegramSafeAreaTop,
@@ -47,6 +51,11 @@ export const app = $state<{
* backend was down during a deploy). App.svelte then renders the boot-error retry screen
* instead of the web login — a Mini App has no manual sign-in to fall back to. */
bootError: boolean;
+ /** On the dedicated /telegram/ entry, set to a privacy-safe diagnostic snapshot when a Mini App
+ * launch carried no sign-in data (empty initData). App.svelte then renders the compact
+ * launch-error screen (screens/TelegramLaunchError) — a shareable probe for why Telegram
+ * delivered no initData (seen on some Android clients) — instead of bouncing to the landing. */
+ launchError: TelegramDiag | null;
/** Whether the lobby's first cold load has settled (success or error). The loading splash
* (components/Splash.svelte) watches it to know when to dismiss; set by screens/Lobby. */
lobbyReady: boolean;
@@ -96,6 +105,7 @@ export const app = $state<{
}>({
ready: false,
bootError: false,
+ launchError: null,
lobbyReady: false,
splashDone: false,
streamAlive: false,
@@ -530,6 +540,35 @@ function syncViewportHeight(): void {
if (h > 0) document.documentElement.style.setProperty('--vvh', `${h}px`);
}
+/**
+ * applyTelegramChrome applies a Mini App launch's visual integration: Telegram's authoritative
+ * colour scheme and theme, the matching header / background / bottom chrome, the safe-area insets,
+ * the swipe-down guard, and immersive fullscreen on mobile. It is idempotent, so both the initial
+ * bootstrap and a manual launch retry call it.
+ */
+function applyTelegramChrome(launch: TelegramLaunch): void {
+ if (launch.theme) applyTelegramTheme(launch.theme);
+ // Inside Telegram the colour scheme is Telegram's to decide; force it explicitly so the OS
+ // prefers-color-scheme (which leaks into the Telegram Desktop webview) cannot fight it. Falls
+ // back to the stored preference when the SDK omits it.
+ applyTheme(telegramColorScheme() ?? app.theme);
+ // Match Telegram's chrome to the app and stop its swipe-down-to-minimise from fighting tile
+ // drag / board scroll.
+ syncTelegramChrome();
+ syncTelegramSafeArea();
+ telegramDisableVerticalSwipes();
+ // On mobile, go immersive fullscreen like Telegram's own Mini Apps; the fullscreenChanged
+ // listener (registered at bootstrap) then re-syncs the safe-area insets. Desktop keeps the bot's
+ // full-size window. No-op on clients predating Bot API 8.0.
+ telegramRequestFullscreen();
+}
+
+/** How long to wait for the dynamically loaded Telegram Mini App SDK before giving up and showing
+ * the launch-error screen. A network that blocks telegram.org makes the script hang rather than
+ * fail fast (a connection refusal resolves immediately via the script's error event), so this only
+ * bounds a true hang; it is generous enough not to misfire on a slow but working network. */
+const TELEGRAM_SDK_TIMEOUT_MS = 10000;
+
export async function bootstrap(): Promise {
const prefs = await loadPrefs();
app.theme = prefs.theme ?? 'auto';
@@ -553,33 +592,30 @@ export async function bootstrap(): Promise {
window.visualViewport.addEventListener('scroll', syncViewportHeight);
}
- // Telegram Mini App launch: apply the platform theme, authenticate via initData,
- // and route any deep-link start parameter. On the dedicated /telegram/ entry path
- // outside Telegram (no initData), refuse to render and send the visitor to the
- // site root.
+ // Load the Telegram Mini App SDK dynamically, with a timeout, on a Telegram entry — it is no
+ // longer a render-blocking
+
+{#if diag}
+