Unified endpoint config type: one Host/IP : Port control in admin

New configSchema type 'endpoint' declared in the module spec: anything
that points at a device or service on the network must be one endpoint
field, rendered identically for every module in /admin as a joined
Host/IP : Port control and stored/handed to the module server as
{ host, port }. Migrates all three connection modules — ProdCom (was a
full URL field), ProPresenter and Timers (were separate host + port
fields). Legacy stored configs (prodcomUrl / flat host+port) keep
working until the admin form is re-saved, at which point the new shape
replaces them. MODULE-GUIDE documents the type and the presentation
rule: no split host/port fields, and full URLs only for genuine web
addresses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNYUv9EyrGSFmCnmH7WYKH
This commit is contained in:
Claude
2026-08-31 19:33:08 +00:00
parent 6412dd9524
commit a06bc921ce
11 changed files with 116 additions and 39 deletions
+6
View File
@@ -93,6 +93,12 @@ body { overflow: auto; }
.module-form { margin-top: 12px; border-top: 1px solid var(--border); padding-top: 12px; }
.module-form .field { max-width: 420px; }
/* endpoint fields: every module's upstream is entered the same way */
.field .endpoint-row { display: flex; align-items: center; gap: 7px; }
.endpoint-row .ep-host { flex: 1; min-width: 0; }
.endpoint-row .ep-port { width: 92px; flex: none; }
.endpoint-row .ep-sep { color: var(--muted); font-weight: 600; }
.module-form-empty { color: var(--muted); font-size: 12px; margin-top: 10px; }
.module-actions {
+26
View File
@@ -96,6 +96,32 @@ function fieldFor(key, spec, value, passwordSet) {
input.checked = Boolean(value);
field.append(input, document.createTextNode(label));
read = () => input.checked;
} else if (type === 'endpoint') {
// One consistent "Host / IP : Port" control for every module's upstream.
const span = document.createElement('span');
span.textContent = label;
const row = document.createElement('span');
row.className = 'endpoint-row';
const host = document.createElement('input');
host.type = 'text';
host.className = 'ep-host';
host.placeholder = 'Host / IP';
host.autocomplete = 'off';
host.spellcheck = false;
host.value = value?.host ? String(value.host) : '';
const sep = document.createElement('span');
sep.className = 'ep-sep';
sep.textContent = ':';
const port = document.createElement('input');
port.type = 'number';
port.className = 'ep-port';
port.placeholder = 'Port';
port.min = '1';
port.max = '65535';
port.value = value?.port ? String(value.port) : '';
row.append(host, sep, port);
field.append(span, row);
read = () => ({ host: host.value.trim(), port: Number(port.value) || 0 });
} else if (type === 'select' && Array.isArray(spec.options)) {
const span = document.createElement('span');
span.textContent = label;