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
+31 -16
View File
@@ -80,20 +80,26 @@
<section class="hero">
<h1>{about.title}</h1>
<p class="tagline">{t('landing.tagline')}</p>
{#if tgLink || vkLink}
<div class="entries">
{#if tgLink}
<a class="entry" href={tgLink} target="_blank" rel="noopener noreferrer" aria-label={t('landing.playTelegram')}>
<img src="telegram-logo.svg" alt="" width="64" height="64" />
</a>
{/if}
{#if vkLink}
<a class="entry" href={vkLink} target="_blank" rel="noopener noreferrer" aria-label={t('landing.playVK')}>
<img src="vk-logo.svg" alt="" width="64" height="64" />
</a>
{/if}
</div>
{/if}
<!-- Platform entries: Telegram/VK are gated by their build vars; the web version (/app/) is
always available. A caption under each clarifies what it opens. -->
<div class="entries">
{#if tgLink}
<a class="entry" href={tgLink} target="_blank" rel="noopener noreferrer" aria-label={t('landing.playTelegram')}>
<img src="telegram-logo.svg" alt="" width="64" height="64" />
<span class="caption">{t('landing.captionTelegram')}</span>
</a>
{/if}
{#if vkLink}
<a class="entry" href={vkLink} target="_blank" rel="noopener noreferrer" aria-label={t('landing.playVK')}>
<img src="vk-logo.svg" alt="" width="64" height="64" />
<span class="caption">{t('landing.captionVK')}</span>
</a>
{/if}
<a class="entry" href="/app/" aria-label={t('landing.playWeb')}>
<img src="favicon.svg" alt="" width="64" height="64" />
<span class="caption">{t('landing.captionWeb')}</span>
</a>
</div>
</section>
<section class="info">
@@ -207,8 +213,8 @@
color: var(--text-muted);
font-size: 1.05rem;
}
/* The platform entries are just the bigger logos in a row (no button chrome, no captions);
each link keeps an aria-label for assistive tech. */
/* The platform entries are the bigger logos in a row, each with a short caption below; the
link keeps an aria-label for assistive tech. */
.entries {
align-self: center;
display: flex;
@@ -217,6 +223,11 @@
}
.entry {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 8px;
text-decoration: none;
color: var(--text);
}
.entry img {
display: block;
@@ -225,6 +236,10 @@
.entry:hover img {
transform: scale(1.06);
}
.caption {
font-size: 0.9rem;
color: var(--text-muted);
}
.info {
background: var(--surface-2);
border-radius: var(--radius-sm);