@camstack/addon-provider-unraid 0.2.27 → 0.2.28

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
@@ -5801,6 +5801,13 @@ var BaseAddon = class {
5801
5801
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5802
5802
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5803
5803
  _registeredCapNames = [];
5804
+ /**
5805
+ * True only after `readAddonStore` actually answered. Constructor
5806
+ * defaults look like stored config when the store is down — a forked
5807
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5808
+ * mode, 2026-08-25) is not "the operator chose this".
5809
+ */
5810
+ settingsStoreReady = false;
5804
5811
  /** Default config values. Provided via constructor. */
5805
5812
  defaults;
5806
5813
  constructor(defaults) {
@@ -6201,7 +6208,9 @@ var BaseAddon = class {
6201
6208
  ];
6202
6209
  let lastErr;
6203
6210
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6204
- return await settings.readAddonStore() ?? {};
6211
+ const stored = await settings.readAddonStore() ?? {};
6212
+ this.settingsStoreReady = true;
6213
+ return stored;
6205
6214
  } catch (err) {
6206
6215
  lastErr = err;
6207
6216
  const msg = err instanceof Error ? err.message : String(err);
@@ -6209,6 +6218,7 @@ var BaseAddon = class {
6209
6218
  if (attempt === delaysMs.length) break;
6210
6219
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6211
6220
  }
6221
+ this.settingsStoreReady = false;
6212
6222
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6213
6223
  return {};
6214
6224
  }
@@ -8112,6 +8122,12 @@ var ModelVariantGroupSchema = object({
8112
8122
  */
8113
8123
  resolution: number().int().positive().optional()
8114
8124
  });
8125
+ var ModelProviderIdSchema = _enum([
8126
+ "camstack",
8127
+ "frigate",
8128
+ "scrypted",
8129
+ "custom"
8130
+ ]);
8115
8131
  var ModelCatalogEntrySchema = object({
8116
8132
  id: string(),
8117
8133
  name: string(),
@@ -8209,6 +8225,12 @@ var ModelCatalogEntrySchema = object({
8209
8225
  */
8210
8226
  group: ModelVariantGroupSchema.optional(),
8211
8227
  /**
8228
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8229
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8230
+ * persisted before this field existed (`inferModelProvider` fills those).
8231
+ */
8232
+ provider: ModelProviderIdSchema.optional(),
8233
+ /**
8212
8234
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8213
8235
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8214
8236
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -12000,6 +12022,27 @@ var LinkedDeviceSchema = object({
12000
12022
  features: array(string()),
12001
12023
  producesTrackedEvents: boolean().optional()
12002
12024
  });
12025
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12026
+ * The batch answer needs the tag; the single-device answer already has it
12027
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12028
+ var LinkedDevicesForDeviceSchema = object({
12029
+ deviceId: number(),
12030
+ mode: LinkedDevicesModeSchema,
12031
+ devices: array(LinkedDeviceSchema)
12032
+ });
12033
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12034
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12035
+ * object literal is exactly how the three drift apart. */
12036
+ var DeviceBindingsForDeviceSchema = object({
12037
+ deviceId: number(),
12038
+ entries: array(object({
12039
+ capName: string(),
12040
+ kind: _enum(["native", "wrapped"]),
12041
+ providerAddonId: string(),
12042
+ providerNodeId: string(),
12043
+ nativeAddonId: string()
12044
+ }))
12045
+ });
12003
12046
  var SavedDeviceRowSchema = object({
12004
12047
  /** Numeric id reserved at allocateDeviceId time. */
12005
12048
  id: number(),
@@ -12225,11 +12268,25 @@ method(object({
12225
12268
  projection: _enum(["full", "slim"]).optional(),
12226
12269
  /** Return only camera devices. Filtering server-side instead of
12227
12270
  * shipping 293 rows to find 12. */
12228
- isCamera: boolean().optional()
12271
+ isCamera: boolean().optional(),
12272
+ /**
12273
+ * Return only these device ids. For the caller that already KNOWS the
12274
+ * handful it wants and needs a field the id-bearing answer does not
12275
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12276
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12277
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12278
+ * refetches on the reconcile interval, on a phone.
12279
+ *
12280
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12281
+ * keys rather than rejecting them (verified against the live hub
12282
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12283
+ * it answers today and the caller filters as it already does.
12284
+ */
12285
+ deviceIds: array(number()).optional()
12229
12286
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12230
12287
  mode: LinkedDevicesModeSchema,
12231
12288
  devices: array(LinkedDeviceSchema)
12232
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12289
+ })), 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({
12233
12290
  deviceId: number(),
12234
12291
  values: record(string(), unknown())
12235
12292
  }), object({ success: literal(true) }), {
@@ -12256,25 +12313,7 @@ method(object({
12256
12313
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12257
12314
  kind: "mutation",
12258
12315
  auth: "admin"
12259
- }), method(object({ deviceId: number() }), object({
12260
- deviceId: number(),
12261
- entries: array(object({
12262
- capName: string(),
12263
- kind: _enum(["native", "wrapped"]),
12264
- providerAddonId: string(),
12265
- providerNodeId: string(),
12266
- nativeAddonId: string()
12267
- }))
12268
- })), method(object({}), array(object({
12269
- deviceId: number(),
12270
- entries: array(object({
12271
- capName: string(),
12272
- kind: _enum(["native", "wrapped"]),
12273
- providerAddonId: string(),
12274
- providerNodeId: string(),
12275
- nativeAddonId: string()
12276
- }))
12277
- }))), method(object({
12316
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12278
12317
  deviceId: number(),
12279
12318
  capName: string(),
12280
12319
  wrapperAddonId: string(),
@@ -14684,12 +14723,15 @@ var NcOccupancyConditionSchema = object({
14684
14723
  * there is no second switch that can disagree with the first and every rule
14685
14724
  * authored before the decision migrates for free (`audioModeOf`):
14686
14725
  *
14687
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14688
- * classifier labels with one of them. No window, no percentage:
14689
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14690
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14691
- * the analyzer's (`classificationMinScore`, per device) a label only
14692
- * reaches this condition if the classifier was already confident enough.
14726
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14727
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14728
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14729
+ * frames is the wrong question for a classifier that labels 1–3 frames
14730
+ * per episode. The count window is the brake that drops a single-frame
14731
+ * false positive; the rule's own `throttle` cooldown is the other. The
14732
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14733
+ * per device) — a label only reaches this condition if the classifier was
14734
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14693
14735
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14694
14736
  * the condition: at least `hitPercent`% of the samples over
14695
14737
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14716,14 +14758,22 @@ var NcOccupancyConditionSchema = object({
14716
14758
  * an operator who typed `dog` mean the same thing.
14717
14759
  */
14718
14760
  var NcAudioConditionSchema = object({
14719
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14761
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14720
14762
  labels: array(string().min(1)).min(1).optional(),
14721
14763
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14722
14764
  dbThreshold: number().min(-96).max(0).optional(),
14723
14765
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14724
14766
  hitPercent: number().int().min(1).max(100).default(60),
14725
14767
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14726
- samplingSeconds: number().int().min(1).max(300).default(10)
14768
+ samplingSeconds: number().int().min(1).max(300).default(10),
14769
+ /**
14770
+ * LABEL MODE: how many labelled frames must land inside
14771
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14772
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14773
+ */
14774
+ confirmHits: number().int().min(1).max(20).optional(),
14775
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14776
+ confirmWindowSec: number().int().min(1).max(60).optional()
14727
14777
  });
14728
14778
  /**
14729
14779
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17097,6 +17147,46 @@ var RecentTracksPageSchema = object({
17097
17147
  /** Cursor for the next page, or null when this page is the last. */
17098
17148
  nextCursor: string().nullable()
17099
17149
  });
17150
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17151
+ var LIST_GROUPS_MAX_LIMIT = 100;
17152
+ var AnalyticsGroupRecordSchema = object({
17153
+ id: string(),
17154
+ deviceId: number().int(),
17155
+ openedAt: number().int(),
17156
+ closedAt: number().int(),
17157
+ timestamp: number().int(),
17158
+ memberCount: number().int(),
17159
+ memberTrackIds: array(string()).readonly(),
17160
+ className: string(),
17161
+ classes: array(string()).readonly(),
17162
+ /** Relative event-media path, or null when the group has no picture yet. */
17163
+ mediaUrl: string().nullable(),
17164
+ singleton: boolean()
17165
+ });
17166
+ var AnalyticsGroupMemberSchema = object({
17167
+ trackId: string(),
17168
+ deviceId: number().int(),
17169
+ className: string(),
17170
+ firstSeen: number().int(),
17171
+ lastSeen: number().int(),
17172
+ mediaUrl: string().nullable()
17173
+ });
17174
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17175
+ var ListGroupsQueryInput = object({
17176
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17177
+ deviceIds: array(number()),
17178
+ /** Window lower bound on `closedAt` (inclusive). */
17179
+ since: number().optional(),
17180
+ /** Window upper bound on `openedAt` (inclusive). */
17181
+ until: number().optional(),
17182
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17183
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17184
+ cursor: string().optional()
17185
+ });
17186
+ var ListGroupsPageSchema = object({
17187
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17188
+ nextCursor: string().nullable()
17189
+ });
17100
17190
  var KeyEventQueryInput = object({
17101
17191
  deviceId: number(),
17102
17192
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17172,7 +17262,9 @@ var TrackCascadeCountsSchema = object({
17172
17262
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17173
17263
  plates: number().int(),
17174
17264
  /** Per-track CLIP search vectors removed (best-effort). */
17175
- embeddings: number().int()
17265
+ embeddings: number().int(),
17266
+ /** Group membership + group rows removed with their last member (best-effort). */
17267
+ groups: number().int()
17176
17268
  });
17177
17269
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17178
17270
  var DiskReconcileCountsSchema = object({
@@ -17318,7 +17410,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17318
17410
  * stationary registry). Default false: the timeline lists passages,
17319
17411
  * not parking records (operator decision, 2026-08-15). */
17320
17412
  includeStationary: boolean().optional()
17321
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17413
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17414
+ deviceId: number(),
17415
+ groupId: string().min(1)
17416
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17322
17417
  kind: "mutation",
17323
17418
  auth: "admin"
17324
17419
  }), 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({
@@ -17628,7 +17723,8 @@ var PipelineModelOptionSchema = object({
17628
17723
  sizeMB: number()
17629
17724
  })),
17630
17725
  group: ModelVariantGroupSchema.optional(),
17631
- legacy: boolean().optional()
17726
+ legacy: boolean().optional(),
17727
+ provider: ModelProviderIdSchema.optional()
17632
17728
  });
17633
17729
  var ConfigFieldBridge = custom();
17634
17730
  var PipelineAddonSchemaSchema = object({
@@ -25742,7 +25838,12 @@ var PlateInfoSchema = object({
25742
25838
  plateBbox: BoundingBoxSchema.optional(),
25743
25839
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25744
25840
  keyFrameMediaKey: string().optional(),
25745
- base64: string().optional()
25841
+ base64: string().optional(),
25842
+ /**
25843
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
25844
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
25845
+ */
25846
+ cropUrl: string().optional()
25746
25847
  });
25747
25848
  var MediaFileLiteSchema = object({
25748
25849
  key: string(),
@@ -31266,6 +31367,12 @@ Object.freeze({
31266
31367
  addonId: null,
31267
31368
  access: "view"
31268
31369
  },
31370
+ "deviceManager.getBindingsBatch": {
31371
+ capName: "device-manager",
31372
+ capScope: "system",
31373
+ addonId: null,
31374
+ access: "view"
31375
+ },
31269
31376
  "deviceManager.getChildren": {
31270
31377
  capName: "device-manager",
31271
31378
  capScope: "system",
@@ -31326,6 +31433,12 @@ Object.freeze({
31326
31433
  addonId: null,
31327
31434
  access: "view"
31328
31435
  },
31436
+ "deviceManager.getLinkedDevicesBatch": {
31437
+ capName: "device-manager",
31438
+ capScope: "system",
31439
+ addonId: null,
31440
+ access: "view"
31441
+ },
31329
31442
  "deviceManager.getRoleDisplayDefaults": {
31330
31443
  capName: "device-manager",
31331
31444
  capScope: "system",
@@ -33096,6 +33209,12 @@ Object.freeze({
33096
33209
  addonId: null,
33097
33210
  access: "view"
33098
33211
  },
33212
+ "pipelineAnalytics.getGroup": {
33213
+ capName: "pipeline-analytics",
33214
+ capScope: "device",
33215
+ addonId: null,
33216
+ access: "view"
33217
+ },
33099
33218
  "pipelineAnalytics.getKeyEvents": {
33100
33219
  capName: "pipeline-analytics",
33101
33220
  capScope: "device",
@@ -33180,6 +33299,12 @@ Object.freeze({
33180
33299
  addonId: null,
33181
33300
  access: "view"
33182
33301
  },
33302
+ "pipelineAnalytics.listGroups": {
33303
+ capName: "pipeline-analytics",
33304
+ capScope: "device",
33305
+ addonId: null,
33306
+ access: "view"
33307
+ },
33183
33308
  "pipelineAnalytics.listOpsLog": {
33184
33309
  capName: "pipeline-analytics",
33185
33310
  capScope: "device",
@@ -35961,6 +36086,11 @@ Object.freeze({
35961
36086
  form: "single",
35962
36087
  optional: false
35963
36088
  }],
36089
+ "deviceManager.getBindingsBatch": [{
36090
+ name: "deviceIds",
36091
+ form: "array",
36092
+ optional: false
36093
+ }],
35964
36094
  "deviceManager.getChildren": [{
35965
36095
  name: "parentDeviceId",
35966
36096
  form: "single",
@@ -36006,6 +36136,11 @@ Object.freeze({
36006
36136
  form: "single",
36007
36137
  optional: false
36008
36138
  }],
36139
+ "deviceManager.getLinkedDevicesBatch": [{
36140
+ name: "deviceIds",
36141
+ form: "array",
36142
+ optional: false
36143
+ }],
36009
36144
  "deviceManager.getSettingsSchema": [{
36010
36145
  name: "deviceId",
36011
36146
  form: "single",
@@ -36026,6 +36161,11 @@ Object.freeze({
36026
36161
  form: "single",
36027
36162
  optional: false
36028
36163
  }],
36164
+ "deviceManager.listAll": [{
36165
+ name: "deviceIds",
36166
+ form: "array",
36167
+ optional: true
36168
+ }],
36029
36169
  "deviceManager.loadConfig": [{
36030
36170
  name: "deviceId",
36031
36171
  form: "single",
@@ -36599,6 +36739,11 @@ Object.freeze({
36599
36739
  form: "single",
36600
36740
  optional: false
36601
36741
  }],
36742
+ "pipelineAnalytics.getGroup": [{
36743
+ name: "deviceId",
36744
+ form: "single",
36745
+ optional: false
36746
+ }],
36602
36747
  "pipelineAnalytics.getKeyEvents": [{
36603
36748
  name: "deviceId",
36604
36749
  form: "single",
@@ -36654,6 +36799,11 @@ Object.freeze({
36654
36799
  form: "array",
36655
36800
  optional: false
36656
36801
  }],
36802
+ "pipelineAnalytics.listGroups": [{
36803
+ name: "deviceIds",
36804
+ form: "array",
36805
+ optional: false
36806
+ }],
36657
36807
  "pipelineAnalytics.listOpsLog": [{
36658
36808
  name: "deviceId",
36659
36809
  form: "single",
package/dist/addon.mjs CHANGED
@@ -5800,6 +5800,13 @@ var BaseAddon = class {
5800
5800
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5801
5801
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5802
5802
  _registeredCapNames = [];
5803
+ /**
5804
+ * True only after `readAddonStore` actually answered. Constructor
5805
+ * defaults look like stored config when the store is down — a forked
5806
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5807
+ * mode, 2026-08-25) is not "the operator chose this".
5808
+ */
5809
+ settingsStoreReady = false;
5803
5810
  /** Default config values. Provided via constructor. */
5804
5811
  defaults;
5805
5812
  constructor(defaults) {
@@ -6200,7 +6207,9 @@ var BaseAddon = class {
6200
6207
  ];
6201
6208
  let lastErr;
6202
6209
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6203
- return await settings.readAddonStore() ?? {};
6210
+ const stored = await settings.readAddonStore() ?? {};
6211
+ this.settingsStoreReady = true;
6212
+ return stored;
6204
6213
  } catch (err) {
6205
6214
  lastErr = err;
6206
6215
  const msg = err instanceof Error ? err.message : String(err);
@@ -6208,6 +6217,7 @@ var BaseAddon = class {
6208
6217
  if (attempt === delaysMs.length) break;
6209
6218
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6210
6219
  }
6220
+ this.settingsStoreReady = false;
6211
6221
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6212
6222
  return {};
6213
6223
  }
@@ -8111,6 +8121,12 @@ var ModelVariantGroupSchema = object({
8111
8121
  */
8112
8122
  resolution: number().int().positive().optional()
8113
8123
  });
8124
+ var ModelProviderIdSchema = _enum([
8125
+ "camstack",
8126
+ "frigate",
8127
+ "scrypted",
8128
+ "custom"
8129
+ ]);
8114
8130
  var ModelCatalogEntrySchema = object({
8115
8131
  id: string(),
8116
8132
  name: string(),
@@ -8208,6 +8224,12 @@ var ModelCatalogEntrySchema = object({
8208
8224
  */
8209
8225
  group: ModelVariantGroupSchema.optional(),
8210
8226
  /**
8227
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8228
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8229
+ * persisted before this field existed (`inferModelProvider` fills those).
8230
+ */
8231
+ provider: ModelProviderIdSchema.optional(),
8232
+ /**
8211
8233
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8212
8234
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8213
8235
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -11999,6 +12021,27 @@ var LinkedDeviceSchema = object({
11999
12021
  features: array(string()),
12000
12022
  producesTrackedEvents: boolean().optional()
12001
12023
  });
12024
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
12025
+ * The batch answer needs the tag; the single-device answer already has it
12026
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
12027
+ var LinkedDevicesForDeviceSchema = object({
12028
+ deviceId: number(),
12029
+ mode: LinkedDevicesModeSchema,
12030
+ devices: array(LinkedDeviceSchema)
12031
+ });
12032
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
12033
+ * `getAllBindings` all answer in. Declared once: three copies of the same
12034
+ * object literal is exactly how the three drift apart. */
12035
+ var DeviceBindingsForDeviceSchema = object({
12036
+ deviceId: number(),
12037
+ entries: array(object({
12038
+ capName: string(),
12039
+ kind: _enum(["native", "wrapped"]),
12040
+ providerAddonId: string(),
12041
+ providerNodeId: string(),
12042
+ nativeAddonId: string()
12043
+ }))
12044
+ });
12002
12045
  var SavedDeviceRowSchema = object({
12003
12046
  /** Numeric id reserved at allocateDeviceId time. */
12004
12047
  id: number(),
@@ -12224,11 +12267,25 @@ method(object({
12224
12267
  projection: _enum(["full", "slim"]).optional(),
12225
12268
  /** Return only camera devices. Filtering server-side instead of
12226
12269
  * shipping 293 rows to find 12. */
12227
- isCamera: boolean().optional()
12270
+ isCamera: boolean().optional(),
12271
+ /**
12272
+ * Return only these device ids. For the caller that already KNOWS the
12273
+ * handful it wants and needs a field the id-bearing answer does not
12274
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12275
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12276
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12277
+ * refetches on the reconcile interval, on a phone.
12278
+ *
12279
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12280
+ * keys rather than rejecting them (verified against the live hub
12281
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12282
+ * it answers today and the caller filters as it already does.
12283
+ */
12284
+ deviceIds: array(number()).optional()
12228
12285
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12229
12286
  mode: LinkedDevicesModeSchema,
12230
12287
  devices: array(LinkedDeviceSchema)
12231
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12288
+ })), 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({
12232
12289
  deviceId: number(),
12233
12290
  values: record(string(), unknown())
12234
12291
  }), object({ success: literal(true) }), {
@@ -12255,25 +12312,7 @@ method(object({
12255
12312
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12256
12313
  kind: "mutation",
12257
12314
  auth: "admin"
12258
- }), method(object({ deviceId: number() }), object({
12259
- deviceId: number(),
12260
- entries: array(object({
12261
- capName: string(),
12262
- kind: _enum(["native", "wrapped"]),
12263
- providerAddonId: string(),
12264
- providerNodeId: string(),
12265
- nativeAddonId: string()
12266
- }))
12267
- })), method(object({}), array(object({
12268
- deviceId: number(),
12269
- entries: array(object({
12270
- capName: string(),
12271
- kind: _enum(["native", "wrapped"]),
12272
- providerAddonId: string(),
12273
- providerNodeId: string(),
12274
- nativeAddonId: string()
12275
- }))
12276
- }))), method(object({
12315
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12277
12316
  deviceId: number(),
12278
12317
  capName: string(),
12279
12318
  wrapperAddonId: string(),
@@ -14683,12 +14722,15 @@ var NcOccupancyConditionSchema = object({
14683
14722
  * there is no second switch that can disagree with the first and every rule
14684
14723
  * authored before the decision migrates for free (`audioModeOf`):
14685
14724
  *
14686
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14687
- * classifier labels with one of them. No window, no percentage:
14688
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14689
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14690
- * the analyzer's (`classificationMinScore`, per device) a label only
14691
- * reaches this condition if the classifier was already confident enough.
14725
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14726
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14727
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14728
+ * frames is the wrong question for a classifier that labels 1–3 frames
14729
+ * per episode. The count window is the brake that drops a single-frame
14730
+ * false positive; the rule's own `throttle` cooldown is the other. The
14731
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14732
+ * per device) — a label only reaches this condition if the classifier was
14733
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14692
14734
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14693
14735
  * the condition: at least `hitPercent`% of the samples over
14694
14736
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14715,14 +14757,22 @@ var NcOccupancyConditionSchema = object({
14715
14757
  * an operator who typed `dog` mean the same thing.
14716
14758
  */
14717
14759
  var NcAudioConditionSchema = object({
14718
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14760
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14719
14761
  labels: array(string().min(1)).min(1).optional(),
14720
14762
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14721
14763
  dbThreshold: number().min(-96).max(0).optional(),
14722
14764
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14723
14765
  hitPercent: number().int().min(1).max(100).default(60),
14724
14766
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14725
- samplingSeconds: number().int().min(1).max(300).default(10)
14767
+ samplingSeconds: number().int().min(1).max(300).default(10),
14768
+ /**
14769
+ * LABEL MODE: how many labelled frames must land inside
14770
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14771
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14772
+ */
14773
+ confirmHits: number().int().min(1).max(20).optional(),
14774
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14775
+ confirmWindowSec: number().int().min(1).max(60).optional()
14726
14776
  });
14727
14777
  /**
14728
14778
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -17096,6 +17146,46 @@ var RecentTracksPageSchema = object({
17096
17146
  /** Cursor for the next page, or null when this page is the last. */
17097
17147
  nextCursor: string().nullable()
17098
17148
  });
17149
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
17150
+ var LIST_GROUPS_MAX_LIMIT = 100;
17151
+ var AnalyticsGroupRecordSchema = object({
17152
+ id: string(),
17153
+ deviceId: number().int(),
17154
+ openedAt: number().int(),
17155
+ closedAt: number().int(),
17156
+ timestamp: number().int(),
17157
+ memberCount: number().int(),
17158
+ memberTrackIds: array(string()).readonly(),
17159
+ className: string(),
17160
+ classes: array(string()).readonly(),
17161
+ /** Relative event-media path, or null when the group has no picture yet. */
17162
+ mediaUrl: string().nullable(),
17163
+ singleton: boolean()
17164
+ });
17165
+ var AnalyticsGroupMemberSchema = object({
17166
+ trackId: string(),
17167
+ deviceId: number().int(),
17168
+ className: string(),
17169
+ firstSeen: number().int(),
17170
+ lastSeen: number().int(),
17171
+ mediaUrl: string().nullable()
17172
+ });
17173
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
17174
+ var ListGroupsQueryInput = object({
17175
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
17176
+ deviceIds: array(number()),
17177
+ /** Window lower bound on `closedAt` (inclusive). */
17178
+ since: number().optional(),
17179
+ /** Window upper bound on `openedAt` (inclusive). */
17180
+ until: number().optional(),
17181
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
17182
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
17183
+ cursor: string().optional()
17184
+ });
17185
+ var ListGroupsPageSchema = object({
17186
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
17187
+ nextCursor: string().nullable()
17188
+ });
17099
17189
  var KeyEventQueryInput = object({
17100
17190
  deviceId: number(),
17101
17191
  /** Window lower bound (track firstSeen ≥ since). */
@@ -17171,7 +17261,9 @@ var TrackCascadeCountsSchema = object({
17171
17261
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
17172
17262
  plates: number().int(),
17173
17263
  /** Per-track CLIP search vectors removed (best-effort). */
17174
- embeddings: number().int()
17264
+ embeddings: number().int(),
17265
+ /** Group membership + group rows removed with their last member (best-effort). */
17266
+ groups: number().int()
17175
17267
  });
17176
17268
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
17177
17269
  var DiskReconcileCountsSchema = object({
@@ -17317,7 +17409,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17317
17409
  * stationary registry). Default false: the timeline lists passages,
17318
17410
  * not parking records (operator decision, 2026-08-15). */
17319
17411
  includeStationary: boolean().optional()
17320
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17412
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17413
+ deviceId: number(),
17414
+ groupId: string().min(1)
17415
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17321
17416
  kind: "mutation",
17322
17417
  auth: "admin"
17323
17418
  }), 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({
@@ -17627,7 +17722,8 @@ var PipelineModelOptionSchema = object({
17627
17722
  sizeMB: number()
17628
17723
  })),
17629
17724
  group: ModelVariantGroupSchema.optional(),
17630
- legacy: boolean().optional()
17725
+ legacy: boolean().optional(),
17726
+ provider: ModelProviderIdSchema.optional()
17631
17727
  });
17632
17728
  var ConfigFieldBridge = custom();
17633
17729
  var PipelineAddonSchemaSchema = object({
@@ -25741,7 +25837,12 @@ var PlateInfoSchema = object({
25741
25837
  plateBbox: BoundingBoxSchema.optional(),
25742
25838
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25743
25839
  keyFrameMediaKey: string().optional(),
25744
- base64: string().optional()
25840
+ base64: string().optional(),
25841
+ /**
25842
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
25843
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
25844
+ */
25845
+ cropUrl: string().optional()
25745
25846
  });
25746
25847
  var MediaFileLiteSchema = object({
25747
25848
  key: string(),
@@ -31265,6 +31366,12 @@ Object.freeze({
31265
31366
  addonId: null,
31266
31367
  access: "view"
31267
31368
  },
31369
+ "deviceManager.getBindingsBatch": {
31370
+ capName: "device-manager",
31371
+ capScope: "system",
31372
+ addonId: null,
31373
+ access: "view"
31374
+ },
31268
31375
  "deviceManager.getChildren": {
31269
31376
  capName: "device-manager",
31270
31377
  capScope: "system",
@@ -31325,6 +31432,12 @@ Object.freeze({
31325
31432
  addonId: null,
31326
31433
  access: "view"
31327
31434
  },
31435
+ "deviceManager.getLinkedDevicesBatch": {
31436
+ capName: "device-manager",
31437
+ capScope: "system",
31438
+ addonId: null,
31439
+ access: "view"
31440
+ },
31328
31441
  "deviceManager.getRoleDisplayDefaults": {
31329
31442
  capName: "device-manager",
31330
31443
  capScope: "system",
@@ -33095,6 +33208,12 @@ Object.freeze({
33095
33208
  addonId: null,
33096
33209
  access: "view"
33097
33210
  },
33211
+ "pipelineAnalytics.getGroup": {
33212
+ capName: "pipeline-analytics",
33213
+ capScope: "device",
33214
+ addonId: null,
33215
+ access: "view"
33216
+ },
33098
33217
  "pipelineAnalytics.getKeyEvents": {
33099
33218
  capName: "pipeline-analytics",
33100
33219
  capScope: "device",
@@ -33179,6 +33298,12 @@ Object.freeze({
33179
33298
  addonId: null,
33180
33299
  access: "view"
33181
33300
  },
33301
+ "pipelineAnalytics.listGroups": {
33302
+ capName: "pipeline-analytics",
33303
+ capScope: "device",
33304
+ addonId: null,
33305
+ access: "view"
33306
+ },
33182
33307
  "pipelineAnalytics.listOpsLog": {
33183
33308
  capName: "pipeline-analytics",
33184
33309
  capScope: "device",
@@ -35960,6 +36085,11 @@ Object.freeze({
35960
36085
  form: "single",
35961
36086
  optional: false
35962
36087
  }],
36088
+ "deviceManager.getBindingsBatch": [{
36089
+ name: "deviceIds",
36090
+ form: "array",
36091
+ optional: false
36092
+ }],
35963
36093
  "deviceManager.getChildren": [{
35964
36094
  name: "parentDeviceId",
35965
36095
  form: "single",
@@ -36005,6 +36135,11 @@ Object.freeze({
36005
36135
  form: "single",
36006
36136
  optional: false
36007
36137
  }],
36138
+ "deviceManager.getLinkedDevicesBatch": [{
36139
+ name: "deviceIds",
36140
+ form: "array",
36141
+ optional: false
36142
+ }],
36008
36143
  "deviceManager.getSettingsSchema": [{
36009
36144
  name: "deviceId",
36010
36145
  form: "single",
@@ -36025,6 +36160,11 @@ Object.freeze({
36025
36160
  form: "single",
36026
36161
  optional: false
36027
36162
  }],
36163
+ "deviceManager.listAll": [{
36164
+ name: "deviceIds",
36165
+ form: "array",
36166
+ optional: true
36167
+ }],
36028
36168
  "deviceManager.loadConfig": [{
36029
36169
  name: "deviceId",
36030
36170
  form: "single",
@@ -36598,6 +36738,11 @@ Object.freeze({
36598
36738
  form: "single",
36599
36739
  optional: false
36600
36740
  }],
36741
+ "pipelineAnalytics.getGroup": [{
36742
+ name: "deviceId",
36743
+ form: "single",
36744
+ optional: false
36745
+ }],
36601
36746
  "pipelineAnalytics.getKeyEvents": [{
36602
36747
  name: "deviceId",
36603
36748
  form: "single",
@@ -36653,6 +36798,11 @@ Object.freeze({
36653
36798
  form: "array",
36654
36799
  optional: false
36655
36800
  }],
36801
+ "pipelineAnalytics.listGroups": [{
36802
+ name: "deviceIds",
36803
+ form: "array",
36804
+ optional: false
36805
+ }],
36656
36806
  "pipelineAnalytics.listOpsLog": [{
36657
36807
  name: "deviceId",
36658
36808
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-unraid",
3
- "version": "0.2.27",
3
+ "version": "0.2.28",
4
4
  "description": "Unraid device-provider addon for CamStack — one Container per Unraid instance fanning out array, disk, docker and notification entities",
5
5
  "keywords": [
6
6
  "camstack",