OverviewFilesCommitsIssuesDocsReleasesPeers

cthuloid commits / 857535c

webui: remember each unit's last address and dial it first
lastAddr is saved from mDNS 'up' events and from add-by-IP, and the unit is dialled at it until discovery reports it again. Falling back to <name>.local is useless on a phone (Android gives apps no DNS config), and on desktops it saves a lookup. Tests 66/66 incl. restart-without-discovery connecting via the remembered IP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
857535c0ba8ad01b4a01013a5af6a3782fb81873
by plankatron <fleeky@gmail.com> · 2026-10-02T16:32:34.000Z

Changed files (2) +64 -2

webui/lib/rig.js +11 -2
……@@ -109,6 +109,10 @@ export class Rig extends EventEmitter {
109109 label: cfg.label || id,
110110 host: cfg.host || id + '.local', // name to dial when mDNS has no address
111111 addr: null, // address from mDNS, preferred: no lookup to wait on
112 // Last address this unit was reached at (mDNS or added by IP). Dialled until
113 // discovery reports it again: on a phone `<name>.local` cannot be resolved at
114 // all (Android gives apps no DNS config), so without it a known lamp is unreachable.
115 lastAddr: cfg.lastAddr || null,
112116 port: cfg.port || 1337,
113117 manual: !!cfg.manual,
114118 online: false,
……@@ -125,7 +129,7 @@ export class Rig extends EventEmitter {
125129 }
126130 // Slots saved as all-null are "no look yet", same as never set.
127131 if (entry.slots && entry.slots.every((s) => s === null)) entry.slots = null
128 entry.unit = new Unit({ id, host: entry.host, port: entry.port, ...this.unitOpts })
132 entry.unit = new Unit({ id, host: entry.lastAddr || entry.host, port: entry.port, ...this.unitOpts })
129133 entry.unit.on('status', () => this._changed())
130134 entry.unit.on('state', (st) => this._onState(entry, st))
131135 entry.unit.on('hello', () => this._changed())
……@@ -154,6 +158,10 @@ export class Rig extends EventEmitter {
154158 e.online = true
155159 e.proto = ev.proto
156160 e.addr = ev.addr
161 if (e.lastAddr !== ev.addr) {
162 e.lastAddr = ev.addr
163 this._save()
164 }
157165 e.port = ev.port
158166 e.unit.host = ev.addr // dial the address: never a .local lookup in the path
159167 e.unit.port = ev.port
……@@ -215,6 +223,7 @@ export class Rig extends EventEmitter {
215223 if (!e) e = this._addUnit(name, { host, port, manual: true })
216224 e.host = host
217225 e.manual = true
226 if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) e.lastAddr = host
218227 if (!e.online) {
219228 e.unit.host = host
220229 e.unit.port = port
……@@ -452,7 +461,7 @@ export class Rig extends EventEmitter {
452461 const units = {}
453462 for (const e of this.units.values()) {
454463 units[e.id] = {
455 label: e.label, host: e.host, port: e.port, manual: e.manual, enabled: e.enabled,
464 label: e.label, host: e.host, lastAddr: e.lastAddr, port: e.port, manual: e.manual, enabled: e.enabled,
456465 redMax: e.redMax, fadeMs: e.fadeMs, slots: e.slots, gamma: e.gamma, freq: e.freq
457466 }
458467 }
webui/test/remember.test.js +53 -0
……@@ -0,0 +1,53 @@
1// A unit seen before is dialled at its LAST IP, even before (or without)
2// discovery reporting it again. On a phone `<name>.local` cannot be resolved at
3// all (Android gives apps no DNS config), so falling back to it means no lamp.
4import test from 'brittle'
5import EventEmitter from 'bare-events'
6import { Rig } from '../lib/rig.js'
7import { FakeUnitV3, waitFor } from './helpers.js'
8
9class FakeDiscovery extends EventEmitter {
10 constructor () { super(); this.available = true; this.reason = '' }
11 start () {}
12 stop () {}
13}
14
15class MemStore {
16 constructor () { this.data = {} }
17 load () { return this.data }
18 save (d) { this.data = JSON.parse(JSON.stringify(d)) }
19}
20
21test('the last discovered address is saved and dialled after a restart', async (t) => {
22 const fake = await new FakeUnitV3('lamp').listen()
23 const store = new MemStore()
24
25 const d1 = new FakeDiscovery()
26 const a = new Rig({ store, discovery: d1, unitOpts: { backoffStartMs: 100 } })
27 a.start()
28 d1.emit('up', { name: 'lamp', host: 'lamp.local', addr: '127.0.0.1', port: fake.port, proto: 3 })
29 a.updateUnit('lamp', { enabled: true })
30 a.flushSave()
31 a.stop()
32 t.is(store.data.units.lamp.lastAddr, '127.0.0.1', 'saved')
33
34 // Restart with a discovery that never reports the unit.
35 const b = new Rig({ store, discovery: new FakeDiscovery(), unitOpts: { backoffStartMs: 100 } })
36 b.start()
37 b.setConnected(true)
38 t.ok(await waitFor(() => b.snapshot().units[0].status === 'connected'), 'connected via the remembered IP')
39 b.stop()
40 await fake.close()
41})
42
43test('adding by IP remembers that IP', async (t) => {
44 const fake = await new FakeUnitV3('lamp2').listen()
45 const store = new MemStore()
46 const r = new Rig({ store, discovery: new FakeDiscovery(), unitOpts: { backoffStartMs: 100 } })
47 r.start()
48 await r.addManual('127.0.0.1', fake.port)
49 r.flushSave()
50 t.is(store.data.units.lamp2.lastAddr, '127.0.0.1')
51 r.stop()
52 await fake.close()
53})