// embed Service // Purpose: Defines the embed Service module and the helpers/state used by this service unit. // Scope: Keeps runtime behavior unchanged while isolating responsibilities into a clear module boundary. const path = require('path'); const fs = require('fs'); const fsp = require('fs/promises'); const sharp = require('sharp'); const logger = require('../../globals/logger').child('embedService'); const { getMode } = require('../modeManager'); const roverManager = require('../roverManager'); const { getActiveDrivers, getTurnQueues } = require('../turnService'); const { getRoomCameras } = require('../roomCameraService'); const { getRoomCameraState } = require('../roomCameraService'); const { resolveDataPath } = require('../../helpers/dataPaths'); const { resolveSiteMetadata } = require('../../helpers/siteMetadata'); const INDEX_HTML_PATH = path.join(__dirname, '..', '..', '..', 'public', 'index.html'); const BITMAP_PATH = path.join(__dirname, '..', '..', '..', 'public', 'bitmap.png'); const ANALYTICS_HTML_PATH = resolveDataPath('analytics.html'); const ANALYTICS_PLACEHOLDER = ''; const SITE_METADATA_PLACEHOLDER = ''; const OG_WIDTH = 1200; const OG_HEIGHT = 630; let cachedIndexHtml = null; let cachedIndexMtimeMs = 0; /* Analytics provider markup belongs to the server operator, not to the shared web build. Loading the snippet once at process startup makes deployment behavior predictable: replacing analytics.html takes effect on the next normal server restart, and no analytics configuration needs to travel over Socket.IO or be exposed through a JSON endpoint. This file is intentionally trusted as raw HTML. Anyone able to write files in the server data directory already controls the deployment, and allowing a complete head snippet is what keeps this integration compatible with Umami, Plausible, Matomo, or a custom provider without provider-specific server code. */ function loadAnalyticsHeadHtml() { if (!fs.existsSync(ANALYTICS_HTML_PATH)) return ''; try { return fs.readFileSync(ANALYTICS_HTML_PATH, 'utf8').trim(); } catch (err) { /* Analytics is observability-only, so a permissions or read error must not prevent operators and drivers from loading the rover controls. */ logger.warn('Unable to read analytics head HTML; continuing without analytics', err.message); return ''; } } const analyticsHeadHtml = loadAnalyticsHeadHtml(); function escapeHtml(value) { return String(value || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function escapeXml(value) { return escapeHtml(value); } async function loadIndexHtml() { const stat = await fsp.stat(INDEX_HTML_PATH); if (!cachedIndexHtml || stat.mtimeMs !== cachedIndexMtimeMs) { cachedIndexHtml = await fsp.readFile(INDEX_HTML_PATH, 'utf8'); cachedIndexMtimeMs = stat.mtimeMs; } return cachedIndexHtml; } function getBaseUrl(req) { const forwardedProto = req.headers['x-forwarded-proto']; const forwardedHost = req.headers['x-forwarded-host']; const proto = forwardedProto ? forwardedProto.split(',')[0].trim() : req.protocol || 'http'; const host = forwardedHost || req.headers.host || 'localhost'; return `${proto}://${host}`; } function getPagePath(req) { /* Canonical URLs should describe the page rather than a tracking/query variant of it. Express's path value excludes the query string and is safe to combine with either the configured public URL or the current request. */ return req.path || '/'; } function joinPublicUrl(baseUrl, pagePath) { const normalizedPath = pagePath.startsWith('/') ? pagePath : `/${pagePath}`; return `${baseUrl}${normalizedPath}`; } function getPrimaryRoomCamera() { const cameras = getRoomCameras(); if (!cameras.length) return null; return cameras[0]; } function sumQueueCounts(turnQueues = {}) { return Object.values(turnQueues).reduce((sum, entry) => { const size = Array.isArray(entry?.queue) ? entry.queue.length : 0; return sum + size; }, 0); } function getPublicRovers() { return roverManager .getRoster() .filter((rover) => roverManager.canReplayRoverId(rover.id)); } function buildEmbedCopy(state, camera) { const roversOnline = state?.rovers?.length || 0; const visibleRoverIds = new Set((state?.rovers || []).map((rover) => String(rover.id))); const driverCount = Object.entries(state?.activeDrivers || {}).reduce((count, [roverId, socketId]) => { if (!socketId) return count; return visibleRoverIds.has(String(roverId)) ? count + 1 : count; }, 0); const mode = state?.mode || 'open'; const modeLabel = { open: 'open drive', turns: 'turns mode', admin: 'admin mode', lockdown: 'locked', }[mode] || mode; let title = 'Roomba Rover'; if (mode === 'lockdown') { title = 'Private mode is on'; } else if (roversOnline === 0) { title = 'Rovers offline - check back soon'; } else if (driverCount > 0) { title = 'Rovers in use - drive a rover'; } else if (mode === 'turns') { title = 'Controls open - jump in'; } else { title = 'Controls open - drive a rover'; } const descriptionParts = []; descriptionParts.push(`${roversOnline} rover${roversOnline === 1 ? '' : 's'} online`); if (driverCount > 0) { descriptionParts.push(`${driverCount} driving`); } else { descriptionParts.push('no active drivers'); } if (mode === 'lockdown') { descriptionParts.push('privacy mode'); } else { descriptionParts.push(modeLabel); } const description = descriptionParts.join(' | '); const statsParts = [ `${roversOnline} online`, driverCount > 0 ? `${driverCount} driving` : 'no drivers', ]; if (mode === 'lockdown') { statsParts.push('privacy mode'); } else { statsParts.push(modeLabel); } const cameraLabel = camera?.name || camera?.id || 'room cam'; return { title, description, subtitle: 'Control a live rover from your browser', stats: statsParts.join(' | '), cameraLabel: mode === 'lockdown' ? 'Room cams hidden' : `Room cam: ${cameraLabel}`, }; } function buildMetaTags({ title, description, imageUrl, pageUrl, canonicalUrl }) { const safeTitle = escapeHtml(title); const safeDescription = escapeHtml(description); const safeImage = escapeHtml(imageUrl); const safeUrl = escapeHtml(pageUrl); const tags = [ '', ``, ``, ``, '', ``, ``, '', '', ``, '', ``, ``, ``, ]; /* Only advertise a canonical address when the operator supplied a valid public URL. Guessing from request headers would permanently identify a LAN hostname or reverse-proxy hop as the public home of the instance. */ if (canonicalUrl) { tags.push(``); } tags.push(''); return tags.join('\n '); } function buildSiteMetadataTags(siteMetadata) { return [ '', ``, ``, `