Files
MultiRoombaRover/server/src/helpers/features.js
T
2026-07-19 22:43:56 -04:00

127 lines
4.9 KiB
JavaScript

// Feature Flags Helper
// Purpose: Normalizes optional server feature availability from config in one place.
// Scope: Keeps hardware/social visibility decisions out of individual UI panels and service callers.
const { loadConfig } = require('./configLoader');
function asBoolean(value, fallback = false) {
/*
Optional feature config is intentionally explicit. A missing `enabled` flag
means "off" for specialty hardware, which makes a fresh public install a
rover-only server until the operator opts into extra devices.
*/
if (typeof value === 'boolean') return value;
return fallback;
}
function asTrimmedString(value) {
return typeof value === 'string' ? value.trim() : '';
}
function getRoomCameraEntries(config) {
const raw = config.roomCameras;
/*
The public config uses `{ enabled, cameras }` so the feature gate is obvious.
Accepting the old array shape here keeps the rest of the server from needing
to know which shape the local config file currently uses.
*/
if (Array.isArray(raw)) return raw;
if (raw && typeof raw === 'object' && Array.isArray(raw.cameras)) return raw.cameras;
return [];
}
function getConfiguredSocials(config) {
/*
Social links have an explicit feature switch. Entries under `links` are just
available data; they do not enable the Links panel by existing.
*/
const links = config.socials && typeof config.socials === 'object' ? config.socials.links : [];
return Array.isArray(links)
? links.filter((entry) => asTrimmedString(entry?.url))
: [];
}
function buildFeatureFlags(config = loadConfig()) {
const homeAssistantConfig = config.homeAssistant || {};
const roomCameraConfig = config.roomCameras || {};
const kinectConfig = config.kinect || {};
const buttonBoxConfig = config.buttonBox || {};
const barcodeScannerConfig = config.barcodeScanner || {};
const balanceBoardConfig = config.balanceBoard || {};
const barcodeGamesConfig = config.barcodeGames || {};
const socialsConfig = config.socials || {};
const interInstanceConfig = config.interInstance || {};
const ptzCameraConfig = config.ptzCamera || {};
const discordConfig = config.discord || {};
const fleetReportsConfig = config.fleetReports || {};
const homeAssistant = Boolean(
asBoolean(homeAssistantConfig.enabled) &&
asTrimmedString(homeAssistantConfig.url) &&
asTrimmedString(homeAssistantConfig.token),
);
const roomCameraEntries = getRoomCameraEntries(config);
const roomCamerasEnabled = Array.isArray(config.roomCameras)
? false
: asBoolean(roomCameraConfig.enabled);
const barcodeScanner = asBoolean(barcodeScannerConfig.enabled);
return {
homeAssistant,
roomCameras: Boolean(roomCamerasEnabled && roomCameraEntries.length),
kinect: asBoolean(kinectConfig.enabled),
buttonBox: asBoolean(buttonBoxConfig.enabled),
barcodeScanner,
// The worker performs its own runtime availability reporting. Advertising
// the feature from the explicit config switch lets the UI show useful
// commissioning and hardware-error states even before a board is paired.
balanceBoard: asBoolean(balanceBoardConfig.enabled),
barcodeGames: Boolean(barcodeScanner && asBoolean(barcodeGamesConfig.enabled)),
lift: Boolean(
homeAssistant &&
asBoolean(homeAssistantConfig.lift?.enabled) &&
asTrimmedString(homeAssistantConfig.lift?.upSwitch) &&
asTrimmedString(homeAssistantConfig.lift?.downSwitch),
),
neato: Boolean(
homeAssistant &&
asBoolean(homeAssistantConfig.neato?.enabled) &&
asTrimmedString(homeAssistantConfig.neato?.device),
),
socials: Boolean(asBoolean(socialsConfig.enabled) && getConfiguredSocials(config).length > 0),
interInstance: asBoolean(interInstanceConfig.enabled),
ptzCamera: Boolean(
asBoolean(ptzCameraConfig.enabled) &&
asTrimmedString(ptzCameraConfig.host) &&
asTrimmedString(ptzCameraConfig.username) &&
asTrimmedString(ptzCameraConfig.password),
),
/*
Discord is an optional transport, not a prerequisite for chat commands.
Requiring both the explicit switch and a token prevents an old token from
silently enabling external connections on installations that have chosen
to run without the integration.
*/
discord: Boolean(asBoolean(discordConfig.enabled) && asTrimmedString(discordConfig.token)),
// Fleet reports are deliberately controlled by one explicit server switch.
// Storage contents, Discord availability, or historical database files must
// never cause the reporting UI to appear on an installation that has not
// opted into the collector.
fleetReports: asBoolean(fleetReportsConfig.enabled),
};
}
function getFeatureFlags() {
return buildFeatureFlags(loadConfig());
}
function isFeatureEnabled(featureName) {
return Boolean(getFeatureFlags()[featureName]);
}
module.exports = {
buildFeatureFlags,
getFeatureFlags,
isFeatureEnabled,
getRoomCameraEntries,
getConfiguredSocials,
};