feat: web UI on socket protocol 2 (step 2b)

The UI talks to the device over the one socket at /ws: the sync fills the
shade model, its events redraw the lists, and every read, command, edit,
setting, scan and reboot is an op. Only /loginContext, /login and the file
transfers stay on HTTP, so a page load opens no other connection.

- The 8080 socket, editJSON/requestResult and the /controller, /rooms,
  /shades, /groups, /room, /shade, /group, settings and command routes are
  no longer used by the UI.
- An edit's callback gets the item once its event is in (ProtoClient
  waitFor); busy edits and saves are sent again.
- The token is kept in sessionStorage. When /ws is refused and
  /loginContext says a login is needed, the client stops retrying and the
  login panel opens. Backup and uploads send the token.
- A banner shows when the device speaks another protocol version.
- Removed dead code: the /pairShade call and its unused /unpairShade twin,
  loadLogin, login.html (broken, linked from nowhere), Secure Config Only's
  checkbox (protocol 2 has two modes) and the HTTP helpers nothing uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-25 20:12:54 +01:00
parent 4415beb4f7
commit 0df740df6a
6 changed files with 570 additions and 944 deletions

View file

@ -296,6 +296,25 @@ test('a new token connects again with it', () => {
assert.match(FakeSocket.last.url, /apikey=new/);
});
test('waitFor resolves with the first event that matches, after the model has it', async () => {
const { c, ws } = connected();
const shadeId = recordedItems('S')[0].s;
const p = c.waitFor('shade', (d) => d.shadeId === shadeId, 3000);
ws.deliver({ v: 2, t: T.shade, d: { s: 250, n: 'Other' } });
ws.deliver({ v: 2, t: T.shade, d: { s: shadeId, n: 'Renamed' } });
const d = await p;
assert.equal(d.name, 'Renamed');
assert.equal(c.model.shades.get(shadeId).name, 'Renamed');
});
test('waitFor gives null when no event comes, and stops listening', async () => {
const { c } = connected();
const p = c.waitFor('room', null, 3000);
c.timers.runAll();
assert.equal(await p, null);
assert.equal((c.listeners.room || []).length, 0);
});
test('subscribe sends the feed number', () => {
const { c, ws } = connected();
c.subscribe('frames');