@camstack/addon-notifiers 1.2.31 → 1.2.32

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.
Files changed (3) hide show
  1. package/dist/addon.js +184 -34
  2. package/dist/addon.mjs +184 -34
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -5831,6 +5831,13 @@ var BaseAddon = class {
5831
5831
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5832
5832
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5833
5833
  _registeredCapNames = [];
5834
+ /**
5835
+ * True only after `readAddonStore` actually answered. Constructor
5836
+ * defaults look like stored config when the store is down — a forked
5837
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5838
+ * mode, 2026-08-25) is not "the operator chose this".
5839
+ */
5840
+ settingsStoreReady = false;
5834
5841
  /** Default config values. Provided via constructor. */
5835
5842
  defaults;
5836
5843
  constructor(defaults) {
@@ -6231,7 +6238,9 @@ var BaseAddon = class {
6231
6238
  ];
6232
6239
  let lastErr;
6233
6240
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6234
- return await settings.readAddonStore() ?? {};
6241
+ const stored = await settings.readAddonStore() ?? {};
6242
+ this.settingsStoreReady = true;
6243
+ return stored;
6235
6244
  } catch (err) {
6236
6245
  lastErr = err;
6237
6246
  const msg = err instanceof Error ? err.message : String(err);
@@ -6239,6 +6248,7 @@ var BaseAddon = class {
6239
6248
  if (attempt === delaysMs.length) break;
6240
6249
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6241
6250
  }
6251
+ this.settingsStoreReady = false;
6242
6252
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6243
6253
  return {};
6244
6254
  }
@@ -8245,6 +8255,12 @@ var ModelVariantGroupSchema = object({
8245
8255
  */
8246
8256
  resolution: number().int().positive().optional()
8247
8257
  });
8258
+ var ModelProviderIdSchema = _enum([
8259
+ "camstack",
8260
+ "frigate",
8261
+ "scrypted",
8262
+ "custom"
8263
+ ]);
8248
8264
  var ModelCatalogEntrySchema = object({
8249
8265
  id: string(),
8250
8266
  name: string(),
@@ -8342,6 +8358,12 @@ var ModelCatalogEntrySchema = object({
8342
8358
  */
8343
8359
  group: ModelVariantGroupSchema.optional(),
8344
8360
  /**
8361
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8362
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8363
+ * persisted before this field existed (`inferModelProvider` fills those).
8364
+ */
8365
+ provider: ModelProviderIdSchema.optional(),
8366
+ /**
8345
8367
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8346
8368
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8347
8369
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -11910,6 +11932,27 @@ var LinkedDeviceSchema = object({
11910
11932
  features: array(string()),
11911
11933
  producesTrackedEvents: boolean().optional()
11912
11934
  });
11935
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
11936
+ * The batch answer needs the tag; the single-device answer already has it
11937
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
11938
+ var LinkedDevicesForDeviceSchema = object({
11939
+ deviceId: number(),
11940
+ mode: LinkedDevicesModeSchema,
11941
+ devices: array(LinkedDeviceSchema)
11942
+ });
11943
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
11944
+ * `getAllBindings` all answer in. Declared once: three copies of the same
11945
+ * object literal is exactly how the three drift apart. */
11946
+ var DeviceBindingsForDeviceSchema = object({
11947
+ deviceId: number(),
11948
+ entries: array(object({
11949
+ capName: string(),
11950
+ kind: _enum(["native", "wrapped"]),
11951
+ providerAddonId: string(),
11952
+ providerNodeId: string(),
11953
+ nativeAddonId: string()
11954
+ }))
11955
+ });
11913
11956
  var SavedDeviceRowSchema = object({
11914
11957
  /** Numeric id reserved at allocateDeviceId time. */
11915
11958
  id: number(),
@@ -12135,11 +12178,25 @@ method(object({
12135
12178
  projection: _enum(["full", "slim"]).optional(),
12136
12179
  /** Return only camera devices. Filtering server-side instead of
12137
12180
  * shipping 293 rows to find 12. */
12138
- isCamera: boolean().optional()
12181
+ isCamera: boolean().optional(),
12182
+ /**
12183
+ * Return only these device ids. For the caller that already KNOWS the
12184
+ * handful it wants and needs a field the id-bearing answer does not
12185
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12186
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12187
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12188
+ * refetches on the reconcile interval, on a phone.
12189
+ *
12190
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12191
+ * keys rather than rejecting them (verified against the live hub
12192
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12193
+ * it answers today and the caller filters as it already does.
12194
+ */
12195
+ deviceIds: array(number()).optional()
12139
12196
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12140
12197
  mode: LinkedDevicesModeSchema,
12141
12198
  devices: array(LinkedDeviceSchema)
12142
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12199
+ })), method(object({ deviceIds: array(number()) }), array(LinkedDevicesForDeviceSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12143
12200
  deviceId: number(),
12144
12201
  values: record(string(), unknown())
12145
12202
  }), object({ success: literal(true) }), {
@@ -12166,25 +12223,7 @@ method(object({
12166
12223
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12167
12224
  kind: "mutation",
12168
12225
  auth: "admin"
12169
- }), method(object({ deviceId: number() }), object({
12170
- deviceId: number(),
12171
- entries: array(object({
12172
- capName: string(),
12173
- kind: _enum(["native", "wrapped"]),
12174
- providerAddonId: string(),
12175
- providerNodeId: string(),
12176
- nativeAddonId: string()
12177
- }))
12178
- })), method(object({}), array(object({
12179
- deviceId: number(),
12180
- entries: array(object({
12181
- capName: string(),
12182
- kind: _enum(["native", "wrapped"]),
12183
- providerAddonId: string(),
12184
- providerNodeId: string(),
12185
- nativeAddonId: string()
12186
- }))
12187
- }))), method(object({
12226
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12188
12227
  deviceId: number(),
12189
12228
  capName: string(),
12190
12229
  wrapperAddonId: string(),
@@ -14570,12 +14609,15 @@ var NcOccupancyConditionSchema = object({
14570
14609
  * there is no second switch that can disagree with the first and every rule
14571
14610
  * authored before the decision migrates for free (`audioModeOf`):
14572
14611
  *
14573
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14574
- * classifier labels with one of them. No window, no percentage:
14575
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14576
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14577
- * the analyzer's (`classificationMinScore`, per device) a label only
14578
- * reaches this condition if the classifier was already confident enough.
14612
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14613
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14614
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14615
+ * frames is the wrong question for a classifier that labels 1–3 frames
14616
+ * per episode. The count window is the brake that drops a single-frame
14617
+ * false positive; the rule's own `throttle` cooldown is the other. The
14618
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14619
+ * per device) — a label only reaches this condition if the classifier was
14620
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14579
14621
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14580
14622
  * the condition: at least `hitPercent`% of the samples over
14581
14623
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14602,14 +14644,22 @@ var NcOccupancyConditionSchema = object({
14602
14644
  * an operator who typed `dog` mean the same thing.
14603
14645
  */
14604
14646
  var NcAudioConditionSchema = object({
14605
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14647
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14606
14648
  labels: array(string().min(1)).min(1).optional(),
14607
14649
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14608
14650
  dbThreshold: number().min(-96).max(0).optional(),
14609
14651
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14610
14652
  hitPercent: number().int().min(1).max(100).default(60),
14611
14653
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14612
- samplingSeconds: number().int().min(1).max(300).default(10)
14654
+ samplingSeconds: number().int().min(1).max(300).default(10),
14655
+ /**
14656
+ * LABEL MODE: how many labelled frames must land inside
14657
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14658
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14659
+ */
14660
+ confirmHits: number().int().min(1).max(20).optional(),
14661
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14662
+ confirmWindowSec: number().int().min(1).max(60).optional()
14613
14663
  });
14614
14664
  /**
14615
14665
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -16983,6 +17033,46 @@ var RecentTracksPageSchema = object({
16983
17033
  /** Cursor for the next page, or null when this page is the last. */
16984
17034
  nextCursor: string().nullable()
16985
17035
  });
17036
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17037
+ var LIST_GROUPS_MAX_LIMIT = 100;
17038
+ var AnalyticsGroupRecordSchema = object({
17039
+ id: string(),
17040
+ deviceId: number().int(),
17041
+ openedAt: number().int(),
17042
+ closedAt: number().int(),
17043
+ timestamp: number().int(),
17044
+ memberCount: number().int(),
17045
+ memberTrackIds: array(string()).readonly(),
17046
+ className: string(),
17047
+ classes: array(string()).readonly(),
17048
+ /** Relative event-media path, or null when the group has no picture yet. */
17049
+ mediaUrl: string().nullable(),
17050
+ singleton: boolean()
17051
+ });
17052
+ var AnalyticsGroupMemberSchema = object({
17053
+ trackId: string(),
17054
+ deviceId: number().int(),
17055
+ className: string(),
17056
+ firstSeen: number().int(),
17057
+ lastSeen: number().int(),
17058
+ mediaUrl: string().nullable()
17059
+ });
17060
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17061
+ var ListGroupsQueryInput = object({
17062
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17063
+ deviceIds: array(number()),
17064
+ /** Window lower bound on `closedAt` (inclusive). */
17065
+ since: number().optional(),
17066
+ /** Window upper bound on `openedAt` (inclusive). */
17067
+ until: number().optional(),
17068
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17069
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17070
+ cursor: string().optional()
17071
+ });
17072
+ var ListGroupsPageSchema = object({
17073
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17074
+ nextCursor: string().nullable()
17075
+ });
16986
17076
  var KeyEventQueryInput = object({
16987
17077
  deviceId: number(),
16988
17078
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17058,7 +17148,9 @@ var TrackCascadeCountsSchema = object({
17058
17148
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17059
17149
  plates: number().int(),
17060
17150
  /** Per-track CLIP search vectors removed (best-effort). */
17061
- embeddings: number().int()
17151
+ embeddings: number().int(),
17152
+ /** Group membership + group rows removed with their last member (best-effort). */
17153
+ groups: number().int()
17062
17154
  });
17063
17155
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17064
17156
  var DiskReconcileCountsSchema = object({
@@ -17204,7 +17296,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17204
17296
  * stationary registry). Default false: the timeline lists passages,
17205
17297
  * not parking records (operator decision, 2026-08-15). */
17206
17298
  includeStationary: boolean().optional()
17207
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17299
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17300
+ deviceId: number(),
17301
+ groupId: string().min(1)
17302
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17208
17303
  kind: "mutation",
17209
17304
  auth: "admin"
17210
17305
  }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
@@ -17514,7 +17609,8 @@ var PipelineModelOptionSchema = object({
17514
17609
  sizeMB: number()
17515
17610
  })),
17516
17611
  group: ModelVariantGroupSchema.optional(),
17517
- legacy: boolean().optional()
17612
+ legacy: boolean().optional(),
17613
+ provider: ModelProviderIdSchema.optional()
17518
17614
  });
17519
17615
  var ConfigFieldBridge = custom();
17520
17616
  var PipelineAddonSchemaSchema = object({
@@ -24098,7 +24194,12 @@ var PlateInfoSchema = object({
24098
24194
  plateBbox: BoundingBoxSchema.optional(),
24099
24195
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
24100
24196
  keyFrameMediaKey: string().optional(),
24101
- base64: string().optional()
24197
+ base64: string().optional(),
24198
+ /**
24199
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
24200
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
24201
+ */
24202
+ cropUrl: string().optional()
24102
24203
  });
24103
24204
  var MediaFileLiteSchema = object({
24104
24205
  key: string(),
@@ -27738,6 +27839,12 @@ Object.freeze({
27738
27839
  addonId: null,
27739
27840
  access: "view"
27740
27841
  },
27842
+ "deviceManager.getBindingsBatch": {
27843
+ capName: "device-manager",
27844
+ capScope: "system",
27845
+ addonId: null,
27846
+ access: "view"
27847
+ },
27741
27848
  "deviceManager.getChildren": {
27742
27849
  capName: "device-manager",
27743
27850
  capScope: "system",
@@ -27798,6 +27905,12 @@ Object.freeze({
27798
27905
  addonId: null,
27799
27906
  access: "view"
27800
27907
  },
27908
+ "deviceManager.getLinkedDevicesBatch": {
27909
+ capName: "device-manager",
27910
+ capScope: "system",
27911
+ addonId: null,
27912
+ access: "view"
27913
+ },
27801
27914
  "deviceManager.getRoleDisplayDefaults": {
27802
27915
  capName: "device-manager",
27803
27916
  capScope: "system",
@@ -29568,6 +29681,12 @@ Object.freeze({
29568
29681
  addonId: null,
29569
29682
  access: "view"
29570
29683
  },
29684
+ "pipelineAnalytics.getGroup": {
29685
+ capName: "pipeline-analytics",
29686
+ capScope: "device",
29687
+ addonId: null,
29688
+ access: "view"
29689
+ },
29571
29690
  "pipelineAnalytics.getKeyEvents": {
29572
29691
  capName: "pipeline-analytics",
29573
29692
  capScope: "device",
@@ -29652,6 +29771,12 @@ Object.freeze({
29652
29771
  addonId: null,
29653
29772
  access: "view"
29654
29773
  },
29774
+ "pipelineAnalytics.listGroups": {
29775
+ capName: "pipeline-analytics",
29776
+ capScope: "device",
29777
+ addonId: null,
29778
+ access: "view"
29779
+ },
29655
29780
  "pipelineAnalytics.listOpsLog": {
29656
29781
  capName: "pipeline-analytics",
29657
29782
  capScope: "device",
@@ -32433,6 +32558,11 @@ Object.freeze({
32433
32558
  form: "single",
32434
32559
  optional: false
32435
32560
  }],
32561
+ "deviceManager.getBindingsBatch": [{
32562
+ name: "deviceIds",
32563
+ form: "array",
32564
+ optional: false
32565
+ }],
32436
32566
  "deviceManager.getChildren": [{
32437
32567
  name: "parentDeviceId",
32438
32568
  form: "single",
@@ -32478,6 +32608,11 @@ Object.freeze({
32478
32608
  form: "single",
32479
32609
  optional: false
32480
32610
  }],
32611
+ "deviceManager.getLinkedDevicesBatch": [{
32612
+ name: "deviceIds",
32613
+ form: "array",
32614
+ optional: false
32615
+ }],
32481
32616
  "deviceManager.getSettingsSchema": [{
32482
32617
  name: "deviceId",
32483
32618
  form: "single",
@@ -32498,6 +32633,11 @@ Object.freeze({
32498
32633
  form: "single",
32499
32634
  optional: false
32500
32635
  }],
32636
+ "deviceManager.listAll": [{
32637
+ name: "deviceIds",
32638
+ form: "array",
32639
+ optional: true
32640
+ }],
32501
32641
  "deviceManager.loadConfig": [{
32502
32642
  name: "deviceId",
32503
32643
  form: "single",
@@ -33071,6 +33211,11 @@ Object.freeze({
33071
33211
  form: "single",
33072
33212
  optional: false
33073
33213
  }],
33214
+ "pipelineAnalytics.getGroup": [{
33215
+ name: "deviceId",
33216
+ form: "single",
33217
+ optional: false
33218
+ }],
33074
33219
  "pipelineAnalytics.getKeyEvents": [{
33075
33220
  name: "deviceId",
33076
33221
  form: "single",
@@ -33126,6 +33271,11 @@ Object.freeze({
33126
33271
  form: "array",
33127
33272
  optional: false
33128
33273
  }],
33274
+ "pipelineAnalytics.listGroups": [{
33275
+ name: "deviceIds",
33276
+ form: "array",
33277
+ optional: false
33278
+ }],
33129
33279
  "pipelineAnalytics.listOpsLog": [{
33130
33280
  name: "deviceId",
33131
33281
  form: "single",
package/dist/addon.mjs CHANGED
@@ -5804,6 +5804,13 @@ var BaseAddon = class {
5804
5804
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5805
5805
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5806
5806
  _registeredCapNames = [];
5807
+ /**
5808
+ * True only after `readAddonStore` actually answered. Constructor
5809
+ * defaults look like stored config when the store is down — a forked
5810
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5811
+ * mode, 2026-08-25) is not "the operator chose this".
5812
+ */
5813
+ settingsStoreReady = false;
5807
5814
  /** Default config values. Provided via constructor. */
5808
5815
  defaults;
5809
5816
  constructor(defaults) {
@@ -6204,7 +6211,9 @@ var BaseAddon = class {
6204
6211
  ];
6205
6212
  let lastErr;
6206
6213
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6207
- return await settings.readAddonStore() ?? {};
6214
+ const stored = await settings.readAddonStore() ?? {};
6215
+ this.settingsStoreReady = true;
6216
+ return stored;
6208
6217
  } catch (err) {
6209
6218
  lastErr = err;
6210
6219
  const msg = err instanceof Error ? err.message : String(err);
@@ -6212,6 +6221,7 @@ var BaseAddon = class {
6212
6221
  if (attempt === delaysMs.length) break;
6213
6222
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6214
6223
  }
6224
+ this.settingsStoreReady = false;
6215
6225
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6216
6226
  return {};
6217
6227
  }
@@ -8218,6 +8228,12 @@ var ModelVariantGroupSchema = object({
8218
8228
  */
8219
8229
  resolution: number().int().positive().optional()
8220
8230
  });
8231
+ var ModelProviderIdSchema = _enum([
8232
+ "camstack",
8233
+ "frigate",
8234
+ "scrypted",
8235
+ "custom"
8236
+ ]);
8221
8237
  var ModelCatalogEntrySchema = object({
8222
8238
  id: string(),
8223
8239
  name: string(),
@@ -8315,6 +8331,12 @@ var ModelCatalogEntrySchema = object({
8315
8331
  */
8316
8332
  group: ModelVariantGroupSchema.optional(),
8317
8333
  /**
8334
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8335
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8336
+ * persisted before this field existed (`inferModelProvider` fills those).
8337
+ */
8338
+ provider: ModelProviderIdSchema.optional(),
8339
+ /**
8318
8340
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8319
8341
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8320
8342
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -11883,6 +11905,27 @@ var LinkedDeviceSchema = object({
11883
11905
  features: array(string()),
11884
11906
  producesTrackedEvents: boolean().optional()
11885
11907
  });
11908
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
11909
+ * The batch answer needs the tag; the single-device answer already has it
11910
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
11911
+ var LinkedDevicesForDeviceSchema = object({
11912
+ deviceId: number(),
11913
+ mode: LinkedDevicesModeSchema,
11914
+ devices: array(LinkedDeviceSchema)
11915
+ });
11916
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
11917
+ * `getAllBindings` all answer in. Declared once: three copies of the same
11918
+ * object literal is exactly how the three drift apart. */
11919
+ var DeviceBindingsForDeviceSchema = object({
11920
+ deviceId: number(),
11921
+ entries: array(object({
11922
+ capName: string(),
11923
+ kind: _enum(["native", "wrapped"]),
11924
+ providerAddonId: string(),
11925
+ providerNodeId: string(),
11926
+ nativeAddonId: string()
11927
+ }))
11928
+ });
11886
11929
  var SavedDeviceRowSchema = object({
11887
11930
  /** Numeric id reserved at allocateDeviceId time. */
11888
11931
  id: number(),
@@ -12108,11 +12151,25 @@ method(object({
12108
12151
  projection: _enum(["full", "slim"]).optional(),
12109
12152
  /** Return only camera devices. Filtering server-side instead of
12110
12153
  * shipping 293 rows to find 12. */
12111
- isCamera: boolean().optional()
12154
+ isCamera: boolean().optional(),
12155
+ /**
12156
+ * Return only these device ids. For the caller that already KNOWS the
12157
+ * handful it wants and needs a field the id-bearing answer does not
12158
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12159
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12160
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12161
+ * refetches on the reconcile interval, on a phone.
12162
+ *
12163
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12164
+ * keys rather than rejecting them (verified against the live hub
12165
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12166
+ * it answers today and the caller filters as it already does.
12167
+ */
12168
+ deviceIds: array(number()).optional()
12112
12169
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12113
12170
  mode: LinkedDevicesModeSchema,
12114
12171
  devices: array(LinkedDeviceSchema)
12115
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12172
+ })), method(object({ deviceIds: array(number()) }), array(LinkedDevicesForDeviceSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12116
12173
  deviceId: number(),
12117
12174
  values: record(string(), unknown())
12118
12175
  }), object({ success: literal(true) }), {
@@ -12139,25 +12196,7 @@ method(object({
12139
12196
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12140
12197
  kind: "mutation",
12141
12198
  auth: "admin"
12142
- }), method(object({ deviceId: number() }), object({
12143
- deviceId: number(),
12144
- entries: array(object({
12145
- capName: string(),
12146
- kind: _enum(["native", "wrapped"]),
12147
- providerAddonId: string(),
12148
- providerNodeId: string(),
12149
- nativeAddonId: string()
12150
- }))
12151
- })), method(object({}), array(object({
12152
- deviceId: number(),
12153
- entries: array(object({
12154
- capName: string(),
12155
- kind: _enum(["native", "wrapped"]),
12156
- providerAddonId: string(),
12157
- providerNodeId: string(),
12158
- nativeAddonId: string()
12159
- }))
12160
- }))), method(object({
12199
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12161
12200
  deviceId: number(),
12162
12201
  capName: string(),
12163
12202
  wrapperAddonId: string(),
@@ -14543,12 +14582,15 @@ var NcOccupancyConditionSchema = object({
14543
14582
  * there is no second switch that can disagree with the first and every rule
14544
14583
  * authored before the decision migrates for free (`audioModeOf`):
14545
14584
  *
14546
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14547
- * classifier labels with one of them. No window, no percentage:
14548
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14549
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14550
- * the analyzer's (`classificationMinScore`, per device) a label only
14551
- * reaches this condition if the classifier was already confident enough.
14585
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14586
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14587
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14588
+ * frames is the wrong question for a classifier that labels 1–3 frames
14589
+ * per episode. The count window is the brake that drops a single-frame
14590
+ * false positive; the rule's own `throttle` cooldown is the other. The
14591
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14592
+ * per device) — a label only reaches this condition if the classifier was
14593
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14552
14594
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14553
14595
  * the condition: at least `hitPercent`% of the samples over
14554
14596
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14575,14 +14617,22 @@ var NcOccupancyConditionSchema = object({
14575
14617
  * an operator who typed `dog` mean the same thing.
14576
14618
  */
14577
14619
  var NcAudioConditionSchema = object({
14578
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14620
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14579
14621
  labels: array(string().min(1)).min(1).optional(),
14580
14622
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14581
14623
  dbThreshold: number().min(-96).max(0).optional(),
14582
14624
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14583
14625
  hitPercent: number().int().min(1).max(100).default(60),
14584
14626
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14585
- samplingSeconds: number().int().min(1).max(300).default(10)
14627
+ samplingSeconds: number().int().min(1).max(300).default(10),
14628
+ /**
14629
+ * LABEL MODE: how many labelled frames must land inside
14630
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14631
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14632
+ */
14633
+ confirmHits: number().int().min(1).max(20).optional(),
14634
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14635
+ confirmWindowSec: number().int().min(1).max(60).optional()
14586
14636
  });
14587
14637
  /**
14588
14638
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -16956,6 +17006,46 @@ var RecentTracksPageSchema = object({
16956
17006
  /** Cursor for the next page, or null when this page is the last. */
16957
17007
  nextCursor: string().nullable()
16958
17008
  });
17009
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17010
+ var LIST_GROUPS_MAX_LIMIT = 100;
17011
+ var AnalyticsGroupRecordSchema = object({
17012
+ id: string(),
17013
+ deviceId: number().int(),
17014
+ openedAt: number().int(),
17015
+ closedAt: number().int(),
17016
+ timestamp: number().int(),
17017
+ memberCount: number().int(),
17018
+ memberTrackIds: array(string()).readonly(),
17019
+ className: string(),
17020
+ classes: array(string()).readonly(),
17021
+ /** Relative event-media path, or null when the group has no picture yet. */
17022
+ mediaUrl: string().nullable(),
17023
+ singleton: boolean()
17024
+ });
17025
+ var AnalyticsGroupMemberSchema = object({
17026
+ trackId: string(),
17027
+ deviceId: number().int(),
17028
+ className: string(),
17029
+ firstSeen: number().int(),
17030
+ lastSeen: number().int(),
17031
+ mediaUrl: string().nullable()
17032
+ });
17033
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17034
+ var ListGroupsQueryInput = object({
17035
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17036
+ deviceIds: array(number()),
17037
+ /** Window lower bound on `closedAt` (inclusive). */
17038
+ since: number().optional(),
17039
+ /** Window upper bound on `openedAt` (inclusive). */
17040
+ until: number().optional(),
17041
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17042
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17043
+ cursor: string().optional()
17044
+ });
17045
+ var ListGroupsPageSchema = object({
17046
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17047
+ nextCursor: string().nullable()
17048
+ });
16959
17049
  var KeyEventQueryInput = object({
16960
17050
  deviceId: number(),
16961
17051
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17031,7 +17121,9 @@ var TrackCascadeCountsSchema = object({
17031
17121
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17032
17122
  plates: number().int(),
17033
17123
  /** Per-track CLIP search vectors removed (best-effort). */
17034
- embeddings: number().int()
17124
+ embeddings: number().int(),
17125
+ /** Group membership + group rows removed with their last member (best-effort). */
17126
+ groups: number().int()
17035
17127
  });
17036
17128
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17037
17129
  var DiskReconcileCountsSchema = object({
@@ -17177,7 +17269,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17177
17269
  * stationary registry). Default false: the timeline lists passages,
17178
17270
  * not parking records (operator decision, 2026-08-15). */
17179
17271
  includeStationary: boolean().optional()
17180
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17272
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17273
+ deviceId: number(),
17274
+ groupId: string().min(1)
17275
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17181
17276
  kind: "mutation",
17182
17277
  auth: "admin"
17183
17278
  }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
@@ -17487,7 +17582,8 @@ var PipelineModelOptionSchema = object({
17487
17582
  sizeMB: number()
17488
17583
  })),
17489
17584
  group: ModelVariantGroupSchema.optional(),
17490
- legacy: boolean().optional()
17585
+ legacy: boolean().optional(),
17586
+ provider: ModelProviderIdSchema.optional()
17491
17587
  });
17492
17588
  var ConfigFieldBridge = custom();
17493
17589
  var PipelineAddonSchemaSchema = object({
@@ -24071,7 +24167,12 @@ var PlateInfoSchema = object({
24071
24167
  plateBbox: BoundingBoxSchema.optional(),
24072
24168
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
24073
24169
  keyFrameMediaKey: string().optional(),
24074
- base64: string().optional()
24170
+ base64: string().optional(),
24171
+ /**
24172
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
24173
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
24174
+ */
24175
+ cropUrl: string().optional()
24075
24176
  });
24076
24177
  var MediaFileLiteSchema = object({
24077
24178
  key: string(),
@@ -27711,6 +27812,12 @@ Object.freeze({
27711
27812
  addonId: null,
27712
27813
  access: "view"
27713
27814
  },
27815
+ "deviceManager.getBindingsBatch": {
27816
+ capName: "device-manager",
27817
+ capScope: "system",
27818
+ addonId: null,
27819
+ access: "view"
27820
+ },
27714
27821
  "deviceManager.getChildren": {
27715
27822
  capName: "device-manager",
27716
27823
  capScope: "system",
@@ -27771,6 +27878,12 @@ Object.freeze({
27771
27878
  addonId: null,
27772
27879
  access: "view"
27773
27880
  },
27881
+ "deviceManager.getLinkedDevicesBatch": {
27882
+ capName: "device-manager",
27883
+ capScope: "system",
27884
+ addonId: null,
27885
+ access: "view"
27886
+ },
27774
27887
  "deviceManager.getRoleDisplayDefaults": {
27775
27888
  capName: "device-manager",
27776
27889
  capScope: "system",
@@ -29541,6 +29654,12 @@ Object.freeze({
29541
29654
  addonId: null,
29542
29655
  access: "view"
29543
29656
  },
29657
+ "pipelineAnalytics.getGroup": {
29658
+ capName: "pipeline-analytics",
29659
+ capScope: "device",
29660
+ addonId: null,
29661
+ access: "view"
29662
+ },
29544
29663
  "pipelineAnalytics.getKeyEvents": {
29545
29664
  capName: "pipeline-analytics",
29546
29665
  capScope: "device",
@@ -29625,6 +29744,12 @@ Object.freeze({
29625
29744
  addonId: null,
29626
29745
  access: "view"
29627
29746
  },
29747
+ "pipelineAnalytics.listGroups": {
29748
+ capName: "pipeline-analytics",
29749
+ capScope: "device",
29750
+ addonId: null,
29751
+ access: "view"
29752
+ },
29628
29753
  "pipelineAnalytics.listOpsLog": {
29629
29754
  capName: "pipeline-analytics",
29630
29755
  capScope: "device",
@@ -32406,6 +32531,11 @@ Object.freeze({
32406
32531
  form: "single",
32407
32532
  optional: false
32408
32533
  }],
32534
+ "deviceManager.getBindingsBatch": [{
32535
+ name: "deviceIds",
32536
+ form: "array",
32537
+ optional: false
32538
+ }],
32409
32539
  "deviceManager.getChildren": [{
32410
32540
  name: "parentDeviceId",
32411
32541
  form: "single",
@@ -32451,6 +32581,11 @@ Object.freeze({
32451
32581
  form: "single",
32452
32582
  optional: false
32453
32583
  }],
32584
+ "deviceManager.getLinkedDevicesBatch": [{
32585
+ name: "deviceIds",
32586
+ form: "array",
32587
+ optional: false
32588
+ }],
32454
32589
  "deviceManager.getSettingsSchema": [{
32455
32590
  name: "deviceId",
32456
32591
  form: "single",
@@ -32471,6 +32606,11 @@ Object.freeze({
32471
32606
  form: "single",
32472
32607
  optional: false
32473
32608
  }],
32609
+ "deviceManager.listAll": [{
32610
+ name: "deviceIds",
32611
+ form: "array",
32612
+ optional: true
32613
+ }],
32474
32614
  "deviceManager.loadConfig": [{
32475
32615
  name: "deviceId",
32476
32616
  form: "single",
@@ -33044,6 +33184,11 @@ Object.freeze({
33044
33184
  form: "single",
33045
33185
  optional: false
33046
33186
  }],
33187
+ "pipelineAnalytics.getGroup": [{
33188
+ name: "deviceId",
33189
+ form: "single",
33190
+ optional: false
33191
+ }],
33047
33192
  "pipelineAnalytics.getKeyEvents": [{
33048
33193
  name: "deviceId",
33049
33194
  form: "single",
@@ -33099,6 +33244,11 @@ Object.freeze({
33099
33244
  form: "array",
33100
33245
  optional: false
33101
33246
  }],
33247
+ "pipelineAnalytics.listGroups": [{
33248
+ name: "deviceIds",
33249
+ form: "array",
33250
+ optional: false
33251
+ }],
33102
33252
  "pipelineAnalytics.listOpsLog": [{
33103
33253
  name: "deviceId",
33104
33254
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-notifiers",
3
- "version": "1.2.31",
3
+ "version": "1.2.32",
4
4
  "description": "System notifiers addon for CamStack — a `notification-output` collection provider hosting per-kind notifier adapters (ntfy, pushover, gotify, telegram, discord, webhook, zentik).",
5
5
  "keywords": [
6
6
  "camstack",