@xiboplayer/pwa 0.7.21 → 0.7.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/assets/__vite-browser-external-DeMPM02e.js +2 -0
  2. package/dist/assets/__vite-browser-external-DeMPM02e.js.map +1 -0
  3. package/dist/assets/chunk-DzMEjpoC.js +1 -0
  4. package/dist/assets/{html2canvas-BAfZNSwU.js → html2canvas-EikzC5d8.js} +2 -2
  5. package/dist/assets/{html2canvas-BAfZNSwU.js.map → html2canvas-EikzC5d8.js.map} +1 -1
  6. package/dist/assets/main-CRdq5ifQ.js +3 -0
  7. package/dist/assets/{main-vwJkNw4Y.js.map → main-CRdq5ifQ.js.map} +1 -1
  8. package/dist/assets/main-DTR2QDcF.js +108 -0
  9. package/dist/assets/main-DTR2QDcF.js.map +1 -0
  10. package/dist/assets/{pdf-Bxz9Nzto.js → pdf-CMz6puSt.js} +1 -1
  11. package/dist/assets/{pdf-Bxz9Nzto.js.map → pdf-CMz6puSt.js.map} +1 -1
  12. package/dist/assets/{setup-B4gZX38p.js → setup-Bw8T9Qq6.js} +2 -2
  13. package/dist/assets/{setup-B4gZX38p.js.map → setup-Bw8T9Qq6.js.map} +1 -1
  14. package/dist/assets/src-A5KHvitf.js +2 -0
  15. package/dist/assets/{src-CROvYSP8.js.map → src-A5KHvitf.js.map} +1 -1
  16. package/dist/assets/{src-DAB0dqGG.js → src-BHsN2u2P.js} +2 -2
  17. package/dist/assets/{src-DAB0dqGG.js.map → src-BHsN2u2P.js.map} +1 -1
  18. package/dist/assets/src-BLUMUwZR.js +1 -0
  19. package/dist/assets/src-BXXcWcHh.js +1 -0
  20. package/dist/assets/src-BxSOopk7.js +1 -0
  21. package/dist/assets/{src-WDu491CE.js → src-BxaX1gGg.js} +2 -2
  22. package/dist/assets/{src-WDu491CE.js.map → src-BxaX1gGg.js.map} +1 -1
  23. package/dist/assets/{src-BtVLiVYZ.js → src-CCAyzQUp.js} +3 -3
  24. package/dist/assets/{src-BtVLiVYZ.js.map → src-CCAyzQUp.js.map} +1 -1
  25. package/dist/assets/src-CWJcD3kA.js +1 -0
  26. package/dist/assets/{src-Cx3tXAAu.js → src-CZ1k5h23.js} +3 -3
  27. package/dist/assets/{src-Cx3tXAAu.js.map → src-CZ1k5h23.js.map} +1 -1
  28. package/dist/assets/src-ClrziKzV.js +16 -0
  29. package/dist/assets/src-ClrziKzV.js.map +1 -0
  30. package/dist/assets/{src-C_Lx4lXp.js → src-CtjjclS4.js} +2 -2
  31. package/dist/assets/{src-C_Lx4lXp.js.map → src-CtjjclS4.js.map} +1 -1
  32. package/dist/assets/src-CuVaZcMo.js +2 -0
  33. package/dist/assets/{src-B_BNICay.js.map → src-CuVaZcMo.js.map} +1 -1
  34. package/dist/assets/src-Cy5OUviT.js +1 -0
  35. package/dist/assets/src-DK5BYonP.js +630 -0
  36. package/dist/assets/src-DK5BYonP.js.map +1 -0
  37. package/dist/assets/src-Dk-W3N33.js +1 -0
  38. package/dist/assets/{src-cUopH0nN.js → src-xPTO7Ts6.js} +3 -3
  39. package/dist/assets/{src-cUopH0nN.js.map → src-xPTO7Ts6.js.map} +1 -1
  40. package/dist/assets/sync-manager-zf1tikPt.js +2 -0
  41. package/dist/assets/sync-manager-zf1tikPt.js.map +1 -0
  42. package/dist/index.html +1 -1
  43. package/dist/setup.html +3 -4
  44. package/dist/sw-pwa.js +2 -2
  45. package/dist/sw-pwa.js.map +1 -1
  46. package/package.json +15 -13
  47. package/dist/assets/chunk-7ZXdHUL4.js +0 -1
  48. package/dist/assets/main-oacre7st.js +0 -108
  49. package/dist/assets/main-oacre7st.js.map +0 -1
  50. package/dist/assets/main-vwJkNw4Y.js +0 -3
  51. package/dist/assets/src-B_BNICay.js +0 -2
  52. package/dist/assets/src-Bjt9ooXK.js +0 -16
  53. package/dist/assets/src-Bjt9ooXK.js.map +0 -1
  54. package/dist/assets/src-CKpVxGpH.js +0 -629
  55. package/dist/assets/src-CKpVxGpH.js.map +0 -1
  56. package/dist/assets/src-CROvYSP8.js +0 -2
  57. package/dist/assets/sync-manager-8Z-qwkod.js +0 -2
  58. package/dist/assets/sync-manager-8Z-qwkod.js.map +0 -1
@@ -0,0 +1 @@
1
+ import{a as e,c as t,i as n,l as r,n as i,o as a,r as o,s,t as c}from"./src-A5KHvitf.js";export{o as InterruptScheduler,i as OverlayScheduler,n as ScheduleManager,c as VERSION,a as buildScheduleQueue,s as calculateTimeline,t as parseLayoutDuration,r as parseLayoutFile,e as scheduleManager};
@@ -1,3 +1,3 @@
1
- import{c as e,l as t,o as n,r,u as i}from"./src-BtVLiVYZ.js";import{r as a}from"./src-B_BNICay.js";import{i as o,n as s}from"./src-CROvYSP8.js";var c={name:`@xiboplayer/core`,version:`0.7.21`,description:`xiboplayer core orchestration and lifecycle management`,type:`module`,main:`./src/index.js`,types:`./src/index.d.ts`,exports:{".":`./src/index.js`,"./player-core":`./src/player-core.js`},scripts:{dev:`vite`,build:`vite build`,preview:`vite preview`,test:`vitest run`,"test:watch":`vitest`,"test:ui":`vitest --ui`,"test:coverage":`vitest run --coverage`},dependencies:{"@xiboplayer/utils":`workspace:*`},peerDependencies:{"@xiboplayer/cache":`workspace:*`,"@xiboplayer/renderer":`workspace:*`,"@xiboplayer/schedule":`workspace:*`,"@xiboplayer/xmds":`workspace:*`},devDependencies:{"@vitest/coverage-v8":`^4.1.3`,"@vitest/ui":`^4.1.4`,jsdom:`^29.0.2`,vite:`^8.0.8`,vitest:`^4.1.2`},keywords:[`xibo`,`digital-signage`,`player`,`core`,`orchestration`],author:`Pau Aliagas <linuxnow@gmail.com>`,license:`AGPL-3.0-or-later`,repository:{type:`git`,url:`git+https://github.com/xibo-players/xiboplayer.git`,directory:`packages/core`},homepage:`https://xiboplayer.org`},l=i(`DataConnector`),u=3e5,d=3,f=class extends e{constructor(){super(),this.connectors=new Map}setConnectors(e){if(this.stopPolling(),this.connectors.clear(),!e||e.length===0){l.debug(`No data connectors configured`);return}for(let t of e){if(!t.dataKey||!t.url){l.warn(`Skipping data connector with missing dataKey or url:`,t);continue}this.connectors.set(t.dataKey,{config:t,data:null,timer:null,lastFetch:null,failures:0}),l.info(`Registered data connector: ${t.dataKey} (interval: ${t.updateInterval}s)`)}l.info(`${this.connectors.size} data connector(s) configured`)}startPolling(){for(let[e,t]of this.connectors.entries()){let{config:n}=t,r=(n.updateInterval||300)*1e3;this.fetchData(t).catch(t=>{l.error(`Initial fetch failed for ${e}:`,t)}),t.timer=setInterval(()=>{this.fetchData(t).catch(t=>{l.error(`Polling fetch failed for ${e}:`,t)})},r),l.debug(`Started polling for ${e} every ${n.updateInterval}s`)}}stopPolling(){for(let[e,t]of this.connectors.entries())t.timer&&(clearInterval(t.timer),t.timer=null,l.debug(`Stopped polling for ${e}`))}getData(e){let t=this.connectors.get(e);return t?t.data:(l.debug(`No data connector found for key: ${e}`),null)}getAvailableKeys(){let e=[];for(let[t,n]of this.connectors.entries())n.data!==null&&e.push(t);return e}async fetchData(e){let{config:t}=e,{dataKey:n,url:i}=t;l.debug(`Fetching data for ${n}: ${i}`);try{let t=await r(i,{method:`GET`,headers:{Accept:`application/json`}},{maxRetries:2,baseDelayMs:2e3});if(!t.ok){l.warn(`Data connector ${n} returned ${t.status}: ${t.statusText}`);return}let a=t.headers.get(`Content-Type`)||``,o;o=a.includes(`application/json`)?await t.json():await t.text();let s=e.data;e.data=o,e.lastFetch=Date.now(),e.failures=0,l.debug(`Data updated for ${n} (fetched at ${new Date(e.lastFetch).toISOString()})`),this._ensureNormalPolling(e),this.emit(`data-updated`,n,o),JSON.stringify(s)!==JSON.stringify(o)&&this.emit(`data-changed`,n,o)}catch(r){if(e.failures=(e.failures||0)+1,l.error(`Failed to fetch data for ${n} (${e.failures}x):`,r),this.emit(`fetch-error`,n,r),e.failures>=d&&e.timer){let r=(t.updateInterval||300)*1e3,i=Math.min(r*2**(e.failures-d+1),u);clearInterval(e.timer),e.timer=setTimeout(()=>{this.fetchData(e).catch(()=>{}),e.timer=setInterval(()=>{this.fetchData(e).catch(()=>{})},i)},i),l.warn(`Circuit breaker: ${n} backing off to ${Math.round(i/1e3)}s`)}}}_ensureNormalPolling(e){if(e.failures===0&&e.timer){let t=(e.config.updateInterval||300)*1e3;clearInterval(e.timer),clearTimeout(e.timer),e.timer=setInterval(()=>{this.fetchData(e).catch(()=>{})},t)}}refreshAll(){this.connectors.size!==0&&(l.info(`Refreshing all ${this.connectors.size} data connector(s)`),this.stopPolling(),this.startPolling())}cleanup(){this.stopPolling(),this.connectors.clear(),this.removeAllListeners(),l.debug(`DataConnectorManager cleaned up`)}},p=i(`Blacklist`),m=class{constructor(e=3){this._entries=new Map,this._threshold=e}recordFailure(e,t){let n=Number(e),r=this._entries.get(n)||{failures:0,blacklisted:!1,reason:``};return r.failures++,r.reason=t,!r.blacklisted&&r.failures>=this._threshold?(r.blacklisted=!0,p.warn(`Layout ${n} blacklisted after ${r.failures} consecutive failures: ${t}`)):r.blacklisted||p.info(`Layout ${n} failure ${r.failures}/${this._threshold}: ${t}`),this._entries.set(n,r),{blacklisted:r.blacklisted,failures:r.failures}}recordSuccess(e){let t=Number(e);if(!this._entries.has(t))return!1;let n=this._entries.get(t);return this._entries.delete(t),n.blacklisted?(p.info(`Layout ${t} removed from blacklist (rendered successfully)`),!0):!1}isBlacklisted(e){return this._entries.get(Number(e))?.blacklisted===!0}getBlacklistedIds(){let e=[];for(let[t,n]of this._entries)n.blacklisted&&e.push(t);return e}reset(){let e=this._entries.size;return e>0&&(p.info(`Blacklist reset (${e} entries cleared)`),this._entries.clear()),e}get size(){return this._entries.size}},h=Object.freeze({COLLECTION_START:`collection-start`,COLLECTION_COMPLETE:`collection-complete`,COLLECTION_ERROR:`collection-error`,REGISTER_COMPLETE:`register-complete`,SCHEDULE_RECEIVED:`schedule-received`,LAYOUTS_SCHEDULED:`layouts-scheduled`,NO_LAYOUTS_SCHEDULED:`no-layouts-scheduled`,TIMELINE_UPDATED:`timeline-updated`,LAYOUT_PREPARE_REQUEST:`layout-prepare-request`,LAYOUT_EXPIRE_CURRENT:`layout-expire-current`,LAYOUT_ALREADY_PLAYING:`layout-already-playing`,CHECK_PENDING_LAYOUT:`check-pending-layout`,FILES_RECEIVED:`files-received`,DOWNLOAD_REQUEST:`download-request`,OVERLAY_LAYOUT_REQUEST:`overlay-layout-request`,REVERT_TO_SCHEDULE:`revert-to-schedule`,SYNC_CONFIG:`sync-config`,XMR_CONNECTED:`xmr-connected`,XMR_RECONNECTED:`xmr-reconnected`,XMR_MISCONFIGURED:`xmr-misconfigured`,NAVIGATE_TO_WIDGET:`navigate-to-widget`,EXECUTE_NATIVE_COMMAND:`execute-native-command`,SCHEDULED_COMMAND:`scheduled-command`,COMMAND_RESULT:`command-result`,SCREENSHOT_REQUEST:`screenshot-request`,SUBMIT_STATS_REQUEST:`submit-stats-request`,SUBMIT_LOGS_REQUEST:`submit-logs-request`,SUBMIT_FAULTS_REQUEST:`submit-faults-request`,CACHE_ANALYSIS:`cache-analysis`,COLLECTION_INTERVAL_SET:`collection-interval-set`,COLLECTION_INTERVAL_UPDATED:`collection-interval-updated`,LOG_LEVEL_CHANGED:`log-level-changed`,OFFLINE_MODE:`offline-mode`,PURGE_REQUEST:`purge-request`,PURGE_ALL_REQUEST:`purge-all-request`}),g=i(`PlayerCore`);async function _(){if(typeof window<`u`&&window.electronAPI?.getLanIpAddress)try{return await window.electronAPI.getLanIpAddress()}catch{}try{let e=await(globalThis.__nativeFetch||globalThis.fetch)(`/system/lan-ip`);if(e.ok){let{ip:t}=await e.json();if(t)return t}}catch{}return``}var v=`xibo-offline-cache`,y=1,b=`cache`;function x(e){return n(e?`${v}-${e}`:v,y,b)}var S=class extends e{constructor(e){super(),this.config=e.config,this.xmds=e.xmds,this.cache=e.cache,this.schedule=e.schedule,this.renderer=e.renderer,this.XmrWrapper=e.xmrWrapper,this.statsCollector=e.statsCollector,this.displaySettings=e.displaySettings,this._cmsId=e.cmsId||null,this.dataConnectorManager=new f,_().then(e=>{this._lanIpAddress=e,g.info(`LAN IP:`,e||`(not discovered)`)}),this.xmr=null,this.currentLayoutId=null,this.collecting=!1,this.collectionInterval=null,this.pendingLayouts=new Map,this._layoutMediaStatus=new Map,this.offlineMode=!1,this._normalCollectInterval=null,this._offlineRetrySeconds=0,this._lastCheckRf=null,this._lastCheckSchedule=null,this._lastTimelineFingerprint=null,this._lastTimeline=null,this._layoutOverride=null,this._lastRequiredFiles=[],this._executedCommands=new Set,this.displayCommands=null,this._faultReportingInterval=null,this._faultReportingSeconds=60,this._layoutBlacklist=new m(3),this._lastLayoutChangeTime=null,this._statusCode=2,this._dynamicLayouts=new Set,this.syncConfig=null,this.syncManager=null,this._layoutDurations=new Map,this._finalDurations=new Set,this._preparingLayoutId=null,this.cacheAnalyzer=this.cache?new a(this.cache):null,this._offlineCache={schedule:null,settings:null,requiredFiles:null},this._offlineDbReady=this._initOfflineCache()}get _queueOptions(){return{dynamicLayouts:this._dynamicLayouts}}_scheduleAutoRevert(e,t,n){t>0&&setTimeout(()=>{this._layoutOverride?.layoutId===e&&(g.info(`${n} duration expired (${t}s), reverting to schedule`),this.revertToSchedule())},t*1e3)}async _initOfflineCache(){try{let e=await x(this._cmsId),t=e.transaction(b,`readonly`).objectStore(b),[n,r,i,a,o,s]=await Promise.all([new Promise(e=>{let n=t.get(`schedule`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`settings`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`requiredFiles`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`durations`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`finalDurations`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`durationsVersion`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)})]);if(Array.isArray(a)&&a.length>0){for(let[e,t]of a)this._layoutDurations.set(e,t);g.info(`[Timeline] Restored ${a.length} cached durations from IDB`)}if(s>=2&&Array.isArray(o)&&o.length>0){for(let e of o)this._finalDurations.add(e);g.info(`[Timeline] Restored ${o.length} final duration keys from IDB`)}else Array.isArray(o)&&o.length>0&&g.info(`[Timeline] Discarded ${o.length} stale final duration keys (pre-v2)`);this._offlineCache={schedule:n,settings:r,requiredFiles:i},this._offlineDb=e,g.info(`Offline cache loaded from IndexedDB`,n?`(has schedule)`:`(empty)`)}catch(e){g.warn(`Failed to load offline cache from IndexedDB:`,e)}}async _offlineSave(e,t){this._offlineCache[e]=t;try{this._offlineDb||=await x(this._cmsId);let n=this._offlineDb.transaction(b,`readwrite`);n.objectStore(b).put(t,e),await new Promise((e,t)=>{n.oncomplete=e,n.onerror=()=>t(n.error)})}catch(t){this._offlineDb=null,g.warn(`Failed to save offline cache:`,e,t)}}hasCachedData(){return this._offlineCache.schedule!==null}isOffline(){return typeof navigator<`u`&&navigator.onLine===!1}isInOfflineMode(){return this.offlineMode}collectOffline(){if(g.warn(`Offline mode — using cached schedule`),this.offlineMode||(this.offlineMode=!0,this.emit(h.OFFLINE_MODE,!0)),this.collectionInterval&&(this._normalCollectInterval?this._offlineRetrySeconds=Math.min(this._offlineRetrySeconds*2,this._normalCollectInterval):(this._normalCollectInterval=this._currentCollectInterval,this._offlineRetrySeconds=30),this._setCollectionTimer(this._offlineRetrySeconds),g.info(`Offline: retry in ${this._offlineRetrySeconds}s`)),!this.collectionInterval){let e=this._offlineCache.settings;e?.settings&&(this.setupCollectionInterval(e.settings),this._normalCollectInterval=this._currentCollectInterval,this._offlineRetrySeconds=30,this._setCollectionTimer(this._offlineRetrySeconds),g.info(`Offline: retry in ${this._offlineRetrySeconds}s`))}let e=this._offlineCache.schedule;e&&(this.schedule.setSchedule(e),this.emit(h.SCHEDULE_RECEIVED,e));let t=this.schedule.getCurrentLayouts();g.info(`Offline layouts:`,t),this.emit(h.LAYOUTS_SCHEDULED,t),this._evaluateAndSwitchLayout(t,`Offline`),this.emit(h.COLLECTION_COMPLETE)}_evaluateAndSwitchLayout(e,t){let n=t?`${t}: `:``,{queue:r}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions);if(r.length>0)if(this.currentLayoutId)r.some(e=>o(e.layoutId)===this.currentLayoutId)?(g.info(`Layout ${this.currentLayoutId} playing — queue updated in background, playback continues`),this.emit(h.LAYOUT_ALREADY_PLAYING,this.currentLayoutId)):(g.info(`Layout ${this.currentLayoutId} no longer in queue — expiring`),this.currentLayoutId=null,this.emit(h.LAYOUT_EXPIRE_CURRENT));else if(this._preparingLayoutId)g.info(`${n}layout ${this._preparingLayoutId} already being prepared, skipping`);else{let e=this.getNextLayout();e&&(this._preparingLayoutId=e.layoutId,g.info(`${n}switching to layout ${e.layoutId}`),this.emit(h.LAYOUT_PREPARE_REQUEST,e.layoutId))}else g.info(`${t?`${t}: n`:`N`}o layouts${t?` in cached schedule`:` scheduled, falling back to default`}`),this.emit(h.NO_LAYOUTS_SCHEDULED);this.logUpcomingTimeline()}async collectNow(){return this._lastCheckRf=null,this._lastCheckSchedule=null,this.collect()}async collect(){if(this.collecting){g.debug(`Collection already in progress, skipping`);return}this.collecting=!0;try{if(await this._offlineDbReady,g.info(`Starting collection cycle...`),this.emit(h.COLLECTION_START),this.isOffline()){if(this.hasCachedData())return this.collecting=!1,this.collectOffline();throw Error(`Offline with no cached data — cannot start playback`)}this.config.ensureXmrKeyPair&&await this.config.ensureXmrKeyPair(),g.debug(`Collection step: registerDisplay`);let e=await this.xmds.registerDisplay();g.info(`Display registered: ${e.code}${e.tags?.length?`, tags: ${e.tags.join(`, `)}`:``}`),g.debug(`Register result:`,JSON.stringify(e)),this._processRegistration(e),g.debug(`Collection step: initializeXmr`),await this.initializeXmr(e);let t=e.checkRf||``,n=e.checkSchedule||``;if(!this._lastCheckRf||this._lastCheckRf!==t){this.resetBlacklist(),g.debug(`Collection step: requiredFiles`);let e=await this.xmds.requiredFiles(),r=e.files||e,i=e.purge||[];if(g.info(`Required files:`,r.length,i.length>0?`(+ ${i.length} purge)`:``),this._lastCheckRf=t,this.emit(h.FILES_RECEIVED,r),this._offlineSave(`requiredFiles`,e),i.length>0&&this.emit(h.PURGE_REQUEST,i),!this._lastCheckSchedule||this._lastCheckSchedule!==n){g.debug(`Collection step: schedule`);let e=await this.xmds.schedule();g.info(`Schedule received`),this._lastCheckSchedule=n,g.debug(`Collection step: processing schedule`),this._applyNewSchedule(e),this.logUpcomingTimeline()}g.debug(`Collection step: download-request + mediaInventory`),this.schedule.getCurrentLayouts();let{queue:a}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions),s=[...new Set(a.map(e=>o(e.layoutId)))];if(this._lastRequiredFiles=r,this.displaySettings?.isInDownloadWindow&&!this.displaySettings.isInDownloadWindow()){let e=this.displaySettings.getNextDownloadWindow?.();g.info(`Outside download window, skipping downloads${e?` (next: ${e.toLocaleTimeString()})`:``}`)}else this.emit(h.DOWNLOAD_REQUEST,{layoutOrder:s,files:r,layoutDependants:Object.fromEntries(this.schedule.getDependantsMap())});this.cacheAnalyzer&&this.cacheAnalyzer.analyze(r).then(e=>{this.emit(h.CACHE_ANALYSIS,e)}).catch(e=>g.warn(`Cache analysis failed:`,e)),this.submitMediaInventory(r)}else if(t&&g.info(`RequiredFiles CRC unchanged, skipping download check`),this._lastCheckSchedule!==n){let e=await this.xmds.schedule();g.info(`Schedule received (RF unchanged but schedule changed)`),this._lastCheckSchedule=n,this._applyNewSchedule(e)}else n&&g.info(`Schedule CRC unchanged, skipping`);await this._fetchWeatherData(),g.debug(`Collection step: evaluateSchedule`);let r=this.schedule.getCurrentLayouts();g.info(`Current layouts:`,r),this.emit(h.LAYOUTS_SCHEDULED,r),this._evaluateAndSwitchLayout(r,``),this._processScheduledCommands(),(e.settings?.statsEnabled===`On`||e.settings?.statsEnabled===`1`)&&(this.statsCollector?(g.info(`Stats enabled, submitting proof of play`),this.emit(h.SUBMIT_STATS_REQUEST)):g.warn(`Stats enabled but no StatsCollector provided`)),this.emit(h.SUBMIT_LOGS_REQUEST),this.emit(h.SUBMIT_FAULTS_REQUEST),!this.collectionInterval&&e.settings&&this.setupCollectionInterval(e.settings),this._faultReportingInterval||this._startFaultReportingAgent(),this.logUpcomingTimeline(),this.emit(h.COLLECTION_COMPLETE)}catch(e){if(this.hasCachedData())return g.warn(`Collection failed, falling back to cached data:`,e?.message||e),this.emit(h.COLLECTION_ERROR,e),this.collecting=!1,this.collectOffline();throw g.error(`Collection error:`,e),this.emit(h.COLLECTION_ERROR,e),e}finally{this.collecting=!1}}_processRegistration(e){if(this._offlineSave(`settings`,e),this.offlineMode&&(this.offlineMode=!1,g.info(`Back online — resuming normal collection`),this.emit(h.OFFLINE_MODE,!1),this._normalCollectInterval&&(this._setCollectionTimer(this._normalCollectInterval),this._normalCollectInterval=null,this._offlineRetrySeconds=0)),this.displaySettings&&e.settings){let n=this.displaySettings.applySettings(e.settings);n.changed.includes(`collectInterval`)&&this.updateCollectionInterval(n.settings.collectInterval),e.settings.logLevel&&t(e.settings.logLevel)&&(g.info(`Log level updated from CMS:`,e.settings.logLevel),this.emit(h.LOG_LEVEL_CHANGED,e.settings.logLevel))}if(this.schedule?.setDisplayProperties&&e.settings&&this.schedule.setDisplayProperties(e.settings),e.syncConfig){let t=JSON.stringify(e.syncConfig);t!==this._lastRawSyncConfig&&(this._lastRawSyncConfig=t,this.syncConfig=e.syncConfig,g.info(`Sync group:`,e.syncConfig.isLead?`LEAD`:`follower → ${e.syncConfig.syncGroup}`,`(switchDelay: ${e.syncConfig.syncSwitchDelay}ms, videoPauseDelay: ${e.syncConfig.syncVideoPauseDelay}ms)`),this.emit(h.SYNC_CONFIG,e.syncConfig))}if(this._applyTagConfig(e.tags),e.commands&&e.commands.length>0){this.displayCommands={};for(let t of e.commands)this.displayCommands[t.commandCode]=t;g.debug(`Display commands:`,Object.keys(this.displayCommands).join(`, `))}this.emit(h.REGISTER_COMPLETE,e)}_applyNewSchedule(e){this.emit(h.SCHEDULE_RECEIVED,e),this.schedule.setSchedule(e),this._executedCommands.clear(),this.updateDataConnectors(),this._offlineSave(`schedule`,e)}async initializeXmr(e){let t=e.settings?.xmrWebSocketAddress||e.settings?.xmrNetworkAddress;if(!t){g.warn(`XMR not configured: no xmrWebSocketAddress or xmrNetworkAddress in CMS settings`),this.emit(h.XMR_MISCONFIGURED,{reason:`missing`,message:`XMR address not configured in CMS. Go to CMS Admin → Settings → Configuration → XMR and set the WebSocket address.`});return}if(t.startsWith(`tcp://`)){g.warn(`XMR address uses tcp:// protocol which is not supported by PWA players: ${t}`),g.warn(`Configure XMR_WS_ADDRESS in CMS Admin → Settings → Configuration → XMR (e.g. wss://your-domain/xmr)`),this.emit(h.XMR_MISCONFIGURED,{reason:`wrong-protocol`,url:t,message:`XMR uses tcp:// protocol (not supported by PWA). Set XMR WebSocket Address to wss://your-domain/xmr in CMS Settings.`});return}if(/example\.(org|com|net)/i.test(t)){g.warn(`XMR address contains placeholder domain: ${t}`),g.warn(`Configure the real XMR address in CMS Admin → Settings → Configuration → XMR`),this.emit(h.XMR_MISCONFIGURED,{reason:`placeholder`,url:t,message:`XMR address is still the default placeholder (${t}). Update it in CMS Settings.`});return}let n=e.settings?.xmrCmsKey||e.settings?.serverKey||this.config.serverKey;g.debug(`XMR CMS Key:`,n?`present`:`missing`),this.xmr?this.xmr.isConnected()?g.debug(`XMR already connected`):(g.info(`XMR disconnected, attempting to reconnect...`),await this.xmr.start(t,n),this.emit(h.XMR_RECONNECTED,t)):(g.info(`Initializing XMR WebSocket:`,t),this.xmr=new this.XmrWrapper(this.config,this),await this.xmr.start(t,n),this.emit(h.XMR_CONNECTED,t))}setupCollectionInterval(e){let t=this.displaySettings?this.displaySettings.getCollectInterval():parseInt(e.collectInterval||`300`,10);this._setCollectionTimer(t),this.emit(h.COLLECTION_INTERVAL_SET,t)}updateCollectionInterval(e){this.collectionInterval&&(this._setCollectionTimer(e),this.emit(h.COLLECTION_INTERVAL_UPDATED,e))}_startFaultReportingAgent(){this._faultReportingInterval&&clearInterval(this._faultReportingInterval),g.info(`Fault reporting agent started (interval: ${this._faultReportingSeconds}s)`),this._faultReportingInterval=setInterval(()=>{this.emit(h.SUBMIT_FAULTS_REQUEST)},this._faultReportingSeconds*1e3)}_setCollectionTimer(e){this.collectionInterval&&clearInterval(this.collectionInterval),this._currentCollectInterval=e,g.info(`Collection interval: ${e}s`),this.collectionInterval=setInterval(()=>{g.debug(`Running scheduled collection cycle...`),this.collect().catch(e=>{g.error(`Collection error:`,e),this.emit(h.COLLECTION_ERROR,e)})},e*1e3)}async requestLayoutChange(e){g.info(`Layout change requested: ${e}`),this.currentLayoutId=null,this.emit(`layout-change-requested`,e)}clearPreparingLayout(){this._preparingLayoutId=null}setCurrentLayout(e){this.currentLayoutId=e,this._preparingLayoutId=null,this._lastLayoutChangeTime=new Date().toISOString(),this._statusCode=1,this.pendingLayouts.delete(e),this._layoutMediaStatus.delete(`${e}.xlf`),this.emit(`layout-current`,e),this._lastTimelineFingerprint=null,this.logUpcomingTimeline()}setPendingLayout(e,t){this.pendingLayouts.set(e,t),this.emit(`layout-pending`,e,t)}clearCurrentLayout(){this.currentLayoutId=null,this.emit(`layout-cleared`)}getNextLayout(){let e=this.schedule.popNextFromQueue(this._layoutDurations,this._queueOptions);if(!e){let e=this.schedule.schedule?.default;return e?{layoutId:o(e),layoutFile:e}:null}let t=o(e.layoutId);if(this.isLayoutBlacklisted(t)){let{queue:e}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions);for(let t=0;t<e.length-1;t++){let e=this.schedule.popNextFromQueue(this._layoutDurations,this._queueOptions);if(e){let t=o(e.layoutId);if(!this.isLayoutBlacklisted(t))return{layoutId:t,layoutFile:e.layoutId}}}g.warn(`All queued layouts are blacklisted, using current entry as fallback`)}return{layoutId:t,layoutFile:e.layoutId}}peekNextLayout(){let e=this.schedule.peekNextInQueue(this._layoutDurations,this._queueOptions);if(!e)return null;let t=o(e.layoutId);if(t===this.currentLayoutId){let e=this.schedule.peekAfterNext(this._layoutDurations,this._queueOptions);if(!e)return null;let t=o(e.layoutId);return t===this.currentLayoutId||this.isLayoutBlacklisted(t)?null:{layoutId:t,layoutFile:e.layoutId}}return this.isLayoutBlacklisted(t)?null:{layoutId:t,layoutFile:e.layoutId}}advanceToNextLayout(){if(this._layoutOverride){g.info(`Layout override active, not advancing schedule`);return}let e=this.getNextLayout();if(!e){if(this.currentLayoutId){g.info(`No layouts in queue, replaying ${this.currentLayoutId} to avoid blank screen`);let e=this.currentLayoutId;this.currentLayoutId=null,this._preparingLayoutId=e,this.emit(h.LAYOUT_PREPARE_REQUEST,e)}else g.info(`No layouts scheduled during advance`),this.emit(h.NO_LAYOUTS_SCHEDULED);return}let{layoutId:t,layoutFile:n}=e,r=this._layoutDurations.get(n)||`?`;if(this._lastTimeline&&this._lastTimeline.length>0){let e=this._lastTimeline.slice(0,2).map(e=>{let t=e.startTime.toLocaleTimeString(`en-GB`,{hour:`2-digit`,minute:`2-digit`,second:`2-digit`});return`${e.layoutFile}(${e.duration}s@${t})`});g.debug(`[Timeline] Layout transition: entering ${n} (${r}s), overlay top: [${e.join(`, `)}]`),this._lastTimeline[0].layoutFile!==n&&g.warn(`[Timeline] Mismatch: entering ${n} but overlay expects ${this._lastTimeline[0].layoutFile}`)}else g.debug(`[Timeline] Layout transition: entering ${n} (${r}s), no timeline data`);if(this.syncManager&&this.schedule.isSyncEvent(n))if(this.isSyncLead()){g.info(`[Sync] Lead requesting coordinated layout change: ${t}`),this._preparingLayoutId=t,this.emit(h.LAYOUT_PREPARE_REQUEST,t),this.syncManager.requestLayoutChange(t).catch(e=>{g.error(`[Sync] Layout change failed:`,e)});return}else if(this.syncManager.transport?.connected){g.info(`[Sync] Follower waiting for lead signal (not advancing independently)`);return}else g.warn(`[Sync] Follower: lead unreachable, advancing independently`);t===this.currentLayoutId&&(g.info(`Next layout ${t} is same as current, triggering replay`),this.currentLayoutId=null);let{queue:i}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions),a=this.schedule.getQueuePosition();g.info(`Advancing to layout ${t} (queue pos ${a}/${i.length})`),this._preparingLayoutId=t,this.emit(h.LAYOUT_PREPARE_REQUEST,t)}advanceToPreviousLayout(){if(this._layoutOverride){g.info(`Layout override active, not going back`);return}let{queue:e}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions);if(e.length<=1){g.info(`Single or empty queue, nothing to go back to`);return}let t=this.schedule.rewindQueue(2,this._layoutDurations,this._queueOptions);if(!t)return;let n=o(t.layoutId);if(n===this.currentLayoutId){g.info(`Previous layout is same as current, nothing to go back to`);return}g.info(`Going back to layout ${n}`),this.emit(h.LAYOUT_PREPARE_REQUEST,n)}notifyMediaReady(e,t=`media`){g.debug(`File ${e} ready (${t})`);for(let[n,r]of this.pendingLayouts.entries()){let i=t===`layout`&&n===parseInt(e),a=t===`media`&&r.includes(e);(i||a)&&(g.debug(`${t} ${e} was needed by pending layout ${n}, checking if ready...`),this.emit(h.CHECK_PENDING_LAYOUT,n,r))}}async notifyLayoutStatus(e){try{let t={currentLayoutId:e,deviceName:this.config?.displayName||``,displayName:this.config?.displayName||``,lastCommandSuccess:this._lastCommandSuccess??!0,code:this._statusCode,lastLayoutChangeTime:this._lastLayoutChangeTime||new Date().toISOString()};this.config?.latitude&&(t.latitude=this.config.latitude),this.config?.longitude&&(t.longitude=this.config.longitude),this._lanIpAddress&&(t.lanIpAddress=this._lanIpAddress),await this.xmds.notifyStatus(t),this.emit(`status-notified`,e)}catch(t){g.warn(`Failed to notify status:`,t),this.emit(`status-notify-failed`,e,t)}}reportGeoLocation(e){let t=parseFloat(e?.latitude),n=parseFloat(e?.longitude);if(isNaN(t)||isNaN(n)){g.warn(`reportGeoLocation: invalid coordinates`,e);return}g.info(`Geo location from CMS: ${t.toFixed(4)}, ${n.toFixed(4)}`),this.schedule?.setLocation&&this.schedule.setLocation(t,n),this.emit(`location-updated`,{latitude:t,longitude:n,source:`cms`}),this.checkSchedule()}async requestGeoLocation(){if(this._geoCache&&Date.now()-this._geoCache.ts<1800*1e3)return this._geoCache.location;if(!this._browserGeoFailed){let e=await this._tryBrowserGeolocation();if(e)return this._cacheGeo(this._applyLocation(e.latitude,e.longitude,`browser`));this._browserGeoFailed=!0}let e=this.config?.googleGeoApiKey;if(e){let t=await this._tryGoogleGeolocation(e);if(t)return this._cacheGeo(this._applyLocation(t.latitude,t.longitude,`google-api`))}let t=await this._tryIpGeolocation();return t?this._cacheGeo(this._applyLocation(t.latitude,t.longitude,`ip-geolocation`)):(g.warn(`All geolocation methods failed`),null)}_cacheGeo(e){return this._geoCache={location:e,ts:Date.now()},e}_applyTagConfig(e){if(!Array.isArray(e)||e.length===0)return;let t={geoApiKey:`googleGeoApiKey`};for(let n of e){let e=n.indexOf(`|`);if(e===-1)continue;let r=n.substring(0,e),i=n.substring(e+1),a=t[r];a&&i&&this.config&&(g.info(`Config from CMS tag: ${r} → ${a}`),this.config[a]=i)}}_applyLocation(e,t,n){return g.info(`Geolocation (${n}): ${e.toFixed(4)}, ${t.toFixed(4)}`),this.schedule?.setLocation&&this.schedule.setLocation(e,t),this.emit(`location-updated`,{latitude:e,longitude:t,source:n}),this.checkSchedule(),{latitude:e,longitude:t}}async _tryBrowserGeolocation(){if(typeof navigator>`u`||!navigator.geolocation)return null;try{let e=await new Promise((e,t)=>{navigator.geolocation.getCurrentPosition(e,t,{timeout:1e4,maximumAge:3e5,enableHighAccuracy:!1})});return{latitude:e.coords.latitude,longitude:e.coords.longitude}}catch(e){return g.warn(`Browser geolocation failed:`,e?.message||e),null}}async _tryGoogleGeolocation(e){try{let t=await fetch(`https://www.googleapis.com/geolocation/v1/geolocate?key=${e}`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({considerIp:!0}),signal:AbortSignal.timeout(5e3)});if(!t.ok)return g.warn(`Google Geolocation API returned ${t.status}`),null;let n=await t.json();return n.location?.lat!=null&&n.location?.lng!=null?{latitude:n.location.lat,longitude:n.location.lng}:null}catch(e){return g.warn(`Google Geolocation API failed:`,e?.message||e),null}}async _tryIpGeolocation(){let e=[{url:`https://ipapi.co/json/`,parse:e=>e.latitude!=null&&e.longitude!=null?{latitude:e.latitude,longitude:e.longitude}:null},{url:`https://freeipapi.com/api/json`,parse:e=>e.latitude!=null&&e.longitude!=null?{latitude:e.latitude,longitude:e.longitude}:null}];for(let t of e)try{let e=await fetch(t.url,{signal:AbortSignal.timeout(5e3)});if(!e.ok)continue;let n=await e.json(),r=t.parse(n);if(r)return r}catch(e){g.warn(`IP geolocation (${t.url}) failed:`,e?.message||e)}return null}checkSchedule(){let e=this.schedule.getCurrentLayouts();this.emit(h.LAYOUTS_SCHEDULED,e),this._evaluateAndSwitchLayout(e,``)}async captureScreenshot(){g.info(`Screenshot requested`),this.emit(h.SCREENSHOT_REQUEST)}async changeLayout(e,t){g.info(`Layout change requested via XMR:`,e);let n=parseInt(e,10),r=t?.duration||0;this._layoutOverride={layoutId:n,type:`change`,duration:r,changeMode:t?.changeMode||`replace`},this.currentLayoutId=null,this.emit(h.LAYOUT_PREPARE_REQUEST,n),this._scheduleAutoRevert(n,r,`Layout override`)}async overlayLayout(e,t){g.info(`Overlay layout requested via XMR:`,e);let n=parseInt(e,10),r=t?.duration||0;this._layoutOverride={layoutId:n,type:`overlay`,duration:r},this.emit(h.OVERLAY_LAYOUT_REQUEST,n),this._scheduleAutoRevert(n,r,`Overlay`)}async revertToSchedule(){g.info(`Reverting to scheduled content`),this._layoutOverride=null,this.currentLayoutId=null,this.emit(h.REVERT_TO_SCHEDULE);let e=this.schedule.getCurrentLayouts();if(e.length>0){let t=e[0],n=o(t);this.emit(h.LAYOUT_PREPARE_REQUEST,n)}else this.emit(h.NO_LAYOUTS_SCHEDULED)}async purgeAll(){return g.info(`Purge all cache requested via XMR`),this._lastCheckRf=null,this._lastCheckSchedule=null,this.emit(h.PURGE_ALL_REQUEST),this.collectNow()}async executeCommand(e,t){if(g.info(`Execute command requested:`,e),!t||!t[e]){g.warn(`Unknown command code:`,e),this._lastCommandSuccess=!1,this.emit(h.COMMAND_RESULT,{code:e,success:!1,reason:`Unknown command`});return}let n=t[e],r=n.commandString||n.value||``;if(r.startsWith(`http|`)){let t=r.split(`|`),n=t[1],i=t[2]||`application/json`;try{let t=await fetch(n,{method:`POST`,headers:{"Content-Type":i},signal:AbortSignal.timeout(1e4)}),r=t.ok;this._lastCommandSuccess=r,g.info(`HTTP command ${e} result: ${t.status}`),this.emit(h.COMMAND_RESULT,{code:e,success:r,status:t.status})}catch(t){this._lastCommandSuccess=!1,g.error(`HTTP command ${e} failed:`,t),this.emit(h.COMMAND_RESULT,{code:e,success:!1,reason:t.message})}}else g.info(`Delegating non-HTTP command to platform layer:`,e),this.emit(h.EXECUTE_NATIVE_COMMAND,{code:e,commandString:r})}triggerWebhook(e){g.info(`Webhook trigger from XMR:`,e),this.handleTrigger(e)}refreshDataConnectors(){g.info(`Data connector refresh requested via XMR`),this.dataConnectorManager.refreshAll(),this.emit(`data-connectors-refreshed`)}async submitMediaInventory(e){if(!(!e||e.length===0))try{let t=Math.floor(Date.now()/1e3),n=`<files>${e.filter(e=>[`media`,`layout`,`resource`,`dependency`,`widget`].includes(e.type)).map(e=>{let n=e.complete===void 0||e.complete?`1`:`0`,r=e.fileType?` fileType="${e.fileType}"`:``;return`<file type="${e.type}" id="${e.id}" complete="${n}" md5="${e.md5||``}" lastChecked="${t}"${r}/>`}).join(``)}</files>`;await this.xmds.mediaInventory(n),g.info(`Media inventory submitted: ${e.length} files`),this.emit(`media-inventory-submitted`,e.length)}catch(e){g.warn(`MediaInventory submission failed:`,e)}}async blackList(e,t,n){try{await this.xmds.blackList(e,t,n),this.emit(`media-blacklisted`,{mediaId:e,type:t,reason:n})}catch(e){g.warn(`BlackList failed:`,e)}}reportLayoutFailure(e,t){let n=Number(e);this._statusCode=3;let{blacklisted:r,failures:i}=this._layoutBlacklist.recordFailure(n,t);r&&i===3&&(this.emit(`layout-blacklisted`,{layoutId:n,reason:t,failures:i}),this.blackList(n,`layout`,t))}reportLayoutSuccess(e){this._layoutBlacklist.recordSuccess(Number(e))&&this.emit(`layout-unblacklisted`,{layoutId:Number(e)})}isLayoutBlacklisted(e){return this._layoutBlacklist.isBlacklisted(e)}getBlacklistedLayouts(){return this._layoutBlacklist.getBlacklistedIds()}resetBlacklist(){this._layoutBlacklist.reset()>0&&this.emit(`blacklist-reset`)}isLayoutOverridden(){return this._layoutOverride!==null}handleTrigger(e){let t=this.schedule.findActionByTrigger(e);if(!t){g.debug(`No scheduled action matches trigger:`,e);return}switch(g.info(`Action triggered: ${t.actionType} (trigger: ${e})`),t.actionType){case`navLayout`:case`navigateToLayout`:t.layoutCode&&this.changeLayout(t.layoutCode);break;case`navWidget`:case`navigateToWidget`:this.emit(h.NAVIGATE_TO_WIDGET,t);break;case`command`:this.emit(`execute-command`,t.commandCode);break;default:g.warn(`Unknown action type:`,t.actionType)}}updateDataConnectors(){let e=this.schedule.getDataConnectors();e.length>0&&g.info(`Configuring ${e.length} data connector(s)`),this.dataConnectorManager.setConnectors(e),e.length>0&&(this.dataConnectorManager.startPolling(),this.emit(`data-connectors-started`,e.length))}_processScheduledCommands(){if(!this.schedule?.getCommands)return;let e=this.schedule.getCommands();if(e.length===0)return;let t=new Date;for(let n of e){if(!n.code||!n.date)continue;let e=`${n.code}|${n.date}`;if(this._executedCommands.has(e))continue;let r=new Date(n.date);if(isNaN(r.getTime())){g.warn(`Scheduled command has invalid date:`,n.date);continue}t>=r&&(g.info(`Executing scheduled command: ${n.code} (scheduled: ${n.date})`),this._executedCommands.add(e),n.code===`collectNow`?setTimeout(()=>this.collectNow().catch(e=>g.error(`collectNow command failed:`,e)),0):this.emit(h.SCHEDULED_COMMAND,n))}}async _fetchWeatherData(){if(!(!this.xmds?.getWeather||!this.schedule?.setWeatherData))try{let e=await this.xmds.getWeather(),t=typeof e==`string`?JSON.parse(e):e;this.schedule.setWeatherData(t),g.info(`Weather data updated:`,Object.keys(t).join(`, `))}catch(e){g.warn(`GetWeather failed (non-critical):`,e?.message||e)}}getDataConnectorManager(){return this.dataConnectorManager}setSyncManager(e){this.syncManager=e,g.info(`SyncManager attached:`,e.isLead?`LEAD`:`FOLLOWER`)}isInSyncGroup(){return this.syncConfig!==null}isSyncLead(){return this.syncConfig?.isLead===!0}getSyncConfig(){return this.syncConfig}logUpcomingTimeline(){if(!this.schedule.getLayoutsAtTime)return;let e=[...this._layoutDurations.entries()].sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${e}:${t}`).join(`|`),t=[...this._layoutMediaStatus.entries()].sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${e}:${t.ready}:${t.missingKey}`).join(`|`),n=[...this.pendingLayouts.keys()].sort().join(`,`),r=this.schedule.getQueuePosition()||0,i=`${this._lastCheckSchedule}|${e}|${this.currentLayoutId}|${r}|${t}|${n}`;if(i===this._lastTimelineFingerprint&&this._lastTimeline){this.emit(h.TIMELINE_UPDATED,this._lastTimeline);return}let{queue:a}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions),o=s(a,this.schedule.getQueuePosition(),{currentLayoutStartedAt:this._lastLayoutChangeTime?new Date(this._lastLayoutChangeTime):null,defaultLayout:this.schedule.schedule?.default||null,durations:this._layoutDurations});if(o.length===0)return;for(let e of o){let t=parseInt(e.layoutFile.replace(`.xlf`,``),10),n=this.pendingLayouts.get(t);if(n&&n.length>0)e.missingMedia=n.map(String);else{let t=this._layoutMediaStatus.get(e.layoutFile);t&&!t.ready&&t.missing.length>0&&(e.missingMedia=t.missing.map(String))}}this._lastTimelineFingerprint=i,this._lastTimeline=o;let c=o.slice(0,20).map(e=>{let t=e.startTime.toLocaleTimeString(`en-GB`,{hour:`2-digit`,minute:`2-digit`,second:`2-digit`}),n=e.endTime.toLocaleTimeString(`en-GB`,{hour:`2-digit`,minute:`2-digit`,second:`2-digit`}),r=e.missingMedia?` [MISSING: ${e.missingMedia.length} files]`:``;return` ${t}-${n} Layout ${e.layoutFile} (${e.duration}s)${e.isDefault?` [default]`:``}${r}`});for(let e of o)e.missingMedia&&g.warn(`[Timeline] Layout ${e.layoutFile}: ${e.missingMedia.length} files missing`);g.info(`[Timeline] Next ${o.length} plays:\n${c.join(`
2
- `)}`),this.emit(h.TIMELINE_UPDATED,o)}setLayoutMediaStatus(e,t,n=[]){let r=this._layoutMediaStatus.get(e),i=n.slice().sort().join(`,`);r&&r.ready===t&&r.missingKey===i||(this._layoutMediaStatus.set(e,{ready:t,missing:n,missingKey:i}),this._lastTimelineFingerprint=null)}recordLayoutDuration(e,t,n=!1){let r=String(e).replace(`.xlf`,``),i=r+`.xlf`;if(this._finalDurations.has(r))return;let a=this._layoutDurations.get(e);a===t&&!n||(this._layoutDurations.set(r,t),this._layoutDurations.set(i,t),n&&(this._finalDurations.add(r),this._finalDurations.add(i)),g.debug(`[Timeline] Duration corrected: layout ${e} ${a||`?`}s → ${t}s${n?` (final)`:``}`),this.schedule.invalidateQueue(),this._timelineRecalcTimer&&clearTimeout(this._timelineRecalcTimer),this._timelineRecalcTimer=setTimeout(()=>{this._timelineRecalcTimer=null,this.logUpcomingTimeline(),this._offlineSave(`durations`,[...this._layoutDurations.entries()]),this._offlineSave(`finalDurations`,[...this._finalDurations]),this._offlineSave(`durationsVersion`,2)},500))}cleanup(){this.collectionInterval&&=(clearInterval(this.collectionInterval),null),this._faultReportingInterval&&=(clearInterval(this._faultReportingInterval),null),this._timelineRecalcTimer&&=(clearTimeout(this._timelineRecalcTimer),null),this.xmr&&=(this.xmr.stop(),null),this.syncManager&&=(this.syncManager.stop(),null),this.dataConnectorManager.cleanup(),this.emit(`cleanup-complete`),this.removeAllListeners()}getCurrentLayoutId(){return this.currentLayoutId}getLayoutDuration(e){let t=String(e);return this._layoutDurations.get(`${t}.xlf`)||this._layoutDurations.get(t)}isCollecting(){return this.collecting}getPendingLayouts(){return Array.from(this.pendingLayouts.keys())}},C=c.version;export{h as CORE_EVENTS,f as DataConnectorManager,S as PlayerCore,C as VERSION};
3
- //# sourceMappingURL=src-Cx3tXAAu.js.map
1
+ import{_ as e,h as t,o as n,p as r,v as i}from"./src-CCAyzQUp.js";import{r as a}from"./src-CuVaZcMo.js";import{l as o,s}from"./src-A5KHvitf.js";var c={name:`@xiboplayer/core`,version:`0.7.23`,description:`xiboplayer core orchestration and lifecycle management`,type:`module`,main:`./src/index.js`,types:`./src/index.d.ts`,exports:{".":`./src/index.js`,"./player-core":`./src/player-core.js`},scripts:{dev:`vite`,build:`vite build`,preview:`vite preview`,test:`vitest run`,"test:watch":`vitest`,"test:ui":`vitest --ui`,"test:coverage":`vitest run --coverage`},dependencies:{"@xiboplayer/utils":`workspace:*`},peerDependencies:{"@xiboplayer/cache":`workspace:*`,"@xiboplayer/renderer":`workspace:*`,"@xiboplayer/schedule":`workspace:*`,"@xiboplayer/xmds":`workspace:*`},devDependencies:{"@vitest/coverage-v8":`^4.1.3`,"@vitest/ui":`^4.1.4`,jsdom:`^29.0.2`,vite:`^8.0.8`,vitest:`^4.1.2`},keywords:[`xibo`,`digital-signage`,`player`,`core`,`orchestration`],author:`Pau Aliagas <linuxnow@gmail.com>`,license:`AGPL-3.0-or-later`,repository:{type:`git`,url:`git+https://github.com/xibo-players/xiboplayer.git`,directory:`packages/core`},homepage:`https://xiboplayer.org`},l=i(`DataConnector`),u=3e5,d=3,f=class extends t{constructor(){super(),this.connectors=new Map}setConnectors(e){if(this.stopPolling(),this.connectors.clear(),!e||e.length===0){l.debug(`No data connectors configured`);return}for(let t of e){if(!t.dataKey||!t.url){l.warn(`Skipping data connector with missing dataKey or url:`,t);continue}this.connectors.set(t.dataKey,{config:t,data:null,timer:null,lastFetch:null,failures:0}),l.info(`Registered data connector: ${t.dataKey} (interval: ${t.updateInterval}s)`)}l.info(`${this.connectors.size} data connector(s) configured`)}startPolling(){for(let[e,t]of this.connectors.entries()){let{config:n}=t,r=(n.updateInterval||300)*1e3;this.fetchData(t).catch(t=>{l.error(`Initial fetch failed for ${e}:`,t)}),t.timer=setInterval(()=>{this.fetchData(t).catch(t=>{l.error(`Polling fetch failed for ${e}:`,t)})},r),l.debug(`Started polling for ${e} every ${n.updateInterval}s`)}}stopPolling(){for(let[e,t]of this.connectors.entries())t.timer&&(clearInterval(t.timer),t.timer=null,l.debug(`Stopped polling for ${e}`))}getData(e){let t=this.connectors.get(e);return t?t.data:(l.debug(`No data connector found for key: ${e}`),null)}getAvailableKeys(){let e=[];for(let[t,n]of this.connectors.entries())n.data!==null&&e.push(t);return e}async fetchData(e){let{config:t}=e,{dataKey:r,url:i}=t;l.debug(`Fetching data for ${r}: ${i}`);try{let t=await n(i,{method:`GET`,headers:{Accept:`application/json`}},{maxRetries:2,baseDelayMs:2e3});if(!t.ok){l.warn(`Data connector ${r} returned ${t.status}: ${t.statusText}`);return}let a=t.headers.get(`Content-Type`)||``,o;o=a.includes(`application/json`)?await t.json():await t.text();let s=e.data;e.data=o,e.lastFetch=Date.now(),e.failures=0,l.debug(`Data updated for ${r} (fetched at ${new Date(e.lastFetch).toISOString()})`),this._ensureNormalPolling(e),this.emit(`data-updated`,r,o),JSON.stringify(s)!==JSON.stringify(o)&&this.emit(`data-changed`,r,o)}catch(n){if(e.failures=(e.failures||0)+1,l.error(`Failed to fetch data for ${r} (${e.failures}x):`,n),this.emit(`fetch-error`,r,n),e.failures>=d&&e.timer){let n=(t.updateInterval||300)*1e3,i=Math.min(n*2**(e.failures-d+1),u);clearInterval(e.timer),e.timer=setTimeout(()=>{this.fetchData(e).catch(()=>{}),e.timer=setInterval(()=>{this.fetchData(e).catch(()=>{})},i)},i),l.warn(`Circuit breaker: ${r} backing off to ${Math.round(i/1e3)}s`)}}}_ensureNormalPolling(e){if(e.failures===0&&e.timer){let t=(e.config.updateInterval||300)*1e3;clearInterval(e.timer),clearTimeout(e.timer),e.timer=setInterval(()=>{this.fetchData(e).catch(()=>{})},t)}}refreshAll(){this.connectors.size!==0&&(l.info(`Refreshing all ${this.connectors.size} data connector(s)`),this.stopPolling(),this.startPolling())}cleanup(){this.stopPolling(),this.connectors.clear(),this.removeAllListeners(),l.debug(`DataConnectorManager cleaned up`)}},p=i(`Blacklist`),m=class{constructor(e=3){this._entries=new Map,this._threshold=e}recordFailure(e,t){let n=Number(e),r=this._entries.get(n)||{failures:0,blacklisted:!1,reason:``};return r.failures++,r.reason=t,!r.blacklisted&&r.failures>=this._threshold?(r.blacklisted=!0,p.warn(`Layout ${n} blacklisted after ${r.failures} consecutive failures: ${t}`)):r.blacklisted||p.info(`Layout ${n} failure ${r.failures}/${this._threshold}: ${t}`),this._entries.set(n,r),{blacklisted:r.blacklisted,failures:r.failures}}recordSuccess(e){let t=Number(e);if(!this._entries.has(t))return!1;let n=this._entries.get(t);return this._entries.delete(t),n.blacklisted?(p.info(`Layout ${t} removed from blacklist (rendered successfully)`),!0):!1}isBlacklisted(e){return this._entries.get(Number(e))?.blacklisted===!0}getBlacklistedIds(){let e=[];for(let[t,n]of this._entries)n.blacklisted&&e.push(t);return e}reset(){let e=this._entries.size;return e>0&&(p.info(`Blacklist reset (${e} entries cleared)`),this._entries.clear()),e}get size(){return this._entries.size}},h=Object.freeze({COLLECTION_START:`collection-start`,COLLECTION_COMPLETE:`collection-complete`,COLLECTION_ERROR:`collection-error`,REGISTER_COMPLETE:`register-complete`,SCHEDULE_RECEIVED:`schedule-received`,LAYOUTS_SCHEDULED:`layouts-scheduled`,NO_LAYOUTS_SCHEDULED:`no-layouts-scheduled`,TIMELINE_UPDATED:`timeline-updated`,LAYOUT_PREPARE_REQUEST:`layout-prepare-request`,LAYOUT_EXPIRE_CURRENT:`layout-expire-current`,LAYOUT_ALREADY_PLAYING:`layout-already-playing`,CHECK_PENDING_LAYOUT:`check-pending-layout`,FILES_RECEIVED:`files-received`,DOWNLOAD_REQUEST:`download-request`,OVERLAY_LAYOUT_REQUEST:`overlay-layout-request`,REVERT_TO_SCHEDULE:`revert-to-schedule`,SYNC_CONFIG:`sync-config`,XMR_CONNECTED:`xmr-connected`,XMR_RECONNECTED:`xmr-reconnected`,XMR_MISCONFIGURED:`xmr-misconfigured`,NAVIGATE_TO_WIDGET:`navigate-to-widget`,EXECUTE_NATIVE_COMMAND:`execute-native-command`,SCHEDULED_COMMAND:`scheduled-command`,COMMAND_RESULT:`command-result`,SCREENSHOT_REQUEST:`screenshot-request`,SUBMIT_STATS_REQUEST:`submit-stats-request`,SUBMIT_LOGS_REQUEST:`submit-logs-request`,SUBMIT_FAULTS_REQUEST:`submit-faults-request`,CACHE_ANALYSIS:`cache-analysis`,COLLECTION_INTERVAL_SET:`collection-interval-set`,COLLECTION_INTERVAL_UPDATED:`collection-interval-updated`,LOG_LEVEL_CHANGED:`log-level-changed`,OFFLINE_MODE:`offline-mode`,PURGE_REQUEST:`purge-request`,PURGE_ALL_REQUEST:`purge-all-request`}),g=i(`PlayerCore`);async function _(){if(typeof window<`u`&&window.electronAPI?.getLanIpAddress)try{return await window.electronAPI.getLanIpAddress()}catch{}try{let e=await(globalThis.__nativeFetch||globalThis.fetch)(`/system/lan-ip`);if(e.ok){let{ip:t}=await e.json();if(t)return t}}catch{}return``}var v=`xibo-offline-cache`,y=1,b=`cache`;function x(e){return r(e?`${v}-${e}`:v,y,b)}var S=class extends t{constructor(e){super(),this.config=e.config,this.xmds=e.xmds,this.cache=e.cache,this.schedule=e.schedule,this.renderer=e.renderer,this.XmrWrapper=e.xmrWrapper,this.statsCollector=e.statsCollector,this.displaySettings=e.displaySettings,this._cmsId=e.cmsId||null,this.dataConnectorManager=new f,_().then(e=>{this._lanIpAddress=e,g.info(`LAN IP:`,e||`(not discovered)`)}),this.xmr=null,this.currentLayoutId=null,this.collecting=!1,this.collectionInterval=null,this.pendingLayouts=new Map,this._layoutMediaStatus=new Map,this.offlineMode=!1,this._normalCollectInterval=null,this._offlineRetrySeconds=0,this._lastCheckRf=null,this._lastCheckSchedule=null,this._lastTimelineFingerprint=null,this._lastTimeline=null,this._layoutOverride=null,this._lastRequiredFiles=[],this._executedCommands=new Set,this.displayCommands=null,this._faultReportingInterval=null,this._faultReportingSeconds=60,this._layoutBlacklist=new m(3),this._lastLayoutChangeTime=null,this._statusCode=2,this._dynamicLayouts=new Set,this.syncConfig=null,this.syncManager=null,this._layoutDurations=new Map,this._finalDurations=new Set,this._preparingLayoutId=null,this.cacheAnalyzer=this.cache?new a(this.cache):null,this._offlineCache={schedule:null,settings:null,requiredFiles:null},this._offlineDbReady=this._initOfflineCache()}get _queueOptions(){return{dynamicLayouts:this._dynamicLayouts}}_scheduleAutoRevert(e,t,n){t>0&&setTimeout(()=>{this._layoutOverride?.layoutId===e&&(g.info(`${n} duration expired (${t}s), reverting to schedule`),this.revertToSchedule())},t*1e3)}async _initOfflineCache(){try{let e=await x(this._cmsId),t=e.transaction(b,`readonly`).objectStore(b),[n,r,i,a,o,s]=await Promise.all([new Promise(e=>{let n=t.get(`schedule`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`settings`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`requiredFiles`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`durations`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`finalDurations`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)}),new Promise(e=>{let n=t.get(`durationsVersion`);n.onsuccess=()=>e(n.result??null),n.onerror=()=>e(null)})]);if(Array.isArray(a)&&a.length>0){for(let[e,t]of a)this._layoutDurations.set(e,t);g.info(`[Timeline] Restored ${a.length} cached durations from IDB`)}if(s>=2&&Array.isArray(o)&&o.length>0){for(let e of o)this._finalDurations.add(e);g.info(`[Timeline] Restored ${o.length} final duration keys from IDB`)}else Array.isArray(o)&&o.length>0&&g.info(`[Timeline] Discarded ${o.length} stale final duration keys (pre-v2)`);this._offlineCache={schedule:n,settings:r,requiredFiles:i},this._offlineDb=e,g.info(`Offline cache loaded from IndexedDB`,n?`(has schedule)`:`(empty)`)}catch(e){g.warn(`Failed to load offline cache from IndexedDB:`,e)}}async _offlineSave(e,t){this._offlineCache[e]=t;try{this._offlineDb||=await x(this._cmsId);let n=this._offlineDb.transaction(b,`readwrite`);n.objectStore(b).put(t,e),await new Promise((e,t)=>{n.oncomplete=e,n.onerror=()=>t(n.error)})}catch(t){this._offlineDb=null,g.warn(`Failed to save offline cache:`,e,t)}}hasCachedData(){return this._offlineCache.schedule!==null}isOffline(){return typeof navigator<`u`&&navigator.onLine===!1}isInOfflineMode(){return this.offlineMode}collectOffline(){if(g.warn(`Offline mode — using cached schedule`),this.offlineMode||(this.offlineMode=!0,this.emit(h.OFFLINE_MODE,!0)),this.collectionInterval&&(this._normalCollectInterval?this._offlineRetrySeconds=Math.min(this._offlineRetrySeconds*2,this._normalCollectInterval):(this._normalCollectInterval=this._currentCollectInterval,this._offlineRetrySeconds=30),this._setCollectionTimer(this._offlineRetrySeconds),g.info(`Offline: retry in ${this._offlineRetrySeconds}s`)),!this.collectionInterval){let e=this._offlineCache.settings;e?.settings&&(this.setupCollectionInterval(e.settings),this._normalCollectInterval=this._currentCollectInterval,this._offlineRetrySeconds=30,this._setCollectionTimer(this._offlineRetrySeconds),g.info(`Offline: retry in ${this._offlineRetrySeconds}s`))}let e=this._offlineCache.schedule;e&&(this.schedule.setSchedule(e),this.emit(h.SCHEDULE_RECEIVED,e));let t=this.schedule.getCurrentLayouts();g.info(`Offline layouts:`,t),this.emit(h.LAYOUTS_SCHEDULED,t),this._evaluateAndSwitchLayout(t,`Offline`),this.emit(h.COLLECTION_COMPLETE)}_evaluateAndSwitchLayout(e,t){let n=t?`${t}: `:``,{queue:r}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions);if(r.length>0)if(this.currentLayoutId)r.some(e=>o(e.layoutId)===this.currentLayoutId)?(g.info(`Layout ${this.currentLayoutId} playing — queue updated in background, playback continues`),this.emit(h.LAYOUT_ALREADY_PLAYING,this.currentLayoutId)):(g.info(`Layout ${this.currentLayoutId} no longer in queue — expiring`),this.currentLayoutId=null,this.emit(h.LAYOUT_EXPIRE_CURRENT));else if(this._preparingLayoutId)g.info(`${n}layout ${this._preparingLayoutId} already being prepared, skipping`);else{let e=this.getNextLayout();e&&(this._preparingLayoutId=e.layoutId,g.info(`${n}switching to layout ${e.layoutId}`),this.emit(h.LAYOUT_PREPARE_REQUEST,e.layoutId))}else g.info(`${t?`${t}: n`:`N`}o layouts${t?` in cached schedule`:` scheduled, falling back to default`}`),this.emit(h.NO_LAYOUTS_SCHEDULED);this.logUpcomingTimeline()}async collectNow(){return this._lastCheckRf=null,this._lastCheckSchedule=null,this.collect()}async collect(){if(this.collecting){g.debug(`Collection already in progress, skipping`);return}this.collecting=!0;try{if(await this._offlineDbReady,g.info(`Starting collection cycle...`),this.emit(h.COLLECTION_START),this.isOffline()){if(this.hasCachedData())return this.collecting=!1,this.collectOffline();throw Error(`Offline with no cached data — cannot start playback`)}this.config.ensureXmrKeyPair&&await this.config.ensureXmrKeyPair(),g.debug(`Collection step: registerDisplay`);let e=await this.xmds.registerDisplay();g.info(`Display registered: ${e.code}${e.tags?.length?`, tags: ${e.tags.join(`, `)}`:``}`),g.debug(`Register result:`,JSON.stringify(e)),this._processRegistration(e),g.debug(`Collection step: initializeXmr`),await this.initializeXmr(e);let t=e.checkRf||``,n=e.checkSchedule||``;if(!this._lastCheckRf||this._lastCheckRf!==t){this.resetBlacklist(),g.debug(`Collection step: requiredFiles`);let e=await this.xmds.requiredFiles(),r=e.files||e,i=e.purge||[];if(g.info(`Required files:`,r.length,i.length>0?`(+ ${i.length} purge)`:``),this._lastCheckRf=t,this.emit(h.FILES_RECEIVED,r),this._offlineSave(`requiredFiles`,e),i.length>0&&this.emit(h.PURGE_REQUEST,i),!this._lastCheckSchedule||this._lastCheckSchedule!==n){g.debug(`Collection step: schedule`);let e=await this.xmds.schedule();g.info(`Schedule received`),this._lastCheckSchedule=n,g.debug(`Collection step: processing schedule`),this._applyNewSchedule(e),this.logUpcomingTimeline()}g.debug(`Collection step: download-request + mediaInventory`),this.schedule.getCurrentLayouts();let{queue:a}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions),s=[...new Set(a.map(e=>o(e.layoutId)))];if(this._lastRequiredFiles=r,this.displaySettings?.isInDownloadWindow&&!this.displaySettings.isInDownloadWindow()){let e=this.displaySettings.getNextDownloadWindow?.();g.info(`Outside download window, skipping downloads${e?` (next: ${e.toLocaleTimeString()})`:``}`)}else this.emit(h.DOWNLOAD_REQUEST,{layoutOrder:s,files:r,layoutDependants:Object.fromEntries(this.schedule.getDependantsMap())});this.cacheAnalyzer&&this.cacheAnalyzer.analyze(r).then(e=>{this.emit(h.CACHE_ANALYSIS,e)}).catch(e=>g.warn(`Cache analysis failed:`,e)),this.submitMediaInventory(r)}else if(t&&g.info(`RequiredFiles CRC unchanged, skipping download check`),this._lastCheckSchedule!==n){let e=await this.xmds.schedule();g.info(`Schedule received (RF unchanged but schedule changed)`),this._lastCheckSchedule=n,this._applyNewSchedule(e)}else n&&g.info(`Schedule CRC unchanged, skipping`);await this._fetchWeatherData(),g.debug(`Collection step: evaluateSchedule`);let r=this.schedule.getCurrentLayouts();g.info(`Current layouts:`,r),this.emit(h.LAYOUTS_SCHEDULED,r),this._evaluateAndSwitchLayout(r,``),this._processScheduledCommands(),(e.settings?.statsEnabled===`On`||e.settings?.statsEnabled===`1`)&&(this.statsCollector?(g.info(`Stats enabled, submitting proof of play`),this.emit(h.SUBMIT_STATS_REQUEST)):g.warn(`Stats enabled but no StatsCollector provided`)),this.emit(h.SUBMIT_LOGS_REQUEST),this.emit(h.SUBMIT_FAULTS_REQUEST),!this.collectionInterval&&e.settings&&this.setupCollectionInterval(e.settings),this._faultReportingInterval||this._startFaultReportingAgent(),this.logUpcomingTimeline(),this.emit(h.COLLECTION_COMPLETE)}catch(e){if(this.hasCachedData())return g.warn(`Collection failed, falling back to cached data:`,e?.message||e),this.emit(h.COLLECTION_ERROR,e),this.collecting=!1,this.collectOffline();throw g.error(`Collection error:`,e),this.emit(h.COLLECTION_ERROR,e),e}finally{this.collecting=!1}}_processRegistration(t){if(this._offlineSave(`settings`,t),this.offlineMode&&(this.offlineMode=!1,g.info(`Back online — resuming normal collection`),this.emit(h.OFFLINE_MODE,!1),this._normalCollectInterval&&(this._setCollectionTimer(this._normalCollectInterval),this._normalCollectInterval=null,this._offlineRetrySeconds=0)),this.displaySettings&&t.settings){let n=this.displaySettings.applySettings(t.settings);n.changed.includes(`collectInterval`)&&this.updateCollectionInterval(n.settings.collectInterval),t.settings.logLevel&&e(t.settings.logLevel)&&(g.info(`Log level updated from CMS:`,t.settings.logLevel),this.emit(h.LOG_LEVEL_CHANGED,t.settings.logLevel))}if(this.schedule?.setDisplayProperties&&t.settings&&this.schedule.setDisplayProperties(t.settings),t.syncConfig){let e=JSON.stringify(t.syncConfig);e!==this._lastRawSyncConfig&&(this._lastRawSyncConfig=e,this.syncConfig=t.syncConfig,g.info(`Sync group:`,t.syncConfig.isLead?`LEAD`:`follower → ${t.syncConfig.syncGroup}`,`(switchDelay: ${t.syncConfig.syncSwitchDelay}ms, videoPauseDelay: ${t.syncConfig.syncVideoPauseDelay}ms)`),this.emit(h.SYNC_CONFIG,t.syncConfig))}if(this._applyTagConfig(t.tags),t.commands&&t.commands.length>0){this.displayCommands={};for(let e of t.commands)this.displayCommands[e.commandCode]=e;g.debug(`Display commands:`,Object.keys(this.displayCommands).join(`, `))}this.emit(h.REGISTER_COMPLETE,t)}_applyNewSchedule(e){this.emit(h.SCHEDULE_RECEIVED,e),this.schedule.setSchedule(e),this._executedCommands.clear(),this.updateDataConnectors(),this._offlineSave(`schedule`,e)}async initializeXmr(e){let t=e.settings?.xmrWebSocketAddress||e.settings?.xmrNetworkAddress;if(!t){g.warn(`XMR not configured: no xmrWebSocketAddress or xmrNetworkAddress in CMS settings`),this.emit(h.XMR_MISCONFIGURED,{reason:`missing`,message:`XMR address not configured in CMS. Go to CMS Admin → Settings → Configuration → XMR and set the WebSocket address.`});return}if(t.startsWith(`tcp://`)){g.warn(`XMR address uses tcp:// protocol which is not supported by PWA players: ${t}`),g.warn(`Configure XMR_WS_ADDRESS in CMS Admin → Settings → Configuration → XMR (e.g. wss://your-domain/xmr)`),this.emit(h.XMR_MISCONFIGURED,{reason:`wrong-protocol`,url:t,message:`XMR uses tcp:// protocol (not supported by PWA). Set XMR WebSocket Address to wss://your-domain/xmr in CMS Settings.`});return}if(/example\.(org|com|net)/i.test(t)){g.warn(`XMR address contains placeholder domain: ${t}`),g.warn(`Configure the real XMR address in CMS Admin → Settings → Configuration → XMR`),this.emit(h.XMR_MISCONFIGURED,{reason:`placeholder`,url:t,message:`XMR address is still the default placeholder (${t}). Update it in CMS Settings.`});return}let n=e.settings?.xmrCmsKey||e.settings?.serverKey||this.config.serverKey;g.debug(`XMR CMS Key:`,n?`present`:`missing`),this.xmr?this.xmr.isConnected()?g.debug(`XMR already connected`):(g.info(`XMR disconnected, attempting to reconnect...`),await this.xmr.start(t,n),this.emit(h.XMR_RECONNECTED,t)):(g.info(`Initializing XMR WebSocket:`,t),this.xmr=new this.XmrWrapper(this.config,this),await this.xmr.start(t,n),this.emit(h.XMR_CONNECTED,t))}setupCollectionInterval(e){let t=this.displaySettings?this.displaySettings.getCollectInterval():parseInt(e.collectInterval||`300`,10);this._setCollectionTimer(t),this.emit(h.COLLECTION_INTERVAL_SET,t)}updateCollectionInterval(e){this.collectionInterval&&(this._setCollectionTimer(e),this.emit(h.COLLECTION_INTERVAL_UPDATED,e))}_startFaultReportingAgent(){this._faultReportingInterval&&clearInterval(this._faultReportingInterval),g.info(`Fault reporting agent started (interval: ${this._faultReportingSeconds}s)`),this._faultReportingInterval=setInterval(()=>{this.emit(h.SUBMIT_FAULTS_REQUEST)},this._faultReportingSeconds*1e3)}_setCollectionTimer(e){this.collectionInterval&&clearInterval(this.collectionInterval),this._currentCollectInterval=e,g.info(`Collection interval: ${e}s`),this.collectionInterval=setInterval(()=>{g.debug(`Running scheduled collection cycle...`),this.collect().catch(e=>{g.error(`Collection error:`,e),this.emit(h.COLLECTION_ERROR,e)})},e*1e3)}async requestLayoutChange(e){g.info(`Layout change requested: ${e}`),this.currentLayoutId=null,this.emit(`layout-change-requested`,e)}clearPreparingLayout(){this._preparingLayoutId=null}setCurrentLayout(e){this.currentLayoutId=e,this._preparingLayoutId=null,this._lastLayoutChangeTime=new Date().toISOString(),this._statusCode=1,this.pendingLayouts.delete(e),this._layoutMediaStatus.delete(`${e}.xlf`),this.emit(`layout-current`,e),this._lastTimelineFingerprint=null,this.logUpcomingTimeline()}setPendingLayout(e,t){this.pendingLayouts.set(e,t),this.emit(`layout-pending`,e,t)}clearCurrentLayout(){this.currentLayoutId=null,this.emit(`layout-cleared`)}getNextLayout(){let e=this.schedule.popNextFromQueue(this._layoutDurations,this._queueOptions);if(!e){let e=this.schedule.schedule?.default;return e?{layoutId:o(e),layoutFile:e}:null}let t=o(e.layoutId);if(this.isLayoutBlacklisted(t)){let{queue:e}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions);for(let t=0;t<e.length-1;t++){let e=this.schedule.popNextFromQueue(this._layoutDurations,this._queueOptions);if(e){let t=o(e.layoutId);if(!this.isLayoutBlacklisted(t))return{layoutId:t,layoutFile:e.layoutId}}}g.warn(`All queued layouts are blacklisted, using current entry as fallback`)}return{layoutId:t,layoutFile:e.layoutId}}peekNextLayout(){let e=this.schedule.peekNextInQueue(this._layoutDurations,this._queueOptions);if(!e)return null;let t=o(e.layoutId);if(t===this.currentLayoutId){let e=this.schedule.peekAfterNext(this._layoutDurations,this._queueOptions);if(!e)return null;let t=o(e.layoutId);return t===this.currentLayoutId||this.isLayoutBlacklisted(t)?null:{layoutId:t,layoutFile:e.layoutId}}return this.isLayoutBlacklisted(t)?null:{layoutId:t,layoutFile:e.layoutId}}advanceToNextLayout(){if(this._layoutOverride){g.info(`Layout override active, not advancing schedule`);return}let e=this.getNextLayout();if(!e){if(this.currentLayoutId){g.info(`No layouts in queue, replaying ${this.currentLayoutId} to avoid blank screen`);let e=this.currentLayoutId;this.currentLayoutId=null,this._preparingLayoutId=e,this.emit(h.LAYOUT_PREPARE_REQUEST,e)}else g.info(`No layouts scheduled during advance`),this.emit(h.NO_LAYOUTS_SCHEDULED);return}let{layoutId:t,layoutFile:n}=e,r=this._layoutDurations.get(n)||`?`;if(this._lastTimeline&&this._lastTimeline.length>0){let e=this._lastTimeline.slice(0,2).map(e=>{let t=e.startTime.toLocaleTimeString(`en-GB`,{hour:`2-digit`,minute:`2-digit`,second:`2-digit`});return`${e.layoutFile}(${e.duration}s@${t})`});g.debug(`[Timeline] Layout transition: entering ${n} (${r}s), overlay top: [${e.join(`, `)}]`),this._lastTimeline[0].layoutFile!==n&&g.warn(`[Timeline] Mismatch: entering ${n} but overlay expects ${this._lastTimeline[0].layoutFile}`)}else g.debug(`[Timeline] Layout transition: entering ${n} (${r}s), no timeline data`);if(this.syncManager&&this.schedule.isSyncEvent(n))if(this.isSyncLead()){g.info(`[Sync] Lead requesting coordinated layout change: ${t}`),this._preparingLayoutId=t,this.emit(h.LAYOUT_PREPARE_REQUEST,t),this.syncManager.requestLayoutChange(t).catch(e=>{g.error(`[Sync] Layout change failed:`,e)});return}else if(this.syncManager.transport?.connected){g.info(`[Sync] Follower waiting for lead signal (not advancing independently)`);return}else g.warn(`[Sync] Follower: lead unreachable, advancing independently`);t===this.currentLayoutId&&(g.info(`Next layout ${t} is same as current, triggering replay`),this.currentLayoutId=null);let{queue:i}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions),a=this.schedule.getQueuePosition();g.info(`Advancing to layout ${t} (queue pos ${a}/${i.length})`),this._preparingLayoutId=t,this.emit(h.LAYOUT_PREPARE_REQUEST,t)}advanceToPreviousLayout(){if(this._layoutOverride){g.info(`Layout override active, not going back`);return}let{queue:e}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions);if(e.length<=1){g.info(`Single or empty queue, nothing to go back to`);return}let t=this.schedule.rewindQueue(2,this._layoutDurations,this._queueOptions);if(!t)return;let n=o(t.layoutId);if(n===this.currentLayoutId){g.info(`Previous layout is same as current, nothing to go back to`);return}g.info(`Going back to layout ${n}`),this.emit(h.LAYOUT_PREPARE_REQUEST,n)}notifyMediaReady(e,t=`media`){g.debug(`File ${e} ready (${t})`);for(let[n,r]of this.pendingLayouts.entries()){let i=t===`layout`&&n===parseInt(e),a=t===`media`&&r.includes(e);(i||a)&&(g.debug(`${t} ${e} was needed by pending layout ${n}, checking if ready...`),this.emit(h.CHECK_PENDING_LAYOUT,n,r))}}async notifyLayoutStatus(e){try{let t={currentLayoutId:e,deviceName:this.config?.displayName||``,displayName:this.config?.displayName||``,lastCommandSuccess:this._lastCommandSuccess??!0,code:this._statusCode,lastLayoutChangeTime:this._lastLayoutChangeTime||new Date().toISOString()};this.config?.latitude&&(t.latitude=this.config.latitude),this.config?.longitude&&(t.longitude=this.config.longitude),this._lanIpAddress&&(t.lanIpAddress=this._lanIpAddress),await this.xmds.notifyStatus(t),this.emit(`status-notified`,e)}catch(t){g.warn(`Failed to notify status:`,t),this.emit(`status-notify-failed`,e,t)}}reportGeoLocation(e){let t=parseFloat(e?.latitude),n=parseFloat(e?.longitude);if(isNaN(t)||isNaN(n)){g.warn(`reportGeoLocation: invalid coordinates`,e);return}g.info(`Geo location from CMS: ${t.toFixed(4)}, ${n.toFixed(4)}`),this.schedule?.setLocation&&this.schedule.setLocation(t,n),this.emit(`location-updated`,{latitude:t,longitude:n,source:`cms`}),this.checkSchedule()}async requestGeoLocation(){if(this._geoCache&&Date.now()-this._geoCache.ts<1800*1e3)return this._geoCache.location;if(!this._browserGeoFailed){let e=await this._tryBrowserGeolocation();if(e)return this._cacheGeo(this._applyLocation(e.latitude,e.longitude,`browser`));this._browserGeoFailed=!0}let e=this.config?.googleGeoApiKey;if(e){let t=await this._tryGoogleGeolocation(e);if(t)return this._cacheGeo(this._applyLocation(t.latitude,t.longitude,`google-api`))}let t=await this._tryIpGeolocation();return t?this._cacheGeo(this._applyLocation(t.latitude,t.longitude,`ip-geolocation`)):(g.warn(`All geolocation methods failed`),null)}_cacheGeo(e){return this._geoCache={location:e,ts:Date.now()},e}_applyTagConfig(e){if(!Array.isArray(e)||e.length===0)return;let t={geoApiKey:`googleGeoApiKey`};for(let n of e){let e=n.indexOf(`|`);if(e===-1)continue;let r=n.substring(0,e),i=n.substring(e+1),a=t[r];a&&i&&this.config&&(g.info(`Config from CMS tag: ${r} → ${a}`),this.config[a]=i)}}_applyLocation(e,t,n){return g.info(`Geolocation (${n}): ${e.toFixed(4)}, ${t.toFixed(4)}`),this.schedule?.setLocation&&this.schedule.setLocation(e,t),this.emit(`location-updated`,{latitude:e,longitude:t,source:n}),this.checkSchedule(),{latitude:e,longitude:t}}async _tryBrowserGeolocation(){if(typeof navigator>`u`||!navigator.geolocation)return null;try{let e=await new Promise((e,t)=>{navigator.geolocation.getCurrentPosition(e,t,{timeout:1e4,maximumAge:3e5,enableHighAccuracy:!1})});return{latitude:e.coords.latitude,longitude:e.coords.longitude}}catch(e){return g.warn(`Browser geolocation failed:`,e?.message||e),null}}async _tryGoogleGeolocation(e){try{let t=await fetch(`https://www.googleapis.com/geolocation/v1/geolocate?key=${e}`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({considerIp:!0}),signal:AbortSignal.timeout(5e3)});if(!t.ok)return g.warn(`Google Geolocation API returned ${t.status}`),null;let n=await t.json();return n.location?.lat!=null&&n.location?.lng!=null?{latitude:n.location.lat,longitude:n.location.lng}:null}catch(e){return g.warn(`Google Geolocation API failed:`,e?.message||e),null}}async _tryIpGeolocation(){let e=[{url:`https://ipapi.co/json/`,parse:e=>e.latitude!=null&&e.longitude!=null?{latitude:e.latitude,longitude:e.longitude}:null},{url:`https://freeipapi.com/api/json`,parse:e=>e.latitude!=null&&e.longitude!=null?{latitude:e.latitude,longitude:e.longitude}:null}];for(let t of e)try{let e=await fetch(t.url,{signal:AbortSignal.timeout(5e3)});if(!e.ok)continue;let n=await e.json(),r=t.parse(n);if(r)return r}catch(e){g.warn(`IP geolocation (${t.url}) failed:`,e?.message||e)}return null}checkSchedule(){let e=this.schedule.getCurrentLayouts();this.emit(h.LAYOUTS_SCHEDULED,e),this._evaluateAndSwitchLayout(e,``)}async captureScreenshot(){g.info(`Screenshot requested`),this.emit(h.SCREENSHOT_REQUEST)}async changeLayout(e,t){g.info(`Layout change requested via XMR:`,e);let n=parseInt(e,10),r=t?.duration||0;this._layoutOverride={layoutId:n,type:`change`,duration:r,changeMode:t?.changeMode||`replace`},this.currentLayoutId=null,this.emit(h.LAYOUT_PREPARE_REQUEST,n),this._scheduleAutoRevert(n,r,`Layout override`)}async overlayLayout(e,t){g.info(`Overlay layout requested via XMR:`,e);let n=parseInt(e,10),r=t?.duration||0;this._layoutOverride={layoutId:n,type:`overlay`,duration:r},this.emit(h.OVERLAY_LAYOUT_REQUEST,n),this._scheduleAutoRevert(n,r,`Overlay`)}async revertToSchedule(){g.info(`Reverting to scheduled content`),this._layoutOverride=null,this.currentLayoutId=null,this.emit(h.REVERT_TO_SCHEDULE);let e=this.schedule.getCurrentLayouts();if(e.length>0){let t=e[0],n=o(t);this.emit(h.LAYOUT_PREPARE_REQUEST,n)}else this.emit(h.NO_LAYOUTS_SCHEDULED)}async purgeAll(){return g.info(`Purge all cache requested via XMR`),this._lastCheckRf=null,this._lastCheckSchedule=null,this.emit(h.PURGE_ALL_REQUEST),this.collectNow()}async executeCommand(e,t){if(g.info(`Execute command requested:`,e),!t||!t[e]){g.warn(`Unknown command code:`,e),this._lastCommandSuccess=!1,this.emit(h.COMMAND_RESULT,{code:e,success:!1,reason:`Unknown command`});return}let n=t[e],r=n.commandString||n.value||``;if(r.startsWith(`http|`)){let t=r.split(`|`),n=t[1],i=t[2]||`application/json`;try{let t=await fetch(n,{method:`POST`,headers:{"Content-Type":i},signal:AbortSignal.timeout(1e4)}),r=t.ok;this._lastCommandSuccess=r,g.info(`HTTP command ${e} result: ${t.status}`),this.emit(h.COMMAND_RESULT,{code:e,success:r,status:t.status})}catch(t){this._lastCommandSuccess=!1,g.error(`HTTP command ${e} failed:`,t),this.emit(h.COMMAND_RESULT,{code:e,success:!1,reason:t.message})}}else g.info(`Delegating non-HTTP command to platform layer:`,e),this.emit(h.EXECUTE_NATIVE_COMMAND,{code:e,commandString:r})}triggerWebhook(e){g.info(`Webhook trigger from XMR:`,e),this.handleTrigger(e)}refreshDataConnectors(){g.info(`Data connector refresh requested via XMR`),this.dataConnectorManager.refreshAll(),this.emit(`data-connectors-refreshed`)}async submitMediaInventory(e){if(!(!e||e.length===0))try{let t=Math.floor(Date.now()/1e3),n=`<files>${e.filter(e=>[`media`,`layout`,`resource`,`dependency`,`widget`].includes(e.type)).map(e=>{let n=e.complete===void 0||e.complete?`1`:`0`,r=e.fileType?` fileType="${e.fileType}"`:``;return`<file type="${e.type}" id="${e.id}" complete="${n}" md5="${e.md5||``}" lastChecked="${t}"${r}/>`}).join(``)}</files>`;await this.xmds.mediaInventory(n),g.info(`Media inventory submitted: ${e.length} files`),this.emit(`media-inventory-submitted`,e.length)}catch(e){g.warn(`MediaInventory submission failed:`,e)}}async blackList(e,t,n){try{await this.xmds.blackList(e,t,n),this.emit(`media-blacklisted`,{mediaId:e,type:t,reason:n})}catch(e){g.warn(`BlackList failed:`,e)}}reportLayoutFailure(e,t){let n=Number(e);this._statusCode=3;let{blacklisted:r,failures:i}=this._layoutBlacklist.recordFailure(n,t);r&&i===3&&(this.emit(`layout-blacklisted`,{layoutId:n,reason:t,failures:i}),this.blackList(n,`layout`,t))}reportLayoutSuccess(e){this._layoutBlacklist.recordSuccess(Number(e))&&this.emit(`layout-unblacklisted`,{layoutId:Number(e)})}isLayoutBlacklisted(e){return this._layoutBlacklist.isBlacklisted(e)}getBlacklistedLayouts(){return this._layoutBlacklist.getBlacklistedIds()}resetBlacklist(){this._layoutBlacklist.reset()>0&&this.emit(`blacklist-reset`)}isLayoutOverridden(){return this._layoutOverride!==null}handleTrigger(e){let t=this.schedule.findActionByTrigger(e);if(!t){g.debug(`No scheduled action matches trigger:`,e);return}switch(g.info(`Action triggered: ${t.actionType} (trigger: ${e})`),t.actionType){case`navLayout`:case`navigateToLayout`:t.layoutCode&&this.changeLayout(t.layoutCode);break;case`navWidget`:case`navigateToWidget`:this.emit(h.NAVIGATE_TO_WIDGET,t);break;case`command`:this.emit(`execute-command`,t.commandCode);break;default:g.warn(`Unknown action type:`,t.actionType)}}updateDataConnectors(){let e=this.schedule.getDataConnectors();e.length>0&&g.info(`Configuring ${e.length} data connector(s)`),this.dataConnectorManager.setConnectors(e),e.length>0&&(this.dataConnectorManager.startPolling(),this.emit(`data-connectors-started`,e.length))}_processScheduledCommands(){if(!this.schedule?.getCommands)return;let e=this.schedule.getCommands();if(e.length===0)return;let t=new Date;for(let n of e){if(!n.code||!n.date)continue;let e=`${n.code}|${n.date}`;if(this._executedCommands.has(e))continue;let r=new Date(n.date);if(isNaN(r.getTime())){g.warn(`Scheduled command has invalid date:`,n.date);continue}t>=r&&(g.info(`Executing scheduled command: ${n.code} (scheduled: ${n.date})`),this._executedCommands.add(e),n.code===`collectNow`?setTimeout(()=>this.collectNow().catch(e=>g.error(`collectNow command failed:`,e)),0):this.emit(h.SCHEDULED_COMMAND,n))}}async _fetchWeatherData(){if(!(!this.xmds?.getWeather||!this.schedule?.setWeatherData))try{let e=await this.xmds.getWeather(),t=typeof e==`string`?JSON.parse(e):e;this.schedule.setWeatherData(t),g.info(`Weather data updated:`,Object.keys(t).join(`, `))}catch(e){g.warn(`GetWeather failed (non-critical):`,e?.message||e)}}getDataConnectorManager(){return this.dataConnectorManager}setSyncManager(e){this.syncManager=e,g.info(`SyncManager attached:`,e.isLead?`LEAD`:`FOLLOWER`)}isInSyncGroup(){return this.syncConfig!==null}isSyncLead(){return this.syncConfig?.isLead===!0}getSyncConfig(){return this.syncConfig}logUpcomingTimeline(){if(!this.schedule.getLayoutsAtTime)return;let e=[...this._layoutDurations.entries()].sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${e}:${t}`).join(`|`),t=[...this._layoutMediaStatus.entries()].sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${e}:${t.ready}:${t.missingKey}`).join(`|`),n=[...this.pendingLayouts.keys()].sort().join(`,`),r=this.schedule.getQueuePosition()||0,i=`${this._lastCheckSchedule}|${e}|${this.currentLayoutId}|${r}|${t}|${n}`;if(i===this._lastTimelineFingerprint&&this._lastTimeline){this.emit(h.TIMELINE_UPDATED,this._lastTimeline);return}let{queue:a}=this.schedule.getScheduleQueue(this._layoutDurations,this._queueOptions),o=s(a,this.schedule.getQueuePosition(),{currentLayoutStartedAt:this._lastLayoutChangeTime?new Date(this._lastLayoutChangeTime):null,defaultLayout:this.schedule.schedule?.default||null,durations:this._layoutDurations});if(o.length===0)return;for(let e of o){let t=parseInt(e.layoutFile.replace(`.xlf`,``),10),n=this.pendingLayouts.get(t);if(n&&n.length>0)e.missingMedia=n.map(String);else{let t=this._layoutMediaStatus.get(e.layoutFile);t&&!t.ready&&t.missing.length>0&&(e.missingMedia=t.missing.map(String))}}this._lastTimelineFingerprint=i,this._lastTimeline=o;let c=o.slice(0,20).map(e=>{let t=e.startTime.toLocaleTimeString(`en-GB`,{hour:`2-digit`,minute:`2-digit`,second:`2-digit`}),n=e.endTime.toLocaleTimeString(`en-GB`,{hour:`2-digit`,minute:`2-digit`,second:`2-digit`}),r=e.missingMedia?` [MISSING: ${e.missingMedia.length} files]`:``;return` ${t}-${n} Layout ${e.layoutFile} (${e.duration}s)${e.isDefault?` [default]`:``}${r}`});for(let e of o)e.missingMedia&&g.warn(`[Timeline] Layout ${e.layoutFile}: ${e.missingMedia.length} files missing`);g.info(`[Timeline] Next ${o.length} plays:\n${c.join(`
2
+ `)}`),this.emit(h.TIMELINE_UPDATED,o)}setLayoutMediaStatus(e,t,n=[]){let r=this._layoutMediaStatus.get(e),i=n.slice().sort().join(`,`);r&&r.ready===t&&r.missingKey===i||(this._layoutMediaStatus.set(e,{ready:t,missing:n,missingKey:i}),this._lastTimelineFingerprint=null)}recordLayoutDuration(e,t,n=!1){let r=String(e).replace(`.xlf`,``),i=r+`.xlf`;if(this._finalDurations.has(r))return;let a=this._layoutDurations.get(e);a===t&&!n||(this._layoutDurations.set(r,t),this._layoutDurations.set(i,t),n&&(this._finalDurations.add(r),this._finalDurations.add(i)),g.debug(`[Timeline] Duration corrected: layout ${e} ${a||`?`}s → ${t}s${n?` (final)`:``}`),this.schedule.invalidateQueue(),this._timelineRecalcTimer&&clearTimeout(this._timelineRecalcTimer),this._timelineRecalcTimer=setTimeout(()=>{this._timelineRecalcTimer=null,this.logUpcomingTimeline(),this._offlineSave(`durations`,[...this._layoutDurations.entries()]),this._offlineSave(`finalDurations`,[...this._finalDurations]),this._offlineSave(`durationsVersion`,2)},500))}cleanup(){this.collectionInterval&&=(clearInterval(this.collectionInterval),null),this._faultReportingInterval&&=(clearInterval(this._faultReportingInterval),null),this._timelineRecalcTimer&&=(clearTimeout(this._timelineRecalcTimer),null),this.xmr&&=(this.xmr.stop(),null),this.syncManager&&=(this.syncManager.stop(),null),this.dataConnectorManager.cleanup(),this.emit(`cleanup-complete`),this.removeAllListeners()}getCurrentLayoutId(){return this.currentLayoutId}getLayoutDuration(e){let t=String(e);return this._layoutDurations.get(`${t}.xlf`)||this._layoutDurations.get(t)}isCollecting(){return this.collecting}getPendingLayouts(){return Array.from(this.pendingLayouts.keys())}},C=c.version;export{f as i,S as n,h as r,C as t};
3
+ //# sourceMappingURL=src-CZ1k5h23.js.map