feat(pwa): installable web app + landing web entry
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m43s

Make the web SPA an installable PWA and surface it to users:

- manifest.webmanifest + an install-only service worker + 192/512/maskable
  icons (ui/public); PWA head tags in index.html; the .webmanifest MIME type
  registered in the gateway (the distroless image has no /etc/mime.types).
- Platform-adaptive install CTA (components/InstallApp.svelte + lib/pwa):
  one-tap on Chromium, manual Add-to-Home-Screen instructions on iOS Safari,
  hidden elsewhere / once installed / inside a Mini App. Shown under the
  logged-out login card and at the bottom of Settings.
- The landing gains a third entry linking /app/ (the brand tile), with a
  caption under all three (Telegram / VK / Веб-версия).

The service worker is navigation-only (network-first, cached-shell fallback);
hashed assets and the Connect stream are untouched. It exists to satisfy
Chromium's installability requirement and is the single growth point for a
future opt-in offline mode.

Tests: pwa.ts unit tests; a webui probe (manifest/sw.js content-type + the
SPA-fallback-to-HTML trap); e2e for the one-tap CTA, the iOS instructions
modal and the landing web entry. Docs: ARCHITECTURE §13, FUNCTIONAL (+ru),
gateway README.
This commit is contained in:
Ilia Denisov
2026-07-05 21:05:52 +02:00
parent d61ba68e9b
commit 51147a1429
26 changed files with 662 additions and 29 deletions
+12
View File
@@ -46,3 +46,15 @@ test('the landing is a normal scrolling document (the SPA document-pin does not
expect(state.shell).toBe(false);
expect(state.bodyPosition).not.toBe('fixed');
});
// The web-version entry (/app/) is always present, alongside the build-var-gated Telegram/VK ones
// (absent here, since e2e sets no VITE_* vars), each with a caption.
test('the landing shows a web-version entry linking /app/, with a caption', async ({ page }) => {
await page.goto('/landing.html');
await expect(page.getByText(/Играй в «Эрудита»/)).toBeVisible(); // Russian by default
const web = page.getByRole('link', { name: 'Играть в браузере' });
await expect(web).toBeVisible();
expect(await web.getAttribute('href')).toContain('/app/');
await expect(page.getByText('Веб-версия')).toBeVisible();
});