@camstack/addon-advanced-notifier 0.1.13 → 0.1.15
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/addon.js +146 -118
- package/dist/addon.js.map +1 -1
- package/dist/addon.mjs +410 -6
- package/dist/addon.mjs.map +1 -1
- package/dist/index.js +19 -412
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +14 -28
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -9
- package/dist/addon.d.mts +0 -23
- package/dist/addon.d.ts +0 -23
- package/dist/chunk-HYUFV4O5.mjs +0 -365
- package/dist/chunk-HYUFV4O5.mjs.map +0 -1
- package/dist/index.d.mts +0 -97
- package/dist/index.d.ts +0 -97
package/dist/addon.js
CHANGED
|
@@ -1,45 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/addon.ts
|
|
21
|
-
var addon_exports = {};
|
|
22
|
-
__export(addon_exports, {
|
|
23
|
-
AdvancedNotifierAddon: () => AdvancedNotifierAddon,
|
|
24
|
-
default: () => addon_default
|
|
25
|
-
});
|
|
26
|
-
module.exports = __toCommonJS(addon_exports);
|
|
27
|
-
|
|
28
|
-
// src/rules/rule-store.ts
|
|
29
|
-
var COLLECTION = "addon-settings";
|
|
30
|
-
var KEY = "advanced-notifier:rules";
|
|
31
|
-
var RuleStore = class {
|
|
32
|
-
backend;
|
|
33
|
-
constructor(backend) {
|
|
34
|
-
this.backend = backend;
|
|
2
|
+
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
|
|
3
|
+
const types = require("@camstack/types");
|
|
4
|
+
const node_crypto = require("node:crypto");
|
|
5
|
+
const COLLECTION$1 = "addon-settings";
|
|
6
|
+
const KEY = "advanced-notifier:rules";
|
|
7
|
+
class RuleStore {
|
|
8
|
+
api;
|
|
9
|
+
constructor(api) {
|
|
10
|
+
this.api = api;
|
|
35
11
|
}
|
|
36
12
|
async getRules() {
|
|
37
|
-
const raw = await this.
|
|
13
|
+
const raw = await this.api.settingsStore.get.query({ collection: COLLECTION$1, key: KEY });
|
|
38
14
|
if (!raw || !Array.isArray(raw)) return [];
|
|
39
15
|
return raw;
|
|
40
16
|
}
|
|
41
17
|
async saveRules(rules) {
|
|
42
|
-
await this.
|
|
18
|
+
await this.api.settingsStore.set.mutate({ collection: COLLECTION$1, key: KEY, value: [...rules] });
|
|
43
19
|
}
|
|
44
20
|
async upsertRule(rule) {
|
|
45
21
|
const rules = await this.getRules();
|
|
@@ -55,10 +31,8 @@ var RuleStore = class {
|
|
|
55
31
|
const rules = await this.getRules();
|
|
56
32
|
await this.saveRules(rules.filter((r) => r.id !== ruleId));
|
|
57
33
|
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// src/rules/rule-engine.ts
|
|
61
|
-
var RuleEngine = class {
|
|
34
|
+
}
|
|
35
|
+
class RuleEngine {
|
|
62
36
|
evaluate(event, rules) {
|
|
63
37
|
return rules.filter((rule) => this.matchesRule(event, rule));
|
|
64
38
|
}
|
|
@@ -71,8 +45,9 @@ var RuleEngine = class {
|
|
|
71
45
|
const { conditions } = rule;
|
|
72
46
|
const data = event.data;
|
|
73
47
|
if (conditions.deviceIds?.length) {
|
|
74
|
-
const
|
|
75
|
-
|
|
48
|
+
const raw = data["deviceId"] ?? event.source.id;
|
|
49
|
+
const deviceId = typeof raw === "number" ? raw : Number(raw);
|
|
50
|
+
if (!Number.isFinite(deviceId) || !conditions.deviceIds.includes(deviceId)) return false;
|
|
76
51
|
}
|
|
77
52
|
if (conditions.classNames?.length) {
|
|
78
53
|
const className = data["className"];
|
|
@@ -95,10 +70,8 @@ var RuleEngine = class {
|
|
|
95
70
|
}
|
|
96
71
|
return true;
|
|
97
72
|
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// src/rules/cooldown.ts
|
|
101
|
-
var CooldownTracker = class {
|
|
73
|
+
}
|
|
74
|
+
class CooldownTracker {
|
|
102
75
|
lastFired = /* @__PURE__ */ new Map();
|
|
103
76
|
canFire(ruleId, cooldownSeconds) {
|
|
104
77
|
const last = this.lastFired.get(ruleId);
|
|
@@ -111,18 +84,14 @@ var CooldownTracker = class {
|
|
|
111
84
|
reset() {
|
|
112
85
|
this.lastFired.clear();
|
|
113
86
|
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// src/template/renderer.ts
|
|
87
|
+
}
|
|
117
88
|
function renderTemplate(template, variables) {
|
|
118
89
|
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
119
90
|
const value = variables[key];
|
|
120
91
|
return value !== void 0 ? value : match;
|
|
121
92
|
});
|
|
122
93
|
}
|
|
123
|
-
|
|
124
|
-
// src/outputs/console.ts
|
|
125
|
-
var ConsoleOutput = class {
|
|
94
|
+
class ConsoleOutput {
|
|
126
95
|
id = "console";
|
|
127
96
|
name = "Console Log";
|
|
128
97
|
icon = "terminal";
|
|
@@ -132,7 +101,8 @@ var ConsoleOutput = class {
|
|
|
132
101
|
}
|
|
133
102
|
async send(notification) {
|
|
134
103
|
this.logger.info(
|
|
135
|
-
`[${notification.severity}] ${notification.title}: ${notification.message}
|
|
104
|
+
`[${notification.severity}] ${notification.title}: ${notification.message}`,
|
|
105
|
+
notification.deviceId !== void 0 ? { tags: { deviceId: notification.deviceId } } : void 0
|
|
136
106
|
);
|
|
137
107
|
}
|
|
138
108
|
async sendTest() {
|
|
@@ -145,23 +115,21 @@ var ConsoleOutput = class {
|
|
|
145
115
|
});
|
|
146
116
|
return { success: true };
|
|
147
117
|
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
var AuditLog = class {
|
|
154
|
-
backend;
|
|
118
|
+
}
|
|
119
|
+
const COLLECTION = "addon-settings";
|
|
120
|
+
const LOG_KEY = "notifier-history:log";
|
|
121
|
+
class AuditLog {
|
|
122
|
+
api;
|
|
155
123
|
maxEntries;
|
|
156
|
-
constructor(
|
|
157
|
-
this.
|
|
124
|
+
constructor(api, maxEntries = 1e3) {
|
|
125
|
+
this.api = api;
|
|
158
126
|
this.maxEntries = maxEntries;
|
|
159
127
|
}
|
|
160
128
|
async record(entry) {
|
|
161
129
|
const entries = await this.getAll();
|
|
162
130
|
const updated = [...entries, entry];
|
|
163
131
|
const trimmed = updated.length > this.maxEntries ? updated.slice(updated.length - this.maxEntries) : updated;
|
|
164
|
-
await this.
|
|
132
|
+
await this.api.settingsStore.set.mutate({ collection: COLLECTION, key: LOG_KEY, value: trimmed });
|
|
165
133
|
}
|
|
166
134
|
async getHistory(filter) {
|
|
167
135
|
let entries = await this.getAll();
|
|
@@ -170,7 +138,7 @@ var AuditLog = class {
|
|
|
170
138
|
}
|
|
171
139
|
if (filter?.deviceId) {
|
|
172
140
|
const deviceId = filter.deviceId;
|
|
173
|
-
entries = entries.filter((e) => e
|
|
141
|
+
entries = entries.filter((e) => e.deviceId === deviceId);
|
|
174
142
|
}
|
|
175
143
|
if (filter?.from !== void 0) {
|
|
176
144
|
const from = filter.from;
|
|
@@ -186,26 +154,26 @@ var AuditLog = class {
|
|
|
186
154
|
return entries;
|
|
187
155
|
}
|
|
188
156
|
async getAll() {
|
|
189
|
-
const raw = await this.
|
|
157
|
+
const raw = await this.api.settingsStore.get.query({ collection: COLLECTION, key: LOG_KEY });
|
|
190
158
|
if (!raw || !Array.isArray(raw)) return [];
|
|
191
159
|
return raw;
|
|
192
160
|
}
|
|
161
|
+
}
|
|
162
|
+
const EVENT_CATEGORIES = [
|
|
163
|
+
types.EventCategory.DetectionRaw,
|
|
164
|
+
types.EventCategory.DetectionResult,
|
|
165
|
+
types.EventCategory.DetectionCameraNative
|
|
166
|
+
];
|
|
167
|
+
const DEFAULT_NOTIFIER_CONFIG = {
|
|
168
|
+
defaultCooldownSeconds: 60,
|
|
169
|
+
maxHistoryEntries: 1e4,
|
|
170
|
+
auditLogRetentionDays: 30,
|
|
171
|
+
consoleOutputEnabled: true
|
|
193
172
|
};
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
var AdvancedNotifierAddon = class {
|
|
199
|
-
manifest = {
|
|
200
|
-
id: "advanced-notifier",
|
|
201
|
-
name: "Advanced Notifier",
|
|
202
|
-
version: "0.1.0",
|
|
203
|
-
capabilities: [
|
|
204
|
-
{ name: "advanced-notifier", mode: "singleton" },
|
|
205
|
-
{ name: "notification-output", mode: "collection" }
|
|
206
|
-
]
|
|
207
|
-
};
|
|
208
|
-
context;
|
|
173
|
+
class AdvancedNotifierAddon extends types.BaseAddon {
|
|
174
|
+
constructor() {
|
|
175
|
+
super({ ...DEFAULT_NOTIFIER_CONFIG });
|
|
176
|
+
}
|
|
209
177
|
// Rule management
|
|
210
178
|
ruleStore;
|
|
211
179
|
ruleEngine;
|
|
@@ -234,21 +202,21 @@ var AdvancedNotifierAddon = class {
|
|
|
234
202
|
this.cachedRules = [...this.cachedRules, rule];
|
|
235
203
|
}
|
|
236
204
|
}).catch((err) => {
|
|
237
|
-
this.
|
|
205
|
+
this.ctx.logger.error("Failed to upsert rule", { meta: { error: String(err) } });
|
|
238
206
|
});
|
|
239
207
|
},
|
|
240
208
|
deleteRule: (ruleId) => {
|
|
241
209
|
this.ruleStore.deleteRule(ruleId).then(() => {
|
|
242
210
|
this.cachedRules = this.cachedRules.filter((r) => r.id !== ruleId);
|
|
243
211
|
}).catch((err) => {
|
|
244
|
-
this.
|
|
212
|
+
this.ctx.logger.error("Failed to delete rule", { tags: { ruleId }, meta: { error: String(err) } });
|
|
245
213
|
});
|
|
246
214
|
},
|
|
247
215
|
testRule: async (ruleId, lookbackMinutes) => {
|
|
248
216
|
const rule = this.cachedRules.find((r) => r.id === ruleId);
|
|
249
217
|
if (!rule) return [];
|
|
250
218
|
const since = new Date(Date.now() - lookbackMinutes * 60 * 1e3);
|
|
251
|
-
const recentEvents = this.
|
|
219
|
+
const recentEvents = this.ctx.eventBus.getRecent(
|
|
252
220
|
{ category: void 0 },
|
|
253
221
|
1e3
|
|
254
222
|
).filter((e) => e.timestamp >= since);
|
|
@@ -268,22 +236,22 @@ var AdvancedNotifierAddon = class {
|
|
|
268
236
|
return this.auditLog.getHistory(filter);
|
|
269
237
|
}
|
|
270
238
|
};
|
|
271
|
-
async
|
|
272
|
-
this.context = context;
|
|
239
|
+
async onInitialize() {
|
|
273
240
|
this.ruleEngine = new RuleEngine();
|
|
274
241
|
this.cooldown = new CooldownTracker();
|
|
275
|
-
this.consoleOutput = new ConsoleOutput(
|
|
242
|
+
this.consoleOutput = new ConsoleOutput(this.ctx.logger.child("console-output"));
|
|
276
243
|
this.outputs = [this.consoleOutput];
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
this.
|
|
244
|
+
const api = this.ctx.api;
|
|
245
|
+
if (api) {
|
|
246
|
+
this.ruleStore = new RuleStore(api);
|
|
247
|
+
this.auditLog = new AuditLog(api);
|
|
280
248
|
this.cachedRules = await this.ruleStore.getRules();
|
|
281
|
-
this.
|
|
249
|
+
this.ctx.logger.info("Loaded notification rules", { meta: { count: this.cachedRules.length } });
|
|
282
250
|
} else {
|
|
283
|
-
this.
|
|
251
|
+
this.ctx.logger.warn("No ctx.api — rules will not be persisted");
|
|
284
252
|
}
|
|
285
253
|
for (const category of EVENT_CATEGORIES) {
|
|
286
|
-
const unsubscribe =
|
|
254
|
+
const unsubscribe = this.ctx.eventBus.subscribe(
|
|
287
255
|
{ category },
|
|
288
256
|
(event) => {
|
|
289
257
|
void this.handleEvent(event);
|
|
@@ -291,42 +259,98 @@ var AdvancedNotifierAddon = class {
|
|
|
291
259
|
);
|
|
292
260
|
this.unsubscribers.push(unsubscribe);
|
|
293
261
|
}
|
|
294
|
-
this.
|
|
262
|
+
this.ctx.logger.info("AdvancedNotifierAddon initialized");
|
|
263
|
+
return [
|
|
264
|
+
{ capability: types.advancedNotifierCapability, provider: this.notifier },
|
|
265
|
+
{ capability: types.notificationOutputCapability, provider: this.consoleOutput }
|
|
266
|
+
];
|
|
295
267
|
}
|
|
296
|
-
async
|
|
268
|
+
async onShutdown() {
|
|
297
269
|
for (const unsub of this.unsubscribers) {
|
|
298
270
|
unsub();
|
|
299
271
|
}
|
|
300
272
|
this.unsubscribers = [];
|
|
301
|
-
this.
|
|
273
|
+
this.ctx.logger.info("AdvancedNotifierAddon shut down");
|
|
302
274
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
275
|
+
// ── Config management (session 5 Sprint B7) ─────────────────────────
|
|
276
|
+
//
|
|
277
|
+
// Rules themselves are opaque JSON documents managed through the
|
|
278
|
+
// `upsertRule`/`deleteRule` cap methods — they do NOT belong in the
|
|
279
|
+
// schema-backed settings API. The schema below exposes addon-level
|
|
280
|
+
// tuning knobs only. All fields are global-scope because advanced-
|
|
281
|
+
// notifier is a system singleton with no per-device state.
|
|
282
|
+
// ── Three-level settings API (Phase 3) ──────────────────────────────
|
|
283
|
+
globalSettingsSchema() {
|
|
284
|
+
return this.schema({
|
|
285
|
+
sections: [{
|
|
286
|
+
id: "advanced-notifier-settings",
|
|
287
|
+
title: "Advanced Notifier",
|
|
288
|
+
columns: 2,
|
|
289
|
+
fields: [
|
|
290
|
+
this.field({
|
|
291
|
+
type: "number",
|
|
292
|
+
key: "defaultCooldownSeconds",
|
|
293
|
+
label: "Default Cooldown",
|
|
294
|
+
description: "Default cooldown between repeated notifications for the same rule.",
|
|
295
|
+
min: 0,
|
|
296
|
+
max: 3600,
|
|
297
|
+
step: 1,
|
|
298
|
+
default: 60,
|
|
299
|
+
unit: "s"
|
|
300
|
+
}),
|
|
301
|
+
this.field({
|
|
302
|
+
type: "number",
|
|
303
|
+
key: "maxHistoryEntries",
|
|
304
|
+
label: "Max History Entries",
|
|
305
|
+
description: "Maximum number of notification history entries to keep.",
|
|
306
|
+
min: 100,
|
|
307
|
+
max: 1e5,
|
|
308
|
+
step: 100,
|
|
309
|
+
default: 1e4
|
|
310
|
+
}),
|
|
311
|
+
this.field({
|
|
312
|
+
type: "number",
|
|
313
|
+
key: "auditLogRetentionDays",
|
|
314
|
+
label: "Audit Log Retention",
|
|
315
|
+
description: "How long to retain notification history entries.",
|
|
316
|
+
min: 1,
|
|
317
|
+
max: 365,
|
|
318
|
+
step: 1,
|
|
319
|
+
default: 30,
|
|
320
|
+
unit: "days"
|
|
321
|
+
}),
|
|
322
|
+
this.field({
|
|
323
|
+
type: "boolean",
|
|
324
|
+
key: "consoleOutputEnabled",
|
|
325
|
+
label: "Console Output Enabled",
|
|
326
|
+
description: "Emit matched rules to the server console.",
|
|
327
|
+
default: true
|
|
328
|
+
})
|
|
329
|
+
]
|
|
330
|
+
}]
|
|
331
|
+
});
|
|
311
332
|
}
|
|
312
333
|
async handleEvent(event) {
|
|
313
334
|
const matchedRules = this.ruleEngine.evaluate(event, this.cachedRules);
|
|
314
335
|
for (const rule of matchedRules) {
|
|
315
|
-
const cooldownSeconds = rule.conditions.cooldownSeconds ??
|
|
336
|
+
const cooldownSeconds = rule.conditions.cooldownSeconds ?? this.config.defaultCooldownSeconds;
|
|
316
337
|
if (!this.cooldown.canFire(rule.id, cooldownSeconds)) {
|
|
317
|
-
this.
|
|
338
|
+
this.ctx.logger.debug("Rule skipped — in cooldown", { meta: { ruleId: rule.id } });
|
|
318
339
|
continue;
|
|
319
340
|
}
|
|
320
341
|
this.cooldown.recordFire(rule.id);
|
|
321
342
|
const variables = this.buildTemplateVariables(event);
|
|
322
343
|
const title = rule.template ? renderTemplate(rule.template.title, variables) : `CamStack Alert: ${rule.name}`;
|
|
323
344
|
const message = rule.template ? renderTemplate(rule.template.body, variables) : `Event ${event.category} from ${event.source.id}`;
|
|
345
|
+
const rawDeviceId = event.data["deviceId"] ?? event.source.id;
|
|
346
|
+
const deviceIdNum = typeof rawDeviceId === "number" ? rawDeviceId : Number(rawDeviceId);
|
|
347
|
+
const deviceId = Number.isFinite(deviceIdNum) ? deviceIdNum : void 0;
|
|
324
348
|
const notification = {
|
|
325
349
|
title,
|
|
326
350
|
message,
|
|
327
351
|
severity: this.priorityToSeverity(rule.priority),
|
|
328
352
|
category: event.category,
|
|
329
|
-
deviceId
|
|
353
|
+
deviceId,
|
|
330
354
|
data: event.data,
|
|
331
355
|
timestamp: event.timestamp.getTime()
|
|
332
356
|
};
|
|
@@ -338,22 +362,23 @@ var AdvancedNotifierAddon = class {
|
|
|
338
362
|
} catch (err) {
|
|
339
363
|
success = false;
|
|
340
364
|
dispatchError = String(err);
|
|
341
|
-
this.
|
|
365
|
+
this.ctx.logger.error("Output failed", { meta: { outputId: output.id, error: dispatchError } });
|
|
342
366
|
}
|
|
343
367
|
}
|
|
344
368
|
if (this.auditLog) {
|
|
345
369
|
const entry = {
|
|
346
|
-
id:
|
|
370
|
+
id: node_crypto.randomUUID(),
|
|
347
371
|
ruleId: rule.id,
|
|
348
372
|
ruleName: rule.name,
|
|
349
373
|
eventId: event.id,
|
|
350
374
|
timestamp: event.timestamp.getTime(),
|
|
351
375
|
outputs: this.outputs.map((o) => o.id),
|
|
352
376
|
success,
|
|
353
|
-
error: dispatchError
|
|
377
|
+
error: dispatchError,
|
|
378
|
+
deviceId
|
|
354
379
|
};
|
|
355
380
|
await this.auditLog.record(entry).catch((err) => {
|
|
356
|
-
this.
|
|
381
|
+
this.ctx.logger.error("Failed to record audit log entry", { meta: { error: String(err) } });
|
|
357
382
|
});
|
|
358
383
|
}
|
|
359
384
|
}
|
|
@@ -361,8 +386,8 @@ var AdvancedNotifierAddon = class {
|
|
|
361
386
|
buildTemplateVariables(event) {
|
|
362
387
|
const data = event.data;
|
|
363
388
|
return {
|
|
364
|
-
deviceId: data["deviceId"] ?? event.source.id,
|
|
365
|
-
deviceName: data["deviceName"] ?? event.source.id,
|
|
389
|
+
deviceId: data["deviceId"] ?? String(event.source.id),
|
|
390
|
+
deviceName: data["deviceName"] ?? String(event.source.id),
|
|
366
391
|
className: data["className"] ?? "",
|
|
367
392
|
confidence: String(data["confidence"] ?? ""),
|
|
368
393
|
zoneId: data["zoneId"] ?? "",
|
|
@@ -376,10 +401,13 @@ var AdvancedNotifierAddon = class {
|
|
|
376
401
|
if (priority === "high") return "warning";
|
|
377
402
|
return "info";
|
|
378
403
|
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
404
|
+
}
|
|
405
|
+
exports.AdvancedNotifierAddon = AdvancedNotifierAddon;
|
|
406
|
+
exports.AuditLog = AuditLog;
|
|
407
|
+
exports.ConsoleOutput = ConsoleOutput;
|
|
408
|
+
exports.CooldownTracker = CooldownTracker;
|
|
409
|
+
exports.RuleEngine = RuleEngine;
|
|
410
|
+
exports.RuleStore = RuleStore;
|
|
411
|
+
exports.default = AdvancedNotifierAddon;
|
|
412
|
+
exports.renderTemplate = renderTemplate;
|
|
413
|
+
//# sourceMappingURL=addon.js.map
|
package/dist/addon.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/addon.ts","../src/rules/rule-store.ts","../src/rules/rule-engine.ts","../src/rules/cooldown.ts","../src/template/renderer.ts","../src/outputs/console.ts","../src/history/audit-log.ts"],"sourcesContent":["import type {\n ICamstackAddon,\n AddonManifest,\n AddonContext,\n IAdvancedNotifier,\n INotificationOutput,\n NotificationRule,\n NotificationTestResult,\n NotificationHistoryEntry,\n NotificationHistoryFilter,\n SystemEvent,\n CapabilityProviderMap,\n} from '@camstack/types'\nimport { RuleStore } from './rules/rule-store.js'\nimport { RuleEngine } from './rules/rule-engine.js'\nimport { CooldownTracker } from './rules/cooldown.js'\nimport { renderTemplate } from './template/renderer.js'\nimport { ConsoleOutput } from './outputs/console.js'\nimport { AuditLog } from './history/audit-log.js'\nimport { randomUUID } from 'node:crypto'\n\nconst EVENT_CATEGORIES = ['detection.raw', 'detection.result', 'detection.camera-native'] as const\n\nexport class AdvancedNotifierAddon implements ICamstackAddon {\n readonly manifest: AddonManifest = {\n id: 'advanced-notifier',\n name: 'Advanced Notifier',\n version: '0.1.0',\n capabilities: [\n { name: 'advanced-notifier', mode: 'singleton' as const },\n { name: 'notification-output', mode: 'collection' as const },\n ],\n }\n\n private context!: AddonContext\n\n // Rule management\n private ruleStore!: RuleStore\n private ruleEngine!: RuleEngine\n private cooldown!: CooldownTracker\n private cachedRules: NotificationRule[] = []\n\n // Outputs\n private consoleOutput!: ConsoleOutput\n private outputs: INotificationOutput[] = []\n\n // Audit log\n private auditLog!: AuditLog\n\n // Event bus unsubscribe handles\n private unsubscribers: Array<() => void> = []\n\n // IAdvancedNotifier implementation (exposed as capability)\n private readonly notifier: IAdvancedNotifier = {\n getRules: () => this.cachedRules as readonly NotificationRule[],\n\n upsertRule: (rule: NotificationRule): void => {\n // Fire-and-forget with error logging\n this.ruleStore.upsertRule(rule).then(() => {\n const idx = this.cachedRules.findIndex(r => r.id === rule.id)\n if (idx >= 0) {\n this.cachedRules = [\n ...this.cachedRules.slice(0, idx),\n rule,\n ...this.cachedRules.slice(idx + 1),\n ]\n } else {\n this.cachedRules = [...this.cachedRules, rule]\n }\n }).catch(err => {\n this.context.logger.error('Failed to upsert rule', { error: String(err) })\n })\n },\n\n deleteRule: (ruleId: string): void => {\n this.ruleStore.deleteRule(ruleId).then(() => {\n this.cachedRules = this.cachedRules.filter(r => r.id !== ruleId)\n }).catch(err => {\n this.context.logger.error('Failed to delete rule', { error: String(err) })\n })\n },\n\n testRule: async (ruleId: string, lookbackMinutes: number): Promise<NotificationTestResult[]> => {\n const rule = this.cachedRules.find(r => r.id === ruleId)\n if (!rule) return []\n const since = new Date(Date.now() - lookbackMinutes * 60 * 1000)\n const recentEvents = this.context.eventBus.getRecent(\n { category: undefined },\n 1000,\n ).filter(e => e.timestamp >= since)\n\n return recentEvents.map(event => {\n const matched = this.ruleEngine.evaluate(event, [rule])\n return {\n ruleId,\n eventId: event.id,\n timestamp: event.timestamp.getTime(),\n wouldFire: matched.length > 0,\n reason: matched.length === 0 ? 'Conditions not met' : undefined,\n }\n })\n },\n\n getHistory: async (filter?: NotificationHistoryFilter): Promise<NotificationHistoryEntry[]> => {\n if (!this.auditLog) return []\n return this.auditLog.getHistory(filter)\n },\n }\n\n async initialize(context: AddonContext): Promise<void> {\n this.context = context\n this.ruleEngine = new RuleEngine()\n this.cooldown = new CooldownTracker()\n this.consoleOutput = new ConsoleOutput(context.logger.child('console-output'))\n this.outputs = [this.consoleOutput]\n\n // Load rules from persistence if settings backend is available\n if (context.settingsBackend) {\n this.ruleStore = new RuleStore(context.settingsBackend)\n this.auditLog = new AuditLog(context.settingsBackend)\n this.cachedRules = await this.ruleStore.getRules()\n this.context.logger.info(`Loaded ${this.cachedRules.length} notification rules`)\n } else {\n this.context.logger.warn('No settingsBackend — rules will not be persisted')\n }\n\n // Subscribe to detection event categories\n for (const category of EVENT_CATEGORIES) {\n const unsubscribe = context.eventBus.subscribe(\n { category },\n (event: SystemEvent) => { void this.handleEvent(event) },\n )\n this.unsubscribers.push(unsubscribe)\n }\n\n this.context.logger.info('AdvancedNotifierAddon initialized')\n }\n\n async shutdown(): Promise<void> {\n for (const unsub of this.unsubscribers) {\n unsub()\n }\n this.unsubscribers = []\n this.context.logger.info('AdvancedNotifierAddon shut down')\n }\n\n getCapabilityProvider<K extends keyof CapabilityProviderMap>(\n name: K,\n ): CapabilityProviderMap[K] | null {\n if (name === 'advanced-notifier') {\n return this.notifier as CapabilityProviderMap[K]\n }\n if (name === 'notification-output') {\n return this.consoleOutput as CapabilityProviderMap[K]\n }\n return null\n }\n\n private async handleEvent(event: SystemEvent): Promise<void> {\n const matchedRules = this.ruleEngine.evaluate(event, this.cachedRules)\n\n for (const rule of matchedRules) {\n const cooldownSeconds = rule.conditions.cooldownSeconds ?? 0\n if (!this.cooldown.canFire(rule.id, cooldownSeconds)) {\n this.context.logger.debug(`Rule ${rule.id} skipped — in cooldown`)\n continue\n }\n this.cooldown.recordFire(rule.id)\n\n const variables = this.buildTemplateVariables(event)\n const title = rule.template\n ? renderTemplate(rule.template.title, variables)\n : `CamStack Alert: ${rule.name}`\n const message = rule.template\n ? renderTemplate(rule.template.body, variables)\n : `Event ${event.category} from ${event.source.id}`\n\n const notification = {\n title,\n message,\n severity: this.priorityToSeverity(rule.priority),\n category: event.category,\n deviceId: (event.data['deviceId'] as string | undefined) ?? event.source.id,\n data: event.data,\n timestamp: event.timestamp.getTime(),\n } as const\n\n let success = true\n let dispatchError: string | undefined\n\n for (const output of this.outputs) {\n try {\n await output.send(notification)\n } catch (err) {\n success = false\n dispatchError = String(err)\n this.context.logger.error(`Output ${output.id} failed`, { error: dispatchError })\n }\n }\n\n if (this.auditLog) {\n const entry: NotificationHistoryEntry = {\n id: randomUUID(),\n ruleId: rule.id,\n ruleName: rule.name,\n eventId: event.id,\n timestamp: event.timestamp.getTime(),\n outputs: this.outputs.map(o => o.id),\n success,\n error: dispatchError,\n }\n await this.auditLog.record(entry).catch(err => {\n this.context.logger.error('Failed to record audit log entry', { error: String(err) })\n })\n }\n }\n }\n\n private buildTemplateVariables(event: SystemEvent): Record<string, string> {\n const data = event.data\n return {\n deviceId: (data['deviceId'] as string | undefined) ?? event.source.id,\n deviceName: (data['deviceName'] as string | undefined) ?? event.source.id,\n className: (data['className'] as string | undefined) ?? '',\n confidence: String(data['confidence'] ?? ''),\n zoneId: (data['zoneId'] as string | undefined) ?? '',\n zoneName: (data['zoneName'] as string | undefined) ?? '',\n category: event.category,\n timestamp: event.timestamp.toISOString(),\n }\n }\n\n private priorityToSeverity(\n priority: NotificationRule['priority'],\n ): 'info' | 'warning' | 'critical' {\n if (priority === 'critical') return 'critical'\n if (priority === 'high') return 'warning'\n return 'info'\n }\n}\n\nexport default AdvancedNotifierAddon\n","import type { ISettingsBackend, NotificationRule } from '@camstack/types'\n\nconst COLLECTION = 'addon-settings' as const\nconst KEY = 'advanced-notifier:rules'\n\nexport class RuleStore {\n private readonly backend: ISettingsBackend\n\n constructor(backend: ISettingsBackend) {\n this.backend = backend\n }\n\n async getRules(): Promise<NotificationRule[]> {\n const raw = await this.backend.get(COLLECTION, KEY)\n if (!raw || !Array.isArray(raw)) return []\n return raw as NotificationRule[]\n }\n\n async saveRules(rules: readonly NotificationRule[]): Promise<void> {\n await this.backend.set(COLLECTION, KEY, [...rules])\n }\n\n async upsertRule(rule: NotificationRule): Promise<void> {\n const rules = await this.getRules()\n const idx = rules.findIndex(r => r.id === rule.id)\n if (idx >= 0) {\n rules[idx] = rule\n } else {\n rules.push(rule)\n }\n await this.saveRules(rules)\n }\n\n async deleteRule(ruleId: string): Promise<void> {\n const rules = await this.getRules()\n await this.saveRules(rules.filter(r => r.id !== ruleId))\n }\n}\n","import type { NotificationRule } from '@camstack/types'\n\ninterface EventData {\n readonly id: string\n readonly timestamp: Date\n readonly source: { readonly type: string; readonly id: string }\n readonly category: string\n readonly data: Record<string, unknown>\n}\n\nexport class RuleEngine {\n evaluate(event: EventData, rules: readonly NotificationRule[]): NotificationRule[] {\n return rules.filter(rule => this.matchesRule(event, rule))\n }\n\n private matchesRule(event: EventData, rule: NotificationRule): boolean {\n if (!rule.enabled) return false\n if (!rule.eventTypes.includes(event.category)) return false\n return this.evaluateConditions(event, rule)\n }\n\n private evaluateConditions(event: EventData, rule: NotificationRule): boolean {\n const { conditions } = rule\n const data = event.data\n\n if (conditions.deviceIds?.length) {\n const deviceId = (data['deviceId'] as string) ?? event.source.id\n if (!conditions.deviceIds.includes(deviceId)) return false\n }\n\n if (conditions.classNames?.length) {\n const className = data['className'] as string | undefined\n if (!className || !conditions.classNames.includes(className)) return false\n }\n\n if (conditions.zoneIds?.length) {\n const zoneId = data['zoneId'] as string | undefined\n if (!zoneId || !conditions.zoneIds.includes(zoneId)) return false\n }\n\n if (conditions.minConfidence !== undefined) {\n const confidence = data['confidence'] as number | undefined\n if (confidence === undefined || confidence < conditions.minConfidence) return false\n }\n\n if (conditions.schedule) {\n const now = new Date()\n const { days, startHour, endHour } = conditions.schedule\n if (!days.includes(now.getDay())) return false\n const hour = now.getHours()\n if (hour < startHour || hour >= endHour) return false\n }\n\n return true\n }\n}\n","export class CooldownTracker {\n private readonly lastFired = new Map<string, number>()\n\n canFire(ruleId: string, cooldownSeconds: number): boolean {\n const last = this.lastFired.get(ruleId)\n if (last === undefined) return true\n return (Date.now() - last) / 1000 >= cooldownSeconds\n }\n\n recordFire(ruleId: string): void {\n this.lastFired.set(ruleId, Date.now())\n }\n\n reset(): void {\n this.lastFired.clear()\n }\n}\n","export function renderTemplate(template: string, variables: Record<string, string>): string {\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (match, key: string) => {\n const value: string | undefined = variables[key]\n return value !== undefined ? value : match\n })\n}\n","import type { INotificationOutput, Notification, IScopedLogger } from '@camstack/types'\n\nexport class ConsoleOutput implements INotificationOutput {\n readonly id = 'console'\n readonly name = 'Console Log'\n readonly icon = 'terminal'\n\n private readonly logger: IScopedLogger\n\n constructor(logger: IScopedLogger) {\n this.logger = logger\n }\n\n async send(notification: Notification): Promise<void> {\n this.logger.info(\n `[${notification.severity}] ${notification.title}: ${notification.message}` +\n (notification.deviceId ? ` (device=${notification.deviceId})` : ''),\n )\n }\n\n async sendTest(): Promise<{ success: boolean; error?: string }> {\n await this.send({\n title: 'Test',\n message: 'Console output working',\n severity: 'info',\n category: 'test',\n timestamp: Date.now(),\n })\n return { success: true }\n }\n}\n","import type {\n ISettingsBackend,\n NotificationHistoryEntry,\n NotificationHistoryFilter,\n} from '@camstack/types'\n\nconst COLLECTION = 'addon-settings' as const\nconst LOG_KEY = 'notifier-history:log'\n\nexport class AuditLog {\n private readonly backend: ISettingsBackend\n private readonly maxEntries: number\n\n constructor(backend: ISettingsBackend, maxEntries = 1000) {\n this.backend = backend\n this.maxEntries = maxEntries\n }\n\n async record(entry: NotificationHistoryEntry): Promise<void> {\n const entries = await this.getAll()\n const updated = [...entries, entry]\n const trimmed =\n updated.length > this.maxEntries ? updated.slice(updated.length - this.maxEntries) : updated\n await this.backend.set(COLLECTION, LOG_KEY, trimmed)\n }\n\n async getHistory(filter?: NotificationHistoryFilter): Promise<NotificationHistoryEntry[]> {\n let entries = await this.getAll()\n if (filter?.ruleId) {\n entries = entries.filter(e => e.ruleId === filter.ruleId)\n }\n if (filter?.deviceId) {\n const deviceId = filter.deviceId\n entries = entries.filter(e => (e as unknown as Record<string, unknown>)['deviceId'] === deviceId)\n }\n if (filter?.from !== undefined) {\n const from = filter.from\n entries = entries.filter(e => e.timestamp >= from)\n }\n if (filter?.to !== undefined) {\n const to = filter.to\n entries = entries.filter(e => e.timestamp <= to)\n }\n if (filter?.limit) {\n entries = entries.slice(-filter.limit)\n }\n return entries\n }\n\n private async getAll(): Promise<NotificationHistoryEntry[]> {\n const raw = await this.backend.get(COLLECTION, LOG_KEY)\n if (!raw || !Array.isArray(raw)) return []\n return raw as NotificationHistoryEntry[]\n }\n}\n"],"mappings":"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","names":["COLLECTION"]}
|
|
1
|
+
{"version":3,"file":"addon.js","sources":["../src/rules/rule-store.ts","../src/rules/rule-engine.ts","../src/rules/cooldown.ts","../src/template/renderer.ts","../src/outputs/console.ts","../src/history/audit-log.ts","../src/addon.ts"],"sourcesContent":["import type { AddonApi, NotificationRule } from '@camstack/types'\n\nconst COLLECTION = 'addon-settings' as const\nconst KEY = 'advanced-notifier:rules'\n\nexport class RuleStore {\n private readonly api: AddonApi\n\n constructor(api: AddonApi) {\n this.api = api\n }\n\n async getRules(): Promise<NotificationRule[]> {\n const raw = await this.api.settingsStore.get.query({ collection: COLLECTION, key: KEY })\n if (!raw || !Array.isArray(raw)) return []\n return raw as NotificationRule[]\n }\n\n async saveRules(rules: readonly NotificationRule[]): Promise<void> {\n await this.api.settingsStore.set.mutate({ collection: COLLECTION, key: KEY, value: [...rules] })\n }\n\n async upsertRule(rule: NotificationRule): Promise<void> {\n const rules = await this.getRules()\n const idx = rules.findIndex(r => r.id === rule.id)\n if (idx >= 0) {\n rules[idx] = rule\n } else {\n rules.push(rule)\n }\n await this.saveRules(rules)\n }\n\n async deleteRule(ruleId: string): Promise<void> {\n const rules = await this.getRules()\n await this.saveRules(rules.filter(r => r.id !== ruleId))\n }\n}\n","import type { NotificationRule } from '@camstack/types'\n\ninterface EventData {\n readonly id: string\n readonly timestamp: Date\n readonly source: { readonly type: string; readonly id: string | number }\n readonly category: string\n readonly data: Record<string, unknown>\n}\n\nexport class RuleEngine {\n evaluate(event: EventData, rules: readonly NotificationRule[]): NotificationRule[] {\n return rules.filter(rule => this.matchesRule(event, rule))\n }\n\n private matchesRule(event: EventData, rule: NotificationRule): boolean {\n if (!rule.enabled) return false\n if (!rule.eventTypes.includes(event.category)) return false\n return this.evaluateConditions(event, rule)\n }\n\n private evaluateConditions(event: EventData, rule: NotificationRule): boolean {\n const { conditions } = rule\n const data = event.data\n\n if (conditions.deviceIds?.length) {\n const raw = data['deviceId'] ?? event.source.id\n const deviceId = typeof raw === 'number' ? raw : Number(raw)\n if (!Number.isFinite(deviceId) || !conditions.deviceIds.includes(deviceId)) return false\n }\n\n if (conditions.classNames?.length) {\n const className = data['className'] as string | undefined\n if (!className || !conditions.classNames.includes(className)) return false\n }\n\n if (conditions.zoneIds?.length) {\n const zoneId = data['zoneId'] as string | undefined\n if (!zoneId || !conditions.zoneIds.includes(zoneId)) return false\n }\n\n if (conditions.minConfidence !== undefined) {\n const confidence = data['confidence'] as number | undefined\n if (confidence === undefined || confidence < conditions.minConfidence) return false\n }\n\n if (conditions.schedule) {\n const now = new Date()\n const { days, startHour, endHour } = conditions.schedule\n if (!days.includes(now.getDay())) return false\n const hour = now.getHours()\n if (hour < startHour || hour >= endHour) return false\n }\n\n return true\n }\n}\n","export class CooldownTracker {\n private readonly lastFired = new Map<string, number>()\n\n canFire(ruleId: string, cooldownSeconds: number): boolean {\n const last = this.lastFired.get(ruleId)\n if (last === undefined) return true\n return (Date.now() - last) / 1000 >= cooldownSeconds\n }\n\n recordFire(ruleId: string): void {\n this.lastFired.set(ruleId, Date.now())\n }\n\n reset(): void {\n this.lastFired.clear()\n }\n}\n","export function renderTemplate(template: string, variables: Record<string, string>): string {\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (match, key: string) => {\n const value: string | undefined = variables[key]\n return value !== undefined ? value : match\n })\n}\n","import type { INotificationOutput, Notification, IScopedLogger } from '@camstack/types'\n\nexport class ConsoleOutput implements INotificationOutput {\n readonly id = 'console'\n readonly name = 'Console Log'\n readonly icon = 'terminal'\n\n private readonly logger: IScopedLogger\n\n constructor(logger: IScopedLogger) {\n this.logger = logger\n }\n\n async send(notification: Notification): Promise<void> {\n this.logger.info(\n `[${notification.severity}] ${notification.title}: ${notification.message}`,\n notification.deviceId !== undefined\n ? { tags: { deviceId: notification.deviceId } }\n : undefined,\n )\n }\n\n async sendTest(): Promise<{ success: boolean; error?: string }> {\n await this.send({\n title: 'Test',\n message: 'Console output working',\n severity: 'info',\n category: 'test',\n timestamp: Date.now(),\n })\n return { success: true }\n }\n}\n","import type {\n AddonApi,\n NotificationHistoryEntry,\n NotificationHistoryFilter,\n} from '@camstack/types'\n\nconst COLLECTION = 'addon-settings' as const\nconst LOG_KEY = 'notifier-history:log'\n\nexport class AuditLog {\n private readonly api: AddonApi\n private readonly maxEntries: number\n\n constructor(api: AddonApi, maxEntries = 1000) {\n this.api = api\n this.maxEntries = maxEntries\n }\n\n async record(entry: NotificationHistoryEntry): Promise<void> {\n const entries = await this.getAll()\n const updated = [...entries, entry]\n const trimmed =\n updated.length > this.maxEntries ? updated.slice(updated.length - this.maxEntries) : updated\n await this.api.settingsStore.set.mutate({ collection: COLLECTION, key: LOG_KEY, value: trimmed })\n }\n\n async getHistory(filter?: NotificationHistoryFilter): Promise<NotificationHistoryEntry[]> {\n let entries = await this.getAll()\n if (filter?.ruleId) {\n entries = entries.filter(e => e.ruleId === filter.ruleId)\n }\n if (filter?.deviceId) {\n const deviceId = filter.deviceId\n entries = entries.filter(e => e.deviceId === deviceId)\n }\n if (filter?.from !== undefined) {\n const from = filter.from\n entries = entries.filter(e => e.timestamp >= from)\n }\n if (filter?.to !== undefined) {\n const to = filter.to\n entries = entries.filter(e => e.timestamp <= to)\n }\n if (filter?.limit) {\n entries = entries.slice(-filter.limit)\n }\n return entries\n }\n\n private async getAll(): Promise<NotificationHistoryEntry[]> {\n const raw = await this.api.settingsStore.get.query({ collection: COLLECTION, key: LOG_KEY })\n if (!raw || !Array.isArray(raw)) return []\n return raw as NotificationHistoryEntry[]\n }\n}\n","import type {\n ProviderRegistration,\n IAdvancedNotifier,\n INotificationOutput,\n NotificationRule,\n NotificationTestResult,\n NotificationHistoryEntry,\n NotificationHistoryFilter,\n SystemEvent,\n} from '@camstack/types'\nimport { BaseAddon, EventCategory, advancedNotifierCapability, notificationOutputCapability } from '@camstack/types'\nimport { RuleStore } from './rules/rule-store.js'\nimport { RuleEngine } from './rules/rule-engine.js'\nimport { CooldownTracker } from './rules/cooldown.js'\nimport { renderTemplate } from './template/renderer.js'\nimport { ConsoleOutput } from './outputs/console.js'\nimport { AuditLog } from './history/audit-log.js'\nimport { randomUUID } from 'node:crypto'\n\nconst EVENT_CATEGORIES = [\n EventCategory.DetectionRaw,\n EventCategory.DetectionResult,\n EventCategory.DetectionCameraNative,\n] as const\n\ninterface NotifierConfig {\n readonly defaultCooldownSeconds: number\n readonly maxHistoryEntries: number\n readonly auditLogRetentionDays: number\n readonly consoleOutputEnabled: boolean\n}\n\nconst DEFAULT_NOTIFIER_CONFIG: NotifierConfig = {\n defaultCooldownSeconds: 60,\n maxHistoryEntries: 10000,\n auditLogRetentionDays: 30,\n consoleOutputEnabled: true,\n}\n\nexport class AdvancedNotifierAddon extends BaseAddon<NotifierConfig> {\n constructor() {\n super({ ...DEFAULT_NOTIFIER_CONFIG })\n }\n\n // Rule management\n private ruleStore!: RuleStore\n private ruleEngine!: RuleEngine\n private cooldown!: CooldownTracker\n private cachedRules: NotificationRule[] = []\n\n // Outputs\n private consoleOutput!: ConsoleOutput\n private outputs: INotificationOutput[] = []\n\n // Audit log\n private auditLog!: AuditLog\n\n // Event bus unsubscribe handles\n private unsubscribers: Array<() => void> = []\n\n // IAdvancedNotifier implementation (exposed as capability)\n private readonly notifier: IAdvancedNotifier = {\n getRules: () => this.cachedRules as readonly NotificationRule[],\n\n upsertRule: (rule: NotificationRule): void => {\n // Fire-and-forget with error logging\n this.ruleStore.upsertRule(rule).then(() => {\n const idx = this.cachedRules.findIndex(r => r.id === rule.id)\n if (idx >= 0) {\n this.cachedRules = [\n ...this.cachedRules.slice(0, idx),\n rule,\n ...this.cachedRules.slice(idx + 1),\n ]\n } else {\n this.cachedRules = [...this.cachedRules, rule]\n }\n }).catch(err => {\n this.ctx.logger.error('Failed to upsert rule', { meta: { error: String(err) } })\n })\n },\n\n deleteRule: (ruleId: string): void => {\n this.ruleStore.deleteRule(ruleId).then(() => {\n this.cachedRules = this.cachedRules.filter(r => r.id !== ruleId)\n }).catch(err => {\n this.ctx.logger.error('Failed to delete rule', { tags: { ruleId }, meta: { error: String(err) } })\n })\n },\n\n testRule: async (ruleId: string, lookbackMinutes: number): Promise<NotificationTestResult[]> => {\n const rule = this.cachedRules.find(r => r.id === ruleId)\n if (!rule) return []\n const since = new Date(Date.now() - lookbackMinutes * 60 * 1000)\n const recentEvents = this.ctx.eventBus.getRecent(\n { category: undefined },\n 1000,\n ).filter(e => e.timestamp >= since)\n\n return recentEvents.map(event => {\n const matched = this.ruleEngine.evaluate(event, [rule])\n return {\n ruleId,\n eventId: event.id,\n timestamp: event.timestamp.getTime(),\n wouldFire: matched.length > 0,\n reason: matched.length === 0 ? 'Conditions not met' : undefined,\n }\n })\n },\n\n getHistory: async (filter?: NotificationHistoryFilter): Promise<NotificationHistoryEntry[]> => {\n if (!this.auditLog) return []\n return this.auditLog.getHistory(filter)\n },\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n this.ruleEngine = new RuleEngine()\n this.cooldown = new CooldownTracker()\n this.consoleOutput = new ConsoleOutput(this.ctx.logger.child('console-output'))\n this.outputs = [this.consoleOutput]\n\n const api = this.ctx.api\n if (api) {\n this.ruleStore = new RuleStore(api)\n this.auditLog = new AuditLog(api)\n this.cachedRules = await this.ruleStore.getRules()\n this.ctx.logger.info('Loaded notification rules', { meta: { count: this.cachedRules.length } })\n } else {\n this.ctx.logger.warn('No ctx.api — rules will not be persisted')\n }\n\n // Subscribe to detection event categories\n for (const category of EVENT_CATEGORIES) {\n const unsubscribe = this.ctx.eventBus.subscribe(\n { category },\n (event: SystemEvent) => { void this.handleEvent(event) },\n )\n this.unsubscribers.push(unsubscribe)\n }\n\n this.ctx.logger.info('AdvancedNotifierAddon initialized')\n\n return [\n { capability: advancedNotifierCapability, provider: this.notifier },\n { capability: notificationOutputCapability, provider: this.consoleOutput },\n ]\n }\n\n protected async onShutdown(): Promise<void> {\n for (const unsub of this.unsubscribers) {\n unsub()\n }\n this.unsubscribers = []\n this.ctx.logger.info('AdvancedNotifierAddon shut down')\n }\n\n // ── Config management (session 5 Sprint B7) ─────────────────────────\n //\n // Rules themselves are opaque JSON documents managed through the\n // `upsertRule`/`deleteRule` cap methods — they do NOT belong in the\n // schema-backed settings API. The schema below exposes addon-level\n // tuning knobs only. All fields are global-scope because advanced-\n // notifier is a system singleton with no per-device state.\n\n // ── Three-level settings API (Phase 3) ──────────────────────────────\n protected globalSettingsSchema() {\n return this.schema({\n sections: [{\n id: 'advanced-notifier-settings',\n title: 'Advanced Notifier',\n columns: 2,\n fields: [\n this.field({\n type: 'number', key: 'defaultCooldownSeconds', label: 'Default Cooldown',\n description: 'Default cooldown between repeated notifications for the same rule.',\n min: 0, max: 3600, step: 1, default: 60, unit: 's',\n }),\n this.field({\n type: 'number', key: 'maxHistoryEntries', label: 'Max History Entries',\n description: 'Maximum number of notification history entries to keep.',\n min: 100, max: 100000, step: 100, default: 10000,\n }),\n this.field({\n type: 'number', key: 'auditLogRetentionDays', label: 'Audit Log Retention',\n description: 'How long to retain notification history entries.',\n min: 1, max: 365, step: 1, default: 30, unit: 'days',\n }),\n this.field({\n type: 'boolean', key: 'consoleOutputEnabled', label: 'Console Output Enabled',\n description: 'Emit matched rules to the server console.',\n default: true,\n }),\n ],\n }],\n })\n }\n\n private async handleEvent(event: SystemEvent): Promise<void> {\n const matchedRules = this.ruleEngine.evaluate(event, this.cachedRules)\n\n for (const rule of matchedRules) {\n const cooldownSeconds = rule.conditions.cooldownSeconds ?? this.config.defaultCooldownSeconds\n if (!this.cooldown.canFire(rule.id, cooldownSeconds)) {\n this.ctx.logger.debug('Rule skipped — in cooldown', { meta: { ruleId: rule.id } })\n continue\n }\n this.cooldown.recordFire(rule.id)\n\n const variables = this.buildTemplateVariables(event)\n const title = rule.template\n ? renderTemplate(rule.template.title, variables)\n : `CamStack Alert: ${rule.name}`\n const message = rule.template\n ? renderTemplate(rule.template.body, variables)\n : `Event ${event.category} from ${event.source.id}`\n\n const rawDeviceId = event.data['deviceId'] ?? event.source.id\n const deviceIdNum = typeof rawDeviceId === 'number' ? rawDeviceId : Number(rawDeviceId)\n const deviceId = Number.isFinite(deviceIdNum) ? deviceIdNum : undefined\n const notification = {\n title,\n message,\n severity: this.priorityToSeverity(rule.priority),\n category: event.category,\n deviceId,\n data: event.data,\n timestamp: event.timestamp.getTime(),\n } as const\n\n let success = true\n let dispatchError: string | undefined\n\n for (const output of this.outputs) {\n try {\n await output.send(notification)\n } catch (err) {\n success = false\n dispatchError = String(err)\n this.ctx.logger.error('Output failed', { meta: { outputId: output.id, error: dispatchError } })\n }\n }\n\n if (this.auditLog) {\n const entry: NotificationHistoryEntry = {\n id: randomUUID(),\n ruleId: rule.id,\n ruleName: rule.name,\n eventId: event.id,\n timestamp: event.timestamp.getTime(),\n outputs: this.outputs.map(o => o.id),\n success,\n error: dispatchError,\n deviceId,\n }\n await this.auditLog.record(entry).catch(err => {\n this.ctx.logger.error('Failed to record audit log entry', { meta: { error: String(err) } })\n })\n }\n }\n }\n\n private buildTemplateVariables(event: SystemEvent): Record<string, string> {\n const data = event.data\n return {\n deviceId: (data['deviceId'] as string | undefined) ?? String(event.source.id),\n deviceName: (data['deviceName'] as string | undefined) ?? String(event.source.id),\n className: (data['className'] as string | undefined) ?? '',\n confidence: String(data['confidence'] ?? ''),\n zoneId: (data['zoneId'] as string | undefined) ?? '',\n zoneName: (data['zoneName'] as string | undefined) ?? '',\n category: event.category,\n timestamp: event.timestamp.toISOString(),\n }\n }\n\n private priorityToSeverity(\n priority: NotificationRule['priority'],\n ): 'info' | 'warning' | 'critical' {\n if (priority === 'critical') return 'critical'\n if (priority === 'high') return 'warning'\n return 'info'\n }\n}\n\nexport default AdvancedNotifierAddon\n"],"names":["COLLECTION","EventCategory","BaseAddon","advancedNotifierCapability","notificationOutputCapability","randomUUID"],"mappings":"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"}
|