Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7973ee0ea4 | ||
|
|
6416dbfd99 |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pulse-signage",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pulse-signage",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.3",
|
||||
"dependencies": {
|
||||
"bootstrap-icons": "1.11.3",
|
||||
"dotenv": "^17.4.2",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pulse-signage",
|
||||
"version": "1.3.2",
|
||||
"version": "1.3.4",
|
||||
"private": false,
|
||||
"description": "Pulse Signage application with MySQL and media uploads",
|
||||
"repository": {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const crypto = require('crypto');
|
||||
|
||||
function normalizeClientName(value) {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
@@ -68,9 +70,49 @@ async function isClientNameAvailable(pool, clientName, excludeDeviceId, liveConn
|
||||
}
|
||||
}
|
||||
|
||||
function buildClientNameLockName(clientName) {
|
||||
return `ps_client_name_${crypto.createHash('sha1').update(String(clientName || '').trim().toLowerCase()).digest('hex')}`;
|
||||
}
|
||||
|
||||
async function withClientNameReservation(pool, clientName, handler) {
|
||||
if (!pool || typeof pool.getConnection !== 'function') {
|
||||
return handler();
|
||||
}
|
||||
|
||||
const normalizedName = normalizeClientName(clientName);
|
||||
if (!normalizedName) {
|
||||
return handler();
|
||||
}
|
||||
|
||||
const connection = await pool.getConnection();
|
||||
const lockName = buildClientNameLockName(normalizedName);
|
||||
let lockAcquired = false;
|
||||
|
||||
try {
|
||||
const [lockRows] = await connection.query('SELECT GET_LOCK(?, 5) AS lock_result', [lockName]);
|
||||
const lockResult = lockRows && lockRows[0] ? Number(lockRows[0].lock_result) : 0;
|
||||
if (lockResult !== 1) {
|
||||
const error = new Error('Client name is busy. Please try again.');
|
||||
error.statusCode = 409;
|
||||
throw error;
|
||||
}
|
||||
|
||||
lockAcquired = true;
|
||||
return await handler();
|
||||
} finally {
|
||||
if (lockAcquired) {
|
||||
try {
|
||||
await connection.query('SELECT RELEASE_LOCK(?)', [lockName]);
|
||||
} catch (_error) {}
|
||||
}
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
normalizeClientName: normalizeClientName,
|
||||
normalizeDeviceId: normalizeDeviceId,
|
||||
collectLiveConnections: collectLiveConnections,
|
||||
isClientNameAvailable: isClientNameAvailable
|
||||
isClientNameAvailable: isClientNameAvailable,
|
||||
withClientNameReservation: withClientNameReservation
|
||||
};
|
||||
+19
-19
@@ -1,4 +1,4 @@
|
||||
const { isClientNameAvailable } = require('../client-name-check');
|
||||
const { isClientNameAvailable, withClientNameReservation } = require('../client-name-check');
|
||||
const { isTransientDbError } = require('./onboarding-store');
|
||||
function normalizeDeviceId(value) {
|
||||
return String(value || '').trim().replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128);
|
||||
@@ -48,27 +48,27 @@ async function commitDeviceBinding(pool, deviceId, clientName, screenSlug, isNam
|
||||
throw new Error('Screen is required.');
|
||||
}
|
||||
|
||||
const [screenRows] = await pool.query('SELECT id, name, slug FROM screens WHERE slug = ?', [normalizedScreenSlug]);
|
||||
if (!screenRows.length) {
|
||||
throw new Error('Screen not found.');
|
||||
}
|
||||
const screen = screenRows[0];
|
||||
return withClientNameReservation(pool, normalizedClientName, async function () {
|
||||
const [screenRows] = await pool.query('SELECT id, name, slug FROM screens WHERE slug = ?', [normalizedScreenSlug]);
|
||||
if (!screenRows.length) {
|
||||
throw new Error('Screen not found.');
|
||||
}
|
||||
const screen = screenRows[0];
|
||||
|
||||
const available = typeof isClientNameAvailableOnScreen === 'function'
|
||||
? await isClientNameAvailable(pool, normalizedClientName, normalizedDeviceId, liveConnections)
|
||||
: true;
|
||||
if (!available) {
|
||||
const error = new Error('Client name already exists.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
const available = await isClientNameAvailable(pool, normalizedClientName, normalizedDeviceId, liveConnections);
|
||||
if (!available) {
|
||||
const error = new Error('Client name already exists.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
await pool.query(
|
||||
'INSERT INTO player_onboarding_devices (device_id, client_name, screen_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE client_name = VALUES(client_name), screen_id = VALUES(screen_id), modified_at = CURRENT_TIMESTAMP',
|
||||
[normalizedDeviceId, normalizedClientName, screen.id]
|
||||
);
|
||||
await pool.query(
|
||||
'INSERT INTO player_onboarding_devices (device_id, client_name, screen_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE client_name = VALUES(client_name), screen_id = VALUES(screen_id), modified_at = CURRENT_TIMESTAMP',
|
||||
[normalizedDeviceId, normalizedClientName, screen.id]
|
||||
);
|
||||
|
||||
return getOnboardingStatus(pool, normalizedDeviceId);
|
||||
return getOnboardingStatus(pool, normalizedDeviceId);
|
||||
});
|
||||
}
|
||||
|
||||
async function bindDeviceToScreen(pool, deviceId, clientName, screenSlug, isNameAvailableOnScreen, playerRuntime, onboardingStore) {
|
||||
|
||||
+3
-2
@@ -16,7 +16,7 @@ const registerAdminScreenCommandRoutes = require('./web/routes/admin-screen-comm
|
||||
const registerAdminContentRoutes = require('./web/routes/admin-content');
|
||||
const { createWebBootstrap } = require('./web/bootstrap');
|
||||
const { createPlayerActionService } = require('./web/player-actions');
|
||||
const { isClientNameAvailable } = require('./client-name-check');
|
||||
const { isClientNameAvailable, withClientNameReservation } = require('./client-name-check');
|
||||
const { createSessionService } = require('./web/session');
|
||||
const {
|
||||
formatDashboardDate,
|
||||
@@ -179,7 +179,8 @@ async function start() {
|
||||
pool: pool,
|
||||
forwardPlayerCommand: playerActionService.forwardPlayerCommand,
|
||||
getScreenConnections: playerActionService.getScreenConnections,
|
||||
isClientNameAvailable: isClientNameAvailable
|
||||
isClientNameAvailable: isClientNameAvailable,
|
||||
withClientNameReservation: withClientNameReservation
|
||||
});
|
||||
|
||||
registerAdminContentRoutes(app, {
|
||||
|
||||
@@ -123,10 +123,37 @@
|
||||
--bs-navbar-nav-link-padding-x: 10px;
|
||||
}
|
||||
|
||||
.app-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar-wrapper {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.min-h-0 {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sidebar-brand .brand-text.fw-light {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-version {
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.sidebar-version__inner {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -82,14 +82,16 @@
|
||||
function renderClientActionCell(client) {
|
||||
var paused = Boolean(client.paused);
|
||||
var pauseButtonClass = 'btn btn-sm btn-info';
|
||||
var pauseButtonIcon = paused ? 'bi-play-fill' : 'bi-pause-fill';
|
||||
var pauseButtonLabel = paused ? 'Resume' : 'Pause';
|
||||
var blackout = Boolean(client.blackout);
|
||||
var blackoutButtonClass = 'btn btn-sm ' + (blackout ? 'btn-success' : 'btn-secondary');
|
||||
var blackoutButtonIcon = blackout ? 'bi-eye' : 'bi-eye-slash';
|
||||
var blackoutButtonLabel = blackout ? 'Restore' : 'Blackout';
|
||||
var reloadConfirmMessage = 'Reloading will restart the player page. Continue?';
|
||||
var blackoutCommandValue = blackout ? 'false' : 'true';
|
||||
|
||||
return '<div class="d-flex flex-wrap gap-1"><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-confirm-message="' + escapeHtml(reloadConfirmMessage) + '" data-async-command><input type="hidden" name="command" value="reload" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-danger" data-action="reload"><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload</button></form><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="previous" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-outline-secondary" data-action="previous" aria-label="Previous slide"><i class="bi bi-skip-backward-fill" aria-hidden="true"></i></button></form><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="next" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-outline-secondary" data-action="next" aria-label="Next slide"><i class="bi bi-skip-forward-fill" aria-hidden="true"></i></button></form><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="pause" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="' + pauseButtonClass + '" data-action="pause"><i class="bi bi-pause-fill me-1" aria-hidden="true"></i>' + pauseButtonLabel + '</button></form><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="blackout" /><input type="hidden" name="blackout" value="' + blackoutCommandValue + '" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="' + blackoutButtonClass + '" data-action="blackout"><i class="bi bi-eye-slash me-1" aria-hidden="true"></i>' + blackoutButtonLabel + '</button></form></div>';
|
||||
return '<div class="d-flex flex-wrap gap-1"><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-confirm-message="' + escapeHtml(reloadConfirmMessage) + '" data-async-command><input type="hidden" name="command" value="reload" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-danger" data-action="reload"><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload</button></form><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="previous" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-outline-secondary" data-action="previous" aria-label="Previous slide"><i class="bi bi-skip-backward-fill" aria-hidden="true"></i></button></form><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="next" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-outline-secondary" data-action="next" aria-label="Next slide"><i class="bi bi-skip-forward-fill" aria-hidden="true"></i></button></form><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="pause" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="' + pauseButtonClass + '" data-action="pause"><i class="bi bi-' + pauseButtonIcon + ' me-1" aria-hidden="true"></i>' + pauseButtonLabel + '</button></form><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="blackout" /><input type="hidden" name="blackout" value="' + blackoutCommandValue + '" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="' + blackoutButtonClass + '" data-action="blackout"><i class="bi bi-' + blackoutButtonIcon + ' me-1" aria-hidden="true"></i>' + blackoutButtonLabel + '</button></form></div>';
|
||||
}
|
||||
|
||||
function updateClientActionCell(cell, client) {
|
||||
@@ -105,7 +107,7 @@
|
||||
|
||||
var paused = Boolean(client.paused);
|
||||
setButtonVariant(pauseButton, ['btn-secondary', 'btn-outline-primary'], 'btn-info');
|
||||
pauseButton.innerHTML = '<i class="bi bi-pause-fill me-1" aria-hidden="true"></i>' + (paused ? 'Resume' : 'Pause');
|
||||
pauseButton.innerHTML = '<i class="bi bi-' + (paused ? 'play-fill' : 'pause-fill') + ' me-1" aria-hidden="true"></i>' + (paused ? 'Resume' : 'Pause');
|
||||
|
||||
var pauseForm = pauseButton.form;
|
||||
if (pauseForm) {
|
||||
@@ -142,8 +144,9 @@
|
||||
}
|
||||
|
||||
var blackout = Boolean(client.blackout);
|
||||
var blackoutButtonIcon = blackout ? 'bi-eye' : 'bi-eye-slash';
|
||||
setButtonVariant(blackoutButton, ['btn-success', 'btn-secondary'], blackout ? 'btn-success' : 'btn-secondary');
|
||||
blackoutButton.innerHTML = '<i class="bi bi-eye-slash me-1" aria-hidden="true"></i>' + (blackout ? 'Restore' : 'Blackout');
|
||||
blackoutButton.innerHTML = '<i class="bi ' + blackoutButtonIcon + ' me-1" aria-hidden="true"></i>' + (blackout ? 'Restore' : 'Blackout');
|
||||
|
||||
var blackoutForm = blackoutButton.form;
|
||||
if (blackoutForm) {
|
||||
@@ -357,8 +360,9 @@
|
||||
var label = allBlackout ? 'Restore all clients' : 'Blackout all clients';
|
||||
var blackoutForm = blackoutButton.form;
|
||||
var blackoutInput = blackoutForm ? blackoutForm.querySelector('input[name="blackout"]') : null;
|
||||
var blackoutButtonIcon = allBlackout ? 'bi-eye' : 'bi-eye-slash';
|
||||
|
||||
blackoutButton.innerHTML = (allBlackout ? '<i class="bi bi-eye me-1" aria-hidden="true"></i>' : '<i class="bi bi-eye-slash me-1" aria-hidden="true"></i>') + escapeHtml(label);
|
||||
blackoutButton.innerHTML = '<i class="bi ' + blackoutButtonIcon + ' me-1" aria-hidden="true"></i>' + escapeHtml(label);
|
||||
setButtonVariant(blackoutButton, ['btn-success', 'btn-danger', 'btn-secondary', 'btn-outline-secondary', 'btn-outline-dark'], 'btn-secondary');
|
||||
if (blackoutInput) {
|
||||
blackoutInput.value = allBlackout ? 'false' : 'true';
|
||||
|
||||
@@ -20,6 +20,57 @@ module.exports = function registerAdminManageRoutes(app, deps) {
|
||||
const forwardPlayerCommand = deps.forwardPlayerCommand;
|
||||
const PLAYER_PUBLIC_BASE_URL = deps.playerPublicBaseUrl;
|
||||
|
||||
async function fetchAllScreenSlugs() {
|
||||
const [rows] = await pool.query('SELECT slug FROM screens ORDER BY slug ASC');
|
||||
return (rows || [])
|
||||
.map(function (row) {
|
||||
return String(row && row.slug ? row.slug : '').trim();
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
app.post('/admin/commands', async function (req, res, next) {
|
||||
try {
|
||||
const command = String((req.body && req.body.command) || req.query.command || '').trim().toLowerCase();
|
||||
const blackoutValue = req.body && Object.prototype.hasOwnProperty.call(req.body, 'blackout')
|
||||
? req.body.blackout
|
||||
: req.query.blackout;
|
||||
|
||||
if (!command) {
|
||||
return res.status(400).json({ error: 'Command is required' });
|
||||
}
|
||||
|
||||
if (command !== 'reload' && command !== 'blackout') {
|
||||
return res.status(400).json({ error: 'Unsupported command' });
|
||||
}
|
||||
|
||||
const slugs = await fetchAllScreenSlugs();
|
||||
if (!slugs.length) {
|
||||
await broadcastDashboardState();
|
||||
return res.json({ ok: true, command: command, sent: 0 });
|
||||
}
|
||||
|
||||
const payload = command === 'blackout'
|
||||
? {
|
||||
command: 'blackout',
|
||||
blackout: blackoutValue === true || blackoutValue === 'true' || blackoutValue === '1' ? true : false
|
||||
}
|
||||
: 'reload';
|
||||
|
||||
const sentCount = await notifyPlayerScreens(slugs, payload);
|
||||
await broadcastDashboardState();
|
||||
|
||||
return res.json({
|
||||
ok: true,
|
||||
command: command,
|
||||
sent: sentCount,
|
||||
blackout: command === 'blackout' ? Boolean(payload.blackout) : undefined
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/admin/playlists', async function (req, res, next) {
|
||||
try {
|
||||
const name = String(req.body.name || '').trim();
|
||||
|
||||
@@ -3,6 +3,7 @@ module.exports = function registerAdminScreenCommandRoutes(app, deps) {
|
||||
const forwardPlayerCommand = deps.forwardPlayerCommand;
|
||||
const getScreenConnections = deps.getScreenConnections;
|
||||
const isClientNameAvailable = deps.isClientNameAvailable;
|
||||
const withClientNameReservation = deps.withClientNameReservation;
|
||||
|
||||
app.post('/admin/screens/:slug/commands', async function (req, res, next) {
|
||||
try {
|
||||
@@ -56,40 +57,66 @@ module.exports = function registerAdminScreenCommandRoutes(app, deps) {
|
||||
unchanged: true
|
||||
});
|
||||
}
|
||||
let liveConnections = [];
|
||||
try {
|
||||
const [screenSlugs] = await pool.query('SELECT slug FROM screens ORDER BY slug ASC');
|
||||
const liveResults = await Promise.all((screenSlugs || []).map(async function (row) {
|
||||
const screenSlug = String(row && row.slug ? row.slug : '').trim();
|
||||
if (!screenSlug || typeof getScreenConnections !== 'function') {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const liveResponse = await getScreenConnections(screenSlug);
|
||||
return Array.isArray(liveResponse && liveResponse.connections) ? liveResponse.connections : [];
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
}));
|
||||
liveConnections = liveResults.flat();
|
||||
} catch (_error) {
|
||||
liveConnections = [];
|
||||
if (typeof withClientNameReservation !== 'function') {
|
||||
return res.status(500).json({ error: 'Client name reservation is unavailable.' });
|
||||
}
|
||||
|
||||
const available = typeof isClientNameAvailable === 'function'
|
||||
? await isClientNameAvailable(pool, clientName, deviceId, liveConnections)
|
||||
: true;
|
||||
if (!available) {
|
||||
return res.status(409).json({ error: 'Client name already exists.' });
|
||||
}
|
||||
return withClientNameReservation(pool, clientName, async function () {
|
||||
let liveConnections = [];
|
||||
try {
|
||||
const [screenSlugs] = await pool.query('SELECT slug FROM screens ORDER BY slug ASC');
|
||||
const liveResults = await Promise.all((screenSlugs || []).map(async function (row) {
|
||||
const screenSlug = String(row && row.slug ? row.slug : '').trim();
|
||||
if (!screenSlug || typeof getScreenConnections !== 'function') {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const liveResponse = await getScreenConnections(screenSlug);
|
||||
return Array.isArray(liveResponse && liveResponse.connections) ? liveResponse.connections : [];
|
||||
} catch (_error) {
|
||||
return [];
|
||||
}
|
||||
}));
|
||||
liveConnections = liveResults.flat();
|
||||
} catch (_error) {
|
||||
liveConnections = [];
|
||||
}
|
||||
|
||||
if (!onboardingRow) {
|
||||
await forwardPlayerCommand(slug, {
|
||||
command: command,
|
||||
clientName: clientName,
|
||||
clientId: connectionId || deviceId || null,
|
||||
deviceId: deviceId || null
|
||||
}, connectionId || deviceId || undefined);
|
||||
const available = await isClientNameAvailable(pool, clientName, deviceId, liveConnections);
|
||||
if (!available) {
|
||||
return res.status(409).json({ error: 'Client name already exists.' });
|
||||
}
|
||||
|
||||
if (!onboardingRow) {
|
||||
await forwardPlayerCommand(slug, {
|
||||
command: command,
|
||||
clientName: clientName,
|
||||
clientId: connectionId || deviceId || null,
|
||||
deviceId: deviceId || null
|
||||
}, connectionId || deviceId || undefined);
|
||||
|
||||
return res.json({
|
||||
screen: screenRows[0],
|
||||
screenSlug: slug,
|
||||
command: command,
|
||||
connectionId: connectionId || null,
|
||||
deviceId: deviceId,
|
||||
clientName: clientName,
|
||||
ok: true,
|
||||
liveOnly: true
|
||||
});
|
||||
}
|
||||
|
||||
const [updateResult] = await pool.query(
|
||||
`UPDATE player_onboarding_devices pod
|
||||
JOIN screens s ON s.id = pod.screen_id
|
||||
SET pod.client_name = ?, pod.modified_at = CURRENT_TIMESTAMP
|
||||
WHERE s.slug = ? AND pod.device_id = ?`,
|
||||
[clientName, slug, deviceId]
|
||||
);
|
||||
if (!updateResult.affectedRows) {
|
||||
return res.status(404).json({ error: 'Client not found' });
|
||||
}
|
||||
|
||||
return res.json({
|
||||
screen: screenRows[0],
|
||||
@@ -98,30 +125,8 @@ module.exports = function registerAdminScreenCommandRoutes(app, deps) {
|
||||
connectionId: connectionId || null,
|
||||
deviceId: deviceId,
|
||||
clientName: clientName,
|
||||
ok: true,
|
||||
liveOnly: true
|
||||
ok: true
|
||||
});
|
||||
}
|
||||
|
||||
const [updateResult] = await pool.query(
|
||||
`UPDATE player_onboarding_devices pod
|
||||
JOIN screens s ON s.id = pod.screen_id
|
||||
SET pod.client_name = ?, pod.modified_at = CURRENT_TIMESTAMP
|
||||
WHERE s.slug = ? AND pod.device_id = ?`,
|
||||
[clientName, slug, deviceId]
|
||||
);
|
||||
if (!updateResult.affectedRows) {
|
||||
return res.status(404).json({ error: 'Client not found' });
|
||||
}
|
||||
|
||||
return res.json({
|
||||
screen: screenRows[0],
|
||||
screenSlug: slug,
|
||||
command: command,
|
||||
connectionId: connectionId || null,
|
||||
deviceId: deviceId,
|
||||
clientName: clientName,
|
||||
ok: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -67,12 +67,16 @@
|
||||
<form method="post" action="/admin/screens/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
||||
<input type="hidden" name="command" value="pause" />
|
||||
<input type="hidden" name="connectionId" value="{{id}}" />
|
||||
<button type="submit" class="btn btn-sm btn-info">{{#if paused}}Resume{{else}}Pause{{/if}}</button>
|
||||
<button type="submit" class="btn btn-sm btn-info">
|
||||
<i class="bi bi-pause-fill me-1" aria-hidden="false"></i>Pause
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/screens/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
||||
<input type="hidden" name="command" value="blackout" />
|
||||
<input type="hidden" name="connectionId" value="{{id}}" />
|
||||
<button type="submit" class="btn btn-sm {{#if blackout}}btn-success{{else}}btn-secondary{{/if}}">{{#if blackout}}Restore{{else}}Blackout{{/if}}</button>
|
||||
<button type="submit" class="btn btn-sm {{#if blackout}}btn-success{{else}}btn-secondary{{/if}}">
|
||||
<i class="bi bi-eye-slash me-1" aria-hidden="false"></i>Blackout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
type="submit"
|
||||
class="btn btn-secondary dashboard-action-button"
|
||||
id="dashboard-blackout-all-button"
|
||||
><i class="bi bi-eye-slash me-1" aria-hidden="true"></i>Blackout all clients</button>
|
||||
><i class="bi bi-eye-slash me-1" aria-hidden="false"></i>Blackout all clients</button>
|
||||
<span class="dashboard-action-help">Blank active players immediately.</span>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
<title>{{title}} - Pulse</title>
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png" />
|
||||
<script src="/assets/js/theme-init.js"></script>
|
||||
<link rel="preload" href="/assets/adminlte/bootstrap-icons/fonts/bootstrap-icons.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
<link rel="preload" href="/assets/adminlte/bootstrap-icons/fonts/bootstrap-icons.woff" as="font" type="font/woff" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="/assets/adminlte/bootstrap-icons/css/bootstrap-icons.min.css" />
|
||||
<link rel="stylesheet" href="/assets/adminlte/css/adminlte.min.css" />
|
||||
<link rel="stylesheet" href="/assets/css/theme-custom.css" />
|
||||
@@ -126,14 +128,14 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<aside class="app-sidebar bg-body-secondary shadow" data-bs-theme="dark">
|
||||
<aside class="app-sidebar bg-body-secondary shadow d-flex flex-column" data-bs-theme="dark">
|
||||
<div class="sidebar-brand">
|
||||
<a href="/admin" class="brand-link">
|
||||
<span class="brand-text fw-light">Pulse Signage</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-wrapper">
|
||||
<nav class="mt-2" aria-label="Main navigation">
|
||||
<div class="sidebar-wrapper d-flex flex-column flex-grow-1 min-h-0">
|
||||
<nav class="mt-2 flex-grow-1" aria-label="Main navigation">
|
||||
<ul class="nav sidebar-menu flex-column" data-lte-toggle="treeview" data-accordion="false" role="menu" id="navigation">
|
||||
<li class="nav-header">MAIN NAVIGATION</li>
|
||||
<li class="nav-item">
|
||||
@@ -187,6 +189,11 @@
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="sidebar-version mt-auto pt-2 text-secondary small">
|
||||
<div class="sidebar-version__inner border-top pt-2">
|
||||
v{{appVersion}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user