mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
big overhaul for server and webui feature matching, things default to disabled and disappear from UI when disabled.
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
const io = require('../../globals/io');
|
||||
const logger = require('../../globals/logger').child('barcodeGameService');
|
||||
const { loadConfig } = require('../../helpers/configLoader');
|
||||
const { isFeatureEnabled } = require('../../helpers/features');
|
||||
const { subscribe } = require('../eventBus');
|
||||
const { sendSystemMessage } = require('../chatService');
|
||||
const { getActiveDrivers } = require('../turnService');
|
||||
@@ -30,6 +31,7 @@ const GAME_DEFINITIONS = [scanQuest, scansPerSecond, mostItems];
|
||||
const GAMES_BY_ID = Object.fromEntries(GAME_DEFINITIONS.map((game) => [game.id, game]));
|
||||
const config = loadConfig();
|
||||
const barcodeGamesConfig = config.barcodeGames || {};
|
||||
const enabled = isFeatureEnabled('barcodeGames');
|
||||
const botName = String(barcodeGamesConfig.botName || barcodeGamesConfig.name || 'Barcode Games').trim() || 'Barcode Games';
|
||||
const botProfileImageUrl = String(barcodeGamesConfig.profileImageUrl || '').trim() || null;
|
||||
|
||||
@@ -1120,34 +1122,43 @@ function broadcastState() {
|
||||
});
|
||||
}
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('barcodeGame:subscribe', (_payload = {}, cb = () => {}) => {
|
||||
socket.join(GAME_SOCKET_ROOM);
|
||||
const state = buildStatePayload(socket);
|
||||
socket.emit('barcodeGame:state', state);
|
||||
cb({ success: true, state });
|
||||
if (enabled) {
|
||||
/*
|
||||
Barcode games are an optional layer on top of the physical scanner station.
|
||||
Keep sockets and scan subscriptions behind the feature gate so disabled
|
||||
installs do not run invisible game state.
|
||||
*/
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('barcodeGame:subscribe', (_payload = {}, cb = () => {}) => {
|
||||
socket.join(GAME_SOCKET_ROOM);
|
||||
const state = buildStatePayload(socket);
|
||||
socket.emit('barcodeGame:state', state);
|
||||
cb({ success: true, state });
|
||||
});
|
||||
|
||||
socket.on('barcodeGame:vote', ({ gameId } = {}, cb = () => {}) => {
|
||||
try {
|
||||
cb(setVote(socket, gameId));
|
||||
} catch (err) {
|
||||
logger.warn('Barcode game vote failed', { error: err.message, gameId });
|
||||
cb({ error: err.message || 'barcode game vote failed' });
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
socket.on('barcodeGame:vote', ({ gameId } = {}, cb = () => {}) => {
|
||||
subscribe('barcode.scanned', (event) => {
|
||||
try {
|
||||
cb(setVote(socket, gameId));
|
||||
handleScan(event.payload);
|
||||
} catch (err) {
|
||||
logger.warn('Barcode game vote failed', { error: err.message, gameId });
|
||||
cb({ error: err.message || 'barcode game vote failed' });
|
||||
// Scanner input should never be able to take down the server. Game failures
|
||||
// are logged and skipped so the scanner page can keep resolving barcodes.
|
||||
logger.warn('Barcode game scan handling failed', { error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
subscribe('barcode.scanned', (event) => {
|
||||
try {
|
||||
handleScan(event.payload);
|
||||
} catch (err) {
|
||||
// Scanner input should never be able to take down the server. Game failures
|
||||
// are logged and skipped so the scanner page can keep resolving barcodes.
|
||||
logger.warn('Barcode game scan handling failed', { error: err.message });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logger.info('Barcode games disabled by config');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildStatePayload,
|
||||
@@ -1155,8 +1166,10 @@ module.exports = {
|
||||
setVote,
|
||||
};
|
||||
|
||||
setInterval(() => {
|
||||
if (settleActiveGameIfNeeded()) {
|
||||
broadcastState();
|
||||
}
|
||||
}, GAME_TICK_MS).unref?.();
|
||||
if (enabled) {
|
||||
setInterval(() => {
|
||||
if (settleActiveGameIfNeeded()) {
|
||||
broadcastState();
|
||||
}
|
||||
}, GAME_TICK_MS).unref?.();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ const fs = require('fs');
|
||||
const io = require('../../globals/io');
|
||||
const logger = require('../../globals/logger').child('barcodeScannerService');
|
||||
const { resolveDataDir, resolveDataPath } = require('../../helpers/dataPaths');
|
||||
const { isFeatureEnabled } = require('../../helpers/features');
|
||||
const { getMode, MODES, modeEvents } = require('../modeManager');
|
||||
const { publishEvent } = require('../eventBus');
|
||||
const { ensureAudioForText, warmAudioForTexts } = require('./ttsCache');
|
||||
@@ -14,6 +15,7 @@ const REGISTRY_PATH = resolveDataPath('barcode-registry.json');
|
||||
const RECENT_SCAN_LIMIT = 8;
|
||||
const VALID_CODE_PATTERN = /^[a-z][0-9]{3}$/;
|
||||
const SCANNER_SOCKET_ROOM = 'barcode-scanner';
|
||||
const enabled = isFeatureEnabled('barcodeScanner');
|
||||
|
||||
let lastKnownGoodRegistry = null;
|
||||
let lastRegistryError = null;
|
||||
@@ -310,39 +312,53 @@ async function applyScan(rawCode) {
|
||||
return { result };
|
||||
}
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('barcode:subscribe', (_payload = {}, cb = () => {}) => {
|
||||
socket.join(SCANNER_SOCKET_ROOM);
|
||||
socket.emit('barcode:state', buildStatePayload());
|
||||
cb({ success: true, state: buildStatePayload() });
|
||||
if (enabled) {
|
||||
/*
|
||||
Barcode scanning is tied to a physical scanner station. Disabled installs
|
||||
should not create the registry file or expose scanner socket commands.
|
||||
*/
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('barcode:subscribe', (_payload = {}, cb = () => {}) => {
|
||||
socket.join(SCANNER_SOCKET_ROOM);
|
||||
socket.emit('barcode:state', buildStatePayload());
|
||||
cb({ success: true, state: buildStatePayload() });
|
||||
});
|
||||
|
||||
socket.on('barcode:scan', async ({ code } = {}, cb = () => {}) => {
|
||||
try {
|
||||
const { result } = await applyScan(code);
|
||||
cb({ success: true, result, state: buildStatePayload() });
|
||||
} catch (err) {
|
||||
// Socket handlers should never let a malformed scan or registry edge case
|
||||
// bubble out to the process. The page gets a normal failed acknowledgement
|
||||
// and the service keeps running for the next scan.
|
||||
logger.warn('Barcode scan failed unexpectedly', err);
|
||||
cb({ error: err.message || 'barcode scan failed' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('barcode:scan', async ({ code } = {}, cb = () => {}) => {
|
||||
try {
|
||||
const { result } = await applyScan(code);
|
||||
cb({ success: true, result, state: buildStatePayload() });
|
||||
} catch (err) {
|
||||
// Socket handlers should never let a malformed scan or registry edge case
|
||||
// bubble out to the process. The page gets a normal failed acknowledgement
|
||||
// and the service keeps running for the next scan.
|
||||
logger.warn('Barcode scan failed unexpectedly', err);
|
||||
cb({ error: err.message || 'barcode scan failed' });
|
||||
}
|
||||
modeEvents.on('change', () => {
|
||||
// Access-mode changes affect whether the scanner page should beep when it
|
||||
// submits a code, so scanner clients need a fresh state packet even without a
|
||||
// new scan.
|
||||
broadcastState();
|
||||
});
|
||||
});
|
||||
|
||||
modeEvents.on('change', () => {
|
||||
// Access-mode changes affect whether the scanner page should beep when it
|
||||
// submits a code, so scanner clients need a fresh state packet even without a
|
||||
// new scan.
|
||||
broadcastState();
|
||||
});
|
||||
|
||||
loadRegistryForScan();
|
||||
loadRegistryForScan();
|
||||
} else {
|
||||
logger.info('Barcode scanner disabled by config');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
REGISTRY_PATH,
|
||||
applyScan,
|
||||
applyScan: (...args) => {
|
||||
if (!enabled) throw new Error('Barcode scanner is disabled');
|
||||
return applyScan(...args);
|
||||
},
|
||||
buildStatePayload,
|
||||
getRegistrySnapshot,
|
||||
getRegistrySnapshot: () => {
|
||||
if (!enabled) return { registry: null, error: 'barcode scanner disabled' };
|
||||
return getRegistrySnapshot();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
const { app } = require('../../globals/http');
|
||||
const io = require('../../globals/io');
|
||||
const logger = require('../../globals/logger').child('buttonBoxService');
|
||||
const { isFeatureEnabled } = require('../../helpers/features');
|
||||
const { resolveDataDir, resolveDataPath } = require('../../helpers/dataPaths');
|
||||
const { publishEvent } = require('../eventBus');
|
||||
const { getRewardById, listRewards } = require('../../rewards');
|
||||
@@ -28,6 +29,7 @@ const DATA_DIR = resolveDataDir();
|
||||
const STORE_PATH = resolveDataPath('buttonbox-state.json');
|
||||
const BUTTON_COUNT = 4;
|
||||
const STORE_VERSION = 1;
|
||||
const enabled = isFeatureEnabled('buttonBox');
|
||||
|
||||
const store = createButtonBoxStore({
|
||||
logger,
|
||||
@@ -62,21 +64,39 @@ const core = createButtonBoxCore({
|
||||
store,
|
||||
});
|
||||
|
||||
registerButtonBoxRoute({
|
||||
app,
|
||||
logger,
|
||||
buttonCount: BUTTON_COUNT,
|
||||
normalizeIp,
|
||||
isLocalNetwork,
|
||||
applyPress: core.applyPress,
|
||||
});
|
||||
if (enabled) {
|
||||
/*
|
||||
The button box is physical local hardware, so disabled public installs
|
||||
should not expose its LAN-only press endpoint or initialize its reward file.
|
||||
*/
|
||||
registerButtonBoxRoute({
|
||||
app,
|
||||
logger,
|
||||
buttonCount: BUTTON_COUNT,
|
||||
normalizeIp,
|
||||
isLocalNetwork,
|
||||
applyPress: core.applyPress,
|
||||
});
|
||||
|
||||
store.loadState();
|
||||
core.recoverEffects().catch((err) => {
|
||||
logger.warn('Button box effect recovery failed', err.message);
|
||||
});
|
||||
store.loadState();
|
||||
core.recoverEffects().catch((err) => {
|
||||
logger.warn('Button box effect recovery failed', err.message);
|
||||
});
|
||||
} else {
|
||||
logger.info('Button box disabled by config');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getButtonBoxState: store.getStateClone,
|
||||
addButtonBoxCount: core.addCount,
|
||||
getButtonBoxState: () => {
|
||||
/*
|
||||
Session sync still includes a buttonBox key for a stable payload shape,
|
||||
but disabled mode must not create/read the persisted button-box store.
|
||||
*/
|
||||
if (!enabled) return { buttons: [] };
|
||||
return store.getStateClone();
|
||||
},
|
||||
addButtonBoxCount: (...args) => {
|
||||
if (!enabled) throw new Error('Button box is disabled');
|
||||
return core.addCount(...args);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// Scope: Exposes stable room-control APIs while delegating internals to focused modules.
|
||||
const logger = require('../../globals/logger').child('homeAssistantService');
|
||||
const { loadConfig } = require('../../helpers/configLoader');
|
||||
const { isFeatureEnabled } = require('../../helpers/features');
|
||||
const { events } = require('./state');
|
||||
const { createRuntimeEngine } = require('./runtimeEngine');
|
||||
const { createTransport } = require('./transport');
|
||||
@@ -10,7 +11,7 @@ const { registerHomeAssistantHooks } = require('./hooks');
|
||||
|
||||
const config = loadConfig();
|
||||
const haConfig = config.homeAssistant || {};
|
||||
const enabled = Boolean(haConfig?.url && haConfig?.token);
|
||||
const enabled = isFeatureEnabled('homeAssistant');
|
||||
|
||||
let callHomeAssistantServiceImpl = async () => {
|
||||
throw new Error('Home Assistant not connected');
|
||||
@@ -35,18 +36,33 @@ callHomeAssistantServiceImpl = transport.callHomeAssistantService;
|
||||
|
||||
runtimeEngine.loadEntityConfig();
|
||||
runtimeEngine.loadTriggerConfig();
|
||||
transport.connect();
|
||||
|
||||
registerHomeAssistantHooks({
|
||||
logger,
|
||||
haConfig,
|
||||
isLightControlLocked: runtimeEngine.isLightControlLocked,
|
||||
setLightsLockedOn: runtimeEngine.setLightsLockedOn,
|
||||
toggleEntity: runtimeEngine.toggleEntity,
|
||||
setEntityState: runtimeEngine.setEntityState,
|
||||
setLightColor: runtimeEngine.setLightColor,
|
||||
setLightWhite: runtimeEngine.setLightWhite,
|
||||
});
|
||||
if (enabled) {
|
||||
/*
|
||||
Loading the module should be harmless on rover-only installs. Only connect
|
||||
to Home Assistant when the central feature gate says the integration exists,
|
||||
so placeholder URLs/tokens in example config cannot start network traffic.
|
||||
*/
|
||||
transport.connect();
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
/*
|
||||
Socket routes are part of the visible Home Assistant feature. Register them
|
||||
only when enabled so disabled installs do not expose hidden controls that
|
||||
the UI has intentionally removed.
|
||||
*/
|
||||
registerHomeAssistantHooks({
|
||||
logger,
|
||||
haConfig,
|
||||
isLightControlLocked: runtimeEngine.isLightControlLocked,
|
||||
setLightsLockedOn: runtimeEngine.setLightsLockedOn,
|
||||
toggleEntity: runtimeEngine.toggleEntity,
|
||||
setEntityState: runtimeEngine.setEntityState,
|
||||
setLightColor: runtimeEngine.setLightColor,
|
||||
setLightWhite: runtimeEngine.setLightWhite,
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getState: runtimeEngine.getState,
|
||||
|
||||
@@ -5,6 +5,7 @@ const EventEmitter = require('events');
|
||||
const io = require('../../globals/io');
|
||||
const logger = require('../../globals/logger').child('liftService');
|
||||
const { loadConfig } = require('../../helpers/configLoader');
|
||||
const { isFeatureEnabled } = require('../../helpers/features');
|
||||
const { getMode, MODES } = require('../modeManager');
|
||||
const { isLockdownAdmin } = require('../roleService');
|
||||
const {
|
||||
@@ -19,6 +20,7 @@ const events = new EventEmitter();
|
||||
const config = loadConfig();
|
||||
const haConfig = config.homeAssistant || {};
|
||||
const liftConfig = haConfig.lift || {};
|
||||
const featureEnabled = isFeatureEnabled('lift');
|
||||
|
||||
const upSwitchId = String(liftConfig.upSwitch || '').trim();
|
||||
const downSwitchId = String(liftConfig.downSwitch || '').trim();
|
||||
@@ -71,7 +73,7 @@ function getState() {
|
||||
const configured = isConfigured();
|
||||
const connected = isHomeAssistantConnected();
|
||||
return {
|
||||
enabled: Boolean(homeAssistantEnabled && configured),
|
||||
enabled: Boolean(featureEnabled && homeAssistantEnabled && configured),
|
||||
configured,
|
||||
connected,
|
||||
entities: {
|
||||
@@ -102,6 +104,7 @@ function emitUpdate() {
|
||||
}
|
||||
|
||||
function assertReady() {
|
||||
if (!featureEnabled) throw new Error('Lift is disabled');
|
||||
if (!isConfigured()) throw new Error('Lift not configured');
|
||||
if (!homeAssistantEnabled) throw new Error('Home Assistant not configured');
|
||||
if (!isHomeAssistantConnected()) throw new Error('Home Assistant not connected');
|
||||
@@ -173,38 +176,47 @@ async function moveDown(actor = 'unknown') {
|
||||
return requestPosition('down', actor);
|
||||
}
|
||||
|
||||
homeAssistantEvents.on('snapshot', emitUpdate);
|
||||
homeAssistantEvents.on('status', emitUpdate);
|
||||
if (featureEnabled) {
|
||||
/*
|
||||
Lift state depends on Home Assistant switch snapshots. Subscribe only when
|
||||
the lift exists so disabled installs do not maintain hardware-specific UI
|
||||
sync paths.
|
||||
*/
|
||||
homeAssistantEvents.on('snapshot', emitUpdate);
|
||||
homeAssistantEvents.on('status', emitUpdate);
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('lift:up', async (_, cb = () => {}) => {
|
||||
try {
|
||||
if (getMode() === MODES.LOCKDOWN && !isLockdownAdmin(socket)) {
|
||||
throw new Error('Server in lockdown');
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('lift:up', async (_, cb = () => {}) => {
|
||||
try {
|
||||
if (getMode() === MODES.LOCKDOWN && !isLockdownAdmin(socket)) {
|
||||
throw new Error('Server in lockdown');
|
||||
}
|
||||
// Lift movement is now a public activity feature. Lockdown still wins
|
||||
// above because that mode is the global safety/admin gate for the room.
|
||||
const resp = await moveUp(socket.id || 'socket');
|
||||
cb({ success: true, ...resp });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
// Lift movement is now a public activity feature. Lockdown still wins
|
||||
// above because that mode is the global safety/admin gate for the room.
|
||||
const resp = await moveUp(socket.id || 'socket');
|
||||
cb({ success: true, ...resp });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('lift:down', async (_, cb = () => {}) => {
|
||||
try {
|
||||
if (getMode() === MODES.LOCKDOWN && !isLockdownAdmin(socket)) {
|
||||
throw new Error('Server in lockdown');
|
||||
socket.on('lift:down', async (_, cb = () => {}) => {
|
||||
try {
|
||||
if (getMode() === MODES.LOCKDOWN && !isLockdownAdmin(socket)) {
|
||||
throw new Error('Server in lockdown');
|
||||
}
|
||||
// Public access intentionally mirrors lift:up so both directions share
|
||||
// the same policy and cannot drift into different permission behavior.
|
||||
const resp = await moveDown(socket.id || 'socket');
|
||||
cb({ success: true, ...resp });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
// Public access intentionally mirrors lift:up so both directions share
|
||||
// the same policy and cannot drift into different permission behavior.
|
||||
const resp = await moveDown(socket.id || 'socket');
|
||||
cb({ success: true, ...resp });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
logger.info('Lift disabled by config');
|
||||
}
|
||||
|
||||
emitUpdate();
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ const EventEmitter = require('events');
|
||||
const io = require('../../globals/io');
|
||||
const logger = require('../../globals/logger').child('neatoService');
|
||||
const { loadConfig } = require('../../helpers/configLoader');
|
||||
const { isFeatureEnabled } = require('../../helpers/features');
|
||||
const { isVerified } = require('../verificationService');
|
||||
const { getMode, MODES } = require('../modeManager');
|
||||
const { isLockdownAdmin } = require('../roleService');
|
||||
@@ -20,6 +21,7 @@ const events = new EventEmitter();
|
||||
const config = loadConfig();
|
||||
const haConfig = config.homeAssistant || {};
|
||||
const neatoConfig = haConfig.neato || {};
|
||||
const featureEnabled = isFeatureEnabled('neato');
|
||||
|
||||
function normalizeDeviceName(value) {
|
||||
const raw = String(value || '').trim().toLowerCase();
|
||||
@@ -117,7 +119,7 @@ function buildState() {
|
||||
const requiredIds = requiredEntityIds();
|
||||
const entitiesAvailable = requiredIds.length > 0 && requiredIds.every((id) => isEntityAvailable(id));
|
||||
const connected = Boolean(haConnected && entitiesAvailable);
|
||||
const enabled = Boolean(homeAssistantEnabled && configured);
|
||||
const enabled = Boolean(featureEnabled && homeAssistantEnabled && configured);
|
||||
|
||||
const controls = {
|
||||
start: {
|
||||
@@ -189,15 +191,25 @@ function emitUpdate() {
|
||||
}
|
||||
}
|
||||
|
||||
homeAssistantEvents.on('snapshot', () => {
|
||||
emitUpdate();
|
||||
});
|
||||
if (featureEnabled) {
|
||||
/*
|
||||
Neato telemetry is derived from Home Assistant entities. Disabled installs
|
||||
should keep the exported API inert instead of tracking HA snapshots for a
|
||||
robot vacuum feature that does not exist on that server.
|
||||
*/
|
||||
homeAssistantEvents.on('snapshot', () => {
|
||||
emitUpdate();
|
||||
});
|
||||
|
||||
homeAssistantEvents.on('status', () => {
|
||||
emitUpdate();
|
||||
});
|
||||
homeAssistantEvents.on('status', () => {
|
||||
emitUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
function assertConfiguredAndConnected() {
|
||||
if (!featureEnabled) {
|
||||
throw new Error('Neato is disabled');
|
||||
}
|
||||
if (!device) {
|
||||
throw new Error('Neato not configured');
|
||||
}
|
||||
@@ -255,7 +267,8 @@ function hasVerifiedSockets() {
|
||||
return false;
|
||||
}
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
if (featureEnabled) {
|
||||
io.on('connection', (socket) => {
|
||||
function assertLockdownAccess() {
|
||||
if (getMode() === MODES.LOCKDOWN && !isLockdownAdmin(socket)) {
|
||||
throw new Error('Server in lockdown');
|
||||
@@ -319,7 +332,10 @@ io.on('connection', (socket) => {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
logger.info('Neato disabled by config');
|
||||
}
|
||||
|
||||
emitUpdate();
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
const EventEmitter = require('events');
|
||||
const logger = require('../../globals/logger').child('roomCameraService');
|
||||
const { loadConfig } = require('../../helpers/configLoader');
|
||||
const { getRoomCameraEntries } = require('../../helpers/features');
|
||||
|
||||
const events = new EventEmitter();
|
||||
const config = loadConfig();
|
||||
@@ -40,7 +41,7 @@ function getRoomCamera(id) {
|
||||
|
||||
function loadFromConfig() {
|
||||
cameraMap.clear();
|
||||
const list = Array.isArray(config.roomCameras) ? config.roomCameras : [];
|
||||
const list = getRoomCameraEntries(config);
|
||||
list.forEach((camera) => {
|
||||
const normalized = normalizeCamera(camera);
|
||||
if (normalized) cameraMap.set(normalized.id, normalized);
|
||||
|
||||
@@ -5,18 +5,34 @@ const { loadFromConfig, getRoomCameras, getRoomCamera, roomCameraEvents } = requ
|
||||
const { createSnapshotEngine } = require('./snapshotEngine');
|
||||
const { registerRoomCameraSocketGateway } = require('./socketGateway');
|
||||
const replay = require('../replayEngineV2/roomCameraReplayBuilder');
|
||||
const { isFeatureEnabled } = require('../../helpers/features');
|
||||
|
||||
const enabled = isFeatureEnabled('roomCameras');
|
||||
|
||||
const snapshotEngine = createSnapshotEngine({ getRoomCameras, roomCameraEvents });
|
||||
snapshotEngine.startAll();
|
||||
if (enabled) {
|
||||
/*
|
||||
Room cameras are optional local hardware/network devices. The service module
|
||||
can still be imported by replay, health, and session code, but disabled
|
||||
installs must not start polling LAN cameras in the background.
|
||||
*/
|
||||
loadFromConfig();
|
||||
snapshotEngine.startAll();
|
||||
}
|
||||
|
||||
registerRoomCameraSocketGateway({
|
||||
getRoomCamera,
|
||||
getRoomCameras,
|
||||
getRoomCameraState: snapshotEngine.getRoomCameraState,
|
||||
roomCameraStreamEvents: snapshotEngine.roomCameraStreamEvents,
|
||||
});
|
||||
|
||||
loadFromConfig();
|
||||
if (enabled) {
|
||||
/*
|
||||
Camera frame sockets are part of the room-camera feature surface. Keeping
|
||||
them behind the same gate prevents disabled features from being callable by
|
||||
hand even though server/index.js still imports this module.
|
||||
*/
|
||||
registerRoomCameraSocketGateway({
|
||||
getRoomCamera,
|
||||
getRoomCameras,
|
||||
getRoomCameraState: snapshotEngine.getRoomCameraState,
|
||||
roomCameraStreamEvents: snapshotEngine.roomCameraStreamEvents,
|
||||
});
|
||||
}
|
||||
|
||||
function buildRoomCameraReplayVideo(options = {}) {
|
||||
return replay.buildRoomCameraReplayVideo(options, { getRoomCamera, getRoomCameras });
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
// Purpose: Defines timing and static social/config constants used by session synchronization behavior.
|
||||
// Scope: Keeps runtime behavior unchanged while isolating constants from orchestration logic.
|
||||
const { loadConfig } = require('../../helpers/configLoader');
|
||||
const { getConfiguredSocials } = require('../../helpers/features');
|
||||
|
||||
const config = loadConfig();
|
||||
const discordInvite = config.discord?.invite || null;
|
||||
const kofiLink = config.kofi?.link || null;
|
||||
const serverTimezone = config.timezone || null;
|
||||
const configuredSocials = Array.isArray(config.socials) ? config.socials : null;
|
||||
const configuredSocials = getConfiguredSocials(config);
|
||||
|
||||
const ACTIVITY_SYNC_COOLDOWN_MS = 3000;
|
||||
const GPIO_TOGGLE_SYNC_COOLDOWN_MS = 1000;
|
||||
|
||||
@@ -32,6 +32,7 @@ const { getGlobalObjective } = require('../globalObjectiveService');
|
||||
const { getAdminReason } = require('../adminReasonService');
|
||||
const { subscribe } = require('../eventBus');
|
||||
const { getSocketIp, isLocalNetwork } = require('../../helpers/ipResolver');
|
||||
const { getFeatureFlags } = require('../../helpers/features');
|
||||
const { getAudioForwardState, audioForwardEvents } = require('../audioForwardService');
|
||||
const { getAudioLevels, audioLevelsEvents } = require('../audioLevelsService');
|
||||
const { getButtonBoxState } = require('../buttonBoxService');
|
||||
@@ -70,6 +71,7 @@ function buildUserEntry(socket) {
|
||||
|
||||
function buildSession(socket) {
|
||||
const overseerVote = getOverseerVoteStatus();
|
||||
const features = getFeatureFlags();
|
||||
const users = Array.from(io.sockets.sockets.values())
|
||||
.map((sock) => buildUserEntry(sock))
|
||||
.filter(Boolean)
|
||||
@@ -82,18 +84,18 @@ function buildSession(socket) {
|
||||
const assignmentRoverId = filterVisibleRoverId(socket, assignment?.roverId);
|
||||
const activeDrivers = filterActiveDriversForSocket(getActiveDrivers(), socket);
|
||||
const turnQueues = filterTurnQueuesForSocket(getTurnQueues(), socket);
|
||||
const socials =
|
||||
configuredSocials?.length
|
||||
? configuredSocials
|
||||
: [
|
||||
...(discordInvite ? [{ id: 'discord', label: 'Discord', url: discordInvite }] : []),
|
||||
...(kofiLink ? [{ id: 'kofi', label: 'Ko-fi', url: kofiLink }] : []),
|
||||
];
|
||||
const socials = features.socials && configuredSocials?.length ? configuredSocials : [];
|
||||
return {
|
||||
socketId: socket?.id || null,
|
||||
role: getRole(socket),
|
||||
mode: getMode(),
|
||||
isLocalNetwork: isLocalNetwork(getSocketIp(socket)),
|
||||
/*
|
||||
Features is the single UI contract for optional server capabilities. A
|
||||
disabled feature should be absent from navigation/layout decisions even
|
||||
though the service module may still be loaded on the Node side.
|
||||
*/
|
||||
features,
|
||||
roster,
|
||||
odometers: roverManager.getOdometersForSocket(socket),
|
||||
assignment: {
|
||||
|
||||
Reference in New Issue
Block a user