mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
old defaults in new schema
This commit is contained in:
@@ -43,6 +43,36 @@ function collectUndocumentedSchemaPaths(schema, pathLabel = '$') {
|
||||
return missing;
|
||||
}
|
||||
|
||||
function collectSchemaPathsMissingInputExamples(schema, value, pathLabel = '$', insideArray = false) {
|
||||
/*
|
||||
Universal defaults such as timeouts and modes are real saved values. Empty
|
||||
strings and newly-created array items are different: they require an
|
||||
installation-specific value, so the admin form must show an example without
|
||||
persisting a fake hostname, credential, or hardware ID. This walk enforces
|
||||
that distinction across both the current default document and array shapes.
|
||||
*/
|
||||
if (!schema || typeof schema !== 'object') return [];
|
||||
|
||||
if (schema.type === 'array') {
|
||||
return collectSchemaPathsMissingInputExamples(schema.items, undefined, `${pathLabel}[]`, true);
|
||||
}
|
||||
|
||||
if (schema.type === 'object') {
|
||||
return Object.entries(schema.properties || {}).flatMap(([key, childSchema]) => (
|
||||
collectSchemaPathsMissingInputExamples(childSchema, value?.[key], `${pathLabel}.${key}`, insideArray)
|
||||
));
|
||||
}
|
||||
|
||||
// Enumerations and checkboxes already communicate their accepted shape
|
||||
// through their controls, so placeholder examples are only required for
|
||||
// otherwise free-form empty scalar inputs.
|
||||
const needsExample = (value === '' || insideArray)
|
||||
&& !Array.isArray(schema.enum)
|
||||
&& schema.type !== 'boolean';
|
||||
if (!needsExample) return [];
|
||||
return Array.isArray(schema.examples) && schema.examples.length ? [] : [pathLabel];
|
||||
}
|
||||
|
||||
test.after(() => {
|
||||
temporaryRoots.forEach((root) => fs.rmSync(root, { recursive: true, force: true }));
|
||||
});
|
||||
@@ -78,6 +108,15 @@ test('every configuration section, collection, item, and option has an operator
|
||||
assert.deepEqual(collectUndocumentedSchemaPaths(rootSchema), []);
|
||||
});
|
||||
|
||||
test('empty installation-specific fields and array item inputs provide schema-owned examples', () => {
|
||||
/*
|
||||
The frontend derives placeholders from these examples generically. Keeping
|
||||
this assertion beside schema composition prevents an empty, unexplained box
|
||||
from returning when a service adds configuration in the future.
|
||||
*/
|
||||
assert.deepEqual(collectSchemaPathsMissingInputExamples(rootSchema, defaultConfig), []);
|
||||
});
|
||||
|
||||
test('service definitions generate public feature paths without a separate registry', () => {
|
||||
/*
|
||||
This order follows the one configuration document, including nested Neato
|
||||
|
||||
@@ -10,6 +10,6 @@ module.exports = {
|
||||
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 }),
|
||||
profileImageUrl: string({ title: 'Profile image URL', description: 'Optional image URL displayed beside barcode-game chat messages; leave blank for no custom image.', maxLength: 2048 }),
|
||||
profileImageUrl: string({ title: 'Profile image URL', description: 'Optional image URL displayed beside barcode-game chat messages; leave blank for no custom image.', examples: ['https://example.com/barcode-games.png'], maxLength: 2048 }),
|
||||
}, { title: 'Barcode games', description: 'Controls the multiplayer games driven by scans received from the barcode scanner service.', required: ['enabled', 'botName', 'profileImageUrl'] }),
|
||||
};
|
||||
|
||||
@@ -16,25 +16,25 @@ module.exports = {
|
||||
},
|
||||
schema: strictObject({
|
||||
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 }),
|
||||
token: string({ title: 'Bot token', description: 'Discord bot token used to log in. The saved value is never returned to the browser.', examples: ['DISCORD_BOT_TOKEN'], 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.', examples: ['123456789012345678'], maxLength: 100 }),
|
||||
siteUrl: string({ title: 'Public site URL', description: 'Public base URL appended to announcement embeds and server-hosted replay links.', examples: ['https://rover.example.com'], maxLength: 2048 }),
|
||||
channels: strictObject({
|
||||
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 }),
|
||||
general: string({ description: 'Channel ID used by the button-box stalker-role and everyone-ping rewards.', examples: ['123456789012345678'], maxLength: 100 }),
|
||||
announcements: string({ description: 'Channel ID used for public-mode openings, objective changes, and all-rovers-unlocked announcements.', examples: ['123456789012345678'], maxLength: 100 }),
|
||||
adminAlerts: string({ description: 'Channel ID used for rover health, battery, dock, help, and daily fleet-report notifications.', examples: ['123456789012345678'], maxLength: 100 }),
|
||||
replay: string({ description: 'Channel ID used to upload generated replay videos when Discord replay delivery is available.', examples: ['123456789012345678'], maxLength: 100 }),
|
||||
humanAlerts: string({ description: 'Channel ID used for physical human-alert button notifications and captured images.', examples: ['123456789012345678'], 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({ 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 }),
|
||||
stalkerPing: string({ description: 'Role ID mentioned by the button-box stalker-ping reward in the general channel.', examples: ['123456789012345678'], maxLength: 100 }),
|
||||
announcementPing: string({ description: 'Role ID mentioned by configured user announcements.', examples: ['123456789012345678'], maxLength: 100 }),
|
||||
adminPing: string({ description: 'Role ID mentioned for important administrative rover, battery, and help alerts.', examples: ['123456789012345678'], maxLength: 100 }),
|
||||
humanAlertPing: string({ description: 'Role ID mentioned when the physical human-alert button is pressed.', examples: ['123456789012345678'], maxLength: 100 }),
|
||||
}, {
|
||||
title: 'Roles',
|
||||
description: 'Discord role IDs mentioned for specific notification categories.',
|
||||
|
||||
@@ -23,7 +23,7 @@ module.exports = {
|
||||
schema: strictObject({
|
||||
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 }),
|
||||
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.', examples: ['REPLACE_WITH_LONG_LIVED_TOKEN'], writeOnly: true, maxLength: 20000 }),
|
||||
[neato.key]: neato.schema,
|
||||
[lift.key]: lift.schema,
|
||||
entities: {
|
||||
@@ -31,8 +31,8 @@ module.exports = {
|
||||
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', 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 }),
|
||||
id: string({ title: 'Entity id', description: 'Exact Home Assistant entity ID, such as light.rover_room or switch.floor_lamp.', examples: ['light.lab_main'], minLength: 1, maxLength: 255 }),
|
||||
name: string({ description: 'Human-readable name shown for this entity in the rover UI.', examples: ['Lab Lights'], 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.',
|
||||
@@ -44,9 +44,9 @@ module.exports = {
|
||||
title: 'Physical button mappings',
|
||||
description: 'Maps Home Assistant entity state changes to built-in rover-server actions.',
|
||||
items: strictObject({
|
||||
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 }),
|
||||
entityId: string({ title: 'Entity id', description: 'Home Assistant entity whose state changes are watched as button presses.', examples: ['sensor.basement_rover_buttons_action'], minLength: 1, maxLength: 255 }),
|
||||
stateEquals: string({ description: 'Exact Home Assistant state that must be reached before the action fires.', examples: ['on'], minLength: 1, maxLength: 255 }),
|
||||
cooldownMs: integer({ description: 'Minimum milliseconds between accepted activations of this mapping.', examples: [15000], 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.',
|
||||
|
||||
@@ -16,15 +16,19 @@ module.exports = {
|
||||
schema: strictObject({
|
||||
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' },
|
||||
item: {
|
||||
description: 'Absolute URL returning an array of peer MultiRover instance entries.',
|
||||
examples: ['https://raw.githubusercontent.com/legop3/multi-roomba-rover-instance-directory/refs/heads/main/directory.json'],
|
||||
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({ description: 'Public base URL peers and users use to reach this server; it also identifies and filters this instance from directory results.', maxLength: 2048 }),
|
||||
publicUrl: string({ description: 'Public base URL peers and users use to reach this server; it also identifies and filters this instance from directory results.', examples: ['https://rover.example.com'], 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 }),
|
||||
description: string({ description: 'Short public summary advertised with this instance.', examples: ['A short public description of this rover server.'], 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'] }),
|
||||
|
||||
@@ -9,8 +9,8 @@ module.exports = {
|
||||
defaultValue: { enabled: false, upSwitch: '', downSwitch: '', 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.', maxLength: 255 }),
|
||||
downSwitch: string({ description: 'Home Assistant switch entity that powers downward lift movement.', maxLength: 255 }),
|
||||
upSwitch: string({ description: 'Home Assistant switch entity that powers upward lift movement.', examples: ['switch.lift_up'], maxLength: 255 }),
|
||||
downSwitch: string({ description: 'Home Assistant switch entity that powers downward lift movement.', examples: ['switch.lift_down'], 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 }),
|
||||
}, {
|
||||
|
||||
@@ -10,7 +10,7 @@ module.exports = {
|
||||
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 },
|
||||
item: { description: 'Hostname or IP address MediaMTX advertises as a WebRTC ICE candidate.', examples: ['rover.example.com', 'media-server.local'], 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'] }),
|
||||
|
||||
@@ -9,7 +9,7 @@ module.exports = {
|
||||
defaultValue: { enabled: false, device: '' },
|
||||
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.', maxLength: 255 }),
|
||||
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 }),
|
||||
}, {
|
||||
title: 'Neato',
|
||||
description: 'Optional Neato robot controls backed by entities published from one ESPHome device through Home Assistant.',
|
||||
|
||||
@@ -28,7 +28,7 @@ module.exports = {
|
||||
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 }),
|
||||
profileImageUrl: string({ title: 'Profile image URL', description: 'Optional image URL displayed beside Overseer chat messages; leave blank for no custom image.', examples: ['https://example.com/overseer.png'], 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'] }),
|
||||
};
|
||||
|
||||
@@ -22,10 +22,10 @@ module.exports = {
|
||||
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 }),
|
||||
host: string({ description: 'Hostname or IP address of the ONVIF camera.', examples: ['192.168.0.8'], 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 }),
|
||||
username: string({ description: 'Camera account username used for ONVIF authentication.', examples: ['admin'], maxLength: 255 }),
|
||||
password: string({ description: 'Camera account password used for ONVIF authentication. The saved value is never returned to the browser.', examples: ['REPLACE_WITH_CAMERA_PASSWORD'], 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.' }),
|
||||
|
||||
@@ -13,11 +13,11 @@ module.exports = {
|
||||
type: 'array',
|
||||
description: 'Room cameras available to the web UI and replay system.',
|
||||
items: strictObject({
|
||||
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 }),
|
||||
id: string({ description: 'Stable camera identifier used in socket requests, selections, and replay source names.', examples: ['lobby'], minLength: 1, maxLength: 80, pattern: '^[a-zA-Z0-9_-]+$' }),
|
||||
name: string({ description: 'Human-readable camera name shown in the UI.', examples: ['Lobby Camera'], minLength: 1, maxLength: 120 }),
|
||||
description: string({ description: 'Short explanation of the camera location or view shown in the UI.', examples: ['Wide shot of the staging area.'], maxLength: 500 }),
|
||||
url: string({ title: 'Snapshot URL', description: 'HTTP URL fetched when the server needs a still image from this camera.', examples: ['http://192.168.0.50/snapshot.jpg'], format: 'uri', maxLength: 2048 }),
|
||||
streamUrl: string({ title: 'Stream URL', description: 'Live stream URL consumed by the server snapshot engine and replay capture path.', examples: ['http://192.168.0.50/stream.mjpg'], maxLength: 2048 }),
|
||||
}, {
|
||||
description: 'One named room camera with its still-image and live-stream sources.',
|
||||
required: ['id', 'name', 'description', 'url', 'streamUrl'],
|
||||
|
||||
@@ -20,11 +20,11 @@ const socials = {
|
||||
title: 'Links',
|
||||
description: 'Ordered social or community links presented to users when this feature is enabled.',
|
||||
items: strictObject({
|
||||
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}$' }),
|
||||
id: string({ description: 'Stable identifier used by the UI to distinguish this link from the others.', examples: ['discord'], minLength: 1, maxLength: 60, pattern: '^[a-zA-Z0-9_-]+$' }),
|
||||
label: string({ description: 'User-facing text displayed on the link button.', examples: ['Discord'], minLength: 1, maxLength: 80 }),
|
||||
url: string({ description: 'Absolute destination opened when a user selects this link.', examples: ['https://discord.gg/your-invite'], format: 'uri', maxLength: 2048 }),
|
||||
icon: string({ description: 'Icon name interpreted by the social-button UI; leave blank to use its fallback presentation.', examples: ['FaDiscord'], maxLength: 80 }),
|
||||
color: string({ description: 'Six-digit hexadecimal accent color used for this link button.', examples: ['#5865F2'], pattern: '^#[0-9a-fA-F]{6}$' }),
|
||||
}, { description: 'One social-link button shown to users.', required: ['id', 'label', 'url', 'icon', 'color'] }),
|
||||
},
|
||||
}, { title: 'Social links', description: 'Controls the optional social and community buttons published to local users and peer instances.', required: ['enabled', 'links'] }),
|
||||
@@ -34,8 +34,8 @@ const driverAd = {
|
||||
key: 'driverAd',
|
||||
defaultValue: { title: '', html: '' },
|
||||
schema: strictObject({
|
||||
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: 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 }),
|
||||
}, { title: 'Driver content', description: 'Operator-managed informational or promotional content displayed in the driver application.', required: ['title', 'html'] }),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user