diff --git a/.dockerignore b/.dockerignore index c6dfaba..c1060d4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,3 +2,5 @@ !package.json !src/ !src/** +!scripts/ +!scripts/** diff --git a/CHANGELOG.md b/CHANGELOG.md index f84da80..13ea946 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. +## 2.5.14 - 2026-08-06 + +### Added + +- Added branded Windows and Linux kiosk launcher downloads on the dashboard, with updated copy that explains the launcher behavior more clearly. +- Kiosk launchers now start the browser in kiosk mode, suppress notifications for Chromium-based browsers, and use the correct Firefox kiosk flag. + +### Changed + +- The web side now resolves the player base URL from the database-backed player registration record instead of depending on a web-container environment fallback. + ## 2.5.13 - 2026-08-05 ### Fixed diff --git a/Dockerfile b/Dockerfile index 1bf6bfb..ae01de8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ COPY package*.json ./ RUN npm install --omit=dev --no-audit --no-fund COPY src ./src +COPY scripts ./scripts RUN mkdir -p /app/src/web/public/vendor/animate.css && cp /app/node_modules/animate.css/animate.min.css /app/src/web/public/vendor/animate.css/animate.min.css RUN mkdir -p /app/src/player/public/vendor/animate.css && cp /app/node_modules/animate.css/animate.min.css /app/src/player/public/vendor/animate.css/animate.min.css diff --git a/package.json b/package.json index 06a426d..48dfc24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.5.13", + "version": "2.5.14", "private": false, "description": "Pulse Signage application with MySQL and media storage", "repository": { diff --git a/scripts/pulse-signage-kiosk.bat b/scripts/pulse-signage-kiosk.bat new file mode 100644 index 0000000..1863eba --- /dev/null +++ b/scripts/pulse-signage-kiosk.bat @@ -0,0 +1,61 @@ +@echo off +setlocal EnableExtensions EnableDelayedExpansion + +set "TARGET_URL=%~1" +if not defined TARGET_URL set "TARGET_URL=http://localhost:8081" + +set "BROWSER_PATH=" +set "BROWSER_KIND=" + +for %%B in (chrome.exe msedge.exe firefox.exe) do if not defined BROWSER_PATH ( + for /f "delims=" %%I in ('where %%B 2^>nul') do if not defined BROWSER_PATH ( + set "BROWSER_PATH=%%I" + set "BROWSER_KIND=%%~nB" + ) +) + +if not defined BROWSER_PATH if not defined BROWSER_KIND ( + for %%P in ( + "%ProgramFiles%\Google\Chrome\Application\chrome.exe" + "%ProgramFiles(x86)%\Google\Chrome\Application\chrome.exe" + "%LocalAppData%\Google\Chrome\Application\chrome.exe" + ) do if not defined BROWSER_PATH if exist "%%~fP" ( + set "BROWSER_PATH=%%~fP" + set "BROWSER_KIND=chrome" + ) +) + +if not defined BROWSER_PATH if not defined BROWSER_KIND ( + for %%P in ( + "%ProgramFiles%\Microsoft\Edge\Application\msedge.exe" + "%ProgramFiles(x86)%\Microsoft\Edge\Application\msedge.exe" + "%LocalAppData%\Microsoft\Edge\Application\msedge.exe" + ) do if not defined BROWSER_PATH if exist "%%~fP" ( + set "BROWSER_PATH=%%~fP" + set "BROWSER_KIND=edge" + ) +) + +if not defined BROWSER_PATH if not defined BROWSER_KIND ( + for %%P in ( + "%ProgramFiles%\Mozilla Firefox\firefox.exe" + "%ProgramFiles(x86)%\Mozilla Firefox\firefox.exe" + "%LocalAppData%\Mozilla Firefox\firefox.exe" + ) do if not defined BROWSER_PATH if exist "%%~fP" ( + set "BROWSER_PATH=%%~fP" + set "BROWSER_KIND=firefox" + ) +) + +if not defined BROWSER_PATH ( + echo No supported browser was found. + echo Tried Chrome, Edge, and Firefox in PATH and common install locations. + exit /b 1 +) + +echo Launching !BROWSER_KIND! in kiosk mode: !TARGET_URL! +if /I "!BROWSER_KIND!"=="firefox" ( + start "" "!BROWSER_PATH!" -kiosk "!TARGET_URL!" +) else ( + start "" "!BROWSER_PATH!" --disable-notifications --kiosk "!TARGET_URL!" +) \ No newline at end of file diff --git a/scripts/pulse-signage-kiosk.sh b/scripts/pulse-signage-kiosk.sh new file mode 100644 index 0000000..0a98f63 --- /dev/null +++ b/scripts/pulse-signage-kiosk.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +target_url="${1:-http://localhost:8081}" + +find_browser() { + local candidate + for candidate in "$@"; do + if command -v "$candidate" >/dev/null 2>&1; then + printf '%s' "$candidate" + return 0 + fi + done + return 1 +} + +browser="" +browser_kind="" + +if browser="$(find_browser google-chrome-stable google-chrome chromium chromium-browser msedge microsoft-edge firefox)"; then + case "$browser" in + firefox) + browser_kind="firefox" + ;; + msedge|microsoft-edge) + browser_kind="edge" + ;; + *) + browser_kind="chrome" + ;; + esac +else + echo "No supported browser was found." + echo "Tried Chrome, Chromium, Edge, and Firefox in PATH." + exit 1 +fi + +echo "Launching ${browser_kind} in kiosk mode: ${target_url}" + +case "$browser_kind" in + firefox) + exec "$browser" -kiosk "$target_url" + ;; + edge|chrome) + exec "$browser" --disable-notifications --kiosk "$target_url" + ;; +esac \ No newline at end of file diff --git a/src/common.js b/src/common.js index a90d613..41e29d9 100644 --- a/src/common.js +++ b/src/common.js @@ -86,6 +86,7 @@ module.exports = { fetchScreenById: data.fetchScreenById, fetchScreenEditData: data.fetchScreenEditData, fetchScreenPlayerUrls: data.fetchScreenPlayerUrls, + fetchPlayerPublicBaseUrl: data.fetchPlayerPublicBaseUrl, fetchScreenPlayerRecord: data.fetchScreenPlayerRecord, fetchTemplateById: data.fetchTemplateById, fetchSlideById: data.fetchSlideById, diff --git a/src/data/index.js b/src/data/index.js index 463d043..c6b066c 100644 --- a/src/data/index.js +++ b/src/data/index.js @@ -7,7 +7,7 @@ const { fetchPlaylistById } = require('./playlists'); const { normalizeDisplayMode, fetchTimetablesData, fetchTimetableGroupsPage, fetchTimetableGroupById, fetchTimetableEntriesByGroupId, buildTimetableGroupPayload } = require('./schedules'); const { fetchApiSourcesData, fetchApiSourcesPage, fetchApiSourceById, fetchApiSourceResponse, buildApiSourcePayload } = require('./api-sources'); const { fetchRssFeedsData, fetchRssFeedsPage, fetchRssFeedById, fetchRssFeedItemsByFeedId, normalizeRssFeedItem, buildRssFeedPayload, fetchRssFeedItems, replaceRssFeedItems } = require('./rss-feeds'); -const { slugify, uniqueScreenSlug, fetchScreenById, fetchScreenEditData, fetchScreenPlayerUrls, fetchScreenPlayerRecord } = require('./screens'); +const { slugify, uniqueScreenSlug, fetchScreenById, fetchScreenEditData, fetchScreenPlayerUrls, fetchPlayerPublicBaseUrl, fetchScreenPlayerRecord } = require('./screens'); const { fetchTemplateById, fetchTemplatesData, extractTemplateRegions, buildTemplatePayload } = require('./templates'); const { fetchCanvasSizesData, fetchCanvasSizeById, buildCanvasSizePayload, MAX_CANVAS_SIZE_DIMENSION } = require('./canvas-sizes'); const { fetchSlideById, buildSlidePayload } = require('./slides'); @@ -61,6 +61,7 @@ module.exports = { fetchScreenById, fetchScreenEditData, fetchScreenPlayerUrls, + fetchPlayerPublicBaseUrl, fetchScreenPlayerRecord, fetchTemplateById, fetchSlideById, diff --git a/src/data/screens.js b/src/data/screens.js index 6589d13..cff9207 100644 --- a/src/data/screens.js +++ b/src/data/screens.js @@ -50,6 +50,17 @@ async function fetchScreenPlayerUrls(pool) { return playerUrls; } +async function fetchPlayerPublicBaseUrl(pool) { + const [rows] = await pool.query(` + SELECT public_base_url + FROM d_players + WHERE device_id = '1' + LIMIT 1 + `); + + return normalizePlayerBaseUrl(rows[0] && rows[0].public_base_url) || null; +} + async function fetchScreenPlayerRecord(pool, slug) { const [rows] = await pool.query(` SELECT p.device_id, p.public_base_url, p.internal_base_url, p.last_seen_at @@ -103,6 +114,7 @@ module.exports = { normalizePlayerBaseUrl, buildScreenPlayerUrl, fetchScreenPlayerUrls, + fetchPlayerPublicBaseUrl, fetchScreenPlayerRecord, uniqueScreenSlug, fetchScreenById, diff --git a/src/web.js b/src/web.js index d3872b8..4fc433f 100644 --- a/src/web.js +++ b/src/web.js @@ -71,7 +71,6 @@ async function start() { pool: pool, common: common, playerInternalBaseUrl: webConfig.playerInternalBaseUrl, - playerPublicBaseUrl: webConfig.playerPublicBaseUrl, uploadDir: webConfig.uploadsDir, formatDashboardDate: formatDashboardDate, notifyPlayerScreens: notifyPlayerScreens, diff --git a/src/web/bootstrap.js b/src/web/bootstrap.js index c3793b3..36b1057 100644 --- a/src/web/bootstrap.js +++ b/src/web/bootstrap.js @@ -9,7 +9,6 @@ function createWebBootstrap(options) { const pool = options && options.pool; const common = options && options.common; const configuredPlayerInternalBaseUrl = String(options && options.playerInternalBaseUrl || '').replace(/\/$/, ''); - const playerPublicBaseUrl = String(options && options.playerPublicBaseUrl || '').replace(/\/$/, ''); const uploadDir = String(options && options.uploadDir || '').trim(); const dashboardRefreshIntervalMs = 5000; const formatDashboardDate = options && options.formatDashboardDate; @@ -155,7 +154,6 @@ function createWebBootstrap(options) { playerSnapshotCache: playerSnapshotCache, playerSnapshotSockets: playerSnapshotSockets, ensurePlayerSnapshotSubscription: ensurePlayerSnapshotSubscription, - playerPublicBaseUrl: playerPublicBaseUrl, formatDashboardDate: formatDashboardDate }); const buildDashboardState = dashboardStateService.buildDashboardState; diff --git a/src/web/handlebars.js b/src/web/handlebars.js index 438699f..2a620ab 100644 --- a/src/web/handlebars.js +++ b/src/web/handlebars.js @@ -58,7 +58,11 @@ function registerUrlHelpers(Handlebars) { }); Handlebars.registerHelper('playerUrl', function (slug) { - const base = (process.env.PLAYER_PUBLIC_BASE_URL || process.env.PLAYER_BASE_URL || 'http://localhost:3001').replace(/\/$/, ''); + const base = String(arguments[1] || '').trim().replace(/\/$/, ''); + if (!base) { + return ''; + } + return `${base}/screen/${encodeURIComponent(slug)}`; }); } diff --git a/src/web/lib/config.js b/src/web/lib/config.js index d921c63..516c718 100644 --- a/src/web/lib/config.js +++ b/src/web/lib/config.js @@ -6,7 +6,6 @@ function createWebConfig() { const thumbnailsDir = path.join(mediaDir, 'thumbnails'); const assetDir = path.join(__dirname, '..', 'public'); const playerInternalBaseUrl = (process.env.PLAYER_INTERNAL_BASE_URL || process.env.PLAYER_BASE_URL || 'http://player:8081').replace(/\/$/, ''); - const playerPublicBaseUrl = (process.env.PLAYER_PUBLIC_BASE_URL || process.env.PLAYER_BASE_URL || 'http://localhost:8081').replace(/\/$/, ''); const sessionCookieName = 'digital_signage_session'; const sessionMaxAgeDays = Number(process.env.SESSION_MAX_AGE_DAYS || 14); const sessionMaxAgeMs = (Number.isFinite(sessionMaxAgeDays) && sessionMaxAgeDays > 0 ? sessionMaxAgeDays : 14) * 24 * 60 * 60 * 1000; @@ -20,7 +19,6 @@ function createWebConfig() { thumbnailsDir: thumbnailsDir, assetDir: assetDir, playerInternalBaseUrl: playerInternalBaseUrl, - playerPublicBaseUrl: playerPublicBaseUrl, sessionCookieName: sessionCookieName, sessionMaxAgeMs: sessionMaxAgeMs, dataSourceStartupRefreshStaggerMs: dataSourceStartupRefreshStaggerMs diff --git a/src/web/public/css/theme-custom.css b/src/web/public/css/theme-custom.css index 4f3984e..e74f3ed 100644 --- a/src/web/public/css/theme-custom.css +++ b/src/web/public/css/theme-custom.css @@ -876,6 +876,67 @@ font-size: 0.875rem; } +.dashboard-launcher-actions { + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: flex-end; + gap: 0.5rem; +} + +.dashboard-launcher-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; +} + +.dashboard-launcher-copy { + flex: 1 1 auto; + min-width: 0; +} + +.dashboard-kiosk-launcher-confirmation { + background: var(--bs-tertiary-bg); +} + +.dashboard-kiosk-launcher-confirmation .form-check.form-switch { + display: flex; + align-items: center; + gap: 0.5rem; + padding-left: 0; +} + +.dashboard-kiosk-launcher-confirmation .form-check-input { + float: none; + margin: 0; + transform: scale(0.92); + transform-origin: left center; +} + +.dashboard-kiosk-launcher-confirmation .form-check-label { + letter-spacing: 0.01em; + line-height: 1.1; +} + +.dashboard-launcher-open-button { + min-width: 5rem; +} + +.dashboard-kiosk-launcher-download { + text-align: left; +} + +.dashboard-kiosk-launcher-download.disabled, +.dashboard-kiosk-launcher-download[aria-disabled='true'] { + pointer-events: none; + opacity: 0.6; +} + +.dashboard-launcher-download { + width: 9rem; +} + .screen-command-card .card-header { padding-top: 1rem; padding-bottom: 1rem; diff --git a/src/web/public/js/dashboard/dashboard-page.js b/src/web/public/js/dashboard/dashboard-page.js index 39c4d24..ff78c84 100644 --- a/src/web/public/js/dashboard/dashboard-page.js +++ b/src/web/public/js/dashboard/dashboard-page.js @@ -773,10 +773,62 @@ }); } + function initKioskLauncherModal() { + var modal = document.getElementById('dashboard-kiosk-launcher-modal'); + if (!modal) { + return; + } + + var checkbox = modal.querySelector('[data-kiosk-launcher-confirm]'); + var downloadLinks = Array.prototype.slice.call(modal.querySelectorAll('[data-kiosk-launcher-download]')); + + function setDownloadsEnabled(enabled) { + downloadLinks.forEach(function (link) { + if (!link) { + return; + } + + link.classList.toggle('disabled', !enabled); + link.setAttribute('aria-disabled', enabled ? 'false' : 'true'); + if (enabled) { + link.removeAttribute('tabindex'); + } else { + link.setAttribute('tabindex', '-1'); + } + }); + } + + function resetModalState() { + if (checkbox) { + checkbox.checked = false; + } + setDownloadsEnabled(false); + } + + if (checkbox) { + checkbox.addEventListener('change', function () { + setDownloadsEnabled(Boolean(checkbox.checked)); + }); + } + + modal.addEventListener('show.bs.modal', resetModalState); + modal.addEventListener('hidden.bs.modal', resetModalState); + modal.addEventListener('click', function (event) { + var target = event.target && event.target.closest ? event.target.closest('[data-kiosk-launcher-download]') : null; + if (target && target.classList.contains('disabled')) { + event.preventDefault(); + event.stopPropagation(); + } + }); + + resetModalState(); + } + window.webHandleDashboardState = handleDashboardState; initClientRenameHandler(); initClientMoveHandler(); + initKioskLauncherModal(); var screenCommandSelect = document.getElementById('screen-command-select'); if (screenCommandSelect) { updateScreenCommandControls(latestDashboardState || readScreenCommandStateFromDom()); diff --git a/src/web/routes/admin/client-commands.js b/src/web/routes/admin/client-commands.js index 9c77ff4..aa311b9 100644 --- a/src/web/routes/admin/client-commands.js +++ b/src/web/routes/admin/client-commands.js @@ -10,7 +10,6 @@ module.exports = function registerScreenCommandRoutes(app, deps) { const isClientNameAvailable = deps.isClientNameAvailable; const withClientNameReservation = deps.withClientNameReservation; const broadcastDashboardState = deps.broadcastDashboardState; - const playerPublicBaseUrl = String(deps.playerPublicBaseUrl || '').trim().replace(/\/$/, ''); const requirePermission = deps.requirePermission; app.post('/clients/:slug/commands', requirePermission('clients.allow'), async function (req, res, next) { @@ -212,7 +211,11 @@ module.exports = function registerScreenCommandRoutes(app, deps) { const targetPlayerRecord = typeof common.fetchScreenPlayerRecord === 'function' ? await common.fetchScreenPlayerRecord(pool, targetScreenSlug) : null; - const targetBaseUrl = String(targetPlayerRecord && targetPlayerRecord.public_base_url || playerPublicBaseUrl || '').trim().replace(/\/$/, ''); + const targetBaseUrl = String( + targetPlayerRecord && targetPlayerRecord.public_base_url || + (typeof common.fetchPlayerPublicBaseUrl === 'function' ? await common.fetchPlayerPublicBaseUrl(pool) : '') || + '' + ).trim().replace(/\/$/, ''); const targetPlayerUrl = targetBaseUrl ? `${targetBaseUrl}/screen/${encodeURIComponent(targetScreenSlug)}` : `/screen/${encodeURIComponent(targetScreenSlug)}`; await forwardPlayerCommand(slug, { diff --git a/src/web/routes/admin/manage.js b/src/web/routes/admin/manage.js index bb58dcb..4341f08 100644 --- a/src/web/routes/admin/manage.js +++ b/src/web/routes/admin/manage.js @@ -11,7 +11,6 @@ module.exports = function registerManageRoutes(app, deps) { const getScreenDeleteBlockMessage = deps.getScreenDeleteBlockMessage; const getScreenConnections = deps.getScreenConnections; const forwardPlayerCommand = deps.forwardPlayerCommand; - const PLAYER_PUBLIC_BASE_URL = deps.playerPublicBaseUrl; const requirePermission = deps.requirePermission; const SCREEN_NAME_MAX_LENGTH = 255; @@ -153,9 +152,14 @@ module.exports = function registerManageRoutes(app, deps) { const playerRecord = typeof common.fetchScreenPlayerRecord === 'function' ? await common.fetchScreenPlayerRecord(pool, previousSlug) : null; + const playerBaseUrl = playerRecord && playerRecord.public_base_url + ? String(playerRecord.public_base_url).replace(/\/$/, '') + : typeof common.fetchPlayerPublicBaseUrl === 'function' + ? await common.fetchPlayerPublicBaseUrl(pool) + : ''; await forwardPlayerCommand(previousSlug, { command: 'redirect', - url: (playerRecord && playerRecord.public_base_url ? String(playerRecord.public_base_url).replace(/\/$/, '') : PLAYER_PUBLIC_BASE_URL) + `/screen/${encodeURIComponent(slug)}` + url: playerBaseUrl ? `${playerBaseUrl}/screen/${encodeURIComponent(slug)}` : `/screen/${encodeURIComponent(slug)}` }); } redirectAfterSave(req, res, '/screens?edit=' + screen.id, { diff --git a/src/web/routes/common.js b/src/web/routes/common.js index 91e96a5..c62cf94 100644 --- a/src/web/routes/common.js +++ b/src/web/routes/common.js @@ -1,7 +1,5 @@ // Shared route helpers for web view rendering and player URL formatting. -const PLAYER_PUBLIC_BASE_URL = (process.env.PLAYER_PUBLIC_BASE_URL || process.env.PLAYER_BASE_URL || 'http://localhost:3001').replace(/\/$/, ''); - function escapeHtml(value) { return String(value ?? '') .replace(/&/g, '&') @@ -11,8 +9,13 @@ function escapeHtml(value) { .replace(/'/g, '''); } -function screenPlayerUrl(slug) { - return `${PLAYER_PUBLIC_BASE_URL}/screen/${encodeURIComponent(slug)}`; +function screenPlayerUrl(slug, baseUrl) { + const normalizedBaseUrl = String(baseUrl || '').trim().replace(/\/$/, ''); + if (!normalizedBaseUrl) { + return ''; + } + + return `${normalizedBaseUrl}/screen/${encodeURIComponent(slug)}`; } module.exports = { diff --git a/src/web/routes/register.js b/src/web/routes/register.js index 53ae840..9c87837 100644 --- a/src/web/routes/register.js +++ b/src/web/routes/register.js @@ -48,7 +48,7 @@ function registerAuthAndAccountRoutes(app, deps) { buildDashboardState: deps.buildDashboardState, fetchScreensByPlaylistId: deps.fetchScreensByPlaylistId, getScreenDeleteBlockMessage: deps.playerActionService.getScreenDeleteBlockMessage, - getScreenConnections: deps.playerActionService.getScreenConnections + getScreenConnections: deps.playerActionService.getScreenConnections, }); registerAccountRoutes(app, { @@ -107,7 +107,6 @@ function registerSignageRoutes(app, deps) { getScreenDeleteBlockMessage: deps.playerActionService.getScreenDeleteBlockMessage, getScreenConnections: deps.playerActionService.getScreenConnections, forwardPlayerCommand: deps.playerActionService.forwardPlayerCommand, - playerPublicBaseUrl: deps.playerPublicBaseUrl, requirePermission: deps.requirePermission }); @@ -129,7 +128,6 @@ function registerSignageRoutes(app, deps) { isClientNameAvailable: deps.isClientNameAvailable, withClientNameReservation: deps.withClientNameReservation, broadcastDashboardState: deps.broadcastDashboardState, - playerPublicBaseUrl: deps.playerPublicBaseUrl, requirePermission: deps.requirePermission }); } diff --git a/src/web/routes/signage/screens/routes.js b/src/web/routes/signage/screens/routes.js index 46af439..7ff797f 100644 --- a/src/web/routes/signage/screens/routes.js +++ b/src/web/routes/signage/screens/routes.js @@ -1,5 +1,7 @@ // Screen route registration and dashboard wiring. +const fs = require('fs'); + module.exports = function registerScreensRoutes(app, deps) { const pool = deps.pool; const common = deps.common; @@ -10,6 +12,12 @@ module.exports = function registerScreensRoutes(app, deps) { const requirePermission = deps.requirePermission; const LIST_PAGE_SIZE = 25; + const batTemplatePath = require.resolve('#root/scripts/pulse-signage-kiosk.bat'); + const shTemplatePath = require.resolve('#root/scripts/pulse-signage-kiosk.sh'); + const launcherDownloadPaths = { + windows: '/downloads/kiosk/pulse-signage-kiosk.bat', + linux: '/downloads/kiosk/pulse-signage-kiosk.sh' + }; function requireQueryPermission(readPermissionKey, editPermissionKey) { return function (req, res, next) { @@ -55,6 +63,36 @@ module.exports = function registerScreensRoutes(app, deps) { }); } + async function buildPlayerUrl() { + if (typeof common.fetchPlayerPublicBaseUrl !== 'function') { + return null; + } + + return common.fetchPlayerPublicBaseUrl(pool); + } + + function buildLauncherContent(templatePath, playerUrl) { + const template = fs.readFileSync(templatePath, 'utf8'); + const normalizedPlayerUrl = String(playerUrl || '').trim(); + if (!normalizedPlayerUrl) { + return null; + } + + if (template.indexOf('http://localhost:8081') === -1) { + throw new Error(`Launcher template is missing the default player URL placeholder: ${templatePath}`); + } + + return template.replace('http://localhost:8081', normalizedPlayerUrl); + } + + function sendLauncherDownload(res, fileName, content) { + res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); + res.set('Pragma', 'no-cache'); + res.attachment(fileName); + res.type('text/plain; charset=utf-8'); + res.send(content); + } + app.get('/screens', requireQueryPermission('screens.read', 'screens.update'), async function (req, res, next) { try { if (req.query.edit) { @@ -62,6 +100,10 @@ module.exports = function registerScreensRoutes(app, deps) { if (!screen) { return res.status(404).send('Screen not found'); } + screen.player_url = await buildPlayerUrl(); + if (screen.player_url) { + screen.launcher_downloads = launcherDownloadPaths; + } screen.inUse = Boolean(await getScreenDeleteBlockMessage(pool, screen, deps.getScreenConnections)); const editData = await common.fetchScreenEditData(pool); return res.send(pages.renderScreenEditPage(screen, editData, req.query.message ? String(req.query.message) : '', req.currentUser)); @@ -99,7 +141,10 @@ module.exports = function registerScreensRoutes(app, deps) { const playerUrlsBySlug = typeof common.fetchScreenPlayerUrls === 'function' ? await common.fetchScreenPlayerUrls(pool) : {}; - screen.player_url = screen.slug && playerUrlsBySlug[screen.slug] ? String(playerUrlsBySlug[screen.slug]).trim() : null; + screen.player_url = String(playerUrlsBySlug[screen.slug] || '').trim() || null; + if (screen.player_url) { + screen.launcher_downloads = launcherDownloadPaths; + } screen.inUse = Boolean(await getScreenDeleteBlockMessage(pool, screen, deps.getScreenConnections)); const editData = await common.fetchScreenEditData(pool); return res.send(pages.renderScreenEditPage(screen, editData, req.query.message ? String(req.query.message) : '', req.currentUser)); @@ -107,5 +152,41 @@ module.exports = function registerScreensRoutes(app, deps) { next(error); } }); + + app.get('/downloads/kiosk/pulse-signage-kiosk.bat', requirePermission('screens.update'), async function (_req, res, next) { + try { + const playerUrl = await buildPlayerUrl(); + if (!playerUrl) { + return res.status(404).send('Player URL is not available yet.'); + } + + const content = buildLauncherContent(batTemplatePath, playerUrl); + if (!content) { + return res.status(404).send('Launcher template is not available.'); + } + + sendLauncherDownload(res, 'pulse-signage-kiosk.bat', content); + } catch (error) { + next(error); + } + }); + + app.get('/downloads/kiosk/pulse-signage-kiosk.sh', requirePermission('screens.update'), async function (_req, res, next) { + try { + const playerUrl = await buildPlayerUrl(); + if (!playerUrl) { + return res.status(404).send('Player URL is not available yet.'); + } + + const content = buildLauncherContent(shTemplatePath, playerUrl); + if (!content) { + return res.status(404).send('Launcher template is not available.'); + } + + sendLauncherDownload(res, 'pulse-signage-kiosk.sh', content); + } catch (error) { + next(error); + } + }); }; diff --git a/src/web/views/signage/clients/list.hbs b/src/web/views/signage/clients/list.hbs index 50c4e2f..32b7084 100644 --- a/src/web/views/signage/clients/list.hbs +++ b/src/web/views/signage/clients/list.hbs @@ -13,58 +13,58 @@
Open the player base URL on a device for onboarding.
+Download the Windows or Linux launcher to start a browser in kiosk mode and open the correct player page for onboarding.
+Kiosk mode is for dedicated signage screens.
+These downloads are scripts, not applications: the Windows download is a .bat file and the Linux download is a .sh file.
+Check this box to enable the Windows and Linux downloads.
+