Vista Cues 1.1.0: a wired-network setting; a sender set by name or off it shows red
The Macs at NA are also on Wi-Fi and the Dante networks, and Bonjour lists each MIDI session once per network. On 2026-10-20 the Tracks and Companion senders were set to Wi-Fi addresses, so the lighting MIDI ProdDash hears rode Wi-Fi. With "Wired network" set (This machine; e.g. 10.1.0.0/16, several with commas), a sender typed as a name or as an address outside it, and a session that joins us from outside it, turn the admin status line and the tile's sender chip red, naming the sender. It still joins, so the cues keep showing. Empty (the default) changes nothing; a typo in the setting fails the module in /admin with a plain message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,44 @@ function emptyState(extra = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The wired network(s) MIDI must ride ("10.1.0.0/16, 10.2.0.0/16"). The Macs
|
||||
* here are also on Wi-Fi and the Dante networks, and Bonjour lists each
|
||||
* session once per network, so a sender typed or picked from the wrong copy
|
||||
* works — until Wi-Fi hiccups mid-service. Returns [{ base, bits, text }];
|
||||
* throws on a typo so the admin page says so at once.
|
||||
*/
|
||||
function parseNets(text) {
|
||||
return String(text || '').split(/[\s,]+/).filter(Boolean).map((n) => {
|
||||
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(?:\/(\d{1,2}))?$/.exec(n);
|
||||
const parts = m ? m.slice(1, 5).map(Number) : [];
|
||||
const bits = m && m[5] !== undefined ? Number(m[5]) : 32;
|
||||
if (!m || parts.some((x) => x > 255) || bits > 32) {
|
||||
throw new Error(`Wired network: "${n}" is not a network like 10.1.0.0/16`);
|
||||
}
|
||||
return { base: ipNumber(parts.join('.')), bits, text: n };
|
||||
});
|
||||
}
|
||||
|
||||
function ipNumber(a) {
|
||||
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(String(a || '').trim());
|
||||
return m ? m.slice(1, 5).reduce((n, x) => n * 256 + Number(x), 0) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Why an address is not on the wired network: '' when it is (or there is no
|
||||
* rule), 'by name' for a host name (it resolves to whichever network answers
|
||||
* first), 'not wired' for an address outside every wired network.
|
||||
*/
|
||||
function offNetwork(nets, address) {
|
||||
if (!nets.length || !address) return '';
|
||||
const v = ipNumber(address);
|
||||
if (v === null) return 'by name';
|
||||
if (address.startsWith('127.')) return '';
|
||||
const span = (bits) => 2 ** (32 - bits);
|
||||
return nets.some((n) => Math.floor(v / span(n.bits)) === Math.floor(n.base / span(n.bits))) ? '' : 'not wired';
|
||||
}
|
||||
|
||||
function endpointOf(v) {
|
||||
const ep = v && typeof v === 'object' ? v : {};
|
||||
return { host: String(ep.host || '').trim(), port: Number(ep.port) || 5004 };
|
||||
@@ -62,6 +100,8 @@ module.exports = {
|
||||
const exclusive = config.exclusive === true;
|
||||
const headTakesOver = config.headTakesOver !== false;
|
||||
const { rules, errors: mappingErrors, groups } = parseMapping(config.cueLists, { middleC });
|
||||
const nets = parseNets(config.wiredNetwork);
|
||||
const netText = nets.map((n) => n.text).join(', ');
|
||||
if (mappingErrors.length) {
|
||||
for (const e of mappingErrors) log(`cue lists line ${e.line} skipped (${e.why}): ${e.text}`);
|
||||
}
|
||||
@@ -187,6 +227,9 @@ module.exports = {
|
||||
label: s.label,
|
||||
name: p ? p.name : '',
|
||||
address: inv?.address || s.host,
|
||||
// A name is judged as typed: it may resolve to Wi-Fi next time even if it is wired now.
|
||||
offNetwork: offNetwork(nets, s.host) || offNetwork(nets, inv?.address || ''),
|
||||
host: s.host,
|
||||
configured: true,
|
||||
connected: Boolean(p),
|
||||
error: p ? '' : (inv?.error || ''),
|
||||
@@ -195,7 +238,7 @@ module.exports = {
|
||||
}
|
||||
for (const p of session.participants.values()) {
|
||||
if (seen.has(p.key) || !p.connected) continue;
|
||||
out.push({ label: p.label || p.name, name: p.name, address: p.address, configured: false, connected: true, error: '', lastHeard: p.lastHeard });
|
||||
out.push({ label: p.label || p.name, name: p.name, address: p.address, offNetwork: offNetwork(nets, p.address), configured: false, connected: true, error: '', lastHeard: p.lastHeard });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -219,6 +262,7 @@ module.exports = {
|
||||
sessionName,
|
||||
port,
|
||||
advertised,
|
||||
wiredNetwork: netText,
|
||||
sources: sources(),
|
||||
current: cur || null,
|
||||
// The rest in sheet order of their group, so a colour chip stays put as it changes.
|
||||
@@ -304,6 +348,16 @@ module.exports = {
|
||||
health() {
|
||||
if (session.error) return { status: 'error', message: session.error };
|
||||
const src = sources();
|
||||
// Off the wired network: still joined (the cues keep working), but said first and in red.
|
||||
const off = src.filter((s) => s.offNetwork);
|
||||
if (off.length) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: `${off.map((s) => (s.offNetwork === 'by name'
|
||||
? `${s.label} is set by name (${s.host})`
|
||||
: `${s.label} ${s.configured ? `(${s.address})` : `at ${s.address}`} is not on the wired network`)).join(' · ')}: use its address on ${netText}`,
|
||||
};
|
||||
}
|
||||
const on = src.filter((s) => s.connected);
|
||||
const lines = mappingErrors.length ? ` · ${mappingErrors.length} cue-list line${mappingErrors.length === 1 ? '' : 's'} not understood (first: line ${mappingErrors[0].line})` : '';
|
||||
const map = rules.length ? '' : ' · no cue lists mapped yet';
|
||||
@@ -323,6 +377,9 @@ module.exports = {
|
||||
};
|
||||
},
|
||||
|
||||
// For the tests.
|
||||
_offNetwork: (text, address) => offNetwork(parseNets(text), address),
|
||||
|
||||
/**
|
||||
* One picker entry, so the + menu gives Vista Cues a heading of its own —
|
||||
* a module with no list sits headless under whichever module precedes it
|
||||
|
||||
Reference in New Issue
Block a user