0a8464f63c
- Introduced version.json and appVersionAssetPlugin for build-time version tracking. - Enhanced settings page with update check/status UI. - Refactored bootstrap to handle SW updates and initiate periodic checks.
348 lines
9.2 KiB
JavaScript
348 lines
9.2 KiB
JavaScript
import Alpine from 'alpinejs';
|
|
|
|
import { logout, restoreSession, verifyConnection } from '../api/auth.js';
|
|
import { listKitchens } from '../api/kitchens.js';
|
|
import { APP_NAME, APP_VERSION } from './config.js';
|
|
import { createRouter, navigate } from './router.js';
|
|
import { createAppStore } from './store.js';
|
|
import { appShell } from '../components/app-shell.js';
|
|
import { navBar } from '../components/nav-bar.js';
|
|
import { registerFeatureData } from '../features/register.js';
|
|
|
|
const APP_UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1000;
|
|
|
|
function createAppUpdateManager() {
|
|
let registration = null;
|
|
let waitingWorker = null;
|
|
|
|
async function fetchServerVersion() {
|
|
try {
|
|
const response = await fetch(`/version.json?ts=${Date.now()}`, {
|
|
cache: 'no-store',
|
|
headers: {
|
|
'cache-control': 'no-cache',
|
|
pragma: 'no-cache',
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Version endpoint failed with HTTP ${response.status}`);
|
|
}
|
|
|
|
const payload = await response.json();
|
|
const serverVersion = String(payload?.version || '').trim();
|
|
const serverBuildTime = String(payload?.buildTime || '').trim();
|
|
|
|
return {
|
|
serverVersion: serverVersion || null,
|
|
serverBuildTime: serverBuildTime || null,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
serverVersion: null,
|
|
serverBuildTime: null,
|
|
error: error instanceof Error ? error.message : 'Unable to reach version endpoint.',
|
|
};
|
|
}
|
|
}
|
|
|
|
function syncWaitingWorker() {
|
|
waitingWorker = registration?.waiting || null;
|
|
}
|
|
|
|
function setupRegistrationHooks() {
|
|
if (!registration) {
|
|
return;
|
|
}
|
|
|
|
syncWaitingWorker();
|
|
|
|
registration.addEventListener('updatefound', () => {
|
|
const installing = registration.installing;
|
|
if (!installing) {
|
|
return;
|
|
}
|
|
|
|
installing.addEventListener('statechange', () => {
|
|
if (installing.state === 'installed' && navigator.serviceWorker.controller) {
|
|
waitingWorker = registration.waiting || installing;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function installServiceWorker() {
|
|
if (!('serviceWorker' in navigator)) {
|
|
return { supported: false };
|
|
}
|
|
|
|
if (import.meta.env.DEV) {
|
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
|
await Promise.all(registrations.map((existingRegistration) => existingRegistration.unregister()));
|
|
return { supported: false, development: true };
|
|
}
|
|
|
|
registration = await navigator.serviceWorker.register('/service-worker.js');
|
|
setupRegistrationHooks();
|
|
|
|
return {
|
|
supported: true,
|
|
registered: true,
|
|
};
|
|
}
|
|
|
|
async function checkForAppUpdate() {
|
|
if (registration) {
|
|
await registration.update().catch(() => {});
|
|
syncWaitingWorker();
|
|
}
|
|
|
|
const server = await fetchServerVersion();
|
|
const hasVersionMismatch = Boolean(server.serverVersion && server.serverVersion !== APP_VERSION);
|
|
|
|
return {
|
|
supported: 'serviceWorker' in navigator,
|
|
currentVersion: APP_VERSION,
|
|
serverVersion: server.serverVersion,
|
|
serverBuildTime: server.serverBuildTime,
|
|
waitingWorker: Boolean(waitingWorker),
|
|
updateAvailable: Boolean(waitingWorker) || hasVersionMismatch,
|
|
hasVersionMismatch,
|
|
serverError: server.error || null,
|
|
};
|
|
}
|
|
|
|
async function waitForControllerChange(previousController, timeoutMs = 4000) {
|
|
if (!('serviceWorker' in navigator)) {
|
|
return;
|
|
}
|
|
|
|
if (navigator.serviceWorker.controller && navigator.serviceWorker.controller !== previousController) {
|
|
return;
|
|
}
|
|
|
|
await new Promise((resolve) => {
|
|
let isDone = false;
|
|
|
|
const finish = () => {
|
|
if (isDone) {
|
|
return;
|
|
}
|
|
|
|
isDone = true;
|
|
navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange);
|
|
clearTimeout(timeout);
|
|
resolve();
|
|
};
|
|
|
|
const onControllerChange = () => {
|
|
finish();
|
|
};
|
|
|
|
const timeout = setTimeout(finish, timeoutMs);
|
|
|
|
navigator.serviceWorker.addEventListener('controllerchange', onControllerChange);
|
|
});
|
|
}
|
|
|
|
async function clearAllServiceWorkerCaches() {
|
|
if (!('caches' in window)) {
|
|
return;
|
|
}
|
|
|
|
const keys = await caches.keys();
|
|
await Promise.all(keys.map((key) => caches.delete(key)));
|
|
}
|
|
|
|
async function applyAppUpdate() {
|
|
const previousController = navigator.serviceWorker?.controller || null;
|
|
|
|
if (registration) {
|
|
await registration.update().catch(() => {});
|
|
syncWaitingWorker();
|
|
}
|
|
|
|
if (waitingWorker) {
|
|
waitingWorker.postMessage({ type: 'SKIP_WAITING' });
|
|
await waitForControllerChange(previousController);
|
|
}
|
|
|
|
await clearAllServiceWorkerCaches().catch(() => {});
|
|
|
|
const nextUrl = new URL(window.location.href);
|
|
nextUrl.searchParams.set('app_update', String(Date.now()));
|
|
window.location.replace(nextUrl.toString());
|
|
}
|
|
|
|
function startPeriodicChecks() {
|
|
if (!registration) {
|
|
return;
|
|
}
|
|
|
|
window.setInterval(() => {
|
|
checkForAppUpdate().catch(() => {});
|
|
}, APP_UPDATE_CHECK_INTERVAL_MS);
|
|
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.visibilityState === 'visible') {
|
|
checkForAppUpdate().catch(() => {});
|
|
}
|
|
});
|
|
}
|
|
|
|
return {
|
|
installServiceWorker,
|
|
checkForAppUpdate,
|
|
applyAppUpdate,
|
|
startPeriodicChecks,
|
|
};
|
|
}
|
|
|
|
export function bootstrapApp() {
|
|
const store = createAppStore();
|
|
Alpine.store('app', store);
|
|
|
|
const appUpdateManager = createAppUpdateManager();
|
|
|
|
registerFeatureData(Alpine, store);
|
|
|
|
const appRoot = document.querySelector('#app');
|
|
appRoot.innerHTML = appShell(
|
|
APP_NAME,
|
|
APP_VERSION,
|
|
import.meta.env.DEV ? 'development' : 'production',
|
|
);
|
|
Alpine.initTree(appRoot);
|
|
const navRoot = document.querySelector('#app-nav');
|
|
|
|
function renderNav() {
|
|
if (!navRoot) {
|
|
return;
|
|
}
|
|
|
|
if (typeof Alpine.destroyTree === 'function') {
|
|
Alpine.destroyTree(navRoot);
|
|
}
|
|
|
|
navRoot.innerHTML = navBar(APP_NAME);
|
|
Alpine.initTree(navRoot);
|
|
}
|
|
|
|
const router = createRouter({
|
|
Alpine,
|
|
store,
|
|
outlet: document.querySelector('#route-view'),
|
|
});
|
|
let authFailureHandled = false;
|
|
|
|
function applyKitchens(kitchens) {
|
|
store.setKitchens(kitchens);
|
|
if (!store.activeKitchen && kitchens.length) {
|
|
store.setActiveKitchen(kitchens[0]);
|
|
}
|
|
renderNav();
|
|
return kitchens;
|
|
}
|
|
|
|
window.__loncApp = {
|
|
navigate,
|
|
async refreshKitchens() {
|
|
return applyKitchens(await listKitchens(store));
|
|
},
|
|
async restoreSession() {
|
|
try {
|
|
const result = await restoreSession(store);
|
|
if (result?.kitchens) {
|
|
applyKitchens(result.kitchens);
|
|
} else if (store.isConnected) {
|
|
await window.__loncApp.refreshKitchens();
|
|
}
|
|
if (store.isConnected) {
|
|
authFailureHandled = false;
|
|
}
|
|
renderNav();
|
|
} catch (error) {
|
|
renderNav();
|
|
if (window.location.hash !== '#/login') {
|
|
navigate('/login');
|
|
}
|
|
}
|
|
},
|
|
async verifyConnection() {
|
|
const result = await verifyConnection(store);
|
|
if (result?.kitchens) {
|
|
applyKitchens(result.kitchens);
|
|
} else if (store.isConnected) {
|
|
await window.__loncApp.refreshKitchens();
|
|
}
|
|
if (store.isConnected) {
|
|
authFailureHandled = false;
|
|
}
|
|
renderNav();
|
|
return result;
|
|
},
|
|
async checkForAppUpdate() {
|
|
return appUpdateManager.checkForAppUpdate();
|
|
},
|
|
async applyAppUpdate() {
|
|
return appUpdateManager.applyAppUpdate();
|
|
},
|
|
handleAuthFailure(error) {
|
|
if (!store.session?.applicationKey || !store.session?.hasValidated || authFailureHandled) {
|
|
return;
|
|
}
|
|
|
|
authFailureHandled = true;
|
|
store.markSessionInvalid();
|
|
renderNav();
|
|
const status = error?.status || error?.cause?.status;
|
|
const message =
|
|
status === 401 || status === 403
|
|
? 'This application key is no longer accepted by Tryton. Please verify it again or disconnect and create a new key.'
|
|
: 'Authenticated requests are no longer succeeding. The application key may have been cancelled, or access is being denied by the server. Please reconnect or create a new key.';
|
|
store.addAlert({
|
|
type: 'warning',
|
|
timeout: 0,
|
|
message,
|
|
});
|
|
navigate('/login');
|
|
window.setTimeout(() => router.render(), 0);
|
|
},
|
|
async logout() {
|
|
await logout(store);
|
|
authFailureHandled = false;
|
|
renderNav();
|
|
navigate('/login');
|
|
},
|
|
router,
|
|
};
|
|
|
|
window.addEventListener('online', () => {
|
|
store.addAlert({ type: 'success', message: 'Connection restored.' });
|
|
});
|
|
|
|
window.addEventListener('offline', () => {
|
|
store.addAlert({
|
|
type: 'warning',
|
|
message: 'You are offline. Cached screens stay available, but API actions may fail.',
|
|
});
|
|
});
|
|
|
|
window.__loncApp
|
|
.restoreSession()
|
|
.finally(() => router.start())
|
|
.catch(() => router.start());
|
|
|
|
renderNav();
|
|
|
|
appUpdateManager
|
|
.installServiceWorker()
|
|
.then(() => appUpdateManager.startPeriodicChecks())
|
|
.catch(() => {
|
|
store.addAlert({
|
|
type: 'warning',
|
|
message: 'PWA installation support could not be initialized.',
|
|
});
|
|
});
|
|
}
|