2026-04-06 09:24:22 +02:00
import Alpine from 'alpinejs' ;
import { logout , restoreSession , verifyConnection } from '../api/auth.js' ;
import { listKitchens } from '../api/kitchens.js' ;
2026-04-10 15:43:39 +02:00
import { APP _NAME , APP _VERSION } from './config.js' ;
2026-04-06 09:24:22 +02:00
import { createRouter , navigate } from './router.js' ;
import { createAppStore } from './store.js' ;
import { appShell } from '../components/app-shell.js' ;
2026-04-06 17:20:47 +02:00
import { navBar } from '../components/nav-bar.js' ;
2026-04-06 09:24:22 +02:00
import { registerFeatureData } from '../features/register.js' ;
2026-04-11 03:19:53 +02:00
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 ) ;
} ) ;
2026-04-06 09:24:22 +02:00
}
2026-04-07 19:52:42 +02:00
2026-04-11 03:19:53 +02:00
async function clearAllServiceWorkerCaches ( ) {
if ( ! ( 'caches' in window ) ) {
return ;
}
const keys = await caches . keys ( ) ;
await Promise . all ( keys . map ( ( key ) => caches . delete ( key ) ) ) ;
2026-04-07 19:52:42 +02:00
}
2026-04-11 03:19:53 +02:00
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 ,
} ;
2026-04-06 09:24:22 +02:00
}
export function bootstrapApp ( ) {
const store = createAppStore ( ) ;
Alpine . store ( 'app' , store ) ;
2026-04-11 03:19:53 +02:00
const appUpdateManager = createAppUpdateManager ( ) ;
2026-04-06 09:24:22 +02:00
registerFeatureData ( Alpine , store ) ;
const appRoot = document . querySelector ( '#app' ) ;
2026-04-10 15:43:39 +02:00
appRoot . innerHTML = appShell (
APP _NAME ,
APP _VERSION ,
import . meta . env . DEV ? 'development' : 'production' ,
) ;
2026-04-06 09:24:22 +02:00
Alpine . initTree ( appRoot ) ;
2026-04-06 17:20:47 +02:00
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 ) ;
}
2026-04-06 09:24:22 +02:00
const router = createRouter ( {
Alpine ,
store ,
outlet : document . querySelector ( '#route-view' ) ,
} ) ;
2026-04-06 18:31:31 +02:00
let authFailureHandled = false ;
2026-04-06 09:24:22 +02:00
2026-04-06 17:07:14 +02:00
function applyKitchens ( kitchens ) {
store . setKitchens ( kitchens ) ;
if ( ! store . activeKitchen && kitchens . length ) {
store . setActiveKitchen ( kitchens [ 0 ] ) ;
}
2026-04-06 17:20:47 +02:00
renderNav ( ) ;
2026-04-06 17:07:14 +02:00
return kitchens ;
}
2026-04-06 09:24:22 +02:00
window . _ _loncApp = {
navigate ,
async refreshKitchens ( ) {
2026-04-06 17:07:14 +02:00
return applyKitchens ( await listKitchens ( store ) ) ;
2026-04-06 09:24:22 +02:00
} ,
async restoreSession ( ) {
try {
2026-04-06 17:07:14 +02:00
const result = await restoreSession ( store ) ;
if ( result ? . kitchens ) {
applyKitchens ( result . kitchens ) ;
} else if ( store . isConnected ) {
2026-04-06 09:24:22 +02:00
await window . _ _loncApp . refreshKitchens ( ) ;
}
2026-04-06 18:31:31 +02:00
if ( store . isConnected ) {
authFailureHandled = false ;
}
2026-04-06 17:20:47 +02:00
renderNav ( ) ;
2026-04-06 09:24:22 +02:00
} catch ( error ) {
2026-04-06 17:20:47 +02:00
renderNav ( ) ;
2026-04-06 09:24:22 +02:00
if ( window . location . hash !== '#/login' ) {
navigate ( '/login' ) ;
}
}
} ,
async verifyConnection ( ) {
2026-04-06 17:07:14 +02:00
const result = await verifyConnection ( store ) ;
if ( result ? . kitchens ) {
applyKitchens ( result . kitchens ) ;
} else if ( store . isConnected ) {
2026-04-06 09:24:22 +02:00
await window . _ _loncApp . refreshKitchens ( ) ;
}
2026-04-06 18:31:31 +02:00
if ( store . isConnected ) {
authFailureHandled = false ;
}
2026-04-06 17:20:47 +02:00
renderNav ( ) ;
2026-04-06 17:07:14 +02:00
return result ;
2026-04-06 09:24:22 +02:00
} ,
2026-04-11 03:19:53 +02:00
async checkForAppUpdate ( ) {
return appUpdateManager . checkForAppUpdate ( ) ;
} ,
async applyAppUpdate ( ) {
return appUpdateManager . applyAppUpdate ( ) ;
} ,
2026-04-06 18:31:31 +02:00
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 ) ;
} ,
2026-04-06 09:24:22 +02:00
async logout ( ) {
await logout ( store ) ;
2026-04-06 18:31:31 +02:00
authFailureHandled = false ;
2026-04-06 17:20:47 +02:00
renderNav ( ) ;
2026-04-06 09:24:22 +02:00
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 ( ) ) ;
2026-04-06 17:20:47 +02:00
renderNav ( ) ;
2026-04-11 03:19:53 +02:00
appUpdateManager
. installServiceWorker ( )
. then ( ( ) => appUpdateManager . startPeriodicChecks ( ) )
. catch ( ( ) => {
store . addAlert ( {
type : 'warning' ,
message : 'PWA installation support could not be initialized.' ,
} ) ;
2026-04-06 09:24:22 +02:00
} ) ;
}