From f49735b360e8a48ca6a0f2e011aab0c6c94ae796 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?=
=?UTF-8?q?=D0=B8=D0=BD?=
Date: Mon, 24 Aug 2026 11:39:47 +0300
Subject: [PATCH] feat: explicit PWA update prompt with build stamp
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- registerType 'prompt' + skipWaiting/clientsClaim false: new SW waits
instead of silently hijacking running sessions
- main.ts registers via virtual:pwa-register; onNeedRefresh shows
dismissible toast (i18n en/ru/cu) with Update button -> updateSW(true)
- No polling — update check only on navigation, zero extra traffic
- Settings About shows __BUILD_DATE__ build stamp for remote debugging
---
src/core/i18n.ts | 13 +++++++++++++
src/main.ts | 27 +++++++++++++++++++++++++++
src/ui/settings-view.ts | 1 +
src/vite-env.d.ts | 3 +++
vite.config.ts | 10 ++++++----
5 files changed, 50 insertions(+), 4 deletions(-)
diff --git a/src/core/i18n.ts b/src/core/i18n.ts
index ee94972..21339a8 100644
--- a/src/core/i18n.ts
+++ b/src/core/i18n.ts
@@ -12,6 +12,10 @@ export interface Translations {
error: string;
notAvailable: string;
+ // App update
+ updateAvailable: string;
+ updateReload: string;
+
// Navigation
nav: {
calendar: string;
@@ -548,6 +552,9 @@ const translations: Record = {
error: 'Error',
notAvailable: 'Not available',
+ updateAvailable: 'New version available',
+ updateReload: 'Update',
+
nav: {
calendar: '📅 Calendar',
service: '📖 Service',
@@ -1116,6 +1123,9 @@ const translations: Record = {
error: 'Ошибка',
notAvailable: 'Недоступно',
+ updateAvailable: 'Достꙋ́пна новаѧ вєрсі́ѧ',
+ updateReload: 'Обнови́ти',
+
nav: {
calendar: '📅 Календарь',
service: '📖 Устав',
@@ -1684,6 +1694,9 @@ const translations: Record = {
error: 'Ошибка',
notAvailable: 'Недоступно',
+ updateAvailable: 'Доступна новая версия',
+ updateReload: 'Обновить',
+
nav: {
calendar: '📅 Календарь',
service: '📖 Уставъ',
diff --git a/src/main.ts b/src/main.ts
index 0820842..1df3a6a 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,5 +1,32 @@
import './styles/main.css';
+import { registerSW } from 'virtual:pwa-register';
import { App } from './ui/app';
+import { getTranslations, type LanguageCode } from './core/i18n';
+import { loadSettings } from './ui/settings-view';
+
+const updateSW = registerSW({
+ immediate: true,
+ onNeedRefresh: () => showUpdateToast(),
+});
+
+function showUpdateToast() {
+ if (document.getElementById('pwa-update-toast')) return;
+ const t = getTranslations(loadSettings().language as LanguageCode);
+ const toast = document.createElement('div');
+ toast.id = 'pwa-update-toast';
+ toast.className = 'fixed bottom-4 left-1/2 -translate-x-1/2 z-[60] bg-surface border border-gold/40 rounded-lg shadow-xl px-4 py-3 flex items-center gap-3 max-w-[calc(100vw-2rem)]';
+ toast.innerHTML = `
+ ${t.updateAvailable}
+
+
+ `;
+ document.body.appendChild(toast);
+ toast.querySelector('#pwa-update-btn')?.addEventListener('click', () => {
+ (toast.querySelector('#pwa-update-btn') as HTMLButtonElement).disabled = true;
+ updateSW(true);
+ });
+ toast.querySelector('#pwa-update-dismiss')?.addEventListener('click', () => toast.remove());
+}
const app = new App(document.getElementById('app')!);
app.init();
diff --git a/src/ui/settings-view.ts b/src/ui/settings-view.ts
index 8f47687..61843b9 100644
--- a/src/ui/settings-view.ts
+++ b/src/ui/settings-view.ts
@@ -319,6 +319,7 @@ export class SettingsView {
${t.settings.aboutLicense}
+ build ${__BUILD_DATE__}
`;
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index 11f02fe..9860a3a 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -1 +1,4 @@
///
+///
+
+declare const __BUILD_DATE__: string;
diff --git a/vite.config.ts b/vite.config.ts
index 1a5afbc..6f593f6 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -6,11 +6,10 @@ import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
tailwindcss(),
- VitePWA({
- registerType: 'autoUpdate',
+ VitePWA({ registerType: 'prompt',
workbox: {
- skipWaiting: true,
- clientsClaim: true,
+ skipWaiting: false,
+ clientsClaim: false,
globPatterns: ['**/*.{js,css,html,woff,png}'],
globIgnores: ['**/data/**'],
maximumFileSizeToCacheInBytes: 20 * 1024 * 1024,
@@ -66,5 +65,8 @@ export default defineConfig({
outDir: 'dist',
sourcemap: true,
},
+ define: {
+ __BUILD_DATE__: JSON.stringify(new Date().toISOString().slice(0, 10)),
+ },
publicDir: 'static',
});
\ No newline at end of file