1/** 2 * Kiosk health for an unattended sales-suite display (present_browser.html with `kiosk=1`). 3 * 4 * Two rings of self-healing sit on top of the display's own retries; this module is the policy for 5 * the inner one and the wire format for the outer one: 6 * 7 * - Inner ring (in the page): the shell tracks heartbeats from present_lite and reloads itself 8 * when the display never comes up or stops beating. 9 * - Outer ring (kiosk/Start-MiiraKiosk.ps1 on the PC): the watchdog reads the tab title through 10 * Edge's remote-debugging `/json` endpoint and relaunches the browser when the title says the 11 * page is stuck or gone. The title is the only channel a script outside the browser can read 12 * without driving it, so the shell publishes state there as `miira-kiosk:<state>:<ms>`. 13 * 14 * States the display reports: 15 * booting page alive, nothing on screen yet 16 * live holding crawl or pushed media is up 17 * rejected the suite key was refused â a reload cannot fix that, so neither ring acts on it 18 * 19 * Pure: the clock, the title sink, the reload and the online probe are injected, so 20 * suite-kiosk-health.test.js can pin the timings. Loaded as a classic script like the other 21 * suite-* modules, so it must not assume a module system. 22 */ 23(function (scope) { 24 var TITLE_PREFIX = 'miira-kiosk'; 25 var TITLE_PATTERN = /^miira-kiosk:(booting|live|rejected):(\d+)$/; 26 var STATES = { booting: true, live: true, rejected: true }; 27 var DEFAULTS = { 28 // Nothing on screen this long after the display was mounted: reload. 29 bootTimeoutMs: 3 * 60 * 1000, 30 // No heartbeat this long (they arrive every 15s): the display's script is wedged, reload. 31 staleMs: 2 * 60 * 1000, 32 // Never reload more often than this, so a page broken in a way a reload cannot fix does not 33 // spin; the watchdog on the PC is the outer ring for that case. 34 minReloadGapMs: 5 * 60 * 1000, 35 }; 36 37 function formatTitle(state, atMs) { 38 return TITLE_PREFIX + ':' + state + ':' + String(atMs); 39 } 40 41 /** `{ state, at }` for a heartbeat title, or null for anything else (an error page, the default). */ 42 function parseTitle(title) { 43 var m = TITLE_PATTERN.exec(String(title || '')); 44 if (!m) return null; 45 return { state: m[1], at: Number(m[2]) }; 46 } 47 48 /** 49 * @param {object} options 50 * @param {() => number} [options.now] clock, ms since epoch 51 * @param {(title: string) => void} [options.setTitle] 52 * @param {(reason: string, atMs: number) => void} [options.reload] 53 * @param {() => boolean} [options.isOnline] navigator.onLine; a reload with no network only 54 * trades a stuck page for an error page 55 * @param {number} [options.lastReloadAt] carried across the reload this module causes 56 */ 57 function createKioskHealth(options) { 58 var opts = options || {}; 59 var bootTimeoutMs = typeof opts.bootTimeoutMs === 'number' ? opts.bootTimeoutMs : DEFAULTS.bootTimeoutMs; 60 var staleMs = typeof opts.staleMs === 'number' ? opts.staleMs : DEFAULTS.staleMs; 61 var minReloadGapMs = typeof opts.minReloadGapMs === 'number' ? opts.minReloadGapMs : DEFAULTS.minReloadGapMs; 62 var now = typeof opts.now === 'function' ? opts.now : function () { return Date.now(); }; 63 var setTitle = typeof opts.setTitle === 'function' ? opts.setTitle : function () {}; 64 var reload = typeof opts.reload === 'function' ? opts.reload : function () {}; 65 var isOnline = typeof opts.isOnline === 'function' ? opts.isOnline : function () { return true; }; 66 67 var state = 'booting'; 68 var mountedAt = null; 69 var lastBeatAt = now(); 70 var lastReloadAt = Number(opts.lastReloadAt) || 0; 71 72 function publish() { 73 setTitle(formatTitle(state, lastBeatAt)); 74 } 75 76 // Publish before anything is mounted. Until this, a shell that was alive but had nothing on 77 // screen yet - retrying a suite the server will not hand over, say - left the tab title at the 78 // page's default, which the watchdog cannot tell apart from a page whose script never ran at
79 // all. It relaunched Edge every four minutes on a browser that was doing exactly what it 80 // should. An empty state in the watchdog log now means the script really did not run. 81 publish(); 82 83 function requestReload(reason) { 84 if (!isOnline()) return 'offline:' + reason; 85 var t = now(); 86 if (t - lastReloadAt < minReloadGapMs) return 'suppressed:' + reason; 87 lastReloadAt = t; 88 reload(reason, t); 89 return 'reload:' + reason; 90 } 91 92 return { 93 /** The display iframe was (re)mounted: the boot clock starts here. */ 94 mounted: function () { 95 mountedAt = now(); 96 lastBeatAt = mountedAt; 97 state = 'booting'; 98 publish(); 99 }, 100 /** A heartbeat from the display. Unknown states are ignored so a typo cannot stall the title. */ 101 beat: function (next) { 102 if (!STATES[next]) return false; 103 if (mountedAt === null) mountedAt = now(); 104 state = next; 105 lastBeatAt = now(); 106 publish(); 107 return true; 108 }, 109 /** 110 * Call on an interval. Returns what it decided â null when there is nothing to do, 111 * `reload:<reason>`, or `suppressed:<reason>` / `offline:<reason>` when a reload was due but 112 * withheld. 113 */ 114 tick: function () { 115 // Nothing mounted: the shell is alive and waiting, so keep the title fresh rather than let 116 // it go stale. This is the shell's own liveness - a wedged page stops calling tick() and 117 // the heartbeat ages out, which is the signal the watchdog wants. 118 if (mountedAt === null) { 119 lastBeatAt = now(); 120 publish(); 121 return null; 122 } 123 if (state === 'rejected') return null; 124 var t = now(); 125 if (state === 'booting' && t - mountedAt >= bootTimeoutMs) return requestReload('boot-timeout'); 126 if (t - lastBeatAt >= staleMs) return requestReload('stale'); 127 return null; 128 }, 129 snapshot: function () { 130 return { state: state, mountedAt: mountedAt, lastBeatAt: lastBeatAt, lastReloadAt: lastReloadAt }; 131 }, 132 }; 133 } 134 135 var api = { 136 TITLE_PREFIX: TITLE_PREFIX, 137 DEFAULTS: DEFAULTS, 138 formatTitle: formatTitle, 139 parseTitle: parseTitle, 140 createKioskHealth: createKioskHealth, 141 }; 142 143 if (typeof module !== 'undefined' && module.exports) module.exports = api; 144 if (scope) scope.MiiraSuiteKioskHealth = api; 145})(typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : null);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.