@nubisco/openbridge 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/notifications.test.d.ts +2 -0
- package/dist/__tests__/notifications.test.d.ts.map +1 -0
- package/dist/__tests__/notifications.test.js +237 -0
- package/dist/__tests__/notifications.test.js.map +1 -0
- package/dist/__tests__/server.test.js +8 -0
- package/dist/__tests__/server.test.js.map +1 -1
- package/dist/auth.d.ts +5 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +17 -2
- package/dist/auth.js.map +1 -1
- package/dist/daemon.d.ts +2 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +49 -1
- package/dist/daemon.js.map +1 -1
- package/dist/notification-routes.d.ts +21 -0
- package/dist/notification-routes.d.ts.map +1 -0
- package/dist/notification-routes.js +37 -0
- package/dist/notification-routes.js.map +1 -0
- package/dist/notifications.d.ts +157 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +364 -0
- package/dist/notifications.js.map +1 -0
- package/dist/platform-client.d.ts +2 -0
- package/dist/platform-client.d.ts.map +1 -1
- package/dist/platform-client.js.map +1 -1
- package/dist/server.d.ts +4 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +27 -1
- package/dist/server.js.map +1 -1
- package/package.json +5 -5
- package/ui-dist/assets/index-CobsrVaQ.css +32 -0
- package/ui-dist/assets/index-HkwdcyTW.js +109 -0
- package/ui-dist/index.html +2 -2
- package/ui-dist/assets/index-CbFkOhrP.js +0 -109
- package/ui-dist/assets/index-CgJV-XD6.css +0 -32
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bell: things that happened to the bridge that a person should hear about.
|
|
3
|
+
*
|
|
4
|
+
* A household bridge produces a great deal of chatter (every poll, every
|
|
5
|
+
* reading) and a handful of events. Only events reach the bell: a device that
|
|
6
|
+
* has stopped answering for long enough to matter, the same device coming
|
|
7
|
+
* back, a plugin that failed, and an update worth installing. Everything else
|
|
8
|
+
* stays in the device timeline and the log, where it already is.
|
|
9
|
+
*
|
|
10
|
+
* Follows "Notifications: Acta's system is the fleet standard" in the
|
|
11
|
+
* workspace AGENTS.md, adapted to a bridge:
|
|
12
|
+
*
|
|
13
|
+
* - Rows are about the house, not about one person, so every row reaches
|
|
14
|
+
* everybody who signs in. What is per person is whether they have read it.
|
|
15
|
+
* - A write that changes nothing tells nobody. Every row carries a key built
|
|
16
|
+
* from the thing and the state that justified it, and a key is announced
|
|
17
|
+
* once, ever. Re-checking for updates every six hours finds the same
|
|
18
|
+
* version and rings for nobody.
|
|
19
|
+
* - The chasing half (reminder email) does not apply. OpenBridge has no email
|
|
20
|
+
* channel, and with nothing configured the queue is left alone.
|
|
21
|
+
*
|
|
22
|
+
* Stored as one small JSON file, capped, because the whole history a person
|
|
23
|
+
* scrolls through is a few hundred rows and a database would be heavier than
|
|
24
|
+
* the feature.
|
|
25
|
+
*/
|
|
26
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
27
|
+
import { dirname } from 'path';
|
|
28
|
+
import { randomUUID } from 'crypto';
|
|
29
|
+
/** Newest rows kept. Older ones fall off the end. */
|
|
30
|
+
export const MAX_NOTIFICATIONS = 200;
|
|
31
|
+
/** Most announced keys remembered, so the file stays bounded too. */
|
|
32
|
+
const MAX_ANNOUNCED = 2000;
|
|
33
|
+
/**
|
|
34
|
+
* How long a device must be silent, continuously, before the bell rings.
|
|
35
|
+
*
|
|
36
|
+
* Ten minutes, set by Jose. Long enough that a router reboot or a slow poll
|
|
37
|
+
* never rings, short enough that a gate that has fallen off the network is
|
|
38
|
+
* noticed the same evening rather than from HomeKit a day later.
|
|
39
|
+
*/
|
|
40
|
+
export const OFFLINE_AFTER_SECONDS = 10 * 60;
|
|
41
|
+
/** The person whose read state is kept when sign-in is off. */
|
|
42
|
+
export const LOCAL_USER = 'local';
|
|
43
|
+
function empty() {
|
|
44
|
+
return { notifications: [], readBy: {}, announced: [], outages: {}, failing: {} };
|
|
45
|
+
}
|
|
46
|
+
export class NotificationStore {
|
|
47
|
+
path;
|
|
48
|
+
clock;
|
|
49
|
+
state;
|
|
50
|
+
listeners = new Set();
|
|
51
|
+
constructor(path, clock = Date.now) {
|
|
52
|
+
this.path = path;
|
|
53
|
+
this.clock = clock;
|
|
54
|
+
this.state = this.load();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Add a row, unless its key has been announced before.
|
|
58
|
+
*
|
|
59
|
+
* Returns the row, or null when nothing was written. Never throws: the bell
|
|
60
|
+
* is a report about the bridge, and failing to write one must not take down
|
|
61
|
+
* the thing it reports on.
|
|
62
|
+
*/
|
|
63
|
+
add(input) {
|
|
64
|
+
if (this.state.announced.includes(input.key))
|
|
65
|
+
return null;
|
|
66
|
+
const row = {
|
|
67
|
+
id: randomUUID(),
|
|
68
|
+
kind: input.kind,
|
|
69
|
+
title: input.title,
|
|
70
|
+
body: input.body,
|
|
71
|
+
variant: input.variant,
|
|
72
|
+
target: input.target,
|
|
73
|
+
createdAt: new Date(this.clock()).toISOString(),
|
|
74
|
+
};
|
|
75
|
+
this.state.notifications.unshift(row);
|
|
76
|
+
this.state.notifications.length = Math.min(this.state.notifications.length, MAX_NOTIFICATIONS);
|
|
77
|
+
this.state.announced.push(input.key);
|
|
78
|
+
if (this.state.announced.length > MAX_ANNOUNCED) {
|
|
79
|
+
this.state.announced.splice(0, this.state.announced.length - MAX_ANNOUNCED);
|
|
80
|
+
}
|
|
81
|
+
this.pruneReadState();
|
|
82
|
+
this.save();
|
|
83
|
+
this.emit({ type: 'added' });
|
|
84
|
+
return row;
|
|
85
|
+
}
|
|
86
|
+
/** The newest rows as one person sees them, and their unread count over every row. */
|
|
87
|
+
list(userId, limit = 50) {
|
|
88
|
+
const read = new Set(this.state.readBy[userId] ?? []);
|
|
89
|
+
const capped = Math.min(Math.max(1, Math.floor(limit) || 50), MAX_NOTIFICATIONS);
|
|
90
|
+
const notifications = this.state.notifications.slice(0, capped).map((n) => ({ ...n, read: read.has(n.id) }));
|
|
91
|
+
return { notifications, unread: this.unreadCount(userId) };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The badge. Counted over every stored row, never over the page the client
|
|
95
|
+
* happened to load, which is how Acta's badge came to understate.
|
|
96
|
+
*/
|
|
97
|
+
unreadCount(userId) {
|
|
98
|
+
const read = new Set(this.state.readBy[userId] ?? []);
|
|
99
|
+
return this.state.notifications.filter((n) => !read.has(n.id)).length;
|
|
100
|
+
}
|
|
101
|
+
/** Mark some rows read for one person, or all of them when no ids are given. */
|
|
102
|
+
markRead(userId, ids) {
|
|
103
|
+
const known = new Set(this.state.notifications.map((n) => n.id));
|
|
104
|
+
const read = new Set(this.state.readBy[userId] ?? []);
|
|
105
|
+
const before = read.size;
|
|
106
|
+
for (const id of ids ?? known)
|
|
107
|
+
if (known.has(id))
|
|
108
|
+
read.add(id);
|
|
109
|
+
if (read.size !== before) {
|
|
110
|
+
this.state.readBy[userId] = [...read];
|
|
111
|
+
this.save();
|
|
112
|
+
this.emit({ type: 'read', userId });
|
|
113
|
+
}
|
|
114
|
+
return this.unreadCount(userId);
|
|
115
|
+
}
|
|
116
|
+
/** Told about every write, for live delivery to open tabs. */
|
|
117
|
+
subscribe(listener) {
|
|
118
|
+
this.listeners.add(listener);
|
|
119
|
+
return () => this.listeners.delete(listener);
|
|
120
|
+
}
|
|
121
|
+
// ─── State the watchers keep across restarts ─────────────────────────────
|
|
122
|
+
outage(deviceId) {
|
|
123
|
+
return this.state.outages[deviceId];
|
|
124
|
+
}
|
|
125
|
+
setOutage(deviceId, outage) {
|
|
126
|
+
if (outage)
|
|
127
|
+
this.state.outages[deviceId] = outage;
|
|
128
|
+
else
|
|
129
|
+
delete this.state.outages[deviceId];
|
|
130
|
+
this.save();
|
|
131
|
+
}
|
|
132
|
+
failing(key) {
|
|
133
|
+
return this.state.failing[key];
|
|
134
|
+
}
|
|
135
|
+
setFailing(key, error) {
|
|
136
|
+
if (error === null) {
|
|
137
|
+
if (!(key in this.state.failing))
|
|
138
|
+
return;
|
|
139
|
+
delete this.state.failing[key];
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
this.state.failing[key] = error;
|
|
143
|
+
}
|
|
144
|
+
this.save();
|
|
145
|
+
}
|
|
146
|
+
pruneReadState() {
|
|
147
|
+
const known = new Set(this.state.notifications.map((n) => n.id));
|
|
148
|
+
for (const [user, ids] of Object.entries(this.state.readBy)) {
|
|
149
|
+
this.state.readBy[user] = ids.filter((id) => known.has(id));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
emit(event) {
|
|
153
|
+
for (const listener of this.listeners) {
|
|
154
|
+
try {
|
|
155
|
+
listener(event);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
/* one closed tab must not stop the others hearing */
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
load() {
|
|
163
|
+
try {
|
|
164
|
+
if (!existsSync(this.path))
|
|
165
|
+
return empty();
|
|
166
|
+
const raw = JSON.parse(readFileSync(this.path, 'utf8'));
|
|
167
|
+
return {
|
|
168
|
+
notifications: Array.isArray(raw.notifications) ? raw.notifications : [],
|
|
169
|
+
readBy: raw.readBy && typeof raw.readBy === 'object' ? raw.readBy : {},
|
|
170
|
+
announced: Array.isArray(raw.announced) ? raw.announced : [],
|
|
171
|
+
outages: raw.outages && typeof raw.outages === 'object' ? raw.outages : {},
|
|
172
|
+
failing: raw.failing && typeof raw.failing === 'object' ? raw.failing : {},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// A corrupt file costs the history, not the bridge.
|
|
177
|
+
return empty();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
save() {
|
|
181
|
+
try {
|
|
182
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
183
|
+
// Written aside and renamed, so a power cut mid-write leaves the old
|
|
184
|
+
// file rather than half of a new one.
|
|
185
|
+
const tmp = `${this.path}.tmp`;
|
|
186
|
+
writeFileSync(tmp, JSON.stringify(this.state));
|
|
187
|
+
renameSync(tmp, this.path);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
/* the bell must not break the bridge it reports on */
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function humanDuration(seconds) {
|
|
195
|
+
if (seconds < 90)
|
|
196
|
+
return `${Math.round(seconds)} seconds`;
|
|
197
|
+
const minutes = seconds / 60;
|
|
198
|
+
if (minutes < 90)
|
|
199
|
+
return `${Math.round(minutes)} minutes`;
|
|
200
|
+
const hours = minutes / 60;
|
|
201
|
+
if (hours < 48)
|
|
202
|
+
return `${Math.round(hours)} hours`;
|
|
203
|
+
return `${Math.round(hours / 24)} days`;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Turn device health, read on every tick, into one row when a device crosses
|
|
207
|
+
* the line and one when it comes back.
|
|
208
|
+
*
|
|
209
|
+
* Health is derived from the `_updatedAt` stamp `reportTelemetry` writes for
|
|
210
|
+
* every device of every native plugin (see device-health.ts), so this needs no
|
|
211
|
+
* cooperation from plugins. A device of a plugin that is not running is
|
|
212
|
+
* skipped: the plugin's own failure explains all of its devices at once, and
|
|
213
|
+
* thirty rows saying "not responding" would bury the one that says why.
|
|
214
|
+
*/
|
|
215
|
+
export function observeDevices(store, devices, now) {
|
|
216
|
+
for (const device of devices) {
|
|
217
|
+
if (device.pluginStatus !== 'running')
|
|
218
|
+
continue;
|
|
219
|
+
const { health } = device;
|
|
220
|
+
const open = store.outage(device.id);
|
|
221
|
+
if (!open) {
|
|
222
|
+
// Stale already allows for the device's own interval, so a device that
|
|
223
|
+
// reports every five minutes is not accused at ten. The ten minutes are
|
|
224
|
+
// a floor on top of that, not a replacement for it.
|
|
225
|
+
const silent = health.silentForSeconds ?? 0;
|
|
226
|
+
if (health.status !== 'stale' || !health.lastSeen || silent < OFFLINE_AFTER_SECONDS)
|
|
227
|
+
continue;
|
|
228
|
+
store.setOutage(device.id, { since: health.lastSeen });
|
|
229
|
+
store.add({
|
|
230
|
+
key: `device.offline:${device.id}:${health.lastSeen}`,
|
|
231
|
+
kind: 'device.offline',
|
|
232
|
+
title: `${device.name} is not responding`,
|
|
233
|
+
body: `No response for ${humanDuration(silent)}. It may be powered off, or its address may have changed.`,
|
|
234
|
+
variant: 'warning',
|
|
235
|
+
target: { kind: 'device', id: device.id },
|
|
236
|
+
});
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (health.status === 'ok') {
|
|
240
|
+
store.setOutage(device.id, null);
|
|
241
|
+
const back = health.lastSeen ? Date.parse(health.lastSeen) : now;
|
|
242
|
+
const outageSeconds = Math.max(0, (back - Date.parse(open.since)) / 1000);
|
|
243
|
+
store.add({
|
|
244
|
+
key: `device.online:${device.id}:${open.since}`,
|
|
245
|
+
kind: 'device.online',
|
|
246
|
+
title: `${device.name} is back online`,
|
|
247
|
+
body: `It answered again after ${humanDuration(outageSeconds)} without a response.`,
|
|
248
|
+
variant: 'success',
|
|
249
|
+
target: { kind: 'device', id: device.id },
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* One row when a plugin enters the error state, and none while it stays
|
|
256
|
+
* there or is retried and fails the same way again.
|
|
257
|
+
*
|
|
258
|
+
* Cleared once the plugin runs, so a crash after a recovery is news again.
|
|
259
|
+
* The failing set is stored, so a plugin that fails the same way at every
|
|
260
|
+
* start does not ring again on every restart of the daemon.
|
|
261
|
+
*/
|
|
262
|
+
export function observePlugins(store, plugins, now) {
|
|
263
|
+
for (const plugin of plugins) {
|
|
264
|
+
const key = `plugin:${plugin.id}`;
|
|
265
|
+
if (plugin.status === 'running') {
|
|
266
|
+
store.setFailing(key, null);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (plugin.status !== 'error' || plugin.disabled)
|
|
270
|
+
continue;
|
|
271
|
+
const error = plugin.error?.trim() || 'It stopped with an error.';
|
|
272
|
+
if (store.failing(key) !== undefined)
|
|
273
|
+
continue;
|
|
274
|
+
store.setFailing(key, error);
|
|
275
|
+
store.add({
|
|
276
|
+
key: `plugin.failed:${plugin.id}:${now}`,
|
|
277
|
+
kind: 'plugin.failed',
|
|
278
|
+
title: `${plugin.name} stopped working`,
|
|
279
|
+
body: firstLine(error),
|
|
280
|
+
variant: 'warning',
|
|
281
|
+
target: { kind: 'plugin', id: plugin.id },
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* A plugin that could not be loaded at all.
|
|
287
|
+
*
|
|
288
|
+
* Distinct from the error state above: a module that throws on import never
|
|
289
|
+
* reaches the registry as a running plugin, so there is no status to watch.
|
|
290
|
+
* The daemon calls this from the places it catches the failure.
|
|
291
|
+
*/
|
|
292
|
+
export function pluginLoadFailed(store, plugin, err, now) {
|
|
293
|
+
const error = firstLine(err instanceof Error ? err.message : String(err));
|
|
294
|
+
const key = `load:${plugin.key}`;
|
|
295
|
+
if (store.failing(key) === error)
|
|
296
|
+
return;
|
|
297
|
+
store.setFailing(key, error);
|
|
298
|
+
store.add({
|
|
299
|
+
key: `plugin.failed:${key}:${now}`,
|
|
300
|
+
kind: 'plugin.failed',
|
|
301
|
+
title: `${plugin.name} could not be loaded`,
|
|
302
|
+
body: error,
|
|
303
|
+
variant: 'warning',
|
|
304
|
+
target: plugin.target,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
/** The counterpart: a plugin that loaded is no longer failing to. */
|
|
308
|
+
export function pluginLoaded(store, pluginKey) {
|
|
309
|
+
store.setFailing(`load:${pluginKey}`, null);
|
|
310
|
+
}
|
|
311
|
+
function firstLine(text) {
|
|
312
|
+
const line = text.split('\n')[0]?.trim() ?? '';
|
|
313
|
+
return line.length > 300 ? `${line.slice(0, 297)}...` : line;
|
|
314
|
+
}
|
|
315
|
+
// ─── Updates ────────────────────────────────────────────────────────────────
|
|
316
|
+
/**
|
|
317
|
+
* Whether `latest` is newer than `current`, by numeric dotted version.
|
|
318
|
+
*
|
|
319
|
+
* Not `!==`: a development build ahead of npm would otherwise be told to
|
|
320
|
+
* "update" to an older release.
|
|
321
|
+
*/
|
|
322
|
+
export function isNewer(latest, current) {
|
|
323
|
+
const parse = (v) => v
|
|
324
|
+
.replace(/^v/, '')
|
|
325
|
+
.split('-')[0]
|
|
326
|
+
.split('.')
|
|
327
|
+
.map((part) => Number.parseInt(part, 10) || 0);
|
|
328
|
+
const a = parse(latest);
|
|
329
|
+
const b = parse(current);
|
|
330
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
331
|
+
const x = a[i] ?? 0;
|
|
332
|
+
const y = b[i] ?? 0;
|
|
333
|
+
if (x !== y)
|
|
334
|
+
return x > y;
|
|
335
|
+
}
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
/** One row per new OpenBridge version, however often the check runs. */
|
|
339
|
+
export function openBridgeUpdate(store, current, latest) {
|
|
340
|
+
if (!latest || !isNewer(latest, current))
|
|
341
|
+
return;
|
|
342
|
+
store.add({
|
|
343
|
+
key: `update.openbridge:${latest}`,
|
|
344
|
+
kind: 'update.openbridge',
|
|
345
|
+
title: `OpenBridge v${latest} is available`,
|
|
346
|
+
body: `You are on v${current}.`,
|
|
347
|
+
variant: 'info',
|
|
348
|
+
target: { kind: 'settings' },
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
/** One row per new plugin version, however often the check runs. */
|
|
352
|
+
export function pluginUpdate(store, plugin, latest) {
|
|
353
|
+
if (!isNewer(latest, plugin.current))
|
|
354
|
+
return;
|
|
355
|
+
store.add({
|
|
356
|
+
key: `update.plugin:${plugin.name}:${latest}`,
|
|
357
|
+
kind: 'update.plugin',
|
|
358
|
+
title: `${plugin.name} v${latest} is available`,
|
|
359
|
+
body: `You are on v${plugin.current}.`,
|
|
360
|
+
variant: 'info',
|
|
361
|
+
target: { kind: 'plugin', id: plugin.id },
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
//# sourceMappingURL=notifications.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications.js","sourceRoot":"","sources":["../src/notifications.ts"],"names":[],"mappings":"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"}
|
|
@@ -27,6 +27,8 @@ export interface TJwtClaims {
|
|
|
27
27
|
app_id?: string;
|
|
28
28
|
/** App slug the token was issued to: matches the app_id used in the SSO request. */
|
|
29
29
|
app?: string;
|
|
30
|
+
/** The same slug, as the standard audience claim. */
|
|
31
|
+
aud?: string | string[];
|
|
30
32
|
plan?: string;
|
|
31
33
|
plan_status?: string;
|
|
32
34
|
iat: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-client.d.ts","sourceRoot":"","sources":["../src/platform-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,sBAAsB;IACrC,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oFAAoF;IACpF,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;CACZ;AA6BD,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,SAAS,CAAsD;IACvE,OAAO,CAAC,YAAY,CAAoC;gBAE5C,OAAO,EAAE,sBAAsB;YAK7B,SAAS;YAaT,YAAY;YAgBZ,SAAS;IAQjB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;CAsBjD"}
|
|
1
|
+
{"version":3,"file":"platform-client.d.ts","sourceRoot":"","sources":["../src/platform-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,sBAAsB;IACrC,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oFAAoF;IACpF,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,qDAAqD;IACrD,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;CACZ;AA6BD,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,OAAO,CAAC,SAAS,CAAsD;IACvE,OAAO,CAAC,YAAY,CAAoC;gBAE5C,OAAO,EAAE,sBAAsB;YAK7B,SAAS;YAaT,YAAY;YAgBZ,SAAS;IAQjB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;CAsBjD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-client.js","sourceRoot":"","sources":["../src/platform-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"platform-client.js","sourceRoot":"","sources":["../src/platform-client.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Fastify from 'fastify';
|
|
2
2
|
import type { PluginRegistry } from '@nubisco/openbridge-core';
|
|
3
3
|
import type { HomebridgeAPI } from '@nubisco/openbridge-compatibility-homebridge';
|
|
4
|
+
import { type NotificationStore } from './notifications.js';
|
|
4
5
|
import { OPENBRIDGE_VERSION } from './version.js';
|
|
5
6
|
export { OPENBRIDGE_VERSION };
|
|
6
7
|
import { DeviceEventLog } from './device-events.js';
|
|
@@ -42,5 +43,7 @@ observations?: ObservationStore | null,
|
|
|
42
43
|
* radiators on a sunny afternoon and one the heating worked to reach are the
|
|
43
44
|
* same number and opposite evidence.
|
|
44
45
|
*/
|
|
45
|
-
observationContext?: ((deviceId: string) => Record<string, unknown> | undefined) | null
|
|
46
|
+
observationContext?: ((deviceId: string) => Record<string, unknown> | undefined) | null,
|
|
47
|
+
/** The bell. Absent in tests that do not exercise it, and then no routes are added. */
|
|
48
|
+
notifications?: NotificationStore | null): Promise<Fastify.FastifyInstance<import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, Fastify.FastifyBaseLogger, Fastify.FastifyTypeProviderDefault>>;
|
|
46
49
|
//# sourceMappingURL=server.d.ts.map
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAA;AAa7B,OAAO,KAAK,EAAE,cAAc,EAAU,MAAM,0BAA0B,CAAA;AAGtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8CAA8C,CAAA;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAA;AAa7B,OAAO,KAAK,EAAE,cAAc,EAAU,MAAM,0BAA0B,CAAA;AAGtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8CAA8C,CAAA;AAgBjF,OAAO,EAAkC,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAS3F,OAAO,EAA4B,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAC3E,OAAO,EAAE,kBAAkB,EAAE,CAAA;AAwC7B,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,EAAE,gBAAgB,EAAuB,MAAM,mBAAmB,CAAA;AACzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAChE,OAAO,EAGL,KAAK,mBAAmB,EAEzB,MAAM,2BAA2B,CAAA;AAElC,MAAM,WAAW,OAAO;IACtB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAsB,YAAY,CAChC,QAAQ,EAAE,cAAc,EACxB,MAAM,GAAE,aAAa,GAAG,IAAW,EACnC,OAAO,GAAE,OAAO,GAAG,IAAW,EAC9B,kBAAkB,GAAE,MAAM,EAAO,EACjC,mBAAmB,GAAE,GAAG,CAAC,MAAM,CAAa,EAC5C,QAAQ,GAAE,GAAG,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAa,EAC3E,kBAAkB,GAAE,GAAG,CAAC,MAAM,CAAa,EAC3C,iBAAiB,GAAE,iBAAiB,GAAG,IAAW,EAClD,mBAAmB,GAAE,mBAAmB,GAAG,IAAW;AACtD;;;;GAIG;AACH,sBAAsB,GAAE,CAAC,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC,GAAG,IAAW;AAC5E,4EAA4E;AAC5E,mBAAmB,GAAE,CAAC,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,IAAW;AACxE,6EAA6E;AAC7E,YAAY,GAAE,cAAc,GAAG,IAAW;AAC1C,+EAA+E;AAC/E,YAAY,GAAE,gBAAgB,GAAG,IAAW;AAC5C;;;;;GAKG;AACH,kBAAkB,GAAE,CAAC,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,GAAG,IAAW;AAC7F,uFAAuF;AACvF,aAAa,GAAE,iBAAiB,GAAG,IAAW,sSA4xD/C"}
|
package/dist/server.js
CHANGED
|
@@ -14,6 +14,8 @@ import { startMetrics, onMetrics, getHistory } from './metrics.js';
|
|
|
14
14
|
import { loadAuthConfig, registerAuthRoutes } from './auth.js';
|
|
15
15
|
import { controlDevice, deviceEvents as readDeviceEvents, deviceMetrics, deviceObservations, listAccessories, listDevices, readLogs, setCharacteristic, } from './services/bridge.js';
|
|
16
16
|
import { AccessStore } from './access.js';
|
|
17
|
+
import { registerNotificationRoutes } from './notification-routes.js';
|
|
18
|
+
import { openBridgeUpdate, pluginUpdate } from './notifications.js';
|
|
17
19
|
import { isMcpEnabled, registerMcpRoutes } from './mcp/index.js';
|
|
18
20
|
import { registerOauthRoutes } from './mcp/oauth.js';
|
|
19
21
|
const log = Logger.create('system');
|
|
@@ -53,7 +55,9 @@ observations = null,
|
|
|
53
55
|
* radiators on a sunny afternoon and one the heating worked to reach are the
|
|
54
56
|
* same number and opposite evidence.
|
|
55
57
|
*/
|
|
56
|
-
observationContext = null
|
|
58
|
+
observationContext = null,
|
|
59
|
+
/** The bell. Absent in tests that do not exercise it, and then no routes are added. */
|
|
60
|
+
notifications = null) {
|
|
57
61
|
const app = Fastify({ logger: false });
|
|
58
62
|
// What the REST routes below and the MCP tools both read from.
|
|
59
63
|
const bridge = {
|
|
@@ -83,6 +87,9 @@ observationContext = null) {
|
|
|
83
87
|
if (authConfig.enabled) {
|
|
84
88
|
log.info(`Platform auth enabled, issuer=${authConfig.issuer ?? 'unset'} authOrigin=${authConfig.authOrigin ?? 'unset'} appId=${authConfig.appId ?? 'unset'}`);
|
|
85
89
|
}
|
|
90
|
+
// ─── Notifications (the bell) ─────────────────────────────────────────────
|
|
91
|
+
if (notifications)
|
|
92
|
+
registerNotificationRoutes(app, notifications, authConfig);
|
|
86
93
|
// ─── MCP ──────────────────────────────────────────────────────────────────
|
|
87
94
|
// A second door onto the same functions, for AI agents. Off unless
|
|
88
95
|
// OPENBRIDGE_MCP=on. On, it is token only, always, whether or not dashboard
|
|
@@ -144,6 +151,8 @@ observationContext = null) {
|
|
|
144
151
|
// Network error, timeout, etc.: fall through to use lastKnownLatest
|
|
145
152
|
}
|
|
146
153
|
const latest = lastKnownLatest?.version ?? null;
|
|
154
|
+
if (notifications)
|
|
155
|
+
openBridgeUpdate(notifications, OPENBRIDGE_VERSION, latest);
|
|
147
156
|
const install = detectInstall();
|
|
148
157
|
return {
|
|
149
158
|
current: OPENBRIDGE_VERSION,
|
|
@@ -161,6 +170,21 @@ observationContext = null) {
|
|
|
161
170
|
releaseNotes: lastKnownLatest?.notes ?? null,
|
|
162
171
|
};
|
|
163
172
|
});
|
|
173
|
+
// The bell hears about a new OpenBridge version without anybody opening the
|
|
174
|
+
// dashboard, on the same six-hour rhythm as the plugin check below. The key
|
|
175
|
+
// is the version, so every check after the first finds nothing to say.
|
|
176
|
+
if (notifications) {
|
|
177
|
+
const checkOwnUpdate = async () => {
|
|
178
|
+
try {
|
|
179
|
+
openBridgeUpdate(notifications, OPENBRIDGE_VERSION, await fetchLatestVersion());
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
/* offline: the next check will try again */
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
setTimeout(() => void checkOwnUpdate(), 90_000).unref();
|
|
186
|
+
setInterval(() => void checkOwnUpdate(), 6 * 60 * 60_000).unref();
|
|
187
|
+
}
|
|
164
188
|
// ─── Update progress WebSocket ────────────────────────────────────────────
|
|
165
189
|
app.get('/ws/updates', { websocket: true }, (ws) => {
|
|
166
190
|
const listener = (msg) => {
|
|
@@ -1185,6 +1209,8 @@ observationContext = null) {
|
|
|
1185
1209
|
if (latest !== currentVersion) {
|
|
1186
1210
|
plugin.availableUpdate = latest;
|
|
1187
1211
|
updatesFound++;
|
|
1212
|
+
if (notifications)
|
|
1213
|
+
pluginUpdate(notifications, { id: plugin.id, name, current: currentVersion }, latest);
|
|
1188
1214
|
}
|
|
1189
1215
|
else {
|
|
1190
1216
|
plugin.availableUpdate = undefined;
|