@camstack/addon-advanced-notifier 0.1.14 → 0.1.16

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