feat: explicit PWA update prompt with build stamp

- 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
This commit is contained in:
catarrh committed 2026-08-24 11:39:47 +03:00
1 parent a005e386a6
commit f49735b360
5 files changed
+50 -4

No files matched your search

+13
View File
@@ -12,6 +12,10 @@ export interface Translations {
error: string; error: string;
notAvailable: string; notAvailable: string;
// App update
updateAvailable: string;
updateReload: string;
// Navigation // Navigation
nav: { nav: {
calendar: string; calendar: string;
@@ -548,6 +552,9 @@ const translations: Record<LanguageCode, Translations> = {
error: 'Error', error: 'Error',
notAvailable: 'Not available', notAvailable: 'Not available',
updateAvailable: 'New version available',
updateReload: 'Update',
nav: { nav: {
calendar: '📅 Calendar', calendar: '📅 Calendar',
service: '📖 Service', service: '📖 Service',
@@ -1116,6 +1123,9 @@ const translations: Record<LanguageCode, Translations> = {
error: 'Ошибка', error: 'Ошибка',
notAvailable: 'Недоступно', notAvailable: 'Недоступно',
updateAvailable: 'Достꙋ́пна новаѧ вєрсі́ѧ',
updateReload: 'Обнови́ти',
nav: { nav: {
calendar: '📅 Календарь', calendar: '📅 Календарь',
service: '📖 Устав', service: '📖 Устав',
@@ -1684,6 +1694,9 @@ const translations: Record<LanguageCode, Translations> = {
error: 'Ошибка', error: 'Ошибка',
notAvailable: 'Недоступно', notAvailable: 'Недоступно',
updateAvailable: 'Доступна новая версия',
updateReload: 'Обновить',
nav: { nav: {
calendar: '📅 Календарь', calendar: '📅 Календарь',
service: '📖 Уставъ', service: '📖 Уставъ',
+27
View File
@@ -1,5 +1,32 @@
import './styles/main.css'; import './styles/main.css';
import { registerSW } from 'virtual:pwa-register';
import { App } from './ui/app'; 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 = `
<span class="text-sm text-navy whitespace-nowrap">${t.updateAvailable}</span>
<button id="pwa-update-btn" class="bg-gold text-navy rounded px-3 py-1 text-sm font-bold hover:bg-gold-dark transition-colors whitespace-nowrap">${t.updateReload}</button>
<button id="pwa-update-dismiss" class="text-navy-light hover:text-navy text-lg leading-none px-1" aria-label="✕">&times;</button>
`;
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')!); const app = new App(document.getElementById('app')!);
app.init(); app.init();
+1
View File
@@ -319,6 +319,7 @@ export class SettingsView {
${t.settings.aboutLicense} ${t.settings.aboutLicense}
</a> </a>
</p> </p>
<p class="text-[10px] text-navy-light mt-2 opacity-70">build ${__BUILD_DATE__}</p>
</div> </div>
</div> </div>
`; `;
+3
View File
@@ -1 +1,4 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
/// <reference types="vite-plugin-pwa/client" />
declare const __BUILD_DATE__: string;
+6 -4
View File
@@ -6,11 +6,10 @@ import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
tailwindcss(), tailwindcss(),
VitePWA({ VitePWA({ registerType: 'prompt',
registerType: 'autoUpdate',
workbox: { workbox: {
skipWaiting: true, skipWaiting: false,
clientsClaim: true, clientsClaim: false,
globPatterns: ['**/*.{js,css,html,woff,png}'], globPatterns: ['**/*.{js,css,html,woff,png}'],
globIgnores: ['**/data/**'], globIgnores: ['**/data/**'],
maximumFileSizeToCacheInBytes: 20 * 1024 * 1024, maximumFileSizeToCacheInBytes: 20 * 1024 * 1024,
@@ -66,5 +65,8 @@ export default defineConfig({
outDir: 'dist', outDir: 'dist',
sourcemap: true, sourcemap: true,
}, },
define: {
__BUILD_DATE__: JSON.stringify(new Date().toISOString().slice(0, 10)),
},
publicDir: 'static', publicDir: 'static',
}); });