@xiboplayer/pwa 0.7.18 → 0.7.20
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.
- package/dist/assets/main-BqJjhM5z.js +3 -0
- package/dist/assets/{main-CkREX2X6.js.map → main-BqJjhM5z.js.map} +1 -1
- package/dist/assets/{main-BG2cZsg-.js → main-C8f1ieZL.js} +6 -6
- package/dist/assets/main-C8f1ieZL.js.map +1 -0
- package/dist/assets/{setup-COzyFqMn.js → setup-Dpox3R4b.js} +2 -2
- package/dist/assets/{setup-COzyFqMn.js.map → setup-Dpox3R4b.js.map} +1 -1
- package/dist/assets/{src-M1enQEwh.js → src-B0l-hkrA.js} +2 -2
- package/dist/assets/{src-M1enQEwh.js.map → src-B0l-hkrA.js.map} +1 -1
- package/dist/assets/{src-BV-4JdnK.js → src-BLwxK97o.js} +2 -2
- package/dist/assets/{src-BV-4JdnK.js.map → src-BLwxK97o.js.map} +1 -1
- package/dist/assets/{src-C3Sg89t9.js → src-Bzheh5E2.js} +2 -2
- package/dist/assets/{src-C3Sg89t9.js.map → src-Bzheh5E2.js.map} +1 -1
- package/dist/assets/{src-BYVnjdc0.js → src-CIERor10.js} +2 -2
- package/dist/assets/{src-BYVnjdc0.js.map → src-CIERor10.js.map} +1 -1
- package/dist/assets/{src-BF_sMbmn.js → src-Cvcg0wpA.js} +2 -2
- package/dist/assets/{src-BF_sMbmn.js.map → src-Cvcg0wpA.js.map} +1 -1
- package/dist/assets/{src-IJdgG16K.js → src-DTJp4-F0.js} +2 -2
- package/dist/assets/{src-IJdgG16K.js.map → src-DTJp4-F0.js.map} +1 -1
- package/dist/assets/{src-BA9Y85MN.js → src-D_pit2UZ.js} +2 -2
- package/dist/assets/{src-BA9Y85MN.js.map → src-D_pit2UZ.js.map} +1 -1
- package/dist/assets/{src-BdgQ2CiL.js → src-DzgDstdA.js} +2 -2
- package/dist/assets/{src-BdgQ2CiL.js.map → src-DzgDstdA.js.map} +1 -1
- package/dist/assets/{src-CfCGHb7u.js → src-GjeUdI7b.js} +2 -2
- package/dist/assets/{src-CfCGHb7u.js.map → src-GjeUdI7b.js.map} +1 -1
- package/dist/assets/{src-DnPIj2iO.js → src-e2yErlKg.js} +2 -2
- package/dist/assets/{src-DnPIj2iO.js.map → src-e2yErlKg.js.map} +1 -1
- package/dist/assets/{sync-manager-C68deMxf.js → sync-manager-B6kqa4bv.js} +2 -2
- package/dist/assets/{sync-manager-C68deMxf.js.map → sync-manager-B6kqa4bv.js.map} +1 -1
- package/dist/index.html +1 -1
- package/dist/setup.html +5 -5
- package/dist/sw-pwa.js +2 -2
- package/dist/sw-pwa.js.map +1 -1
- package/package.json +13 -13
- package/dist/assets/main-BG2cZsg-.js.map +0 -1
- package/dist/assets/main-CkREX2X6.js +0 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup-COzyFqMn.js","names":[],"sources":["../../setup.html?html-proxy&index=1.js"],"sourcesContent":["\n import { config } from '@xiboplayer/utils';\n import { RestClient, XmdsClient } from '@xiboplayer/xmds';\n\n // Inject version from Vite build\n const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '?';\n document.getElementById('version-footer').textContent = `xiboplayer v${appVersion}`;\n\n // ── Elements ──\n const form = document.getElementById('setup-form');\n const errorEl = document.getElementById('error');\n const submitBtn = document.getElementById('submit-btn');\n const phaseUnlock = document.getElementById('phase-unlock');\n const phaseSetup = document.getElementById('phase-setup');\n const phaseWaiting = document.getElementById('phase-waiting');\n const countdownEl = document.getElementById('countdown');\n const waitingNameEl = document.getElementById('waiting-display-name');\n const btnBack = document.getElementById('btn-back');\n const successFlash = document.getElementById('success-flash');\n\n // Show hardware key\n const hwKey = config.hardwareKey;\n document.getElementById('hw-key-display').textContent = hwKey;\n document.getElementById('hw-key-waiting').textContent = hwKey;\n\n // ── State ──\n const previousCmsUrl = config.cmsUrl || '';\n let pollTimer = null;\n let countdownTimer = null;\n let pollSeconds = 15;\n\n // ── Transport: try REST first, fall back to SOAP ──\n let transport = null;\n async function getTransport() {\n if (transport) return transport;\n try {\n const rest = new RestClient(config);\n await rest.registerDisplay();\n console.log('[Setup] Using REST transport');\n transport = rest;\n } catch (e) {\n console.log('[Setup] REST unavailable, using XMDS/SOAP:', e.message);\n transport = new XmdsClient(config);\n }\n return transport;\n }\n\n // ── Helpers ──\n function showPhase(phase) {\n phaseUnlock.classList.remove('active');\n phaseSetup.classList.remove('active');\n phaseWaiting.classList.remove('active');\n phase.classList.add('active');\n }\n\n function showError(msg) {\n errorEl.textContent = msg;\n errorEl.style.display = 'block';\n }\n\n function hideError() {\n errorEl.style.display = 'none';\n }\n\n function startCountdown() {\n let remaining = pollSeconds;\n countdownEl.textContent = remaining;\n clearInterval(countdownTimer);\n countdownTimer = setInterval(() => {\n remaining--;\n countdownEl.textContent = Math.max(0, remaining);\n if (remaining <= 0) clearInterval(countdownTimer);\n }, 1000);\n }\n\n async function showSuccess() {\n // If CMS URL changed, wipe all cached content (media, layouts, widgets\n // from the old CMS are useless) and the SW's cached index.html (which\n // may have a stale config injection script).\n const newUrl = document.getElementById('cms-url').value.trim().replace(/\\/$/, '');\n if (newUrl !== previousCmsUrl) {\n console.log('[Setup] CMS URL changed — wiping cached content');\n const cacheNames = await caches.keys();\n await Promise.all(cacheNames.map(name => caches.delete(name)));\n }\n successFlash.classList.add('active');\n setTimeout(() => {\n window.location.href = '/player/index.html' + window.location.search;\n }, 800);\n }\n\n // ── Authorization Polling ──\n async function checkAuthorization() {\n try {\n const client = await getTransport();\n const result = await client.registerDisplay();\n console.log('[Setup] Poll result:', result.code, result.message);\n\n if (result.code === 'READY') {\n // Authorized!\n clearInterval(pollTimer);\n clearInterval(countdownTimer);\n showSuccess();\n return;\n }\n\n // Still waiting — restart countdown\n startCountdown();\n } catch (error) {\n console.warn('[Setup] Poll failed:', error.message);\n startCountdown();\n }\n }\n\n function startPolling(displayName) {\n waitingNameEl.textContent = displayName;\n showPhase(phaseWaiting);\n startCountdown();\n pollTimer = setInterval(checkAuthorization, pollSeconds * 1000);\n }\n\n function stopPolling() {\n clearInterval(pollTimer);\n clearInterval(countdownTimer);\n pollTimer = null;\n }\n\n // ── Auto-authorize via CMS API (through /api/* forward proxy) ──\n async function tryAutoAuthorize(cmsUrl, hardwareKey) {\n const clientId = document.getElementById('api-client-id').value.trim();\n const clientSecret = document.getElementById('api-client-secret').value.trim();\n if (!clientId || !clientSecret) return false;\n\n try {\n // Step 1: OAuth2 token via proxy\n const tokenResp = await fetch('/api/authorize/access_token', {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({ grant_type: 'client_credentials', client_id: clientId, client_secret: clientSecret }),\n });\n if (!tokenResp.ok) throw new Error(`OAuth2 failed: ${tokenResp.status}`);\n const { access_token } = await tokenResp.json();\n\n // Step 2: Find display by hardwareKey via proxy\n const displayResp = await fetch(\n `/api/display?hardwareKey=${encodeURIComponent(hardwareKey)}`,\n { headers: { Authorization: `Bearer ${access_token}` } }\n );\n if (!displayResp.ok) throw new Error(`Find display failed: ${displayResp.status}`);\n const displays = await displayResp.json();\n if (!displays.length) { console.log('[Setup] Display not found via API (may not be registered yet)'); return false; }\n\n const display = displays[0];\n if (display.licensed === 1) { console.log('[Setup] Display already authorized'); return true; }\n\n // Step 3: Authorize display via proxy\n const authResp = await fetch(\n `/api/display/authorise/${display.displayId}`,\n { method: 'PUT', headers: { Authorization: `Bearer ${access_token}` } }\n );\n if (!authResp.ok) throw new Error(`Authorize failed: ${authResp.status}`);\n\n console.log('[Setup] Display auto-authorized via proxy!');\n config.data.apiClientId = clientId;\n config.data.apiClientSecret = clientSecret;\n return true;\n } catch (error) {\n console.warn('[Setup] Auto-authorize failed:', error.message);\n return false;\n }\n }\n\n // ── Unlock gate for reconfiguration ──\n const isReconfigure = config.isConfigured() && config.cmsKey;\n const alreadyUnlocked = new URLSearchParams(window.location.search).has('unlocked');\n const btnBackPlayer = document.getElementById('btn-back-player');\n if (isReconfigure && !alreadyUnlocked) {\n // Show unlock phase instead of setup form\n showPhase(phaseUnlock);\n }\n if (isReconfigure || alreadyUnlocked) {\n // Show \"Back to Player\" on the setup form\n btnBackPlayer.style.display = 'block';\n // Pre-fill form with existing config\n document.getElementById('cms-url').value = config.cmsUrl;\n document.getElementById('cms-key').value = config.cmsKey;\n document.getElementById('display-name').value = config.displayName;\n if (config.data.apiClientId) {\n document.getElementById('api-client-id').value = config.data.apiClientId;\n document.getElementById('api-client-secret').value = config.data.apiClientSecret || '';\n }\n }\n\n // ── Back to Player (cancel reconfiguration) ──\n function goBackToPlayer() {\n stopPolling();\n window.location.href = '/player/index.html' + window.location.search;\n }\n btnBackPlayer.addEventListener('click', goBackToPlayer);\n\n // Esc also goes back to player when reconfiguring\n if (isReconfigure || alreadyUnlocked) {\n document.addEventListener('keydown', (e) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n goBackToPlayer();\n }\n });\n }\n\n // ── Unlock form handler ──\n document.getElementById('unlock-form').addEventListener('submit', (e) => {\n e.preventDefault();\n const unlockError = document.getElementById('unlock-error');\n const enteredKey = document.getElementById('unlock-key').value.trim();\n\n if (enteredKey === config.cmsKey) {\n unlockError.style.display = 'none';\n // Pre-fill form with existing config\n document.getElementById('cms-url').value = config.cmsUrl;\n document.getElementById('cms-key').value = config.cmsKey;\n document.getElementById('display-name').value = config.displayName;\n if (config.data.apiClientId) {\n document.getElementById('api-client-id').value = config.data.apiClientId;\n document.getElementById('api-client-secret').value = config.data.apiClientSecret || '';\n }\n showPhase(phaseSetup);\n } else {\n unlockError.textContent = 'Incorrect CMS key';\n unlockError.style.display = 'block';\n }\n });\n\n // ── Pre-fill if first-time setup (no unlock gate) ──\n if (!isReconfigure && config.isConfigured()) {\n document.getElementById('cms-url').value = config.cmsUrl;\n document.getElementById('cms-key').value = config.cmsKey;\n document.getElementById('display-name').value = config.displayName;\n if (config.data.apiClientId) {\n document.getElementById('api-client-id').value = config.data.apiClientId;\n document.getElementById('api-client-secret').value = config.data.apiClientSecret || '';\n }\n }\n\n // ── Form Submit ──\n form.addEventListener('submit', async (e) => {\n e.preventDefault();\n hideError();\n\n const cmsUrl = document.getElementById('cms-url').value.trim().replace(/\\/$/, '');\n const cmsKey = document.getElementById('cms-key').value.trim();\n const displayName = document.getElementById('display-name').value.trim();\n\n // Write config.json via proxy (master config file)\n // The boot path (proxy injection → Config.load()) handles the rest.\n try {\n submitBtn.textContent = 'Saving...';\n submitBtn.disabled = true;\n\n // Always POST to proxy — updates in-memory config for REST auth,\n // cache-through, and API forwarding. Works on all players.\n const saveResp = await fetch('/config', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ cmsUrl, cmsKey, displayName }),\n });\n if (!saveResp.ok) throw new Error('Failed to save config');\n\n // Shell persistence (Electron config.json) — optional, on top of proxy\n const electronAPI = window.electronAPI || window.parent?.electronAPI;\n if (electronAPI?.setConfig) {\n await electronAPI.setConfig({ cmsUrl, cmsKey, displayName });\n }\n\n // Also update in-memory config for the test connection below\n config.data.cmsUrl = cmsUrl;\n config.data.cmsKey = cmsKey;\n config.data.displayName = displayName;\n config.save();\n\n // Backup hardware key to IndexedDB for stability\n backupHardwareKey(config.hardwareKey);\n\n // Test connection\n submitBtn.textContent = 'Connecting...';\n\n // Try REST first, fall back to SOAP\n transport = null; // reset transport for fresh detection\n const client = await getTransport();\n const result = await client.registerDisplay();\n\n if (result.code === 'READY') {\n showSuccess();\n } else {\n // Try auto-authorize via CMS API if credentials provided\n submitBtn.textContent = 'Authorizing...';\n const autoAuthed = await tryAutoAuthorize(cmsUrl, hwKey);\n\n if (autoAuthed) {\n // Persist API client credentials so they survive restarts\n config.save();\n await fetch('/config', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ apiClientId: config.data.apiClientId, apiClientSecret: config.data.apiClientSecret }),\n });\n if (electronAPI?.setConfig) {\n await electronAPI.setConfig({ apiClientId: config.data.apiClientId, apiClientSecret: config.data.apiClientSecret });\n }\n\n // Verify registration\n const recheck = await client.registerDisplay();\n if (recheck.code === 'READY') {\n showSuccess();\n return;\n }\n }\n\n // Not authorized yet — switch to polling phase\n startPolling(displayName);\n }\n } catch (error) {\n showError(`Connection failed: ${error.message}`);\n submitBtn.textContent = 'Connect';\n submitBtn.disabled = false;\n }\n });\n\n // ── Back button ──\n btnBack.addEventListener('click', () => {\n stopPolling();\n showPhase(phaseSetup);\n submitBtn.textContent = 'Connect';\n submitBtn.disabled = false;\n });\n\n // ── IndexedDB hardware key backup ──\n async function backupHardwareKey(key) {\n try {\n const req = indexedDB.open('xibo-hw-backup', 1);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains('keys')) {\n db.createObjectStore('keys');\n }\n };\n req.onsuccess = () => {\n const db = req.result;\n const tx = db.transaction('keys', 'readwrite');\n tx.objectStore('keys').put(key, 'hardwareKey');\n tx.oncomplete = () => db.close();\n };\n } catch (e) {\n console.warn('[Setup] IndexedDB backup failed:', e);\n }\n }\n\n // ── Enable submit ──\n submitBtn.disabled = false;\n submitBtn.textContent = 'Connect';\n\n // ── Auto-resume polling if already registered but not authorized ──\n // Skip auto-resume when reconfiguring (user explicitly opened setup)\n if (config.isConfigured() && !isReconfigure) {\n (async () => {\n try {\n const client = await getTransport();\n const result = await client.registerDisplay();\n if (result.code === 'READY') {\n window.location.href = '/player/index.html' + window.location.search;\n } else {\n // Not authorized — go straight to polling\n startPolling(config.displayName);\n }\n } catch (e) {\n // Connection failed, show form\n console.log('[Setup] Auto-check failed, showing form');\n }\n })();\n }\n "],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"setup-Dpox3R4b.js","names":[],"sources":["../../setup.html?html-proxy&index=1.js"],"sourcesContent":["\n import { config } from '@xiboplayer/utils';\n import { RestClient, XmdsClient } from '@xiboplayer/xmds';\n\n // Inject version from Vite build\n const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '?';\n document.getElementById('version-footer').textContent = `xiboplayer v${appVersion}`;\n\n // ── Elements ──\n const form = document.getElementById('setup-form');\n const errorEl = document.getElementById('error');\n const submitBtn = document.getElementById('submit-btn');\n const phaseUnlock = document.getElementById('phase-unlock');\n const phaseSetup = document.getElementById('phase-setup');\n const phaseWaiting = document.getElementById('phase-waiting');\n const countdownEl = document.getElementById('countdown');\n const waitingNameEl = document.getElementById('waiting-display-name');\n const btnBack = document.getElementById('btn-back');\n const successFlash = document.getElementById('success-flash');\n\n // Show hardware key\n const hwKey = config.hardwareKey;\n document.getElementById('hw-key-display').textContent = hwKey;\n document.getElementById('hw-key-waiting').textContent = hwKey;\n\n // ── State ──\n const previousCmsUrl = config.cmsUrl || '';\n let pollTimer = null;\n let countdownTimer = null;\n let pollSeconds = 15;\n\n // ── Transport: try REST first, fall back to SOAP ──\n let transport = null;\n async function getTransport() {\n if (transport) return transport;\n try {\n const rest = new RestClient(config);\n await rest.registerDisplay();\n console.log('[Setup] Using REST transport');\n transport = rest;\n } catch (e) {\n console.log('[Setup] REST unavailable, using XMDS/SOAP:', e.message);\n transport = new XmdsClient(config);\n }\n return transport;\n }\n\n // ── Helpers ──\n function showPhase(phase) {\n phaseUnlock.classList.remove('active');\n phaseSetup.classList.remove('active');\n phaseWaiting.classList.remove('active');\n phase.classList.add('active');\n }\n\n function showError(msg) {\n errorEl.textContent = msg;\n errorEl.style.display = 'block';\n }\n\n function hideError() {\n errorEl.style.display = 'none';\n }\n\n function startCountdown() {\n let remaining = pollSeconds;\n countdownEl.textContent = remaining;\n clearInterval(countdownTimer);\n countdownTimer = setInterval(() => {\n remaining--;\n countdownEl.textContent = Math.max(0, remaining);\n if (remaining <= 0) clearInterval(countdownTimer);\n }, 1000);\n }\n\n async function showSuccess() {\n // If CMS URL changed, wipe all cached content (media, layouts, widgets\n // from the old CMS are useless) and the SW's cached index.html (which\n // may have a stale config injection script).\n const newUrl = document.getElementById('cms-url').value.trim().replace(/\\/$/, '');\n if (newUrl !== previousCmsUrl) {\n console.log('[Setup] CMS URL changed — wiping cached content');\n const cacheNames = await caches.keys();\n await Promise.all(cacheNames.map(name => caches.delete(name)));\n }\n successFlash.classList.add('active');\n setTimeout(() => {\n window.location.href = '/player/index.html' + window.location.search;\n }, 800);\n }\n\n // ── Authorization Polling ──\n async function checkAuthorization() {\n try {\n const client = await getTransport();\n const result = await client.registerDisplay();\n console.log('[Setup] Poll result:', result.code, result.message);\n\n if (result.code === 'READY') {\n // Authorized!\n clearInterval(pollTimer);\n clearInterval(countdownTimer);\n showSuccess();\n return;\n }\n\n // Still waiting — restart countdown\n startCountdown();\n } catch (error) {\n console.warn('[Setup] Poll failed:', error.message);\n startCountdown();\n }\n }\n\n function startPolling(displayName) {\n waitingNameEl.textContent = displayName;\n showPhase(phaseWaiting);\n startCountdown();\n pollTimer = setInterval(checkAuthorization, pollSeconds * 1000);\n }\n\n function stopPolling() {\n clearInterval(pollTimer);\n clearInterval(countdownTimer);\n pollTimer = null;\n }\n\n // ── Auto-authorize via CMS API (through /api/* forward proxy) ──\n async function tryAutoAuthorize(cmsUrl, hardwareKey) {\n const clientId = document.getElementById('api-client-id').value.trim();\n const clientSecret = document.getElementById('api-client-secret').value.trim();\n if (!clientId || !clientSecret) return false;\n\n try {\n // Step 1: OAuth2 token via proxy\n const tokenResp = await fetch('/api/authorize/access_token', {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({ grant_type: 'client_credentials', client_id: clientId, client_secret: clientSecret }),\n });\n if (!tokenResp.ok) throw new Error(`OAuth2 failed: ${tokenResp.status}`);\n const { access_token } = await tokenResp.json();\n\n // Step 2: Find display by hardwareKey via proxy\n const displayResp = await fetch(\n `/api/display?hardwareKey=${encodeURIComponent(hardwareKey)}`,\n { headers: { Authorization: `Bearer ${access_token}` } }\n );\n if (!displayResp.ok) throw new Error(`Find display failed: ${displayResp.status}`);\n const displays = await displayResp.json();\n if (!displays.length) { console.log('[Setup] Display not found via API (may not be registered yet)'); return false; }\n\n const display = displays[0];\n if (display.licensed === 1) { console.log('[Setup] Display already authorized'); return true; }\n\n // Step 3: Authorize display via proxy\n const authResp = await fetch(\n `/api/display/authorise/${display.displayId}`,\n { method: 'PUT', headers: { Authorization: `Bearer ${access_token}` } }\n );\n if (!authResp.ok) throw new Error(`Authorize failed: ${authResp.status}`);\n\n console.log('[Setup] Display auto-authorized via proxy!');\n config.data.apiClientId = clientId;\n config.data.apiClientSecret = clientSecret;\n return true;\n } catch (error) {\n console.warn('[Setup] Auto-authorize failed:', error.message);\n return false;\n }\n }\n\n // ── Unlock gate for reconfiguration ──\n const isReconfigure = config.isConfigured() && config.cmsKey;\n const alreadyUnlocked = new URLSearchParams(window.location.search).has('unlocked');\n const btnBackPlayer = document.getElementById('btn-back-player');\n if (isReconfigure && !alreadyUnlocked) {\n // Show unlock phase instead of setup form\n showPhase(phaseUnlock);\n }\n if (isReconfigure || alreadyUnlocked) {\n // Show \"Back to Player\" on the setup form\n btnBackPlayer.style.display = 'block';\n // Pre-fill form with existing config\n document.getElementById('cms-url').value = config.cmsUrl;\n document.getElementById('cms-key').value = config.cmsKey;\n document.getElementById('display-name').value = config.displayName;\n if (config.data.apiClientId) {\n document.getElementById('api-client-id').value = config.data.apiClientId;\n document.getElementById('api-client-secret').value = config.data.apiClientSecret || '';\n }\n }\n\n // ── Back to Player (cancel reconfiguration) ──\n function goBackToPlayer() {\n stopPolling();\n window.location.href = '/player/index.html' + window.location.search;\n }\n btnBackPlayer.addEventListener('click', goBackToPlayer);\n\n // Esc also goes back to player when reconfiguring\n if (isReconfigure || alreadyUnlocked) {\n document.addEventListener('keydown', (e) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n goBackToPlayer();\n }\n });\n }\n\n // ── Unlock form handler ──\n document.getElementById('unlock-form').addEventListener('submit', (e) => {\n e.preventDefault();\n const unlockError = document.getElementById('unlock-error');\n const enteredKey = document.getElementById('unlock-key').value.trim();\n\n if (enteredKey === config.cmsKey) {\n unlockError.style.display = 'none';\n // Pre-fill form with existing config\n document.getElementById('cms-url').value = config.cmsUrl;\n document.getElementById('cms-key').value = config.cmsKey;\n document.getElementById('display-name').value = config.displayName;\n if (config.data.apiClientId) {\n document.getElementById('api-client-id').value = config.data.apiClientId;\n document.getElementById('api-client-secret').value = config.data.apiClientSecret || '';\n }\n showPhase(phaseSetup);\n } else {\n unlockError.textContent = 'Incorrect CMS key';\n unlockError.style.display = 'block';\n }\n });\n\n // ── Pre-fill if first-time setup (no unlock gate) ──\n if (!isReconfigure && config.isConfigured()) {\n document.getElementById('cms-url').value = config.cmsUrl;\n document.getElementById('cms-key').value = config.cmsKey;\n document.getElementById('display-name').value = config.displayName;\n if (config.data.apiClientId) {\n document.getElementById('api-client-id').value = config.data.apiClientId;\n document.getElementById('api-client-secret').value = config.data.apiClientSecret || '';\n }\n }\n\n // ── Form Submit ──\n form.addEventListener('submit', async (e) => {\n e.preventDefault();\n hideError();\n\n const cmsUrl = document.getElementById('cms-url').value.trim().replace(/\\/$/, '');\n const cmsKey = document.getElementById('cms-key').value.trim();\n const displayName = document.getElementById('display-name').value.trim();\n\n // Write config.json via proxy (master config file)\n // The boot path (proxy injection → Config.load()) handles the rest.\n try {\n submitBtn.textContent = 'Saving...';\n submitBtn.disabled = true;\n\n // Always POST to proxy — updates in-memory config for REST auth,\n // cache-through, and API forwarding. Works on all players.\n const saveResp = await fetch('/config', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ cmsUrl, cmsKey, displayName }),\n });\n if (!saveResp.ok) throw new Error('Failed to save config');\n\n // Shell persistence (Electron config.json) — optional, on top of proxy\n const electronAPI = window.electronAPI || window.parent?.electronAPI;\n if (electronAPI?.setConfig) {\n await electronAPI.setConfig({ cmsUrl, cmsKey, displayName });\n }\n\n // Also update in-memory config for the test connection below\n config.data.cmsUrl = cmsUrl;\n config.data.cmsKey = cmsKey;\n config.data.displayName = displayName;\n config.save();\n\n // Backup hardware key to IndexedDB for stability\n backupHardwareKey(config.hardwareKey);\n\n // Test connection\n submitBtn.textContent = 'Connecting...';\n\n // Try REST first, fall back to SOAP\n transport = null; // reset transport for fresh detection\n const client = await getTransport();\n const result = await client.registerDisplay();\n\n if (result.code === 'READY') {\n showSuccess();\n } else {\n // Try auto-authorize via CMS API if credentials provided\n submitBtn.textContent = 'Authorizing...';\n const autoAuthed = await tryAutoAuthorize(cmsUrl, hwKey);\n\n if (autoAuthed) {\n // Persist API client credentials so they survive restarts\n config.save();\n await fetch('/config', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ apiClientId: config.data.apiClientId, apiClientSecret: config.data.apiClientSecret }),\n });\n if (electronAPI?.setConfig) {\n await electronAPI.setConfig({ apiClientId: config.data.apiClientId, apiClientSecret: config.data.apiClientSecret });\n }\n\n // Verify registration\n const recheck = await client.registerDisplay();\n if (recheck.code === 'READY') {\n showSuccess();\n return;\n }\n }\n\n // Not authorized yet — switch to polling phase\n startPolling(displayName);\n }\n } catch (error) {\n showError(`Connection failed: ${error.message}`);\n submitBtn.textContent = 'Connect';\n submitBtn.disabled = false;\n }\n });\n\n // ── Back button ──\n btnBack.addEventListener('click', () => {\n stopPolling();\n showPhase(phaseSetup);\n submitBtn.textContent = 'Connect';\n submitBtn.disabled = false;\n });\n\n // ── IndexedDB hardware key backup ──\n async function backupHardwareKey(key) {\n try {\n const req = indexedDB.open('xibo-hw-backup', 1);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains('keys')) {\n db.createObjectStore('keys');\n }\n };\n req.onsuccess = () => {\n const db = req.result;\n const tx = db.transaction('keys', 'readwrite');\n tx.objectStore('keys').put(key, 'hardwareKey');\n tx.oncomplete = () => db.close();\n };\n } catch (e) {\n console.warn('[Setup] IndexedDB backup failed:', e);\n }\n }\n\n // ── Enable submit ──\n submitBtn.disabled = false;\n submitBtn.textContent = 'Connect';\n\n // ── Auto-resume polling if already registered but not authorized ──\n // Skip auto-resume when reconfiguring (user explicitly opened setup)\n if (config.isConfigured() && !isReconfigure) {\n (async () => {\n try {\n const client = await getTransport();\n const result = await client.registerDisplay();\n if (result.code === 'READY') {\n window.location.href = '/player/index.html' + window.location.search;\n } else {\n // Not authorized — go straight to polling\n startPolling(config.displayName);\n }\n } catch (e) {\n // Connection failed, show form\n console.log('[Setup] Auto-check failed, showing form');\n }\n })();\n }\n "],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{c as e,l as t,o as n,r,u as i}from"./src-C3Sg89t9.js";import{r as a}from"./src-BYVnjdc0.js";import{i as o,n as s}from"./src-BdgQ2CiL.js";var c={name:`@xiboplayer/core`,version:`0.7.18`,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(`
|
|
1
|
+
import{c as e,l as t,o as n,r,u as i}from"./src-Bzheh5E2.js";import{r as a}from"./src-CIERor10.js";import{i as o,n as s}from"./src-DzgDstdA.js";var c={name:`@xiboplayer/core`,version:`0.7.20`,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
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-
|
|
3
|
+
//# sourceMappingURL=src-B0l-hkrA.js.map
|