mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
leg
This commit is contained in:
@@ -445,7 +445,7 @@ Implemented on 2026-09-14:
|
||||
- Replaced RJSF's unthemed Bootstrap markup with a generic MultiRover tree renderer. The complete document now follows schema order as indented object, array, item, and key/value rows; array controls remain readable text beside each item, and the route-specific styling lives outside the global stylesheet.
|
||||
- Replaced the editor's custom section borders, header backgrounds, and indentation guides with the application's shared `CardFrame` at every object, array, and array-item layer. Scalar settings remain compact key/value rows, descriptions use the wider value column, and collection actions stay beside their content instead of moving to the far edge.
|
||||
- Disabled RJSF's internal checkbox label and description generically, leaving the shared field row as the single owner of each boolean setting's name, required marker, and description.
|
||||
- Restored the former example YAML's installation-specific values as schema-owned input examples. The generic form shows them as placeholders, including inside newly added array items and secret replacement inputs, without saving example URLs, credentials, IDs, or hardware addresses as real configuration.
|
||||
- Restored the former example YAML's installation-specific values as both schema-owned input examples and the actual initial values for non-secret settings and collection shapes. The only intentionally empty defaults are the three credentials and active driver HTML; their placeholders still explain the expected input without falsely marking credentials as configured or publishing sample content.
|
||||
- Strengthened top-level hierarchy with a 1.5-rem sibling gap, distinct dark-blue top-level `CardFrame` headers, and repeated indentation for each nested card's contents.
|
||||
- Traced all 156 schema nodes to their runtime consumers and added operator-facing descriptions for every root, section, collection, array item, and scalar option. A recursive configuration test now rejects any future schema node without a description; currently reserved settings explicitly state that they have no runtime effect.
|
||||
- Converged feature control into service-owned configuration: each public feature opts in beside its own schema, and the configuration system derives those exact `enabled` switches for sessions and command discovery. The former server feature registry was removed; configuration completeness and hardware availability remain visible as runtime status instead of becoming hidden enablement rules.
|
||||
@@ -455,7 +455,7 @@ Implemented on 2026-09-14:
|
||||
|
||||
Local verification completed:
|
||||
|
||||
- All 105 server tests passed, including complete schema-description and input-example coverage, file-backed setup-code lifecycle and symlink rejection, service-definition-derived feature projection, schema-derived secret paths, configuration defaults and strict validation, full-document revision conflicts, secret preservation, administrator invariants, explicit setup-file import, and the earlier filesystem coverage.
|
||||
- All 106 server tests passed, including populated legacy-style default coverage, complete schema-description and input-example coverage, file-backed setup-code lifecycle and symlink rejection, service-definition-derived feature projection, schema-derived secret paths, configuration defaults and strict validation, full-document revision conflicts, secret preservation, administrator invariants, explicit setup-file import, and the earlier filesystem coverage.
|
||||
- Focused admin, route, and identity UI lint passed.
|
||||
- All 20 existing focused web UI tests passed.
|
||||
- The production web UI build completed successfully and regenerated the checked-in server assets.
|
||||
|
||||
@@ -73,6 +73,24 @@ function collectSchemaPathsMissingInputExamples(schema, value, pathLabel = '$',
|
||||
return Array.isArray(schema.examples) && schema.examples.length ? [] : [pathLabel];
|
||||
}
|
||||
|
||||
function collectEmptyStringPaths(value, pathLabel = '$') {
|
||||
/*
|
||||
Empty-string policy is intentionally tested by path because these four
|
||||
fields are exceptional for security or visible behavior, not omissions in
|
||||
the legacy-style default document. Walking the complete value also catches
|
||||
an accidentally blank field inside a pre-populated example collection.
|
||||
*/
|
||||
if (Array.isArray(value)) {
|
||||
return value.flatMap((item, index) => collectEmptyStringPaths(item, `${pathLabel}[${index}]`));
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.entries(value).flatMap(([key, childValue]) => (
|
||||
collectEmptyStringPaths(childValue, `${pathLabel}.${key}`)
|
||||
));
|
||||
}
|
||||
return value === '' ? [pathLabel] : [];
|
||||
}
|
||||
|
||||
test.after(() => {
|
||||
temporaryRoots.forEach((root) => fs.rmSync(root, { recursive: true, force: true }));
|
||||
});
|
||||
@@ -85,6 +103,25 @@ test('safe defaults form a complete valid configuration with integrations disabl
|
||||
assert.equal(defaultConfig.balanceBoard.enabled, false);
|
||||
});
|
||||
|
||||
test('legacy-style defaults populate every non-secret and inactive-content value', () => {
|
||||
/*
|
||||
Credentials must not masquerade as configured, and driver HTML would be
|
||||
immediately visible without an enable switch. Every other free-form value
|
||||
should match the populated template behavior operators had with YAML.
|
||||
*/
|
||||
assert.deepEqual(collectEmptyStringPaths(defaultConfig), [
|
||||
'$.homeAssistant.token',
|
||||
'$.ptzCamera.password',
|
||||
'$.discord.token',
|
||||
'$.driverAd.html',
|
||||
]);
|
||||
assert.ok(defaultConfig.interInstance.directoryUrls.length > 0);
|
||||
assert.ok(defaultConfig.homeAssistant.entities.length > 0);
|
||||
assert.ok(defaultConfig.homeAssistant.buttons.length > 0);
|
||||
assert.ok(defaultConfig.roomCameras.cameras.length > 0);
|
||||
assert.ok(defaultConfig.socials.links.length > 0);
|
||||
});
|
||||
|
||||
test('service definitions determine document order and write-only secret handling', () => {
|
||||
/*
|
||||
The generic browser form and backend persistence both consume this one
|
||||
@@ -174,7 +211,10 @@ test('generated feature flags use only each declared enabled switch', () => {
|
||||
|
||||
test('normalization fills missing legacy fields but strict validation rejects unknown fields', () => {
|
||||
const normalized = normalizeConfig({ media: { whepBaseUrl: 'http://localhost:8889/video' } });
|
||||
assert.deepEqual(normalized.media.additionalHosts, []);
|
||||
// Missing fields now receive the same populated template defaults as a new
|
||||
// installation; normalization must not silently revert this one collection
|
||||
// to the former empty-safe-default policy.
|
||||
assert.deepEqual(normalized.media.additionalHosts, ['rover.example.com', 'media-server.local']);
|
||||
assert.doesNotThrow(() => assertValidConfig(normalized));
|
||||
|
||||
const invalid = normalizeConfig({ media: { whepBaseUrl: 'http://localhost:8889/video', misspelledHost: 'x' } });
|
||||
|
||||
@@ -6,7 +6,9 @@ const { strictObject, string, boolean } = require('../../configuration/schemaHel
|
||||
module.exports = {
|
||||
key: 'barcodeGames',
|
||||
feature: true,
|
||||
defaultValue: { enabled: false, botName: 'Barcode Games', profileImageUrl: '' },
|
||||
// Disabled-by-default feature state is independent from its complete visual
|
||||
// identity, matching how the legacy YAML template represented this service.
|
||||
defaultValue: { enabled: false, botName: 'Barcode Games', profileImageUrl: 'https://example.com/barcode-games.png' },
|
||||
schema: strictObject({
|
||||
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 }),
|
||||
|
||||
@@ -6,13 +6,26 @@ const { strictObject, string, boolean } = require('../../configuration/schemaHel
|
||||
module.exports = {
|
||||
key: 'discord',
|
||||
feature: true,
|
||||
// Channel and role IDs retain the fully populated legacy-template shape, but
|
||||
// the credential remains empty and the bot cannot start until explicitly enabled.
|
||||
defaultValue: {
|
||||
enabled: false,
|
||||
token: '',
|
||||
guildId: '',
|
||||
siteUrl: '',
|
||||
channels: { general: '', announcements: '', adminAlerts: '', replay: '', humanAlerts: '' },
|
||||
roles: { stalkerPing: '', announcementPing: '', adminPing: '', humanAlertPing: '' },
|
||||
guildId: '123456789012345678',
|
||||
siteUrl: 'https://rover.example.com',
|
||||
channels: {
|
||||
general: '123456789012345678',
|
||||
announcements: '123456789012345678',
|
||||
adminAlerts: '123456789012345678',
|
||||
replay: '123456789012345678',
|
||||
humanAlerts: '123456789012345678',
|
||||
},
|
||||
roles: {
|
||||
stalkerPing: '123456789012345678',
|
||||
announcementPing: '123456789012345678',
|
||||
adminPing: '123456789012345678',
|
||||
humanAlertPing: '123456789012345678',
|
||||
},
|
||||
},
|
||||
schema: strictObject({
|
||||
enabled: boolean({ description: 'Logs the Discord bot in and enables commands, chat bridges, replay delivery, and configured announcements after restart.' }),
|
||||
|
||||
@@ -11,14 +11,45 @@ module.exports = {
|
||||
// Retain the actual child definitions so generic configuration metadata can
|
||||
// discover their feature switches without repeating nested paths centrally.
|
||||
nestedDefinitions: [neato, lift],
|
||||
// Example entities and triggers are real initial document values, as they
|
||||
// were in the YAML template. Home Assistant stays inert until enabled and a
|
||||
// real secret is deliberately installed by the operator.
|
||||
defaultValue: {
|
||||
enabled: false,
|
||||
url: 'http://127.0.0.1:8123',
|
||||
token: '',
|
||||
[neato.key]: neato.defaultValue,
|
||||
[lift.key]: lift.defaultValue,
|
||||
entities: [],
|
||||
buttons: [],
|
||||
entities: [
|
||||
{ id: 'light.lab_main', name: 'Lab Lights' },
|
||||
{ id: 'switch.dock_power', name: 'Dock Power' },
|
||||
],
|
||||
buttons: [
|
||||
{
|
||||
entityId: 'sensor.basement_rover_buttons_action',
|
||||
stateEquals: 'on',
|
||||
cooldownMs: 15000,
|
||||
action: 'humanAlert',
|
||||
},
|
||||
{
|
||||
entityId: 'sensor.basement_rover_buttons_action',
|
||||
stateEquals: 'double',
|
||||
cooldownMs: 2000,
|
||||
action: 'modeTurns',
|
||||
},
|
||||
{
|
||||
entityId: 'sensor.basement_rover_buttons_action',
|
||||
stateEquals: 'hold',
|
||||
cooldownMs: 2000,
|
||||
action: 'modeAdmin',
|
||||
},
|
||||
{
|
||||
entityId: 'sensor.basement_rover_buttons_action',
|
||||
stateEquals: 'toggle',
|
||||
cooldownMs: 1000,
|
||||
action: 'lightsLockToggle',
|
||||
},
|
||||
],
|
||||
},
|
||||
schema: strictObject({
|
||||
enabled: boolean({ description: 'Connects to Home Assistant and enables configured room entities, physical-button triggers, Neato controls, and lift controls after restart.' }),
|
||||
|
||||
@@ -6,12 +6,19 @@ const { strictObject, string, boolean, integer, stringArray } = require('../../c
|
||||
module.exports = {
|
||||
key: 'interInstance',
|
||||
feature: true,
|
||||
// Optional behavior remains disabled, but a new configuration now starts
|
||||
// with the same complete, editable template that the former YAML supplied.
|
||||
defaultValue: {
|
||||
enabled: false,
|
||||
directoryUrls: [],
|
||||
directoryUrls: ['https://raw.githubusercontent.com/legop3/multi-roomba-rover-instance-directory/refs/heads/main/directory.json'],
|
||||
pollIntervalMs: 30000,
|
||||
requestTimeoutMs: 5000,
|
||||
profile: { publicUrl: '', name: 'MultiRover', description: '', color: '#38bdf8' },
|
||||
profile: {
|
||||
publicUrl: 'https://rover.example.com',
|
||||
name: 'Example Rover Server',
|
||||
description: 'A short public description of this rover server.',
|
||||
color: '#38bdf8',
|
||||
},
|
||||
},
|
||||
schema: strictObject({
|
||||
enabled: boolean({ description: 'Publishes this server\'s public instance information and polls the configured directories for peer servers.' }),
|
||||
|
||||
@@ -6,7 +6,15 @@ const { strictObject, string, boolean, integer } = require('../../configuration/
|
||||
module.exports = {
|
||||
key: 'lift',
|
||||
feature: true,
|
||||
defaultValue: { enabled: false, upSwitch: '', downSwitch: '', interlockMs: 2000, commandCooldownMs: 3000 },
|
||||
// These inert example entity IDs preserve the complete former YAML shape;
|
||||
// the explicit feature switch remains the only activation signal.
|
||||
defaultValue: {
|
||||
enabled: false,
|
||||
upSwitch: 'switch.lift_up',
|
||||
downSwitch: 'switch.lift_down',
|
||||
interlockMs: 2000,
|
||||
commandCooldownMs: 3000,
|
||||
},
|
||||
schema: strictObject({
|
||||
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.', examples: ['switch.lift_up'], maxLength: 255 }),
|
||||
|
||||
@@ -5,7 +5,12 @@ const { strictObject, string, stringArray } = require('../../configuration/schem
|
||||
|
||||
module.exports = {
|
||||
key: 'media',
|
||||
defaultValue: { whepBaseUrl: 'http://127.0.0.1:8889/video', additionalHosts: [] },
|
||||
defaultValue: {
|
||||
// Signaling remains server-local because the internal `/video` proxy owns
|
||||
// browser access; only ICE transport addresses come from the legacy sample.
|
||||
whepBaseUrl: 'http://127.0.0.1:8889/video',
|
||||
additionalHosts: ['rover.example.com', 'media-server.local'],
|
||||
},
|
||||
schema: strictObject({
|
||||
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({
|
||||
|
||||
@@ -6,7 +6,9 @@ const { strictObject, string, boolean } = require('../../configuration/schemaHel
|
||||
module.exports = {
|
||||
key: 'neato',
|
||||
feature: true,
|
||||
defaultValue: { enabled: false, device: '' },
|
||||
// Keeping the example device in the saved template explains the required
|
||||
// ESPHome naming shape while `enabled: false` prevents accidental control.
|
||||
defaultValue: { enabled: false, device: 'neato_vacuum' },
|
||||
schema: strictObject({
|
||||
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.', examples: ['neato_vacuum'], maxLength: 255 }),
|
||||
|
||||
@@ -5,6 +5,8 @@ const { strictObject, string, boolean, integer } = require('../../configuration/
|
||||
|
||||
module.exports = {
|
||||
key: 'overseerControl',
|
||||
// The integration is still opt-in; populated presentation values keep its
|
||||
// initial document complete without causing the model loop to start.
|
||||
defaultValue: {
|
||||
enabled: false,
|
||||
mode: 'autonomous',
|
||||
@@ -15,7 +17,7 @@ module.exports = {
|
||||
name: 'The Overseer',
|
||||
model: 'qwen2.5:7b-instruct',
|
||||
ollamaServer: 'http://127.0.0.1:11434',
|
||||
profileImageUrl: '',
|
||||
profileImageUrl: 'https://example.com/overseer.png',
|
||||
gateIntervalMs: 2000,
|
||||
},
|
||||
schema: strictObject({
|
||||
|
||||
@@ -6,13 +6,15 @@ const { strictObject, string, boolean, integer } = require('../../configuration/
|
||||
module.exports = {
|
||||
key: 'ptzCamera',
|
||||
feature: true,
|
||||
// Non-secret commissioning values mirror the legacy template. The password
|
||||
// remains empty and `enabled: false` prevents an accidental camera login.
|
||||
defaultValue: {
|
||||
enabled: false,
|
||||
name: 'PTZ Camera',
|
||||
color: '#38bdf8',
|
||||
host: '',
|
||||
host: '192.168.0.8',
|
||||
onvifPort: 8000,
|
||||
username: '',
|
||||
username: 'admin',
|
||||
password: '',
|
||||
profileToken: '003',
|
||||
turnDurationMs: 300000,
|
||||
|
||||
@@ -6,7 +6,27 @@ const { strictObject, string, boolean } = require('../../configuration/schemaHel
|
||||
module.exports = {
|
||||
key: 'roomCameras',
|
||||
feature: true,
|
||||
defaultValue: { enabled: false, cameras: [] },
|
||||
// The example catalog documents the complete repeated-item shape as actual
|
||||
// initial configuration while the feature switch prevents network requests.
|
||||
defaultValue: {
|
||||
enabled: false,
|
||||
cameras: [
|
||||
{
|
||||
id: 'lobby',
|
||||
name: 'Lobby Camera',
|
||||
description: 'Wide shot of the staging area.',
|
||||
url: 'http://192.168.0.50/snapshot.jpg',
|
||||
streamUrl: 'http://192.168.0.50/stream.mjpg',
|
||||
},
|
||||
{
|
||||
id: 'workshop',
|
||||
name: 'Workshop Bench',
|
||||
description: 'Shows the workbench and charging docks.',
|
||||
url: 'http://192.168.0.51/snapshot.jpg',
|
||||
streamUrl: 'http://192.168.0.51/stream.mjpg',
|
||||
},
|
||||
],
|
||||
},
|
||||
schema: strictObject({
|
||||
enabled: boolean({ description: 'Publishes the configured room-camera catalog and enables camera snapshots and streams after restart.' }),
|
||||
cameras: {
|
||||
|
||||
@@ -12,7 +12,27 @@ const timezone = {
|
||||
const socials = {
|
||||
key: 'socials',
|
||||
feature: true,
|
||||
defaultValue: { enabled: false, links: [] },
|
||||
// A complete starter list matches the former YAML template without exposing
|
||||
// it to users until the service-owned enabled switch is deliberately set.
|
||||
defaultValue: {
|
||||
enabled: false,
|
||||
links: [
|
||||
{
|
||||
id: 'discord',
|
||||
label: 'Discord',
|
||||
url: 'https://discord.gg/your-invite',
|
||||
icon: 'FaDiscord',
|
||||
color: '#5865F2',
|
||||
},
|
||||
{
|
||||
id: 'kofi',
|
||||
label: 'Ko-fi',
|
||||
url: 'https://ko-fi.com/your-handle',
|
||||
icon: 'FaCoffee',
|
||||
color: '#29ABE0',
|
||||
},
|
||||
],
|
||||
},
|
||||
schema: strictObject({
|
||||
enabled: boolean({ title: 'Enabled', description: 'Shows the configured social-link buttons in driver and inter-instance views.' }),
|
||||
links: {
|
||||
@@ -32,7 +52,9 @@ const socials = {
|
||||
|
||||
const driverAd = {
|
||||
key: 'driverAd',
|
||||
defaultValue: { title: '', html: '' },
|
||||
// The title is harmless presentation metadata, while the HTML stays empty so
|
||||
// a new installation never displays active sample content to drivers.
|
||||
defaultValue: { title: 'Advertisement', html: '' },
|
||||
schema: strictObject({
|
||||
title: string({ description: 'Heading displayed above the operator-provided content on the driver page; leave blank to use the card fallback.', examples: ['Advertisement'], maxLength: 120 }),
|
||||
html: string({ title: 'HTML', description: 'Trusted operator HTML shown to drivers.', examples: ['<a href="https://example.com" target="_blank" rel="noopener noreferrer"><img src="https://example.com/ad.png" alt="Advertisement"></a>'], maxLength: 100000 }),
|
||||
|
||||
Reference in New Issue
Block a user