diff --git a/README.md b/README.md index 1796d73..6e5709b 100644 --- a/README.md +++ b/README.md @@ -499,6 +499,28 @@ server { } ``` +#### Don't add security headers at the proxy + +The app already sets `X-Frame-Options`, `Strict-Transport-Security`, `Content-Security-Policy`, +`X-Content-Type-Options`, etc. via [helmet](https://helmetjs.github.io/), and manages them +**per route**: widget/kiosk renders and the device preview deliberately remove or relax +`X-Frame-Options` so they can be framed, while the dashboard keeps the strict policy. + +A proxy-level header block (nginx `add_header X-Frame-Options DENY;`, a Caddy +`header { ... }` snippet, or a "security headers" preset) *adds a second copy* of these +headers on top of the app's. Browsers treat conflicting duplicate `X-Frame-Options` +values as `deny`, which breaks the dashboard's device Preview (a same-origin iframe of +`/player`) and widget previews with console errors like: + +``` +Refused to display 'https://…' in a frame because it set multiple +'X-Frame-Options' headers with conflicting values ('DENY, SAMEORIGIN'). +Falling back to 'deny'. +``` + +Let the proxy handle TLS, compression, and body-size limits only, and leave security +headers to the app. + ### Updating To update a running instance to the latest version: diff --git a/frontend/js/i18n/de.js b/frontend/js/i18n/de.js index 8429af2..2fe1010 100644 --- a/frontend/js/i18n/de.js +++ b/frontend/js/i18n/de.js @@ -347,6 +347,9 @@ export default { 'device.assign.select_first': 'Erst etwas auswählen', 'device.assign.kiosk_widget_name': 'Kiosk: {name}', 'device.toast.screenshot_requested': 'Screenshot angefordert', + 'device.toast.screenshot_unsupported': 'Der Player dieses Displays unterstützt keine Screenshots', + 'device.toast.screenshot_offline': 'Display ist offline — Screenshot nicht angefordert', + 'device.toast.screenshot_failed': 'Screenshot-Anfrage fehlgeschlagen — keine Antwort vom Server', 'device.toast.renamed': 'Bildschirm umbenannt', 'device.toast.removing': 'Wird entfernt...', 'device.toast.removed': 'Bildschirm entfernt', diff --git a/frontend/js/i18n/en.js b/frontend/js/i18n/en.js index 821c15d..adcdaef 100644 --- a/frontend/js/i18n/en.js +++ b/frontend/js/i18n/en.js @@ -648,6 +648,9 @@ export default { 'device.assign.kiosk_widget_name': 'Kiosk: {name}', // Toasts 'device.toast.screenshot_requested': 'Screenshot requested', + 'device.toast.screenshot_unsupported': 'This display\'s player can\'t take screenshots', + 'device.toast.screenshot_offline': 'Display is offline — screenshot not requested', + 'device.toast.screenshot_failed': 'Screenshot request failed — no response from server', 'device.toast.renamed': 'Display renamed', 'device.toast.removing': 'Removing...', 'device.toast.removed': 'Display removed', diff --git a/frontend/js/i18n/es.js b/frontend/js/i18n/es.js index 9a6da03..ca1c47d 100644 --- a/frontend/js/i18n/es.js +++ b/frontend/js/i18n/es.js @@ -377,6 +377,9 @@ export default { 'device.assign.select_first': 'Primero selecciona algo', 'device.assign.kiosk_widget_name': 'Kiosco: {name}', 'device.toast.screenshot_requested': 'Captura solicitada', + 'device.toast.screenshot_unsupported': 'El reproductor de esta pantalla no admite capturas de pantalla', + 'device.toast.screenshot_offline': 'La pantalla está desconectada — captura no solicitada', + 'device.toast.screenshot_failed': 'La solicitud de captura falló — sin respuesta del servidor', 'device.toast.renamed': 'Pantalla renombrada', 'device.toast.removing': 'Eliminando...', 'device.toast.removed': 'Pantalla eliminada', diff --git a/frontend/js/i18n/fr.js b/frontend/js/i18n/fr.js index 438f36c..8acb3ec 100644 --- a/frontend/js/i18n/fr.js +++ b/frontend/js/i18n/fr.js @@ -347,6 +347,9 @@ export default { 'device.assign.select_first': 'Sélectionnez d\'abord un élément', 'device.assign.kiosk_widget_name': 'Kiosque : {name}', 'device.toast.screenshot_requested': 'Capture demandée', + 'device.toast.screenshot_unsupported': 'Le lecteur de cet écran ne prend pas en charge les captures d\'écran', + 'device.toast.screenshot_offline': 'Écran hors ligne — capture non demandée', + 'device.toast.screenshot_failed': 'Échec de la demande de capture — pas de réponse du serveur', 'device.toast.renamed': 'Écran renommé', 'device.toast.removing': 'Suppression...', 'device.toast.removed': 'Écran retiré', diff --git a/frontend/js/i18n/it.js b/frontend/js/i18n/it.js index 091c2d9..c5ad537 100644 --- a/frontend/js/i18n/it.js +++ b/frontend/js/i18n/it.js @@ -371,6 +371,9 @@ export default { 'device.assign.kiosk_widget_name': 'Chiosco: {name}', // Toasts 'device.toast.screenshot_requested': 'Richiesta screenshot inviata', + 'device.toast.screenshot_unsupported': 'Il player di questo schermo non supporta gli screenshot', + 'device.toast.screenshot_offline': 'Lo schermo è offline — screenshot non richiesto', + 'device.toast.screenshot_failed': 'Richiesta screenshot non riuscita — nessuna risposta dal server', 'device.toast.renamed': 'Schermo rinominato', 'device.toast.removing': 'Rimozione in corso...', 'device.toast.removed': 'Schermo rimosso', diff --git a/frontend/js/i18n/pt.js b/frontend/js/i18n/pt.js index e7a90d5..3dcd64a 100644 --- a/frontend/js/i18n/pt.js +++ b/frontend/js/i18n/pt.js @@ -347,6 +347,9 @@ export default { 'device.assign.select_first': 'Selecione algo primeiro', 'device.assign.kiosk_widget_name': 'Quiosque: {name}', 'device.toast.screenshot_requested': 'Captura solicitada', + 'device.toast.screenshot_unsupported': 'O player desta tela não suporta capturas de tela', + 'device.toast.screenshot_offline': 'A tela está offline — captura não solicitada', + 'device.toast.screenshot_failed': 'Falha na solicitação de captura — sem resposta do servidor', 'device.toast.renamed': 'Tela renomeada', 'device.toast.removing': 'Removendo...', 'device.toast.removed': 'Tela removida', diff --git a/frontend/js/socket.js b/frontend/js/socket.js index a1e5840..e5e2d20 100644 --- a/frontend/js/socket.js +++ b/frontend/js/socket.js @@ -112,9 +112,22 @@ function emit(event, data) { if (cbs) cbs.forEach(cb => cb(data)); } -export function requestScreenshot(deviceId) { +// Optional callback receives the server-side ack: { delivered, reason, capability }. +// reason is 'offline' (no live connection) or 'unsupported' (this player type can't +// take screenshots). Callers without a callback keep firing-and-forgetting — the +// dashboard grid and the device-detail 5s poll stay silent; only the explicit +// Screenshot button asks for the verdict. +export function requestScreenshot(deviceId, callback) { console.log('requestScreenshot:', deviceId, 'socket connected:', dashboardSocket?.connected); - if (dashboardSocket) dashboardSocket.emit('dashboard:request-screenshot', { device_id: deviceId }); + if (!dashboardSocket) return; + if (typeof callback === 'function') { + dashboardSocket.timeout(5000).emit('dashboard:request-screenshot', { device_id: deviceId }, (err, ack) => { + if (err) callback({ delivered: false, reason: 'no_ack' }); + else callback(ack || { delivered: false, reason: 'no_ack' }); + }); + } else { + dashboardSocket.emit('dashboard:request-screenshot', { device_id: deviceId }); + } } export function startRemote(deviceId) { diff --git a/frontend/js/views/device-detail.js b/frontend/js/views/device-detail.js index 32e6fba..7a26256 100644 --- a/frontend/js/views/device-detail.js +++ b/frontend/js/views/device-detail.js @@ -1084,10 +1084,16 @@ function setupActions(device) { document.getElementById('devicePreviewBtn')?.addEventListener('click', () => showDevicePreview(device)); - // Screenshot button + // Screenshot button — pass a callback so the server's verdict surfaces as a toast + // instead of the request silently going nowhere (offline device, or a player type + // that can't capture at all, e.g. BrightSign). document.getElementById('screenshotBtn')?.addEventListener('click', () => { - requestScreenshot(device.id); - showToast(t('device.toast.screenshot_requested'), 'info'); + requestScreenshot(device.id, (ack) => { + if (ack?.delivered) showToast(t('device.toast.screenshot_requested'), 'info'); + else if (ack?.reason === 'unsupported') showToast(t('device.toast.screenshot_unsupported'), 'warning'); + else if (ack?.reason === 'offline') showToast(t('device.toast.screenshot_offline'), 'warning'); + else showToast(t('device.toast.screenshot_failed'), 'error'); + }); }); // Rename