Merge pull request #243 from a10kiloham/screenshot-ack-and-proxy-docs

Toast the screenshot-request verdict; document proxy header pitfall
This commit is contained in:
screentinker 2026-08-07 08:34:36 -05:00 committed by GitHub
commit 127a6265e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 64 additions and 5 deletions

View file

@ -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:

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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é',

View file

@ -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',

View file

@ -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',

View file

@ -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) {

View file

@ -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