descriptionslop

This commit is contained in:
legop3
2026-09-14 12:34:22 -04:00
parent 4635e1b40c
commit 3c385126ae
37 changed files with 339 additions and 174 deletions
@@ -20,6 +20,29 @@ function createTestDatabase() {
return createConfigurationDatabase({ databasePath: path.join(root, 'configuration.sqlite') });
}
function collectUndocumentedSchemaPaths(schema, pathLabel = '$') {
/*
The admin editor is entirely schema-generated, so missing schema prose is
missing operator documentation. Walk objects, arrays, array item schemas,
and scalar leaves instead of checking only named service definitions; this
makes every visible level of the hierarchy uphold the same contract.
*/
if (!schema || typeof schema !== 'object') return [];
const missing = typeof schema.description === 'string' && schema.description.trim()
? []
: [pathLabel];
if (schema.properties) {
Object.entries(schema.properties).forEach(([key, childSchema]) => {
missing.push(...collectUndocumentedSchemaPaths(childSchema, `${pathLabel}.${key}`));
});
}
if (schema.items) {
missing.push(...collectUndocumentedSchemaPaths(schema.items, `${pathLabel}[]`));
}
return missing;
}
test.after(() => {
temporaryRoots.forEach((root) => fs.rmSync(root, { recursive: true, force: true }));
});
@@ -46,6 +69,15 @@ test('service definitions determine document order and write-only secret handlin
assert.equal(rootSchema.properties.discord.properties.token.writeOnly, true);
});
test('every configuration section, collection, item, and option has an operator description', () => {
/*
New configuration remains self-documenting by default. Reporting every
dotted path in one assertion gives a contributor an exact repair list and
avoids recreating a separately maintained documentation registry.
*/
assert.deepEqual(collectUndocumentedSchemaPaths(rootSchema), []);
});
test('service definitions generate public feature paths without a separate registry', () => {
/*
This order follows the one configuration document, including nested Neato
+1
View File
@@ -61,6 +61,7 @@ const properties = Object.fromEntries(
);
const rootSchema = strictObject(properties, {
title: 'Configuration',
description: 'Complete server configuration. Changes are validated and saved as one revision, then loaded when the application restarts.',
required: definitions.map(({ key }) => key),
});
@@ -13,13 +13,13 @@ module.exports = {
externalSpectatorAccess: 'on',
},
schema: strictObject({
multiTabProtection: string({ enum: ['allowed', 'verifiedOnly', 'notAllowed'] }),
pauseHiddenRoverVideo: boolean(),
multiTabProtection: string({ description: 'Controls multiple active driver tabs: allowed permits everyone, verifiedOnly limits ordinary unverified users, and notAllowed limits all non-admin users.', enum: ['allowed', 'verifiedOnly', 'notAllowed'] }),
pauseHiddenRoverVideo: boolean({ description: 'Stops a rover video player while its browser surface is hidden, reducing unnecessary client and server bandwidth.' }),
nonTurnVideo: strictObject({
mode: string({ enum: ['snapshots', 'live'] }),
userThreshold: integer({ minimum: 0, maximum: 100000 }),
}, { required: ['mode', 'userThreshold'] }),
externalSpectatorVideo: string({ enum: ['snapshots', 'live'] }),
externalSpectatorAccess: string({ enum: ['off', 'on', 'verifiedOnly', 'admin'] }),
}, { title: 'Bandwidth savings', required: ['multiTabProtection', 'pauseHiddenRoverVideo', 'nonTurnVideo', 'externalSpectatorVideo', 'externalSpectatorAccess'] }),
mode: string({ description: 'snapshots replaces non-turn live rover video after the threshold is exceeded; live always permits live video.', enum: ['snapshots', 'live'] }),
userThreshold: integer({ description: 'Maximum controllable-user count allowed before snapshot mode activates for non-turn viewers; zero activates it whenever any controllable user exists.', minimum: 0, maximum: 100000 }),
}, { description: 'Controls whether users who are not currently driving receive live rover video or periodic snapshots.', required: ['mode', 'userThreshold'] }),
externalSpectatorVideo: string({ description: 'Video delivered to non-local spectator pages: snapshots conserves upload bandwidth, while live permits continuous playback.', enum: ['snapshots', 'live'] }),
externalSpectatorAccess: string({ description: 'Access for ordinary non-local spectators: off denies them, on permits them, verifiedOnly requires verification, and admin requires the spectator access grant. Local users and administrators remain allowed.', enum: ['off', 'on', 'verifiedOnly', 'admin'] }),
}, { title: 'Bandwidth savings', description: 'Defines server-owned policies for duplicate driver tabs and when live video is replaced with snapshots.', required: ['multiTabProtection', 'pauseHiddenRoverVideo', 'nonTurnVideo', 'externalSpectatorVideo', 'externalSpectatorAccess'] }),
};
@@ -7,9 +7,9 @@ module.exports = {
key: 'audioForward',
defaultValue: { enabled: true, ffmpegBin: 'ffmpeg', streamSuffix: '-fwd', maxUploadBytes: 8388608 },
schema: strictObject({
enabled: boolean(),
ffmpegBin: string({ title: 'ffmpeg executable', minLength: 1, maxLength: 500 }),
streamSuffix: string({ minLength: 1, maxLength: 80 }),
maxUploadBytes: integer({ minimum: 262144, maximum: 1073741824 }),
}, { title: 'Audio forwarding', required: ['enabled', 'ffmpegBin', 'streamSuffix', 'maxUploadBytes'] }),
enabled: boolean({ description: 'Enables verified current drivers to publish microphone or uploaded audio to their assigned rover.' }),
ffmpegBin: string({ title: 'ffmpeg executable', description: 'Executable name or path used to run the long-lived audio publishing and playback workers.', minLength: 1, maxLength: 500 }),
streamSuffix: string({ description: 'Suffix appended to each rover ID to form its internal MediaMTX forwarded-audio stream path.', minLength: 1, maxLength: 80 }),
maxUploadBytes: integer({ description: 'Maximum accepted size in bytes for one uploaded audio clip.', minimum: 262144, maximum: 1073741824 }),
}, { title: 'Audio forwarding', description: 'Controls browser-to-rover audio publishing, temporary uploaded clips, and the ffmpeg workers that feed MediaMTX.', required: ['enabled', 'ffmpegBin', 'streamSuffix', 'maxUploadBytes'] }),
};
@@ -7,9 +7,9 @@ module.exports = {
key: 'audioLevels',
defaultValue: { hornGain: 1, ttsGain: 1, forwardGain: 1, maxPersonalAdjustmentPercent: 50 },
schema: strictObject({
hornGain: number({ minimum: 0, maximum: 4 }),
ttsGain: number({ title: 'TTS gain', minimum: 0, maximum: 4 }),
forwardGain: number({ minimum: 0, maximum: 4 }),
maxPersonalAdjustmentPercent: integer({ minimum: 0, maximum: 100 }),
}, { title: 'Audio levels', required: ['hornGain', 'ttsGain', 'forwardGain', 'maxPersonalAdjustmentPercent'] }),
hornGain: number({ description: 'Server base multiplier for external horn playback, from silent at 0 through four times gain at 4.', minimum: 0, maximum: 4 }),
ttsGain: number({ title: 'TTS gain', description: 'Server base multiplier for text-to-speech playback, from silent at 0 through four times gain at 4.', minimum: 0, maximum: 4 }),
forwardGain: number({ description: 'Server base multiplier for browser-forwarded and uploaded audio, from silent at 0 through four times gain at 4.', minimum: 0, maximum: 4 }),
maxPersonalAdjustmentPercent: integer({ description: 'Largest positive or negative percentage adjustment permitted for users granted personal audio controls; zero disables personal variation.', minimum: 0, maximum: 100 }),
}, { title: 'Audio levels', description: 'Sets the initial server-owned playback gains and the allowed range for per-user adjustments.', required: ['hornGain', 'ttsGain', 'forwardGain', 'maxPersonalAdjustmentPercent'] }),
};
@@ -7,5 +7,12 @@ module.exports = {
key: 'balanceBoard',
feature: true,
defaultValue: { enabled: false, simulate: false },
schema: strictObject({ enabled: boolean(), simulate: boolean() }, { title: 'Balance Board', required: ['enabled', 'simulate'] }),
schema: strictObject({
enabled: boolean({ description: 'Starts the Wii Balance Board service and exposes its readings and controls after restart.' }),
simulate: boolean({ description: 'Runs the native worker with generated cyclic sensor data instead of connecting to Bluetooth hardware.' }),
}, {
title: 'Balance Board',
description: 'Optional Wii Balance Board input service with a development simulation mode.',
required: ['enabled', 'simulate'],
}),
};
@@ -8,8 +8,8 @@ module.exports = {
feature: true,
defaultValue: { enabled: false, botName: 'Barcode Games', profileImageUrl: '' },
schema: strictObject({
enabled: boolean(),
botName: string({ minLength: 1, maxLength: 80 }),
profileImageUrl: string({ title: 'Profile image URL', maxLength: 2048 }),
}, { title: 'Barcode games', required: ['enabled', 'botName', 'profileImageUrl'] }),
enabled: boolean({ description: 'Enables shared barcode-game voting, participation, scoring, and game-state publication.' }),
botName: string({ description: 'Nickname used for barcode-game lifecycle messages posted into chat.', minLength: 1, maxLength: 80 }),
profileImageUrl: string({ title: 'Profile image URL', description: 'Optional image URL displayed beside barcode-game chat messages; leave blank for no custom image.', maxLength: 2048 }),
}, { title: 'Barcode games', description: 'Controls the multiplayer games driven by scans received from the barcode scanner service.', required: ['enabled', 'botName', 'profileImageUrl'] }),
};
@@ -7,5 +7,11 @@ module.exports = {
key: 'barcodeScanner',
feature: true,
defaultValue: { enabled: false },
schema: strictObject({ enabled: boolean() }, { title: 'Barcode scanner', required: ['enabled'] }),
schema: strictObject({
enabled: boolean({ description: 'Registers barcode scanning, barcode administration, and scan-triggered server behavior after restart.' }),
}, {
title: 'Barcode scanner',
description: 'Optional physical barcode scanning and barcode registry service.',
required: ['enabled'],
}),
};
@@ -7,5 +7,11 @@ module.exports = {
key: 'buttonBox',
feature: true,
defaultValue: { enabled: false },
schema: strictObject({ enabled: boolean() }, { title: 'Button box', required: ['enabled'] }),
schema: strictObject({
enabled: boolean({ description: 'Registers the physical button-box input route and enables its persistent button rewards and effects after restart.' }),
}, {
title: 'Button box',
description: 'Optional physical button-box input and reward system.',
required: ['enabled'],
}),
};
@@ -15,22 +15,34 @@ module.exports = {
roles: { stalkerPing: '', announcementPing: '', adminPing: '', humanAlertPing: '' },
},
schema: strictObject({
enabled: boolean(),
token: string({ title: 'Bot token', writeOnly: true, maxLength: 10000 }),
guildId: string({ title: 'Guild id', maxLength: 100 }),
siteUrl: string({ title: 'Public site URL', maxLength: 2048 }),
enabled: boolean({ description: 'Logs the Discord bot in and enables commands, chat bridges, replay delivery, and configured announcements after restart.' }),
token: string({ title: 'Bot token', description: 'Discord bot token used to log in. The saved value is never returned to the browser.', writeOnly: true, maxLength: 10000 }),
guildId: string({ title: 'Guild id', description: 'Reserved Discord server identifier. The current bot runtime does not restrict commands or events using this value.', maxLength: 100 }),
siteUrl: string({ title: 'Public site URL', description: 'Public base URL appended to announcement embeds and server-hosted replay links.', maxLength: 2048 }),
channels: strictObject({
general: string({ maxLength: 100 }),
announcements: string({ maxLength: 100 }),
adminAlerts: string({ maxLength: 100 }),
replay: string({ maxLength: 100 }),
humanAlerts: string({ maxLength: 100 }),
}, { required: ['general', 'announcements', 'adminAlerts', 'replay', 'humanAlerts'] }),
general: string({ description: 'Channel ID used by the button-box stalker-role and everyone-ping rewards.', maxLength: 100 }),
announcements: string({ description: 'Channel ID used for public-mode openings, objective changes, and all-rovers-unlocked announcements.', maxLength: 100 }),
adminAlerts: string({ description: 'Channel ID used for rover health, battery, dock, help, and daily fleet-report notifications.', maxLength: 100 }),
replay: string({ description: 'Channel ID used to upload generated replay videos when Discord replay delivery is available.', maxLength: 100 }),
humanAlerts: string({ description: 'Channel ID used for physical human-alert button notifications and captured images.', maxLength: 100 }),
}, {
title: 'Channels',
description: 'Discord channel IDs that route each category of bot output.',
required: ['general', 'announcements', 'adminAlerts', 'replay', 'humanAlerts'],
}),
roles: strictObject({
stalkerPing: string({ maxLength: 100 }),
announcementPing: string({ maxLength: 100 }),
adminPing: string({ maxLength: 100 }),
humanAlertPing: string({ maxLength: 100 }),
}, { required: ['stalkerPing', 'announcementPing', 'adminPing', 'humanAlertPing'] }),
}, { title: 'Discord', required: ['enabled', 'token', 'guildId', 'siteUrl', 'channels', 'roles'] }),
stalkerPing: string({ description: 'Role ID mentioned by the button-box stalker-ping reward in the general channel.', maxLength: 100 }),
announcementPing: string({ description: 'Role ID mentioned by configured user announcements.', maxLength: 100 }),
adminPing: string({ description: 'Role ID mentioned for important administrative rover, battery, and help alerts.', maxLength: 100 }),
humanAlertPing: string({ description: 'Role ID mentioned when the physical human-alert button is pressed.', maxLength: 100 }),
}, {
title: 'Roles',
description: 'Discord role IDs mentioned for specific notification categories.',
required: ['stalkerPing', 'announcementPing', 'adminPing', 'humanAlertPing'],
}),
}, {
title: 'Discord',
description: 'Optional Discord bot credentials, public URL, and notification routing.',
required: ['enabled', 'token', 'guildId', 'siteUrl', 'channels', 'roles'],
}),
};
@@ -14,21 +14,43 @@ module.exports = {
privacy: { retainChatBodies: true },
},
schema: strictObject({
enabled: boolean(),
enabled: boolean({ description: 'Starts persistent fleet metric collection, reports, retention cleanup, and configured daily delivery after restart.' }),
retention: strictObject({
detailedDays: integer({ description: 'Zero retains indefinitely.', minimum: 0, maximum: 36500 }),
minuteSamplesDays: integer({ description: 'Zero retains indefinitely.', minimum: 0, maximum: 36500 }),
}, { required: ['detailedDays', 'minuteSamplesDays'] }),
detailedDays: integer({ description: 'Days to retain detailed events, command observations, sessions, and other non-minute fleet records. Zero retains them indefinitely.', minimum: 0, maximum: 36500 }),
minuteSamplesDays: integer({ description: 'Days to retain per-minute rover metric aggregates. Zero retains them indefinitely.', minimum: 0, maximum: 36500 }),
}, {
title: 'Retention',
description: 'Automatic cleanup windows for the two classes of fleet-report records.',
required: ['detailedDays', 'minuteSamplesDays'],
}),
battery: strictObject({
enabled: boolean(),
maximumIntegrationGapSeconds: number({ minimum: 0.1, maximum: 3600 }),
minimumCapacityTestDepthPercent: number({ minimum: 0, maximum: 100 }),
}, { required: ['enabled', 'maximumIntegrationGapSeconds', 'minimumCapacityTestDepthPercent'] }),
enabled: boolean({ description: 'Collects high-frequency battery sensor readings and derives charging, discharge, energy, and capacity metrics.' }),
maximumIntegrationGapSeconds: number({ description: 'Largest allowed gap in seconds between battery readings before energy integration treats the telemetry as discontinuous.', minimum: 0.1, maximum: 3600 }),
minimumCapacityTestDepthPercent: number({ description: 'Minimum observed full-to-low discharge depth required before a continuous session qualifies as a high-confidence capacity test. Runtime enforces at least 10 percent.', minimum: 0, maximum: 100 }),
}, {
title: 'Battery',
description: 'Battery telemetry collection and quality thresholds used by fleet reports.',
required: ['enabled', 'maximumIntegrationGapSeconds', 'minimumCapacityTestDepthPercent'],
}),
discord: strictObject({
enabled: boolean(),
sendAt: string({ pattern: '^([01]\\d|2[0-3]):[0-5]\\d$' }),
timezone: string({ minLength: 1, maxLength: 100 }),
}, { required: ['enabled', 'sendAt', 'timezone'] }),
privacy: strictObject({ retainChatBodies: boolean() }, { required: ['retainChatBodies'] }),
}, { title: 'Fleet reports', required: ['enabled', 'retention', 'battery', 'discord', 'privacy'] }),
enabled: boolean({ description: 'Sends one completed daily fleet report to the configured Discord administrative-alert channel.' }),
sendAt: string({ description: 'Local time of day to send the daily report, written as 24-hour HH:mm.', pattern: '^([01]\\d|2[0-3]):[0-5]\\d$' }),
timezone: string({ description: 'IANA timezone used to interpret the delivery time and determine each completed report day.', minLength: 1, maxLength: 100 }),
}, {
title: 'Discord delivery',
description: 'Schedule for sending completed daily fleet summaries through the Discord bot.',
required: ['enabled', 'sendAt', 'timezone'],
}),
privacy: strictObject({
retainChatBodies: boolean({ description: 'Reserved privacy preference. The current collector does not read this setting and preserves complete event payloads, including chat content, regardless of its value.' }),
}, {
title: 'Privacy',
description: 'Privacy controls reserved for any future fleet-report collection of message content.',
required: ['retainChatBodies'],
}),
}, {
title: 'Fleet reports',
description: 'Persistent fleet operations reporting, retention, battery analysis, delivery, and privacy preferences.',
required: ['enabled', 'retention', 'battery', 'discord', 'privacy'],
}),
};
@@ -21,29 +21,41 @@ module.exports = {
buttons: [],
},
schema: strictObject({
enabled: boolean(),
url: string({ title: 'Server URL', format: 'uri', maxLength: 2048 }),
token: string({ title: 'Long-lived access token', writeOnly: true, maxLength: 20000 }),
enabled: boolean({ description: 'Connects to Home Assistant and enables configured room entities, physical-button triggers, Neato controls, and lift controls after restart.' }),
url: string({ title: 'Server URL', description: 'Base URL of the Home Assistant server used for its REST and WebSocket APIs.', format: 'uri', maxLength: 2048 }),
token: string({ title: 'Long-lived access token', description: 'Home Assistant long-lived access token used to authenticate every API request. The saved value is never returned to the browser.', writeOnly: true, maxLength: 20000 }),
[neato.key]: neato.schema,
[lift.key]: lift.schema,
entities: {
type: 'array',
title: 'Room entities',
description: 'Home Assistant lights and switches exposed to the room-light controls and button-box actions.',
items: strictObject({
id: string({ title: 'Entity id', minLength: 1, maxLength: 255 }),
name: string({ minLength: 1, maxLength: 120 }),
type: string({ enum: ['light', 'switch'] }),
}, { required: ['id', 'name'] }),
id: string({ title: 'Entity id', description: 'Exact Home Assistant entity ID, such as light.rover_room or switch.floor_lamp.', minLength: 1, maxLength: 255 }),
name: string({ description: 'Human-readable name shown for this entity in the rover UI.', minLength: 1, maxLength: 120 }),
type: string({ description: 'Control behavior to expose: lights receive brightness-aware commands, while switches receive simple on and off commands.', enum: ['light', 'switch'] }),
}, {
description: 'One Home Assistant entity that the rover server can display and control.',
required: ['id', 'name'],
}),
},
buttons: {
type: 'array',
title: 'Physical button mappings',
description: 'Maps Home Assistant entity state changes to built-in rover-server actions.',
items: strictObject({
entityId: string({ title: 'Entity id', minLength: 1, maxLength: 255 }),
stateEquals: string({ minLength: 1, maxLength: 255 }),
cooldownMs: integer({ minimum: 0, maximum: 86400000 }),
action: string({ enum: ['humanAlert', 'modeTurns', 'modeAdmin', 'lightsLockToggle'] }),
}, { required: ['entityId', 'stateEquals', 'cooldownMs', 'action'] }),
entityId: string({ title: 'Entity id', description: 'Home Assistant entity whose state changes are watched as button presses.', minLength: 1, maxLength: 255 }),
stateEquals: string({ description: 'Exact Home Assistant state that must be reached before the action fires.', minLength: 1, maxLength: 255 }),
cooldownMs: integer({ description: 'Minimum milliseconds between accepted activations of this mapping.', minimum: 0, maximum: 86400000 }),
action: string({ description: 'Built-in action to run: raise a human alert, switch to turns mode, switch to admin mode, or toggle the room-light lock.', enum: ['humanAlert', 'modeTurns', 'modeAdmin', 'lightsLockToggle'] }),
}, {
description: 'One watched Home Assistant state transition and the server action it triggers.',
required: ['entityId', 'stateEquals', 'cooldownMs', 'action'],
}),
},
}, { title: 'Home Assistant', required: ['enabled', 'url', 'token', 'neato', 'lift', 'entities', 'buttons'] }),
}, {
title: 'Home Assistant',
description: 'Connection, controllable entity catalog, and hardware-trigger mappings for the shared Home Assistant integration.',
required: ['enabled', 'url', 'token', 'neato', 'lift', 'entities', 'buttons'],
}),
};
@@ -14,15 +14,18 @@ module.exports = {
profile: { publicUrl: '', name: 'MultiRover', description: '', color: '#38bdf8' },
},
schema: strictObject({
enabled: boolean(),
directoryUrls: stringArray({ item: { format: 'uri' } }),
pollIntervalMs: integer({ minimum: 1000, maximum: 86400000 }),
requestTimeoutMs: integer({ minimum: 250, maximum: 120000 }),
enabled: boolean({ description: 'Publishes this server\'s public instance information and polls the configured directories for peer servers.' }),
directoryUrls: stringArray({
item: { description: 'Absolute URL returning an array of peer MultiRover instance entries.', format: 'uri' },
array: { description: 'Directory endpoints polled to discover other public MultiRover servers.' },
}),
pollIntervalMs: integer({ description: 'Milliseconds between peer-directory refreshes.', minimum: 1000, maximum: 86400000 }),
requestTimeoutMs: integer({ description: 'Maximum milliseconds allowed for each directory or peer information request before it is aborted.', minimum: 250, maximum: 120000 }),
profile: strictObject({
publicUrl: string({ maxLength: 2048 }),
name: string({ minLength: 1, maxLength: 120 }),
description: string({ maxLength: 500 }),
color: string({ pattern: '^#[0-9a-fA-F]{6}$' }),
}, { required: ['publicUrl', 'name', 'description', 'color'] }),
}, { title: 'Inter-instance directory', required: ['enabled', 'directoryUrls', 'pollIntervalMs', 'requestTimeoutMs', 'profile'] }),
publicUrl: string({ description: 'Public base URL peers and users use to reach this server; it also identifies and filters this instance from directory results.', maxLength: 2048 }),
name: string({ description: 'Public instance name advertised to peer servers.', minLength: 1, maxLength: 120 }),
description: string({ description: 'Short public summary advertised with this instance.', maxLength: 500 }),
color: string({ description: 'Six-digit hexadecimal accent color advertised for this instance.', pattern: '^#[0-9a-fA-F]{6}$' }),
}, { description: 'Public identity this server publishes through the inter-instance information endpoint.', required: ['publicUrl', 'name', 'description', 'color'] }),
}, { title: 'Inter-instance directory', description: 'Controls discovery and public information exchange between independent MultiRover servers.', required: ['enabled', 'directoryUrls', 'pollIntervalMs', 'requestTimeoutMs', 'profile'] }),
};
@@ -8,7 +8,11 @@ module.exports = {
feature: true,
defaultValue: { enabled: false, captureCooldownMs: 10000 },
schema: strictObject({
enabled: boolean(),
captureCooldownMs: integer({ minimum: 0, maximum: 3600000 }),
}, { title: 'Kinect', required: ['enabled', 'captureCooldownMs'] }),
enabled: boolean({ description: 'Starts the Kinect worker and exposes authorized frame capture after restart.' }),
captureCooldownMs: integer({ description: 'Minimum milliseconds between accepted Kinect frame-capture requests across all clients.', minimum: 0, maximum: 3600000 }),
}, {
title: 'Kinect',
description: 'Optional Kinect frame capture and its server-wide request cooldown.',
required: ['enabled', 'captureCooldownMs'],
}),
};
@@ -8,10 +8,14 @@ module.exports = {
feature: true,
defaultValue: { enabled: false, upSwitch: '', downSwitch: '', interlockMs: 2000, commandCooldownMs: 3000 },
schema: strictObject({
enabled: boolean(),
upSwitch: string({ maxLength: 255 }),
downSwitch: string({ maxLength: 255 }),
interlockMs: integer({ minimum: 0, maximum: 600000 }),
commandCooldownMs: integer({ minimum: 0, maximum: 600000 }),
}, { title: 'Lift', required: ['enabled', 'upSwitch', 'downSwitch', 'interlockMs', 'commandCooldownMs'] }),
enabled: boolean({ description: 'Enables lift status and commands through the two configured Home Assistant switches after restart.' }),
upSwitch: string({ description: 'Home Assistant switch entity that powers upward lift movement.', maxLength: 255 }),
downSwitch: string({ description: 'Home Assistant switch entity that powers downward lift movement.', maxLength: 255 }),
interlockMs: integer({ description: 'Milliseconds to wait after turning off the opposing direction before energizing the requested direction. Runtime always enforces at least 250 ms.', minimum: 0, maximum: 600000 }),
commandCooldownMs: integer({ description: 'Minimum milliseconds between lift commands. Runtime never allows this to be shorter than the interlock delay.', minimum: 0, maximum: 600000 }),
}, {
title: 'Lift',
description: 'Bidirectional lift control using interlocked Home Assistant switch entities.',
required: ['enabled', 'upSwitch', 'downSwitch', 'interlockMs', 'commandCooldownMs'],
}),
};
@@ -7,9 +7,9 @@ module.exports = {
key: 'llmCommentary',
defaultValue: { enabled: false, model: 'qwen2.5:7b-instruct', ollamaServer: 'http://127.0.0.1:11434', frequency: 120000 },
schema: strictObject({
enabled: boolean(),
model: string({ minLength: 1, maxLength: 200 }),
ollamaServer: string({ title: 'Ollama server', format: 'uri', maxLength: 2048 }),
enabled: boolean({ description: 'Starts periodic AI commentary generation from current rover, user, and chat activity.' }),
model: string({ description: 'Ollama model name used to generate commentary.', minLength: 1, maxLength: 200 }),
ollamaServer: string({ title: 'Ollama server', description: 'Base URL of the Ollama API used for commentary generation.', format: 'uri', maxLength: 2048 }),
frequency: integer({ description: 'Commentary interval in milliseconds.', minimum: 1000, maximum: 86400000 }),
}, { title: 'LLM commentary', required: ['enabled', 'model', 'ollamaServer', 'frequency'] }),
}, { title: 'LLM commentary', description: 'Generates periodic AI-authored chat commentary from recent server activity through Ollama.', required: ['enabled', 'model', 'ollamaServer', 'frequency'] }),
};
@@ -7,7 +7,11 @@ module.exports = {
key: 'media',
defaultValue: { whepBaseUrl: 'http://127.0.0.1:8889/video', additionalHosts: [] },
schema: strictObject({
whepBaseUrl: string({ title: 'WHEP base URL', format: 'uri', maxLength: 2048 }),
additionalHosts: stringArray({ title: 'Additional ICE hosts', item: { minLength: 1, maxLength: 255 }, array: { uniqueItems: true } }),
}, { title: 'Media', required: ['whepBaseUrl', 'additionalHosts'] }),
whepBaseUrl: string({ title: 'WHEP base URL', description: 'Base HTTP URL used to build browser WHEP playback and WHIP audio-publishing endpoints.', format: 'uri', maxLength: 2048 }),
additionalHosts: stringArray({
title: 'Additional ICE hosts',
item: { description: 'Hostname or IP address MediaMTX advertises as a WebRTC ICE candidate.', minLength: 1, maxLength: 255 },
array: { description: 'Additional public or LAN hostnames and addresses browsers may use to reach MediaMTX WebRTC transport.', uniqueItems: true },
}),
}, { title: 'Media', description: 'Controls browser signaling addresses and WebRTC network candidates generated for the managed MediaMTX process.', required: ['whepBaseUrl', 'additionalHosts'] }),
};
@@ -8,7 +8,11 @@ module.exports = {
feature: true,
defaultValue: { enabled: false, device: '' },
schema: strictObject({
enabled: boolean(),
device: string({ description: 'ESPHome device name.', maxLength: 255 }),
}, { title: 'Neato', required: ['enabled', 'device'] }),
enabled: boolean({ description: 'Exposes Neato status and commands through the configured Home Assistant ESPHome device after restart.' }),
device: string({ description: 'ESPHome device name used to derive the Neato entity IDs in Home Assistant; punctuation is normalized to underscores.', maxLength: 255 }),
}, {
title: 'Neato',
description: 'Optional Neato robot controls backed by entities published from one ESPHome device through Home Assistant.',
required: ['enabled', 'device'],
}),
};
@@ -7,7 +7,11 @@ module.exports = {
key: 'commands',
defaultValue: { prefix: 'rs', timeStatusCommand: 'ts' },
schema: strictObject({
prefix: string({ minLength: 1, maxLength: 20 }),
timeStatusCommand: nullableString({ description: 'Leave empty to disable the bare shortcut.', maxLength: 20 }),
}, { title: 'Commands', required: ['prefix', 'timeStatusCommand'] }),
prefix: string({ description: 'Text placed before operator commands in web chat and Discord, such as rs help.', minLength: 1, maxLength: 20 }),
timeStatusCommand: nullableString({ description: 'Optional command accepted without the normal prefix for the current time and rover status. Leave empty to disable the shortcut.', maxLength: 20 }),
}, {
title: 'Commands',
description: 'Shared text syntax used by operator commands across web chat and Discord.',
required: ['prefix', 'timeStatusCommand'],
}),
};
@@ -19,16 +19,16 @@ module.exports = {
gateIntervalMs: 2000,
},
schema: strictObject({
enabled: boolean(),
mode: string({ enum: ['autonomous', 'directAddress'] }),
observeOnly: boolean(),
postToolsOnlyMessages: boolean(),
tiebreakerEnable: boolean(),
runWhileNoPeopleOnline: boolean(),
name: string({ minLength: 1, maxLength: 80 }),
model: string({ minLength: 1, maxLength: 200 }),
ollamaServer: string({ title: 'Ollama server', format: 'uri', maxLength: 2048 }),
profileImageUrl: string({ title: 'Profile image URL', maxLength: 2048 }),
gateIntervalMs: integer({ minimum: 250, maximum: 3600000 }),
}, { title: 'Overseer Control', required: ['enabled', 'mode', 'observeOnly', 'postToolsOnlyMessages', 'tiebreakerEnable', 'runWhileNoPeopleOnline', 'name', 'model', 'ollamaServer', 'profileImageUrl', 'gateIntervalMs'] }),
enabled: boolean({ description: 'Enables the AI Overseer and its configured autonomous or direct-address execution path.' }),
mode: string({ description: 'autonomous runs repeatedly when the user vote permits it; directAddress runs only when chat begins with the configured Overseer name.', enum: ['autonomous', 'directAddress'] }),
observeOnly: boolean({ description: 'Lets the model evaluate state without executing tools or posting its generated chat response.' }),
postToolsOnlyMessages: boolean({ description: 'Posts a chat feed entry for executed tool calls even when the model did not also produce chat text.' }),
tiebreakerEnable: boolean({ description: 'Allows autonomous execution when eligible users are evenly split between enabling and disabling the Overseer.' }),
runWhileNoPeopleOnline: boolean({ description: 'Allows autonomous execution when no eligible users are online to vote.' }),
name: string({ description: 'Chat identity for Overseer messages and the phrase that triggers direct-address mode.', minLength: 1, maxLength: 80 }),
model: string({ description: 'Ollama model name used for Overseer decisions.', minLength: 1, maxLength: 200 }),
ollamaServer: string({ title: 'Ollama server', description: 'Base URL of the Ollama API used for Overseer decisions.', format: 'uri', maxLength: 2048 }),
profileImageUrl: string({ title: 'Profile image URL', description: 'Optional image URL displayed beside Overseer chat messages; leave blank for no custom image.', maxLength: 2048 }),
gateIntervalMs: integer({ description: 'Milliseconds waited after a completed autonomous decision before evaluating the next one.', minimum: 250, maximum: 3600000 }),
}, { title: 'Overseer Control', description: 'Controls the AI agent that observes server state, optionally executes approved tools, and can speak in chat.', required: ['enabled', 'mode', 'observeOnly', 'postToolsOnlyMessages', 'tiebreakerEnable', 'runWhileNoPeopleOnline', 'name', 'model', 'ollamaServer', 'profileImageUrl', 'gateIntervalMs'] }),
};
@@ -19,15 +19,19 @@ module.exports = {
replayEnabled: false,
},
schema: strictObject({
enabled: boolean(),
name: string({ minLength: 1, maxLength: 120 }),
color: string({ pattern: '^#[0-9a-fA-F]{6}$' }),
host: string({ maxLength: 255 }),
onvifPort: integer({ title: 'ONVIF port', minimum: 1, maximum: 65535 }),
username: string({ maxLength: 255 }),
password: string({ writeOnly: true, maxLength: 10000 }),
profileToken: string({ maxLength: 255 }),
turnDurationMs: integer({ minimum: 1000, maximum: 86400000 }),
replayEnabled: boolean(),
}, { title: 'PTZ camera', required: ['enabled', 'name', 'color', 'host', 'onvifPort', 'username', 'password', 'profileToken', 'turnDurationMs', 'replayEnabled'] }),
enabled: boolean({ description: 'Connects to the configured ONVIF camera and exposes its controls after restart.' }),
name: string({ description: 'Human-readable camera name shown in the control interface.', minLength: 1, maxLength: 120 }),
color: string({ description: 'Six-digit hexadecimal accent color used to identify this camera in the UI.', pattern: '^#[0-9a-fA-F]{6}$' }),
host: string({ description: 'Hostname or IP address of the ONVIF camera.', maxLength: 255 }),
onvifPort: integer({ title: 'ONVIF port', description: 'TCP port used for ONVIF control requests.', minimum: 1, maximum: 65535 }),
username: string({ description: 'Camera account username used for ONVIF authentication.', maxLength: 255 }),
password: string({ description: 'Camera account password used for ONVIF authentication. The saved value is never returned to the browser.', writeOnly: true, maxLength: 10000 }),
profileToken: string({ description: 'ONVIF media profile token used for stream discovery, presets, status, and movement commands.', maxLength: 255 }),
turnDurationMs: integer({ description: 'Milliseconds assigned to each queued user turn controlling the PTZ camera.', minimum: 1000, maximum: 86400000 }),
replayEnabled: boolean({ description: 'Allows this camera to appear as an available replay source.' }),
}, {
title: 'PTZ camera',
description: 'Optional ONVIF pan-tilt-zoom camera connection, presentation, turn timing, and replay availability.',
required: ['enabled', 'name', 'color', 'host', 'onvifPort', 'username', 'password', 'profileToken', 'turnDurationMs', 'replayEnabled'],
}),
};
@@ -8,16 +8,24 @@ module.exports = {
feature: true,
defaultValue: { enabled: false, cameras: [] },
schema: strictObject({
enabled: boolean(),
enabled: boolean({ description: 'Publishes the configured room-camera catalog and enables camera snapshots and streams after restart.' }),
cameras: {
type: 'array',
description: 'Room cameras available to the web UI and replay system.',
items: strictObject({
id: string({ minLength: 1, maxLength: 80, pattern: '^[a-zA-Z0-9_-]+$' }),
name: string({ minLength: 1, maxLength: 120 }),
description: string({ maxLength: 500 }),
url: string({ title: 'Snapshot URL', format: 'uri', maxLength: 2048 }),
streamUrl: string({ title: 'Stream URL', maxLength: 2048 }),
}, { required: ['id', 'name', 'description', 'url', 'streamUrl'] }),
id: string({ description: 'Stable camera identifier used in socket requests, selections, and replay source names.', minLength: 1, maxLength: 80, pattern: '^[a-zA-Z0-9_-]+$' }),
name: string({ description: 'Human-readable camera name shown in the UI.', minLength: 1, maxLength: 120 }),
description: string({ description: 'Short explanation of the camera location or view shown in the UI.', maxLength: 500 }),
url: string({ title: 'Snapshot URL', description: 'HTTP URL fetched when the server needs a still image from this camera.', format: 'uri', maxLength: 2048 }),
streamUrl: string({ title: 'Stream URL', description: 'Live stream URL consumed by the server snapshot engine and replay capture path.', maxLength: 2048 }),
}, {
description: 'One named room camera with its still-image and live-stream sources.',
required: ['id', 'name', 'description', 'url', 'streamUrl'],
}),
},
}, { title: 'Room cameras', required: ['enabled', 'cameras'] }),
}, {
title: 'Room cameras',
description: 'Optional catalog of fixed cameras used for room views, server-produced snapshots, and replay sources.',
required: ['enabled', 'cameras'],
}),
};
@@ -14,28 +14,29 @@ const socials = {
feature: true,
defaultValue: { enabled: false, links: [] },
schema: strictObject({
enabled: boolean({ title: 'Enabled' }),
enabled: boolean({ title: 'Enabled', description: 'Shows the configured social-link buttons in driver and inter-instance views.' }),
links: {
type: 'array',
title: 'Links',
description: 'Ordered social or community links presented to users when this feature is enabled.',
items: strictObject({
id: string({ minLength: 1, maxLength: 60, pattern: '^[a-zA-Z0-9_-]+$' }),
label: string({ minLength: 1, maxLength: 80 }),
url: string({ format: 'uri', maxLength: 2048 }),
icon: string({ maxLength: 80 }),
color: string({ pattern: '^#[0-9a-fA-F]{6}$' }),
}, { required: ['id', 'label', 'url', 'icon', 'color'] }),
id: string({ description: 'Stable identifier used by the UI to distinguish this link from the others.', minLength: 1, maxLength: 60, pattern: '^[a-zA-Z0-9_-]+$' }),
label: string({ description: 'User-facing text displayed on the link button.', minLength: 1, maxLength: 80 }),
url: string({ description: 'Absolute destination opened when a user selects this link.', format: 'uri', maxLength: 2048 }),
icon: string({ description: 'Icon name interpreted by the social-button UI; leave blank to use its fallback presentation.', maxLength: 80 }),
color: string({ description: 'Six-digit hexadecimal accent color used for this link button.', pattern: '^#[0-9a-fA-F]{6}$' }),
}, { description: 'One social-link button shown to users.', required: ['id', 'label', 'url', 'icon', 'color'] }),
},
}, { title: 'Social links', required: ['enabled', 'links'] }),
}, { title: 'Social links', description: 'Controls the optional social and community buttons published to local users and peer instances.', required: ['enabled', 'links'] }),
};
const driverAd = {
key: 'driverAd',
defaultValue: { title: '', html: '' },
schema: strictObject({
title: string({ maxLength: 120 }),
title: string({ description: 'Heading displayed above the operator-provided content on the driver page; leave blank to use the card fallback.', maxLength: 120 }),
html: string({ title: 'HTML', description: 'Trusted operator HTML shown to drivers.', maxLength: 100000 }),
}, { title: 'Driver content', required: ['title', 'html'] }),
}, { title: 'Driver content', description: 'Operator-managed informational or promotional content displayed in the driver application.', required: ['title', 'html'] }),
};
function getConfiguredSocials(config) {