@nubisco/openbridge 1.5.2 → 1.7.0

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.
@@ -0,0 +1,37 @@
1
+ import { readSession } from './auth.js';
2
+ import { LOCAL_USER } from './notifications.js';
3
+ export function notificationUser(authConfig, req) {
4
+ if (!authConfig)
5
+ return LOCAL_USER;
6
+ return readSession(authConfig, req)?.userId ?? LOCAL_USER;
7
+ }
8
+ export function registerNotificationRoutes(app, store, authConfig) {
9
+ app.get('/api/notifications', async (req) => {
10
+ const limit = Number(req.query.limit ?? 50);
11
+ return store.list(notificationUser(authConfig, req), Number.isFinite(limit) ? limit : 50);
12
+ });
13
+ app.post('/api/notifications/read', async (req) => {
14
+ const raw = req.body?.ids;
15
+ const ids = Array.isArray(raw) ? raw.filter((id) => typeof id === 'string') : undefined;
16
+ return { unread: store.markRead(notificationUser(authConfig, req), ids) };
17
+ });
18
+ app.get('/ws/notifications', { websocket: true }, (ws, req) => {
19
+ const userId = notificationUser(authConfig, req);
20
+ const unsubscribe = store.subscribe((event) => {
21
+ // A row added reaches everybody. Somebody reading reaches only their
22
+ // own other tabs, whose badge just went stale.
23
+ if (event.type === 'read' && event.userId !== userId)
24
+ return;
25
+ if (ws.readyState !== ws.OPEN)
26
+ return;
27
+ try {
28
+ ws.send(JSON.stringify({ type: 'changed' }));
29
+ }
30
+ catch {
31
+ /* disconnected */
32
+ }
33
+ });
34
+ ws.on('close', unsubscribe);
35
+ });
36
+ }
37
+ //# sourceMappingURL=notification-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-routes.js","sourceRoot":"","sources":["../src/notification-routes.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,WAAW,EAAoB,MAAM,WAAW,CAAA;AACzD,OAAO,EAAE,UAAU,EAA0B,MAAM,oBAAoB,CAAA;AAEvE,MAAM,UAAU,gBAAgB,CAAC,UAA8B,EAAE,GAAmB;IAClF,IAAI,CAAC,UAAU;QAAE,OAAO,UAAU,CAAA;IAClC,OAAO,WAAW,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,MAAM,IAAI,UAAU,CAAA;AAC3D,CAAC;AAED,MAAM,UAAU,0BAA0B,CACxC,GAAoB,EACpB,KAAwB,EACxB,UAA8B;IAE9B,GAAG,CAAC,GAAG,CAAsC,oBAAoB,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QAC/E,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,CAAA;QAC3C,OAAO,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAC3F,CAAC,CAAC,CAAA;IAEF,GAAG,CAAC,IAAI,CAA0C,yBAAyB,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QACzF,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,EAAE,GAAG,CAAA;QACzB,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QACrG,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,gBAAgB,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,CAAA;IAC3E,CAAC,CAAC,CAAA;IAEF,GAAG,CAAC,GAAG,CAAC,mBAAmB,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,EAAa,EAAE,GAAG,EAAE,EAAE;QACvE,MAAM,MAAM,GAAG,gBAAgB,CAAC,UAAU,EAAE,GAAG,CAAC,CAAA;QAChD,MAAM,WAAW,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;YAC5C,qEAAqE;YACrE,+CAA+C;YAC/C,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM;gBAAE,OAAM;YAC5D,IAAI,EAAE,CAAC,UAAU,KAAK,EAAE,CAAC,IAAI;gBAAE,OAAM;YACrC,IAAI,CAAC;gBACH,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,CAAA;YAC9C,CAAC;YAAC,MAAM,CAAC;gBACP,kBAAkB;YACpB,CAAC;QACH,CAAC,CAAC,CAAA;QACF,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC,CAAA;IAC7B,CAAC,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,157 @@
1
+ import type { DeviceHealth, PluginStatus } from '@nubisco/openbridge-core';
2
+ /** Newest rows kept. Older ones fall off the end. */
3
+ export declare const MAX_NOTIFICATIONS = 200;
4
+ /**
5
+ * How long a device must be silent, continuously, before the bell rings.
6
+ *
7
+ * Ten minutes, set by Jose. Long enough that a router reboot or a slow poll
8
+ * never rings, short enough that a gate that has fallen off the network is
9
+ * noticed the same evening rather than from HomeKit a day later.
10
+ */
11
+ export declare const OFFLINE_AFTER_SECONDS: number;
12
+ /** The person whose read state is kept when sign-in is off. */
13
+ export declare const LOCAL_USER = "local";
14
+ export type NotificationKind = 'device.offline' | 'device.online' | 'plugin.failed' | 'update.openbridge' | 'update.plugin';
15
+ /** Colour by consequence: warning needs doing, success is relief, info is news. */
16
+ export type NotificationVariant = 'warning' | 'success' | 'info';
17
+ /** Where a row opens. Null makes the row honestly inert. */
18
+ export type NotificationTarget = {
19
+ kind: 'device';
20
+ id: string;
21
+ } | {
22
+ kind: 'plugin';
23
+ id: string;
24
+ } | {
25
+ kind: 'settings';
26
+ } | {
27
+ kind: 'config';
28
+ };
29
+ export interface StoredNotification {
30
+ id: string;
31
+ kind: NotificationKind;
32
+ title: string;
33
+ body: string;
34
+ variant: NotificationVariant;
35
+ target: NotificationTarget | null;
36
+ createdAt: string;
37
+ }
38
+ export interface NotificationView extends StoredNotification {
39
+ read: boolean;
40
+ }
41
+ export interface NotificationInput {
42
+ /** The thing and the state that justified it. Announced at most once. */
43
+ key: string;
44
+ kind: NotificationKind;
45
+ title: string;
46
+ body: string;
47
+ variant: NotificationVariant;
48
+ target: NotificationTarget | null;
49
+ }
50
+ interface Outage {
51
+ /** When the device last answered, which is when the outage began. */
52
+ since: string;
53
+ }
54
+ export declare class NotificationStore {
55
+ private readonly path;
56
+ private readonly clock;
57
+ private state;
58
+ private listeners;
59
+ constructor(path: string, clock?: () => number);
60
+ /**
61
+ * Add a row, unless its key has been announced before.
62
+ *
63
+ * Returns the row, or null when nothing was written. Never throws: the bell
64
+ * is a report about the bridge, and failing to write one must not take down
65
+ * the thing it reports on.
66
+ */
67
+ add(input: NotificationInput): StoredNotification | null;
68
+ /** The newest rows as one person sees them, and their unread count over every row. */
69
+ list(userId: string, limit?: number): {
70
+ notifications: NotificationView[];
71
+ unread: number;
72
+ };
73
+ /**
74
+ * The badge. Counted over every stored row, never over the page the client
75
+ * happened to load, which is how Acta's badge came to understate.
76
+ */
77
+ unreadCount(userId: string): number;
78
+ /** Mark some rows read for one person, or all of them when no ids are given. */
79
+ markRead(userId: string, ids?: string[]): number;
80
+ /** Told about every write, for live delivery to open tabs. */
81
+ subscribe(listener: (event: {
82
+ type: 'added' | 'read';
83
+ userId?: string;
84
+ }) => void): () => void;
85
+ outage(deviceId: string): Outage | undefined;
86
+ setOutage(deviceId: string, outage: Outage | null): void;
87
+ failing(key: string): string | undefined;
88
+ setFailing(key: string, error: string | null): void;
89
+ private pruneReadState;
90
+ private emit;
91
+ private load;
92
+ private save;
93
+ }
94
+ export interface ObservedDevice {
95
+ id: string;
96
+ name: string;
97
+ pluginStatus: string;
98
+ health: DeviceHealth;
99
+ }
100
+ /**
101
+ * Turn device health, read on every tick, into one row when a device crosses
102
+ * the line and one when it comes back.
103
+ *
104
+ * Health is derived from the `_updatedAt` stamp `reportTelemetry` writes for
105
+ * every device of every native plugin (see device-health.ts), so this needs no
106
+ * cooperation from plugins. A device of a plugin that is not running is
107
+ * skipped: the plugin's own failure explains all of its devices at once, and
108
+ * thirty rows saying "not responding" would bury the one that says why.
109
+ */
110
+ export declare function observeDevices(store: NotificationStore, devices: ObservedDevice[], now: number): void;
111
+ export interface ObservedPlugin {
112
+ id: string;
113
+ name: string;
114
+ status: PluginStatus;
115
+ error?: string;
116
+ disabled?: boolean;
117
+ }
118
+ /**
119
+ * One row when a plugin enters the error state, and none while it stays
120
+ * there or is retried and fails the same way again.
121
+ *
122
+ * Cleared once the plugin runs, so a crash after a recovery is news again.
123
+ * The failing set is stored, so a plugin that fails the same way at every
124
+ * start does not ring again on every restart of the daemon.
125
+ */
126
+ export declare function observePlugins(store: NotificationStore, plugins: ObservedPlugin[], now: number): void;
127
+ /**
128
+ * A plugin that could not be loaded at all.
129
+ *
130
+ * Distinct from the error state above: a module that throws on import never
131
+ * reaches the registry as a running plugin, so there is no status to watch.
132
+ * The daemon calls this from the places it catches the failure.
133
+ */
134
+ export declare function pluginLoadFailed(store: NotificationStore, plugin: {
135
+ key: string;
136
+ name: string;
137
+ target: NotificationTarget | null;
138
+ }, err: unknown, now: number): void;
139
+ /** The counterpart: a plugin that loaded is no longer failing to. */
140
+ export declare function pluginLoaded(store: NotificationStore, pluginKey: string): void;
141
+ /**
142
+ * Whether `latest` is newer than `current`, by numeric dotted version.
143
+ *
144
+ * Not `!==`: a development build ahead of npm would otherwise be told to
145
+ * "update" to an older release.
146
+ */
147
+ export declare function isNewer(latest: string, current: string): boolean;
148
+ /** One row per new OpenBridge version, however often the check runs. */
149
+ export declare function openBridgeUpdate(store: NotificationStore, current: string, latest: string | null): void;
150
+ /** One row per new plugin version, however often the check runs. */
151
+ export declare function pluginUpdate(store: NotificationStore, plugin: {
152
+ id: string;
153
+ name: string;
154
+ current: string;
155
+ }, latest: string): void;
156
+ export {};
157
+ //# sourceMappingURL=notifications.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../src/notifications.ts"],"names":[],"mappings":"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"}
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,IAAI,CAAA;AACnF,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAC9B,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAGnC,qDAAqD;AACrD,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAA;AAEpC,qEAAqE;AACrE,MAAM,aAAa,GAAG,IAAI,CAAA;AAE1B;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,GAAG,EAAE,CAAA;AAE5C,+DAA+D;AAC/D,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAAA;AA4DjC,SAAS,KAAK;IACZ,OAAO,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAA;AACnF,CAAC;AAED,MAAM,OAAO,iBAAiB;IAKT;IACA;IALX,KAAK,CAAW;IAChB,SAAS,GAAG,IAAI,GAAG,EAAgE,CAAA;IAE3F,YACmB,IAAY,EACZ,QAAsB,IAAI,CAAC,GAAG;QAD9B,SAAI,GAAJ,IAAI,CAAQ;QACZ,UAAK,GAAL,KAAK,CAAyB;QAE/C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAA;IAC1B,CAAC;IAED;;;;;;OAMG;IACH,GAAG,CAAC,KAAwB;QAC1B,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAA;QACzD,MAAM,GAAG,GAAuB;YAC9B,EAAE,EAAE,UAAU,EAAE;YAChB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,SAAS,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,WAAW,EAAE;SAChD,CAAA;QACD,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QACrC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAA;QAC9F,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACpC,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,aAAa,EAAE,CAAC;YAChD,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,aAAa,CAAC,CAAA;QAC7E,CAAC;QACD,IAAI,CAAC,cAAc,EAAE,CAAA;QACrB,IAAI,CAAC,IAAI,EAAE,CAAA;QACX,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;QAC5B,OAAO,GAAG,CAAA;IACZ,CAAC;IAED,sFAAsF;IACtF,IAAI,CAAC,MAAc,EAAE,KAAK,GAAG,EAAE;QAC7B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAA;QACrD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,iBAAiB,CAAC,CAAA;QAChF,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;QAC5G,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAA;IAC5D,CAAC;IAED;;;OAGG;IACH,WAAW,CAAC,MAAc;QACxB,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAA;QACrD,OAAO,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAA;IACvE,CAAC;IAED,gFAAgF;IAChF,QAAQ,CAAC,MAAc,EAAE,GAAc;QACrC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QAChE,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAA;QACrD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAA;QACxB,KAAK,MAAM,EAAE,IAAI,GAAG,IAAI,KAAK;YAAE,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAC9D,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACzB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAA;YACrC,IAAI,CAAC,IAAI,EAAE,CAAA;YACX,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;QACrC,CAAC;QACD,OAAO,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAA;IACjC,CAAC;IAED,8DAA8D;IAC9D,SAAS,CAAC,QAAsE;QAC9E,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QAC5B,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC9C,CAAC;IAED,4EAA4E;IAE5E,MAAM,CAAC,QAAgB;QACrB,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;IACrC,CAAC;IAED,SAAS,CAAC,QAAgB,EAAE,MAAqB;QAC/C,IAAI,MAAM;YAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAA;;YAC5C,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;QACxC,IAAI,CAAC,IAAI,EAAE,CAAA;IACb,CAAC;IAED,OAAO,CAAC,GAAW;QACjB,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAChC,CAAC;IAED,UAAU,CAAC,GAAW,EAAE,KAAoB;QAC1C,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,IAAI,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;gBAAE,OAAM;YACxC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAChC,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;QACjC,CAAC;QACD,IAAI,CAAC,IAAI,EAAE,CAAA;IACb,CAAC;IAEO,cAAc;QACpB,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QAChE,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;YAC5D,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;QAC7D,CAAC;IACH,CAAC;IAEO,IAAI,CAAC,KAAkD;QAC7D,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACtC,IAAI,CAAC;gBACH,QAAQ,CAAC,KAAK,CAAC,CAAA;YACjB,CAAC;YAAC,MAAM,CAAC;gBACP,qDAAqD;YACvD,CAAC;QACH,CAAC;IACH,CAAC;IAEO,IAAI;QACV,IAAI,CAAC;YACH,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,OAAO,KAAK,EAAE,CAAA;YAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAuB,CAAA;YAC7E,OAAO;gBACL,aAAa,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE;gBACxE,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;gBACtE,SAAS,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;gBAC5D,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE;gBAC1E,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE;aAC3E,CAAA;QACH,CAAC;QAAC,MAAM,CAAC;YACP,oDAAoD;YACpD,OAAO,KAAK,EAAE,CAAA;QAChB,CAAC;IACH,CAAC;IAEO,IAAI;QACV,IAAI,CAAC;YACH,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;YAClD,qEAAqE;YACrE,sCAAsC;YACtC,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,IAAI,MAAM,CAAA;YAC9B,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;YAC9C,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,CAAA;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,sDAAsD;QACxD,CAAC;IACH,CAAC;CACF;AAWD,SAAS,aAAa,CAAC,OAAe;IACpC,IAAI,OAAO,GAAG,EAAE;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAA;IACzD,MAAM,OAAO,GAAG,OAAO,GAAG,EAAE,CAAA;IAC5B,IAAI,OAAO,GAAG,EAAE;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAA;IACzD,MAAM,KAAK,GAAG,OAAO,GAAG,EAAE,CAAA;IAC1B,IAAI,KAAK,GAAG,EAAE;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAA;IACnD,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC,OAAO,CAAA;AACzC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAAC,KAAwB,EAAE,OAAyB,EAAE,GAAW;IAC7F,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,MAAM,CAAC,YAAY,KAAK,SAAS;YAAE,SAAQ;QAC/C,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAAA;QACzB,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QAEpC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,uEAAuE;YACvE,wEAAwE;YACxE,oDAAoD;YACpD,MAAM,MAAM,GAAG,MAAM,CAAC,gBAAgB,IAAI,CAAC,CAAA;YAC3C,IAAI,MAAM,CAAC,MAAM,KAAK,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,MAAM,GAAG,qBAAqB;gBAAE,SAAQ;YAC7F,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAA;YACtD,KAAK,CAAC,GAAG,CAAC;gBACR,GAAG,EAAE,kBAAkB,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC,QAAQ,EAAE;gBACrD,IAAI,EAAE,gBAAgB;gBACtB,KAAK,EAAE,GAAG,MAAM,CAAC,IAAI,oBAAoB;gBACzC,IAAI,EAAE,mBAAmB,aAAa,CAAC,MAAM,CAAC,2DAA2D;gBACzG,OAAO,EAAE,SAAS;gBAClB,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE;aAC1C,CAAC,CAAA;YACF,SAAQ;QACV,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;YAC3B,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;YAChC,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,CAAA;YAChE,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;YACzE,KAAK,CAAC,GAAG,CAAC;gBACR,GAAG,EAAE,iBAAiB,MAAM,CAAC,EAAE,IAAI,IAAI,CAAC,KAAK,EAAE;gBAC/C,IAAI,EAAE,eAAe;gBACrB,KAAK,EAAE,GAAG,MAAM,CAAC,IAAI,iBAAiB;gBACtC,IAAI,EAAE,2BAA2B,aAAa,CAAC,aAAa,CAAC,sBAAsB;gBACnF,OAAO,EAAE,SAAS;gBAClB,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE;aAC1C,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;AACH,CAAC;AAYD;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,KAAwB,EAAE,OAAyB,EAAE,GAAW;IAC7F,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,UAAU,MAAM,CAAC,EAAE,EAAE,CAAA;QACjC,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YAChC,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;YAC3B,SAAQ;QACV,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,KAAK,OAAO,IAAI,MAAM,CAAC,QAAQ;YAAE,SAAQ;QAC1D,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,2BAA2B,CAAA;QACjE,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,SAAS;YAAE,SAAQ;QAC9C,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QAC5B,KAAK,CAAC,GAAG,CAAC;YACR,GAAG,EAAE,iBAAiB,MAAM,CAAC,EAAE,IAAI,GAAG,EAAE;YACxC,IAAI,EAAE,eAAe;YACrB,KAAK,EAAE,GAAG,MAAM,CAAC,IAAI,kBAAkB;YACvC,IAAI,EAAE,SAAS,CAAC,KAAK,CAAC;YACtB,OAAO,EAAE,SAAS;YAClB,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE;SAC1C,CAAC,CAAA;IACJ,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAAwB,EACxB,MAAwE,EACxE,GAAY,EACZ,GAAW;IAEX,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;IACzE,MAAM,GAAG,GAAG,QAAQ,MAAM,CAAC,GAAG,EAAE,CAAA;IAChC,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK;QAAE,OAAM;IACxC,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;IAC5B,KAAK,CAAC,GAAG,CAAC;QACR,GAAG,EAAE,iBAAiB,GAAG,IAAI,GAAG,EAAE;QAClC,IAAI,EAAE,eAAe;QACrB,KAAK,EAAE,GAAG,MAAM,CAAC,IAAI,sBAAsB;QAC3C,IAAI,EAAE,KAAK;QACX,OAAO,EAAE,SAAS;QAClB,MAAM,EAAE,MAAM,CAAC,MAAM;KACtB,CAAC,CAAA;AACJ,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,YAAY,CAAC,KAAwB,EAAE,SAAiB;IACtE,KAAK,CAAC,UAAU,CAAC,QAAQ,SAAS,EAAE,EAAE,IAAI,CAAC,CAAA;AAC7C,CAAC;AAED,SAAS,SAAS,CAAC,IAAY;IAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;IAC9C,OAAO,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;AAC9D,CAAC;AAED,+EAA+E;AAE/E;;;;;GAKG;AACH,MAAM,UAAU,OAAO,CAAC,MAAc,EAAE,OAAe;IACrD,MAAM,KAAK,GAAG,CAAC,CAAS,EAAE,EAAE,CAC1B,CAAC;SACE,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;SACjB,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE;SACd,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAA;IAClD,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAA;IACvB,MAAM,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,CAAA;IACxB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACtD,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;QACnB,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;QACnB,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,CAAA;IAC3B,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,gBAAgB,CAAC,KAAwB,EAAE,OAAe,EAAE,MAAqB;IAC/F,IAAI,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC;QAAE,OAAM;IAChD,KAAK,CAAC,GAAG,CAAC;QACR,GAAG,EAAE,qBAAqB,MAAM,EAAE;QAClC,IAAI,EAAE,mBAAmB;QACzB,KAAK,EAAE,eAAe,MAAM,eAAe;QAC3C,IAAI,EAAE,eAAe,OAAO,GAAG;QAC/B,OAAO,EAAE,MAAM;QACf,MAAM,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE;KAC7B,CAAC,CAAA;AACJ,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,YAAY,CAC1B,KAAwB,EACxB,MAAqD,EACrD,MAAc;IAEd,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC;QAAE,OAAM;IAC5C,KAAK,CAAC,GAAG,CAAC;QACR,GAAG,EAAE,iBAAiB,MAAM,CAAC,IAAI,IAAI,MAAM,EAAE;QAC7C,IAAI,EAAE,eAAe;QACrB,KAAK,EAAE,GAAG,MAAM,CAAC,IAAI,KAAK,MAAM,eAAe;QAC/C,IAAI,EAAE,eAAe,MAAM,CAAC,OAAO,GAAG;QACtC,OAAO,EAAE,MAAM;QACf,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE;KAC1C,CAAC,CAAA;AACJ,CAAC"}
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): 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
+ 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
@@ -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;AAuBjF,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,sSAwwD9F"}
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"}