Add onboarding weather and template gradients
This commit is contained in:
+172
-22
@@ -13,6 +13,7 @@ const { commitDeviceBinding, bindPlayerToScreen, getOnboardingStatus, getPlayerP
|
||||
const { createStyledQrCodeSvg } = require('../data/qr-code');
|
||||
const { verifyPageAuthToken } = require('#src/request-auth');
|
||||
|
||||
|
||||
function createThinClientConfig() {
|
||||
return {
|
||||
port: Number(process.env.THIN_CLIENT_PORT || 8090),
|
||||
@@ -354,6 +355,27 @@ async function start() {
|
||||
return socket && socket.playerDeviceId ? String(socket.playerDeviceId).trim() : '';
|
||||
}
|
||||
|
||||
function findPlayerByPairingCode(pairingCode) {
|
||||
const normalizedCode = String(pairingCode || '').trim().toUpperCase();
|
||||
if (!normalizedCode) {
|
||||
return null;
|
||||
}
|
||||
for (const [deviceId, socket] of playerSockets.entries()) {
|
||||
const pairingCodes = socket && Array.isArray(socket.pairingSessions)
|
||||
? socket.pairingSessions.map(function (entry) { return entry.code; })
|
||||
: (socket && Array.isArray(socket.pairingCodes) ? socket.pairingCodes : [socket && socket.pairingCode]);
|
||||
if (socket && pairingCodes.some(function (code) {
|
||||
return String(code || '').trim().toUpperCase() === normalizedCode;
|
||||
})) {
|
||||
const session = socket.pairingSessions && socket.pairingSessions.find(function (entry) {
|
||||
return String(entry.code || '').trim().toUpperCase() === normalizedCode;
|
||||
});
|
||||
return { deviceId: deviceId, socket: socket, clientId: session && session.clientId ? session.clientId : null, code: normalizedCode };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function removeConnectedPlayerSocket(socket) {
|
||||
if (!socket || !socket.playerDeviceId) {
|
||||
return false;
|
||||
@@ -514,6 +536,21 @@ async function start() {
|
||||
};
|
||||
}
|
||||
|
||||
function requireOnboardingAuth(req, res, next) {
|
||||
const token = String(req.headers['x-pulse-page-auth'] || '').trim();
|
||||
const payload = verifyPageAuthToken(token);
|
||||
if (payload && ['onboarding', 'player'].indexOf(String(payload.scope || '').trim()) !== -1) {
|
||||
req.playerPageAuth = payload;
|
||||
return next();
|
||||
}
|
||||
|
||||
if (verifyRequestAuth(req)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return res.status(401).json({ error: 'Onboarding authentication required.' });
|
||||
}
|
||||
|
||||
app.get('/api/media/config', requireRequestAuth, function (_req, res) {
|
||||
res.json({
|
||||
mediaDir: config.mediaDir,
|
||||
@@ -630,12 +667,45 @@ async function start() {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/screens/:slug/announcements/refresh', requireRequestAuth, function (req, res) {
|
||||
const slug = String(req.params.slug || '').trim();
|
||||
if (!slug) {
|
||||
return res.status(400).json({ error: 'Screen slug is required.' });
|
||||
}
|
||||
|
||||
const targetPlayers = Array.from(playerSockets.values()).filter(function (socket) {
|
||||
return socket && socket.readyState === WebSocket.OPEN;
|
||||
}).map(function (socket) {
|
||||
return { socket: socket };
|
||||
});
|
||||
let sent = 0;
|
||||
targetPlayers.forEach(function (target) {
|
||||
if (!target || !target.socket || target.socket.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
target.socket.send(JSON.stringify({
|
||||
type: 'command',
|
||||
command: 'announcement-refresh',
|
||||
screenSlug: slug,
|
||||
sentAt: new Date().toISOString()
|
||||
}));
|
||||
sent += 1;
|
||||
} catch (_error) {
|
||||
}
|
||||
});
|
||||
|
||||
return res.json({ ok: true, screenSlug: slug, sent: sent });
|
||||
});
|
||||
|
||||
app.get('/api/onboarding/status', async function (req, res, next) {
|
||||
try {
|
||||
const deviceId = normalizeDeviceId(req.query.deviceId) || getConnectedPlayerDeviceId();
|
||||
const requestedDeviceId = normalizeDeviceId(req.query.deviceId);
|
||||
const deviceId = requestedDeviceId;
|
||||
const status = await getOnboardingStatus(pool, deviceId);
|
||||
res.json({
|
||||
deviceId: normalizeDeviceId(deviceId),
|
||||
deviceId: requestedDeviceId,
|
||||
onboarded: Boolean(status && status.screen_id),
|
||||
clientName: status ? status.client_name : null,
|
||||
screenId: status ? status.screen_id : null,
|
||||
@@ -648,6 +718,58 @@ async function start() {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/onboarding/resolve', requireRequestAuth, function (req, res) {
|
||||
const pairing = findPlayerByPairingCode(req.query.pairingCode);
|
||||
if (!pairing) {
|
||||
return res.status(401).json({ error: 'A valid kiosk pairing code is required.' });
|
||||
}
|
||||
res.json({
|
||||
deviceId: pairing.deviceId,
|
||||
clientId: pairing.clientId || null,
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/onboarding/url', requireRequestAuth, function (req, res) {
|
||||
const pairing = findPlayerByPairingCode(req.query.pairingCode);
|
||||
if (!pairing) {
|
||||
return res.status(401).json({ error: 'A valid kiosk pairing code is required.' });
|
||||
}
|
||||
const webBaseUrl = String(process.env.WEB_PUBLIC_URL || '').trim().replace(/\/$/, '');
|
||||
if (!webBaseUrl) {
|
||||
return res.status(503).json({ error: 'WEB_PUBLIC_URL is not configured on the bridge.' });
|
||||
}
|
||||
res.json({ url: `${webBaseUrl}/pairing?code=${encodeURIComponent(pairing.code)}` });
|
||||
});
|
||||
|
||||
app.get('/api/onboarding/qr', requireRequestAuth, async function (req, res, next) {
|
||||
try {
|
||||
const pairing = findPlayerByPairingCode(req.query.pairingCode);
|
||||
if (!pairing) {
|
||||
return res.status(401).json({ error: 'A valid kiosk pairing code is required.' });
|
||||
}
|
||||
const webBaseUrl = String(process.env.WEB_PUBLIC_URL || '').trim().replace(/\/$/, '');
|
||||
if (!webBaseUrl) {
|
||||
return res.status(503).json({ error: 'WEB_PUBLIC_URL is not configured on the bridge.' });
|
||||
}
|
||||
const svg = await createStyledQrCodeSvg({
|
||||
value: `${webBaseUrl}/pairing?code=${encodeURIComponent(pairing.code)}`,
|
||||
qr_margin: 20,
|
||||
qr_dots_type: 'dots',
|
||||
qr_dots_color: '#f4f8f5',
|
||||
qr_corners_square_type: 'dot',
|
||||
qr_corners_square_color: '#f4f8f5',
|
||||
qr_corners_dot_type: 'dot',
|
||||
qr_corners_dot_color: '#f0bd70',
|
||||
qr_background_transparent: true
|
||||
});
|
||||
res.set('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||
res.set('Cache-Control', 'no-store');
|
||||
res.send(svg);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/onboarding/screens', requirePageAuth(['onboarding', 'player']), async function (_req, res, next) {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT id, name, slug FROM d_screens ORDER BY name ASC, id ASC');
|
||||
@@ -657,25 +779,18 @@ async function start() {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/onboarding/qr', async function (req, res, next) {
|
||||
app.post('/api/onboarding', express.json(), requireOnboardingAuth, async function (req, res, next) {
|
||||
try {
|
||||
const deviceId = normalizeDeviceId(req.query.deviceId) || getConnectedPlayerDeviceId();
|
||||
if (!deviceId) {
|
||||
return res.status(400).json({ error: 'Device ID is required' });
|
||||
const pairingCode = String(req.body && req.body.pairingCode || '').trim().toUpperCase();
|
||||
const pairing = findPlayerByPairingCode(pairingCode);
|
||||
const clientId = normalizeDeviceId(req.body && req.body.clientId);
|
||||
if (!pairing) {
|
||||
return res.status(401).json({ error: 'A valid kiosk pairing code is required.' });
|
||||
}
|
||||
const onboardingUrl = `${getPlayerPublicBaseUrl(req)}/onboard?deviceId=${encodeURIComponent(deviceId)}`;
|
||||
const svg = await createStyledQrCodeSvg({ value: onboardingUrl, qr_margin: 20 });
|
||||
res.set('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||
res.set('Cache-Control', 'no-store');
|
||||
res.send(svg);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/onboarding', requirePageAuth(['onboarding', 'player']), express.json(), async function (req, res, next) {
|
||||
try {
|
||||
const deviceId = normalizeDeviceId(req.body && req.body.deviceId) || getConnectedPlayerDeviceId();
|
||||
if (!clientId) {
|
||||
return res.status(400).json({ error: 'Client ID is required.' });
|
||||
}
|
||||
const deviceId = pairing.deviceId;
|
||||
const clientName = String((req.body && req.body.clientName) || '').trim();
|
||||
const screenSlug = String((req.body && req.body.screenSlug) || '').trim();
|
||||
if (!deviceId) {
|
||||
@@ -688,8 +803,13 @@ async function start() {
|
||||
return res.status(400).json({ error: 'Screen is required' });
|
||||
}
|
||||
|
||||
const status = await commitDeviceBinding(pool, deviceId, clientName, screenSlug, null, []);
|
||||
const status = await commitDeviceBinding(pool, clientId, clientName, screenSlug, null, []);
|
||||
await bindPlayerToScreen(pool, deviceId, screenSlug);
|
||||
await sendPlayerCommandToSocket(pairing.socket, {
|
||||
command: 'redirect',
|
||||
url: `${String(pairing.socket.publicBaseUrl || getPlayerPublicBaseUrl(req)).replace(/\/$/, '')}/screen/${encodeURIComponent(screenSlug)}`,
|
||||
clientId: clientId
|
||||
});
|
||||
res.json({
|
||||
deviceId: deviceId,
|
||||
clientName: status ? status.client_name : clientName,
|
||||
@@ -709,6 +829,11 @@ async function start() {
|
||||
|
||||
app.get('/api/screens/:slug/playlist', requirePageAuth(['player']), async function (req, res, next) {
|
||||
try {
|
||||
const clientId = String(req.headers['x-pulse-client-id'] || '').trim();
|
||||
const status = await getOnboardingStatus(pool, clientId);
|
||||
if (!clientId || !status || String(status.screen_slug || '').trim() !== String(req.params.slug || '').trim()) {
|
||||
return res.status(403).json({ error: 'This browser tab is not authorized for this screen.' });
|
||||
}
|
||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
||||
const data = await playerPlaylistService.buildScreenPlaylist(req.params.slug);
|
||||
if (!data.screen) {
|
||||
@@ -729,6 +854,11 @@ async function start() {
|
||||
|
||||
app.get('/api/screens/:slug/announcement', requirePageAuth(['player']), async function (req, res, next) {
|
||||
try {
|
||||
const clientId = String(req.headers['x-pulse-client-id'] || '').trim();
|
||||
const status = await getOnboardingStatus(pool, clientId);
|
||||
if (!clientId || !status || String(status.screen_slug || '').trim() !== String(req.params.slug || '').trim()) {
|
||||
return res.status(403).json({ error: 'This browser tab is not authorized for this screen.' });
|
||||
}
|
||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
||||
const announcement = typeof common.fetchActiveAnnouncement === 'function'
|
||||
? await common.fetchActiveAnnouncement(pool, req.params.slug)
|
||||
@@ -841,6 +971,17 @@ async function start() {
|
||||
}
|
||||
|
||||
socket.playerDeviceId = deviceId;
|
||||
socket.pairingSessions = Array.isArray(payload.pairingSessions) ? payload.pairingSessions.map(function (entry) {
|
||||
return {
|
||||
deviceId: normalizeDeviceId(entry && entry.deviceId),
|
||||
clientId: normalizeDeviceId(entry && entry.clientId),
|
||||
code: String(entry && entry.code || '').trim().toUpperCase()
|
||||
};
|
||||
}).filter(function (entry) { return entry.deviceId && entry.code; }) : [];
|
||||
socket.pairingCodes = Array.from(new Set((Array.isArray(payload.pairingCodes) ? payload.pairingCodes : [payload.pairingCode]).map(function (value) {
|
||||
return String(value || '').trim().toUpperCase();
|
||||
}).filter(Boolean)));
|
||||
socket.pairingCode = socket.pairingCodes[0] || '';
|
||||
|
||||
if (messageType === 'snapshot') {
|
||||
const slug = String(payload.slug || '').trim();
|
||||
@@ -853,19 +994,28 @@ async function start() {
|
||||
}
|
||||
|
||||
if (messageType === 'register') {
|
||||
socket.publicBaseUrl = String(payload.publicBaseUrl || '').trim().replace(/\/$/, '');
|
||||
const player = await upsertPlayerRegistration(pool, {
|
||||
deviceId: deviceId,
|
||||
publicBaseUrl: payload.publicBaseUrl,
|
||||
internalBaseUrl: payload.internalBaseUrl
|
||||
});
|
||||
|
||||
const previousSocket = playerSockets.get(deviceId);
|
||||
playerSockets.set(deviceId, socket);
|
||||
if (previousSocket && previousSocket !== socket && previousSocket.readyState !== WebSocket.CLOSED) {
|
||||
try {
|
||||
previousSocket.close(1000, 'Replaced by a newer player connection.');
|
||||
} catch (_error) {
|
||||
}
|
||||
}
|
||||
logBridge(`Player ${formatPlayerConnectionLabel(deviceId)} has connected`);
|
||||
socket.send(JSON.stringify({ type: 'registered', ok: true, player: player }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageType === 'heartbeat') {
|
||||
socket.publicBaseUrl = String(payload.publicBaseUrl || socket.publicBaseUrl || '').trim().replace(/\/$/, '');
|
||||
const player = await recordPlayerHeartbeat(pool, {
|
||||
deviceId: deviceId,
|
||||
publicBaseUrl: payload.publicBaseUrl,
|
||||
@@ -928,15 +1078,15 @@ async function start() {
|
||||
});
|
||||
|
||||
socket.on('close', function () {
|
||||
clearPlayerSnapshotSources(socket.playerDeviceId);
|
||||
if (removeConnectedPlayerSocket(socket)) {
|
||||
clearPlayerSnapshotSources(socket.playerDeviceId);
|
||||
logPlayerDisconnect(socket);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', function () {
|
||||
clearPlayerSnapshotSources(socket.playerDeviceId);
|
||||
if (removeConnectedPlayerSocket(socket)) {
|
||||
clearPlayerSnapshotSources(socket.playerDeviceId);
|
||||
logPlayerDisconnect(socket);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user