@camstack/addon-advanced-notifier 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,512 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
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/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ AdvancedNotifierAddon: () => AdvancedNotifierAddon,
24
+ AuditLog: () => AuditLog,
25
+ ConsoleOutput: () => ConsoleOutput,
26
+ CooldownTracker: () => CooldownTracker,
27
+ HomeAssistantOutput: () => HomeAssistantOutput,
28
+ NotificationBatcher: () => NotificationBatcher,
29
+ RuleEngine: () => RuleEngine,
30
+ RuleStore: () => RuleStore,
31
+ WebhookOutput: () => WebhookOutput,
32
+ renderTemplate: () => renderTemplate
33
+ });
34
+ module.exports = __toCommonJS(src_exports);
35
+
36
+ // src/rules/rule-store.ts
37
+ var COLLECTION = "addon-settings";
38
+ var KEY = "advanced-notifier:rules";
39
+ var RuleStore = class {
40
+ backend;
41
+ constructor(backend) {
42
+ this.backend = backend;
43
+ }
44
+ async getRules() {
45
+ const raw = await this.backend.get(COLLECTION, KEY);
46
+ if (!raw || !Array.isArray(raw)) return [];
47
+ return raw;
48
+ }
49
+ async saveRules(rules) {
50
+ await this.backend.set(COLLECTION, KEY, [...rules]);
51
+ }
52
+ async upsertRule(rule) {
53
+ const rules = await this.getRules();
54
+ const idx = rules.findIndex((r) => r.id === rule.id);
55
+ if (idx >= 0) {
56
+ rules[idx] = rule;
57
+ } else {
58
+ rules.push(rule);
59
+ }
60
+ await this.saveRules(rules);
61
+ }
62
+ async deleteRule(ruleId) {
63
+ const rules = await this.getRules();
64
+ await this.saveRules(rules.filter((r) => r.id !== ruleId));
65
+ }
66
+ };
67
+
68
+ // src/rules/rule-engine.ts
69
+ var RuleEngine = class {
70
+ evaluate(event, rules) {
71
+ return rules.filter((rule) => this.matchesRule(event, rule));
72
+ }
73
+ matchesRule(event, rule) {
74
+ if (!rule.enabled) return false;
75
+ if (!rule.eventTypes.includes(event.category)) return false;
76
+ return this.evaluateConditions(event, rule);
77
+ }
78
+ evaluateConditions(event, rule) {
79
+ const { conditions } = rule;
80
+ const data = event.data;
81
+ if (conditions.deviceIds?.length) {
82
+ const deviceId = data["deviceId"] ?? event.source.id;
83
+ if (!conditions.deviceIds.includes(deviceId)) return false;
84
+ }
85
+ if (conditions.classNames?.length) {
86
+ const className = data["className"];
87
+ if (!className || !conditions.classNames.includes(className)) return false;
88
+ }
89
+ if (conditions.zoneIds?.length) {
90
+ const zoneId = data["zoneId"];
91
+ if (!zoneId || !conditions.zoneIds.includes(zoneId)) return false;
92
+ }
93
+ if (conditions.minConfidence !== void 0) {
94
+ const confidence = data["confidence"];
95
+ if (confidence === void 0 || confidence < conditions.minConfidence) return false;
96
+ }
97
+ if (conditions.schedule) {
98
+ const now = /* @__PURE__ */ new Date();
99
+ const { days, startHour, endHour } = conditions.schedule;
100
+ if (!days.includes(now.getDay())) return false;
101
+ const hour = now.getHours();
102
+ if (hour < startHour || hour >= endHour) return false;
103
+ }
104
+ return true;
105
+ }
106
+ };
107
+
108
+ // src/rules/cooldown.ts
109
+ var CooldownTracker = class {
110
+ lastFired = /* @__PURE__ */ new Map();
111
+ canFire(ruleId, cooldownSeconds) {
112
+ const last = this.lastFired.get(ruleId);
113
+ if (last === void 0) return true;
114
+ return (Date.now() - last) / 1e3 >= cooldownSeconds;
115
+ }
116
+ recordFire(ruleId) {
117
+ this.lastFired.set(ruleId, Date.now());
118
+ }
119
+ reset() {
120
+ this.lastFired.clear();
121
+ }
122
+ };
123
+
124
+ // src/template/renderer.ts
125
+ function renderTemplate(template, variables) {
126
+ return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
127
+ const value = variables[key];
128
+ return value !== void 0 ? value : match;
129
+ });
130
+ }
131
+
132
+ // src/outputs/console.ts
133
+ var ConsoleOutput = class {
134
+ id = "console";
135
+ name = "Console Log";
136
+ icon = "terminal";
137
+ logger;
138
+ constructor(logger) {
139
+ this.logger = logger;
140
+ }
141
+ async send(notification) {
142
+ this.logger.info(
143
+ `[${notification.severity}] ${notification.title}: ${notification.message}` + (notification.deviceId ? ` (device=${notification.deviceId})` : "")
144
+ );
145
+ }
146
+ async sendTest() {
147
+ await this.send({
148
+ title: "Test",
149
+ message: "Console output working",
150
+ severity: "info",
151
+ category: "test",
152
+ timestamp: Date.now()
153
+ });
154
+ return { success: true };
155
+ }
156
+ };
157
+
158
+ // src/history/audit-log.ts
159
+ var COLLECTION2 = "addon-settings";
160
+ var LOG_KEY = "notifier-history:log";
161
+ var AuditLog = class {
162
+ backend;
163
+ maxEntries;
164
+ constructor(backend, maxEntries = 1e3) {
165
+ this.backend = backend;
166
+ this.maxEntries = maxEntries;
167
+ }
168
+ async record(entry) {
169
+ const entries = await this.getAll();
170
+ const updated = [...entries, entry];
171
+ const trimmed = updated.length > this.maxEntries ? updated.slice(updated.length - this.maxEntries) : updated;
172
+ await this.backend.set(COLLECTION2, LOG_KEY, trimmed);
173
+ }
174
+ async getHistory(filter) {
175
+ let entries = await this.getAll();
176
+ if (filter?.ruleId) {
177
+ entries = entries.filter((e) => e.ruleId === filter.ruleId);
178
+ }
179
+ if (filter?.deviceId) {
180
+ const deviceId = filter.deviceId;
181
+ entries = entries.filter((e) => e["deviceId"] === deviceId);
182
+ }
183
+ if (filter?.from !== void 0) {
184
+ const from = filter.from;
185
+ entries = entries.filter((e) => e.timestamp >= from);
186
+ }
187
+ if (filter?.to !== void 0) {
188
+ const to = filter.to;
189
+ entries = entries.filter((e) => e.timestamp <= to);
190
+ }
191
+ if (filter?.limit) {
192
+ entries = entries.slice(-filter.limit);
193
+ }
194
+ return entries;
195
+ }
196
+ async getAll() {
197
+ const raw = await this.backend.get(COLLECTION2, LOG_KEY);
198
+ if (!raw || !Array.isArray(raw)) return [];
199
+ return raw;
200
+ }
201
+ };
202
+
203
+ // src/addon.ts
204
+ var import_node_crypto = require("crypto");
205
+ var EVENT_CATEGORIES = ["detection.raw", "detection.result", "detection.camera-native"];
206
+ var AdvancedNotifierAddon = class {
207
+ manifest = {
208
+ id: "advanced-notifier",
209
+ name: "Advanced Notifier",
210
+ version: "0.1.0",
211
+ capabilities: [
212
+ { name: "advanced-notifier", mode: "singleton" },
213
+ { name: "notification-output", mode: "collection" }
214
+ ]
215
+ };
216
+ context;
217
+ // Rule management
218
+ ruleStore;
219
+ ruleEngine;
220
+ cooldown;
221
+ cachedRules = [];
222
+ // Outputs
223
+ consoleOutput;
224
+ outputs = [];
225
+ // Audit log
226
+ auditLog;
227
+ // Event bus unsubscribe handles
228
+ unsubscribers = [];
229
+ // IAdvancedNotifier implementation (exposed as capability)
230
+ notifier = {
231
+ getRules: () => this.cachedRules,
232
+ upsertRule: (rule) => {
233
+ this.ruleStore.upsertRule(rule).then(() => {
234
+ const idx = this.cachedRules.findIndex((r) => r.id === rule.id);
235
+ if (idx >= 0) {
236
+ this.cachedRules = [
237
+ ...this.cachedRules.slice(0, idx),
238
+ rule,
239
+ ...this.cachedRules.slice(idx + 1)
240
+ ];
241
+ } else {
242
+ this.cachedRules = [...this.cachedRules, rule];
243
+ }
244
+ }).catch((err) => {
245
+ this.context.logger.error("Failed to upsert rule", { error: String(err) });
246
+ });
247
+ },
248
+ deleteRule: (ruleId) => {
249
+ this.ruleStore.deleteRule(ruleId).then(() => {
250
+ this.cachedRules = this.cachedRules.filter((r) => r.id !== ruleId);
251
+ }).catch((err) => {
252
+ this.context.logger.error("Failed to delete rule", { error: String(err) });
253
+ });
254
+ },
255
+ testRule: async (ruleId, lookbackMinutes) => {
256
+ const rule = this.cachedRules.find((r) => r.id === ruleId);
257
+ if (!rule) return [];
258
+ const since = new Date(Date.now() - lookbackMinutes * 60 * 1e3);
259
+ const recentEvents = this.context.eventBus.getRecent(
260
+ { category: void 0 },
261
+ 1e3
262
+ ).filter((e) => e.timestamp >= since);
263
+ return recentEvents.map((event) => {
264
+ const matched = this.ruleEngine.evaluate(event, [rule]);
265
+ return {
266
+ ruleId,
267
+ eventId: event.id,
268
+ timestamp: event.timestamp.getTime(),
269
+ wouldFire: matched.length > 0,
270
+ reason: matched.length === 0 ? "Conditions not met" : void 0
271
+ };
272
+ });
273
+ },
274
+ getHistory: async (filter) => {
275
+ if (!this.auditLog) return [];
276
+ return this.auditLog.getHistory(filter);
277
+ }
278
+ };
279
+ async initialize(context) {
280
+ this.context = context;
281
+ this.ruleEngine = new RuleEngine();
282
+ this.cooldown = new CooldownTracker();
283
+ this.consoleOutput = new ConsoleOutput(context.logger.child("console-output"));
284
+ this.outputs = [this.consoleOutput];
285
+ if (context.settingsBackend) {
286
+ this.ruleStore = new RuleStore(context.settingsBackend);
287
+ this.auditLog = new AuditLog(context.settingsBackend);
288
+ this.cachedRules = await this.ruleStore.getRules();
289
+ this.context.logger.info(`Loaded ${this.cachedRules.length} notification rules`);
290
+ } else {
291
+ this.context.logger.warn("No settingsBackend \u2014 rules will not be persisted");
292
+ }
293
+ for (const category of EVENT_CATEGORIES) {
294
+ const unsubscribe = context.eventBus.subscribe(
295
+ { category },
296
+ (event) => {
297
+ void this.handleEvent(event);
298
+ }
299
+ );
300
+ this.unsubscribers.push(unsubscribe);
301
+ }
302
+ this.context.logger.info("AdvancedNotifierAddon initialized");
303
+ }
304
+ async shutdown() {
305
+ for (const unsub of this.unsubscribers) {
306
+ unsub();
307
+ }
308
+ this.unsubscribers = [];
309
+ this.context.logger.info("AdvancedNotifierAddon shut down");
310
+ }
311
+ getCapabilityProvider(name) {
312
+ if (name === "advanced-notifier") {
313
+ return this.notifier;
314
+ }
315
+ if (name === "notification-output") {
316
+ return this.consoleOutput;
317
+ }
318
+ return null;
319
+ }
320
+ async handleEvent(event) {
321
+ const matchedRules = this.ruleEngine.evaluate(event, this.cachedRules);
322
+ for (const rule of matchedRules) {
323
+ const cooldownSeconds = rule.conditions.cooldownSeconds ?? 0;
324
+ if (!this.cooldown.canFire(rule.id, cooldownSeconds)) {
325
+ this.context.logger.debug(`Rule ${rule.id} skipped \u2014 in cooldown`);
326
+ continue;
327
+ }
328
+ this.cooldown.recordFire(rule.id);
329
+ const variables = this.buildTemplateVariables(event);
330
+ const title = rule.template ? renderTemplate(rule.template.title, variables) : `CamStack Alert: ${rule.name}`;
331
+ const message = rule.template ? renderTemplate(rule.template.body, variables) : `Event ${event.category} from ${event.source.id}`;
332
+ const notification = {
333
+ title,
334
+ message,
335
+ severity: this.priorityToSeverity(rule.priority),
336
+ category: event.category,
337
+ deviceId: event.data["deviceId"] ?? event.source.id,
338
+ data: event.data,
339
+ timestamp: event.timestamp.getTime()
340
+ };
341
+ let success = true;
342
+ let dispatchError;
343
+ for (const output of this.outputs) {
344
+ try {
345
+ await output.send(notification);
346
+ } catch (err) {
347
+ success = false;
348
+ dispatchError = String(err);
349
+ this.context.logger.error(`Output ${output.id} failed`, { error: dispatchError });
350
+ }
351
+ }
352
+ if (this.auditLog) {
353
+ const entry = {
354
+ id: (0, import_node_crypto.randomUUID)(),
355
+ ruleId: rule.id,
356
+ ruleName: rule.name,
357
+ eventId: event.id,
358
+ timestamp: event.timestamp.getTime(),
359
+ outputs: this.outputs.map((o) => o.id),
360
+ success,
361
+ error: dispatchError
362
+ };
363
+ await this.auditLog.record(entry).catch((err) => {
364
+ this.context.logger.error("Failed to record audit log entry", { error: String(err) });
365
+ });
366
+ }
367
+ }
368
+ }
369
+ buildTemplateVariables(event) {
370
+ const data = event.data;
371
+ return {
372
+ deviceId: data["deviceId"] ?? event.source.id,
373
+ deviceName: data["deviceName"] ?? event.source.id,
374
+ className: data["className"] ?? "",
375
+ confidence: String(data["confidence"] ?? ""),
376
+ zoneId: data["zoneId"] ?? "",
377
+ zoneName: data["zoneName"] ?? "",
378
+ category: event.category,
379
+ timestamp: event.timestamp.toISOString()
380
+ };
381
+ }
382
+ priorityToSeverity(priority) {
383
+ if (priority === "critical") return "critical";
384
+ if (priority === "high") return "warning";
385
+ return "info";
386
+ }
387
+ };
388
+
389
+ // src/outputs/webhook.ts
390
+ var WebhookOutput = class {
391
+ id = "webhook";
392
+ name = "Webhook";
393
+ icon = "webhook";
394
+ url;
395
+ constructor(url) {
396
+ this.url = url;
397
+ }
398
+ async send(notification) {
399
+ await fetch(this.url, {
400
+ method: "POST",
401
+ headers: { "Content-Type": "application/json" },
402
+ body: JSON.stringify(notification)
403
+ });
404
+ }
405
+ async sendTest() {
406
+ try {
407
+ await this.send({
408
+ title: "Test Notification",
409
+ message: "This is a test from CamStack Advanced Notifier",
410
+ severity: "info",
411
+ category: "test",
412
+ timestamp: Date.now()
413
+ });
414
+ return { success: true };
415
+ } catch (err) {
416
+ return { success: false, error: String(err) };
417
+ }
418
+ }
419
+ };
420
+
421
+ // src/outputs/home-assistant.ts
422
+ var HomeAssistantOutput = class {
423
+ id = "home-assistant";
424
+ name = "Home Assistant";
425
+ icon = "home";
426
+ url;
427
+ token;
428
+ constructor(url, token) {
429
+ this.url = url.replace(/\/$/, "");
430
+ this.token = token;
431
+ }
432
+ async send(notification) {
433
+ await fetch(`${this.url}/api/events/camstack_notification`, {
434
+ method: "POST",
435
+ headers: {
436
+ "Content-Type": "application/json",
437
+ Authorization: `Bearer ${this.token}`
438
+ },
439
+ body: JSON.stringify({
440
+ title: notification.title,
441
+ message: notification.message,
442
+ severity: notification.severity,
443
+ category: notification.category,
444
+ device_id: notification.deviceId,
445
+ data: notification.data
446
+ })
447
+ });
448
+ }
449
+ async sendTest() {
450
+ try {
451
+ await this.send({
452
+ title: "CamStack Test",
453
+ message: "Connection verified",
454
+ severity: "info",
455
+ category: "test",
456
+ timestamp: Date.now()
457
+ });
458
+ return { success: true };
459
+ } catch (err) {
460
+ return { success: false, error: String(err) };
461
+ }
462
+ }
463
+ };
464
+
465
+ // src/batching/batcher.ts
466
+ var NotificationBatcher = class {
467
+ windowMs;
468
+ handler;
469
+ batches = /* @__PURE__ */ new Map();
470
+ constructor(windowMs, handler) {
471
+ this.windowMs = windowMs;
472
+ this.handler = handler;
473
+ }
474
+ add(frameKey, event) {
475
+ let batch = this.batches.get(frameKey);
476
+ if (!batch) {
477
+ batch = {
478
+ events: [],
479
+ timer: setTimeout(() => this.flush(frameKey), this.windowMs)
480
+ };
481
+ this.batches.set(frameKey, batch);
482
+ }
483
+ batch.events.push(event);
484
+ }
485
+ flush(frameKey) {
486
+ const batch = this.batches.get(frameKey);
487
+ if (!batch) return;
488
+ this.batches.delete(frameKey);
489
+ this.handler(frameKey, batch.events);
490
+ }
491
+ shutdown() {
492
+ for (const [key, batch] of this.batches) {
493
+ clearTimeout(batch.timer);
494
+ this.handler(key, batch.events);
495
+ }
496
+ this.batches.clear();
497
+ }
498
+ };
499
+ // Annotate the CommonJS export names for ESM import in node:
500
+ 0 && (module.exports = {
501
+ AdvancedNotifierAddon,
502
+ AuditLog,
503
+ ConsoleOutput,
504
+ CooldownTracker,
505
+ HomeAssistantOutput,
506
+ NotificationBatcher,
507
+ RuleEngine,
508
+ RuleStore,
509
+ WebhookOutput,
510
+ renderTemplate
511
+ });
512
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.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","../src/addon.ts","../src/outputs/webhook.ts","../src/outputs/home-assistant.ts","../src/batching/batcher.ts"],"sourcesContent":["export { AdvancedNotifierAddon } from './addon.js'\nexport { renderTemplate } from './template/renderer.js'\nexport { CooldownTracker } from './rules/cooldown.js'\nexport { RuleEngine } from './rules/rule-engine.js'\nexport { RuleStore } from './rules/rule-store.js'\nexport { WebhookOutput } from './outputs/webhook.js'\nexport { HomeAssistantOutput } from './outputs/home-assistant.js'\nexport { ConsoleOutput } from './outputs/console.js'\nexport { NotificationBatcher } from './batching/batcher.js'\nexport { AuditLog } from './history/audit-log.js'\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","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 { INotificationOutput, Notification } from '@camstack/types'\n\nexport class WebhookOutput implements INotificationOutput {\n readonly id = 'webhook'\n readonly name = 'Webhook'\n readonly icon = 'webhook'\n\n private readonly url: string\n\n constructor(url: string) {\n this.url = url\n }\n\n async send(notification: Notification): Promise<void> {\n await fetch(this.url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(notification),\n })\n }\n\n async sendTest(): Promise<{ success: boolean; error?: string }> {\n try {\n await this.send({\n title: 'Test Notification',\n message: 'This is a test from CamStack Advanced Notifier',\n severity: 'info',\n category: 'test',\n timestamp: Date.now(),\n })\n return { success: true }\n } catch (err) {\n return { success: false, error: String(err) }\n }\n }\n}\n","import type { INotificationOutput, Notification } from '@camstack/types'\n\nexport class HomeAssistantOutput implements INotificationOutput {\n readonly id = 'home-assistant'\n readonly name = 'Home Assistant'\n readonly icon = 'home'\n\n private readonly url: string\n private readonly token: string\n\n constructor(url: string, token: string) {\n this.url = url.replace(/\\/$/, '')\n this.token = token\n }\n\n async send(notification: Notification): Promise<void> {\n await fetch(`${this.url}/api/events/camstack_notification`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.token}`,\n },\n body: JSON.stringify({\n title: notification.title,\n message: notification.message,\n severity: notification.severity,\n category: notification.category,\n device_id: notification.deviceId,\n data: notification.data,\n }),\n })\n }\n\n async sendTest(): Promise<{ success: boolean; error?: string }> {\n try {\n await this.send({\n title: 'CamStack Test',\n message: 'Connection verified',\n severity: 'info',\n category: 'test',\n timestamp: Date.now(),\n })\n return { success: true }\n } catch (err) {\n return { success: false, error: String(err) }\n }\n }\n}\n","export class NotificationBatcher {\n private readonly windowMs: number\n private readonly handler: (frameKey: string, events: unknown[]) => void\n private readonly batches = new Map<\n string,\n { events: unknown[]; timer: ReturnType<typeof setTimeout> }\n >()\n\n constructor(windowMs: number, handler: (frameKey: string, events: unknown[]) => void) {\n this.windowMs = windowMs\n this.handler = handler\n }\n\n add(frameKey: string, event: unknown): void {\n let batch = this.batches.get(frameKey)\n if (!batch) {\n batch = {\n events: [],\n timer: setTimeout(() => this.flush(frameKey), this.windowMs),\n }\n this.batches.set(frameKey, batch)\n }\n batch.events.push(event)\n }\n\n private flush(frameKey: string): void {\n const batch = this.batches.get(frameKey)\n if (!batch) return\n this.batches.delete(frameKey)\n this.handler(frameKey, batch.events)\n }\n\n shutdown(): void {\n for (const [key, batch] of this.batches) {\n clearTimeout(batch.timer)\n this.handler(key, batch.events)\n }\n this.batches.clear()\n }\n}\n"],"mappings":"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","names":["COLLECTION"]}