@camstack/addon-remote-storage 1.2.26 → 1.2.27

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/s3.addon.js CHANGED
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_shared = require("./shared-UN8idVfB.js");
5
+ const require_shared = require("./shared-DSlbJzYc.js");
6
6
  let node_stream = require("node:stream");
7
7
  let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
8
8
  let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
package/dist/s3.addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { c as BaseAddon, i as rearmIdleAbort, n as getOptionalBasePath, o as scheduleIdleAbort, s as storageProviderCapability, t as createSessionId } from "./shared-BwialOVX.mjs";
1
+ import { c as BaseAddon, i as rearmIdleAbort, n as getOptionalBasePath, o as scheduleIdleAbort, s as storageProviderCapability, t as createSessionId } from "./shared-3YApqgLa.mjs";
2
2
  import { PassThrough } from "node:stream";
3
3
  import { DeleteObjectCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
4
4
  import { Upload } from "@aws-sdk/lib-storage";
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_shared = require("./shared-UN8idVfB.js");
5
+ const require_shared = require("./shared-DSlbJzYc.js");
6
6
  let ssh2 = require("ssh2");
7
7
  let node_path = require("node:path");
8
8
  node_path = require_shared.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { a as safeJoinRemotePath, c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as storageProviderCapability, t as createSessionId } from "./shared-BwialOVX.mjs";
1
+ import { a as safeJoinRemotePath, c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as storageProviderCapability, t as createSessionId } from "./shared-3YApqgLa.mjs";
2
2
  import { Client } from "ssh2";
3
3
  import * as path from "node:path";
4
4
  //#region src/providers/sftp/sftp-config-schema.ts
@@ -5802,6 +5802,13 @@ var BaseAddon = class {
5802
5802
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5803
5803
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5804
5804
  _registeredCapNames = [];
5805
+ /**
5806
+ * True only after `readAddonStore` actually answered. Constructor
5807
+ * defaults look like stored config when the store is down — a forked
5808
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5809
+ * mode, 2026-08-25) is not "the operator chose this".
5810
+ */
5811
+ settingsStoreReady = false;
5805
5812
  /** Default config values. Provided via constructor. */
5806
5813
  defaults;
5807
5814
  constructor(defaults) {
@@ -6202,7 +6209,9 @@ var BaseAddon = class {
6202
6209
  ];
6203
6210
  let lastErr;
6204
6211
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6205
- return await settings.readAddonStore() ?? {};
6212
+ const stored = await settings.readAddonStore() ?? {};
6213
+ this.settingsStoreReady = true;
6214
+ return stored;
6206
6215
  } catch (err) {
6207
6216
  lastErr = err;
6208
6217
  const msg = err instanceof Error ? err.message : String(err);
@@ -6210,6 +6219,7 @@ var BaseAddon = class {
6210
6219
  if (attempt === delaysMs.length) break;
6211
6220
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6212
6221
  }
6222
+ this.settingsStoreReady = false;
6213
6223
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6214
6224
  return {};
6215
6225
  }
@@ -8097,6 +8107,12 @@ var ModelVariantGroupSchema = object({
8097
8107
  */
8098
8108
  resolution: number().int().positive().optional()
8099
8109
  });
8110
+ var ModelProviderIdSchema = _enum([
8111
+ "camstack",
8112
+ "frigate",
8113
+ "scrypted",
8114
+ "custom"
8115
+ ]);
8100
8116
  var ModelCatalogEntrySchema = object({
8101
8117
  id: string(),
8102
8118
  name: string(),
@@ -8194,6 +8210,12 @@ var ModelCatalogEntrySchema = object({
8194
8210
  */
8195
8211
  group: ModelVariantGroupSchema.optional(),
8196
8212
  /**
8213
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8214
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8215
+ * persisted before this field existed (`inferModelProvider` fills those).
8216
+ */
8217
+ provider: ModelProviderIdSchema.optional(),
8218
+ /**
8197
8219
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8198
8220
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8199
8221
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -11721,6 +11743,27 @@ var LinkedDeviceSchema = object({
11721
11743
  features: array(string()),
11722
11744
  producesTrackedEvents: boolean().optional()
11723
11745
  });
11746
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
11747
+ * The batch answer needs the tag; the single-device answer already has it
11748
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
11749
+ var LinkedDevicesForDeviceSchema = object({
11750
+ deviceId: number(),
11751
+ mode: LinkedDevicesModeSchema,
11752
+ devices: array(LinkedDeviceSchema)
11753
+ });
11754
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
11755
+ * `getAllBindings` all answer in. Declared once: three copies of the same
11756
+ * object literal is exactly how the three drift apart. */
11757
+ var DeviceBindingsForDeviceSchema = object({
11758
+ deviceId: number(),
11759
+ entries: array(object({
11760
+ capName: string(),
11761
+ kind: _enum(["native", "wrapped"]),
11762
+ providerAddonId: string(),
11763
+ providerNodeId: string(),
11764
+ nativeAddonId: string()
11765
+ }))
11766
+ });
11724
11767
  var SavedDeviceRowSchema = object({
11725
11768
  /** Numeric id reserved at allocateDeviceId time. */
11726
11769
  id: number(),
@@ -11946,11 +11989,25 @@ method(object({
11946
11989
  projection: _enum(["full", "slim"]).optional(),
11947
11990
  /** Return only camera devices. Filtering server-side instead of
11948
11991
  * shipping 293 rows to find 12. */
11949
- isCamera: boolean().optional()
11992
+ isCamera: boolean().optional(),
11993
+ /**
11994
+ * Return only these device ids. For the caller that already KNOWS the
11995
+ * handful it wants and needs a field the id-bearing answer does not
11996
+ * carry — the viewer's linked-devices panel joins `type` and `online`
11997
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
11998
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
11999
+ * refetches on the reconcile interval, on a phone.
12000
+ *
12001
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12002
+ * keys rather than rejecting them (verified against the live hub
12003
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12004
+ * it answers today and the caller filters as it already does.
12005
+ */
12006
+ deviceIds: array(number()).optional()
11950
12007
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
11951
12008
  mode: LinkedDevicesModeSchema,
11952
12009
  devices: array(LinkedDeviceSchema)
11953
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12010
+ })), 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({
11954
12011
  deviceId: number(),
11955
12012
  values: record(string(), unknown())
11956
12013
  }), object({ success: literal(true) }), {
@@ -11977,25 +12034,7 @@ method(object({
11977
12034
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
11978
12035
  kind: "mutation",
11979
12036
  auth: "admin"
11980
- }), method(object({ deviceId: number() }), object({
11981
- deviceId: number(),
11982
- entries: array(object({
11983
- capName: string(),
11984
- kind: _enum(["native", "wrapped"]),
11985
- providerAddonId: string(),
11986
- providerNodeId: string(),
11987
- nativeAddonId: string()
11988
- }))
11989
- })), method(object({}), array(object({
11990
- deviceId: number(),
11991
- entries: array(object({
11992
- capName: string(),
11993
- kind: _enum(["native", "wrapped"]),
11994
- providerAddonId: string(),
11995
- providerNodeId: string(),
11996
- nativeAddonId: string()
11997
- }))
11998
- }))), method(object({
12037
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
11999
12038
  deviceId: number(),
12000
12039
  capName: string(),
12001
12040
  wrapperAddonId: string(),
@@ -14367,12 +14406,15 @@ var NcOccupancyConditionSchema = object({
14367
14406
  * there is no second switch that can disagree with the first and every rule
14368
14407
  * authored before the decision migrates for free (`audioModeOf`):
14369
14408
  *
14370
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14371
- * classifier labels with one of them. No window, no percentage:
14372
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14373
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14374
- * the analyzer's (`classificationMinScore`, per device) a label only
14375
- * reaches this condition if the classifier was already confident enough.
14409
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14410
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14411
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14412
+ * frames is the wrong question for a classifier that labels 1–3 frames
14413
+ * per episode. The count window is the brake that drops a single-frame
14414
+ * false positive; the rule's own `throttle` cooldown is the other. The
14415
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14416
+ * per device) — a label only reaches this condition if the classifier was
14417
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14376
14418
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14377
14419
  * the condition: at least `hitPercent`% of the samples over
14378
14420
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14399,14 +14441,22 @@ var NcOccupancyConditionSchema = object({
14399
14441
  * an operator who typed `dog` mean the same thing.
14400
14442
  */
14401
14443
  var NcAudioConditionSchema = object({
14402
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14444
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14403
14445
  labels: array(string().min(1)).min(1).optional(),
14404
14446
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14405
14447
  dbThreshold: number().min(-96).max(0).optional(),
14406
14448
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14407
14449
  hitPercent: number().int().min(1).max(100).default(60),
14408
14450
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14409
- samplingSeconds: number().int().min(1).max(300).default(10)
14451
+ samplingSeconds: number().int().min(1).max(300).default(10),
14452
+ /**
14453
+ * LABEL MODE: how many labelled frames must land inside
14454
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14455
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14456
+ */
14457
+ confirmHits: number().int().min(1).max(20).optional(),
14458
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14459
+ confirmWindowSec: number().int().min(1).max(60).optional()
14410
14460
  });
14411
14461
  /**
14412
14462
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -16780,6 +16830,46 @@ var RecentTracksPageSchema = object({
16780
16830
  /** Cursor for the next page, or null when this page is the last. */
16781
16831
  nextCursor: string().nullable()
16782
16832
  });
16833
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
16834
+ var LIST_GROUPS_MAX_LIMIT = 100;
16835
+ var AnalyticsGroupRecordSchema = object({
16836
+ id: string(),
16837
+ deviceId: number().int(),
16838
+ openedAt: number().int(),
16839
+ closedAt: number().int(),
16840
+ timestamp: number().int(),
16841
+ memberCount: number().int(),
16842
+ memberTrackIds: array(string()).readonly(),
16843
+ className: string(),
16844
+ classes: array(string()).readonly(),
16845
+ /** Relative event-media path, or null when the group has no picture yet. */
16846
+ mediaUrl: string().nullable(),
16847
+ singleton: boolean()
16848
+ });
16849
+ var AnalyticsGroupMemberSchema = object({
16850
+ trackId: string(),
16851
+ deviceId: number().int(),
16852
+ className: string(),
16853
+ firstSeen: number().int(),
16854
+ lastSeen: number().int(),
16855
+ mediaUrl: string().nullable()
16856
+ });
16857
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
16858
+ var ListGroupsQueryInput = object({
16859
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
16860
+ deviceIds: array(number()),
16861
+ /** Window lower bound on `closedAt` (inclusive). */
16862
+ since: number().optional(),
16863
+ /** Window upper bound on `openedAt` (inclusive). */
16864
+ until: number().optional(),
16865
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
16866
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
16867
+ cursor: string().optional()
16868
+ });
16869
+ var ListGroupsPageSchema = object({
16870
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
16871
+ nextCursor: string().nullable()
16872
+ });
16783
16873
  var KeyEventQueryInput = object({
16784
16874
  deviceId: number(),
16785
16875
  /** Window lower bound (track firstSeen ≥ since). */
@@ -16855,7 +16945,9 @@ var TrackCascadeCountsSchema = object({
16855
16945
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
16856
16946
  plates: number().int(),
16857
16947
  /** Per-track CLIP search vectors removed (best-effort). */
16858
- embeddings: number().int()
16948
+ embeddings: number().int(),
16949
+ /** Group membership + group rows removed with their last member (best-effort). */
16950
+ groups: number().int()
16859
16951
  });
16860
16952
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
16861
16953
  var DiskReconcileCountsSchema = object({
@@ -17001,7 +17093,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17001
17093
  * stationary registry). Default false: the timeline lists passages,
17002
17094
  * not parking records (operator decision, 2026-08-15). */
17003
17095
  includeStationary: boolean().optional()
17004
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17096
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17097
+ deviceId: number(),
17098
+ groupId: string().min(1)
17099
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17005
17100
  kind: "mutation",
17006
17101
  auth: "admin"
17007
17102
  }), 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({
@@ -17311,7 +17406,8 @@ var PipelineModelOptionSchema = object({
17311
17406
  sizeMB: number()
17312
17407
  })),
17313
17408
  group: ModelVariantGroupSchema.optional(),
17314
- legacy: boolean().optional()
17409
+ legacy: boolean().optional(),
17410
+ provider: ModelProviderIdSchema.optional()
17315
17411
  });
17316
17412
  var ConfigFieldBridge = custom();
17317
17413
  var PipelineAddonSchemaSchema = object({
@@ -23940,7 +24036,12 @@ var PlateInfoSchema = object({
23940
24036
  plateBbox: BoundingBoxSchema.optional(),
23941
24037
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
23942
24038
  keyFrameMediaKey: string().optional(),
23943
- base64: string().optional()
24039
+ base64: string().optional(),
24040
+ /**
24041
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
24042
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
24043
+ */
24044
+ cropUrl: string().optional()
23944
24045
  });
23945
24046
  var MediaFileLiteSchema = object({
23946
24047
  key: string(),
@@ -27580,6 +27681,12 @@ Object.freeze({
27580
27681
  addonId: null,
27581
27682
  access: "view"
27582
27683
  },
27684
+ "deviceManager.getBindingsBatch": {
27685
+ capName: "device-manager",
27686
+ capScope: "system",
27687
+ addonId: null,
27688
+ access: "view"
27689
+ },
27583
27690
  "deviceManager.getChildren": {
27584
27691
  capName: "device-manager",
27585
27692
  capScope: "system",
@@ -27640,6 +27747,12 @@ Object.freeze({
27640
27747
  addonId: null,
27641
27748
  access: "view"
27642
27749
  },
27750
+ "deviceManager.getLinkedDevicesBatch": {
27751
+ capName: "device-manager",
27752
+ capScope: "system",
27753
+ addonId: null,
27754
+ access: "view"
27755
+ },
27643
27756
  "deviceManager.getRoleDisplayDefaults": {
27644
27757
  capName: "device-manager",
27645
27758
  capScope: "system",
@@ -29410,6 +29523,12 @@ Object.freeze({
29410
29523
  addonId: null,
29411
29524
  access: "view"
29412
29525
  },
29526
+ "pipelineAnalytics.getGroup": {
29527
+ capName: "pipeline-analytics",
29528
+ capScope: "device",
29529
+ addonId: null,
29530
+ access: "view"
29531
+ },
29413
29532
  "pipelineAnalytics.getKeyEvents": {
29414
29533
  capName: "pipeline-analytics",
29415
29534
  capScope: "device",
@@ -29494,6 +29613,12 @@ Object.freeze({
29494
29613
  addonId: null,
29495
29614
  access: "view"
29496
29615
  },
29616
+ "pipelineAnalytics.listGroups": {
29617
+ capName: "pipeline-analytics",
29618
+ capScope: "device",
29619
+ addonId: null,
29620
+ access: "view"
29621
+ },
29497
29622
  "pipelineAnalytics.listOpsLog": {
29498
29623
  capName: "pipeline-analytics",
29499
29624
  capScope: "device",
@@ -32275,6 +32400,11 @@ Object.freeze({
32275
32400
  form: "single",
32276
32401
  optional: false
32277
32402
  }],
32403
+ "deviceManager.getBindingsBatch": [{
32404
+ name: "deviceIds",
32405
+ form: "array",
32406
+ optional: false
32407
+ }],
32278
32408
  "deviceManager.getChildren": [{
32279
32409
  name: "parentDeviceId",
32280
32410
  form: "single",
@@ -32320,6 +32450,11 @@ Object.freeze({
32320
32450
  form: "single",
32321
32451
  optional: false
32322
32452
  }],
32453
+ "deviceManager.getLinkedDevicesBatch": [{
32454
+ name: "deviceIds",
32455
+ form: "array",
32456
+ optional: false
32457
+ }],
32323
32458
  "deviceManager.getSettingsSchema": [{
32324
32459
  name: "deviceId",
32325
32460
  form: "single",
@@ -32340,6 +32475,11 @@ Object.freeze({
32340
32475
  form: "single",
32341
32476
  optional: false
32342
32477
  }],
32478
+ "deviceManager.listAll": [{
32479
+ name: "deviceIds",
32480
+ form: "array",
32481
+ optional: true
32482
+ }],
32343
32483
  "deviceManager.loadConfig": [{
32344
32484
  name: "deviceId",
32345
32485
  form: "single",
@@ -32913,6 +33053,11 @@ Object.freeze({
32913
33053
  form: "single",
32914
33054
  optional: false
32915
33055
  }],
33056
+ "pipelineAnalytics.getGroup": [{
33057
+ name: "deviceId",
33058
+ form: "single",
33059
+ optional: false
33060
+ }],
32916
33061
  "pipelineAnalytics.getKeyEvents": [{
32917
33062
  name: "deviceId",
32918
33063
  form: "single",
@@ -32968,6 +33113,11 @@ Object.freeze({
32968
33113
  form: "array",
32969
33114
  optional: false
32970
33115
  }],
33116
+ "pipelineAnalytics.listGroups": [{
33117
+ name: "deviceIds",
33118
+ form: "array",
33119
+ optional: false
33120
+ }],
32971
33121
  "pipelineAnalytics.listOpsLog": [{
32972
33122
  name: "deviceId",
32973
33123
  form: "single",
@@ -5825,6 +5825,13 @@ var BaseAddon = class {
5825
5825
  _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
5826
5826
  /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
5827
5827
  _registeredCapNames = [];
5828
+ /**
5829
+ * True only after `readAddonStore` actually answered. Constructor
5830
+ * defaults look like stored config when the store is down — a forked
5831
+ * addon that auto-starts from those defaults (cloudflare-tunnel quick
5832
+ * mode, 2026-08-25) is not "the operator chose this".
5833
+ */
5834
+ settingsStoreReady = false;
5828
5835
  /** Default config values. Provided via constructor. */
5829
5836
  defaults;
5830
5837
  constructor(defaults) {
@@ -6225,7 +6232,9 @@ var BaseAddon = class {
6225
6232
  ];
6226
6233
  let lastErr;
6227
6234
  for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
6228
- return await settings.readAddonStore() ?? {};
6235
+ const stored = await settings.readAddonStore() ?? {};
6236
+ this.settingsStoreReady = true;
6237
+ return stored;
6229
6238
  } catch (err) {
6230
6239
  lastErr = err;
6231
6240
  const msg = err instanceof Error ? err.message : String(err);
@@ -6233,6 +6242,7 @@ var BaseAddon = class {
6233
6242
  if (attempt === delaysMs.length) break;
6234
6243
  await new Promise((r) => setTimeout(r, delaysMs[attempt]));
6235
6244
  }
6245
+ this.settingsStoreReady = false;
6236
6246
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
6237
6247
  return {};
6238
6248
  }
@@ -8120,6 +8130,12 @@ var ModelVariantGroupSchema = object({
8120
8130
  */
8121
8131
  resolution: number().int().positive().optional()
8122
8132
  });
8133
+ var ModelProviderIdSchema = _enum([
8134
+ "camstack",
8135
+ "frigate",
8136
+ "scrypted",
8137
+ "custom"
8138
+ ]);
8123
8139
  var ModelCatalogEntrySchema = object({
8124
8140
  id: string(),
8125
8141
  name: string(),
@@ -8217,6 +8233,12 @@ var ModelCatalogEntrySchema = object({
8217
8233
  */
8218
8234
  group: ModelVariantGroupSchema.optional(),
8219
8235
  /**
8236
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8237
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8238
+ * persisted before this field existed (`inferModelProvider` fills those).
8239
+ */
8240
+ provider: ModelProviderIdSchema.optional(),
8241
+ /**
8220
8242
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8221
8243
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8222
8244
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -11744,6 +11766,27 @@ var LinkedDeviceSchema = object({
11744
11766
  features: array(string()),
11745
11767
  producesTrackedEvents: boolean().optional()
11746
11768
  });
11769
+ /** One camera's resolved linked set, tagged with the camera it belongs to.
11770
+ * The batch answer needs the tag; the single-device answer already has it
11771
+ * from the input, which is why `getLinkedDevices` keeps the untagged shape. */
11772
+ var LinkedDevicesForDeviceSchema = object({
11773
+ deviceId: number(),
11774
+ mode: LinkedDevicesModeSchema,
11775
+ devices: array(LinkedDeviceSchema)
11776
+ });
11777
+ /** One device's binding map — the shape `getBindings`, `getBindingsBatch` and
11778
+ * `getAllBindings` all answer in. Declared once: three copies of the same
11779
+ * object literal is exactly how the three drift apart. */
11780
+ var DeviceBindingsForDeviceSchema = object({
11781
+ deviceId: number(),
11782
+ entries: array(object({
11783
+ capName: string(),
11784
+ kind: _enum(["native", "wrapped"]),
11785
+ providerAddonId: string(),
11786
+ providerNodeId: string(),
11787
+ nativeAddonId: string()
11788
+ }))
11789
+ });
11747
11790
  var SavedDeviceRowSchema = object({
11748
11791
  /** Numeric id reserved at allocateDeviceId time. */
11749
11792
  id: number(),
@@ -11969,11 +12012,25 @@ method(object({
11969
12012
  projection: _enum(["full", "slim"]).optional(),
11970
12013
  /** Return only camera devices. Filtering server-side instead of
11971
12014
  * shipping 293 rows to find 12. */
11972
- isCamera: boolean().optional()
12015
+ isCamera: boolean().optional(),
12016
+ /**
12017
+ * Return only these device ids. For the caller that already KNOWS the
12018
+ * handful it wants and needs a field the id-bearing answer does not
12019
+ * carry — the viewer's linked-devices panel joins `type` and `online`
12020
+ * onto ~8 linked ids and, unfiltered, dragged the fleet across to do
12021
+ * it: 433 KB slim / 958 KB full for 967 devices, on a query that
12022
+ * refetches on the reconcile interval, on a phone.
12023
+ *
12024
+ * Safe to send at a hub that predates it: Zod STRIPS unknown input
12025
+ * keys rather than rejecting them (verified against the live hub
12026
+ * 2026-08-25 — 967 rows came back), so an old hub answers exactly what
12027
+ * it answers today and the caller filters as it already does.
12028
+ */
12029
+ deviceIds: array(number()).optional()
11973
12030
  }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
11974
12031
  mode: LinkedDevicesModeSchema,
11975
12032
  devices: array(LinkedDeviceSchema)
11976
- })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
12033
+ })), 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({
11977
12034
  deviceId: number(),
11978
12035
  values: record(string(), unknown())
11979
12036
  }), object({ success: literal(true) }), {
@@ -12000,25 +12057,7 @@ method(object({
12000
12057
  }), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
12001
12058
  kind: "mutation",
12002
12059
  auth: "admin"
12003
- }), method(object({ deviceId: number() }), object({
12004
- deviceId: number(),
12005
- entries: array(object({
12006
- capName: string(),
12007
- kind: _enum(["native", "wrapped"]),
12008
- providerAddonId: string(),
12009
- providerNodeId: string(),
12010
- nativeAddonId: string()
12011
- }))
12012
- })), method(object({}), array(object({
12013
- deviceId: number(),
12014
- entries: array(object({
12015
- capName: string(),
12016
- kind: _enum(["native", "wrapped"]),
12017
- providerAddonId: string(),
12018
- providerNodeId: string(),
12019
- nativeAddonId: string()
12020
- }))
12021
- }))), method(object({
12060
+ }), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
12022
12061
  deviceId: number(),
12023
12062
  capName: string(),
12024
12063
  wrapperAddonId: string(),
@@ -14390,12 +14429,15 @@ var NcOccupancyConditionSchema = object({
14390
14429
  * there is no second switch that can disagree with the first and every rule
14391
14430
  * authored before the decision migrates for free (`audioModeOf`):
14392
14431
  *
14393
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14394
- * classifier labels with one of them. No window, no percentage:
14395
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14396
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14397
- * the analyzer's (`classificationMinScore`, per device) a label only
14398
- * reaches this condition if the classifier was already confident enough.
14432
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14433
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14434
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14435
+ * frames is the wrong question for a classifier that labels 1–3 frames
14436
+ * per episode. The count window is the brake that drops a single-frame
14437
+ * false positive; the rule's own `throttle` cooldown is the other. The
14438
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14439
+ * per device) — a label only reaches this condition if the classifier was
14440
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14399
14441
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14400
14442
  * the condition: at least `hitPercent`% of the samples over
14401
14443
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14422,14 +14464,22 @@ var NcOccupancyConditionSchema = object({
14422
14464
  * an operator who typed `dog` mean the same thing.
14423
14465
  */
14424
14466
  var NcAudioConditionSchema = object({
14425
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14467
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14426
14468
  labels: array(string().min(1)).min(1).optional(),
14427
14469
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14428
14470
  dbThreshold: number().min(-96).max(0).optional(),
14429
14471
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14430
14472
  hitPercent: number().int().min(1).max(100).default(60),
14431
14473
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14432
- samplingSeconds: number().int().min(1).max(300).default(10)
14474
+ samplingSeconds: number().int().min(1).max(300).default(10),
14475
+ /**
14476
+ * LABEL MODE: how many labelled frames must land inside
14477
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14478
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14479
+ */
14480
+ confirmHits: number().int().min(1).max(20).optional(),
14481
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14482
+ confirmWindowSec: number().int().min(1).max(60).optional()
14433
14483
  });
14434
14484
  /**
14435
14485
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -16803,6 +16853,46 @@ var RecentTracksPageSchema = object({
16803
16853
  /** Cursor for the next page, or null when this page is the last. */
16804
16854
  nextCursor: string().nullable()
16805
16855
  });
16856
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
16857
+ var LIST_GROUPS_MAX_LIMIT = 100;
16858
+ var AnalyticsGroupRecordSchema = object({
16859
+ id: string(),
16860
+ deviceId: number().int(),
16861
+ openedAt: number().int(),
16862
+ closedAt: number().int(),
16863
+ timestamp: number().int(),
16864
+ memberCount: number().int(),
16865
+ memberTrackIds: array(string()).readonly(),
16866
+ className: string(),
16867
+ classes: array(string()).readonly(),
16868
+ /** Relative event-media path, or null when the group has no picture yet. */
16869
+ mediaUrl: string().nullable(),
16870
+ singleton: boolean()
16871
+ });
16872
+ var AnalyticsGroupMemberSchema = object({
16873
+ trackId: string(),
16874
+ deviceId: number().int(),
16875
+ className: string(),
16876
+ firstSeen: number().int(),
16877
+ lastSeen: number().int(),
16878
+ mediaUrl: string().nullable()
16879
+ });
16880
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
16881
+ var ListGroupsQueryInput = object({
16882
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
16883
+ deviceIds: array(number()),
16884
+ /** Window lower bound on `closedAt` (inclusive). */
16885
+ since: number().optional(),
16886
+ /** Window upper bound on `openedAt` (inclusive). */
16887
+ until: number().optional(),
16888
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
16889
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
16890
+ cursor: string().optional()
16891
+ });
16892
+ var ListGroupsPageSchema = object({
16893
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
16894
+ nextCursor: string().nullable()
16895
+ });
16806
16896
  var KeyEventQueryInput = object({
16807
16897
  deviceId: number(),
16808
16898
  /** Window lower bound (track firstSeen ≥ since). */
@@ -16878,7 +16968,9 @@ var TrackCascadeCountsSchema = object({
16878
16968
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
16879
16969
  plates: number().int(),
16880
16970
  /** Per-track CLIP search vectors removed (best-effort). */
16881
- embeddings: number().int()
16971
+ embeddings: number().int(),
16972
+ /** Group membership + group rows removed with their last member (best-effort). */
16973
+ groups: number().int()
16882
16974
  });
16883
16975
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
16884
16976
  var DiskReconcileCountsSchema = object({
@@ -17024,7 +17116,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17024
17116
  * stationary registry). Default false: the timeline lists passages,
17025
17117
  * not parking records (operator decision, 2026-08-15). */
17026
17118
  includeStationary: boolean().optional()
17027
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17119
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17120
+ deviceId: number(),
17121
+ groupId: string().min(1)
17122
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17028
17123
  kind: "mutation",
17029
17124
  auth: "admin"
17030
17125
  }), 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({
@@ -17334,7 +17429,8 @@ var PipelineModelOptionSchema = object({
17334
17429
  sizeMB: number()
17335
17430
  })),
17336
17431
  group: ModelVariantGroupSchema.optional(),
17337
- legacy: boolean().optional()
17432
+ legacy: boolean().optional(),
17433
+ provider: ModelProviderIdSchema.optional()
17338
17434
  });
17339
17435
  var ConfigFieldBridge = custom();
17340
17436
  var PipelineAddonSchemaSchema = object({
@@ -23963,7 +24059,12 @@ var PlateInfoSchema = object({
23963
24059
  plateBbox: BoundingBoxSchema.optional(),
23964
24060
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
23965
24061
  keyFrameMediaKey: string().optional(),
23966
- base64: string().optional()
24062
+ base64: string().optional(),
24063
+ /**
24064
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
24065
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
24066
+ */
24067
+ cropUrl: string().optional()
23967
24068
  });
23968
24069
  var MediaFileLiteSchema = object({
23969
24070
  key: string(),
@@ -27603,6 +27704,12 @@ Object.freeze({
27603
27704
  addonId: null,
27604
27705
  access: "view"
27605
27706
  },
27707
+ "deviceManager.getBindingsBatch": {
27708
+ capName: "device-manager",
27709
+ capScope: "system",
27710
+ addonId: null,
27711
+ access: "view"
27712
+ },
27606
27713
  "deviceManager.getChildren": {
27607
27714
  capName: "device-manager",
27608
27715
  capScope: "system",
@@ -27663,6 +27770,12 @@ Object.freeze({
27663
27770
  addonId: null,
27664
27771
  access: "view"
27665
27772
  },
27773
+ "deviceManager.getLinkedDevicesBatch": {
27774
+ capName: "device-manager",
27775
+ capScope: "system",
27776
+ addonId: null,
27777
+ access: "view"
27778
+ },
27666
27779
  "deviceManager.getRoleDisplayDefaults": {
27667
27780
  capName: "device-manager",
27668
27781
  capScope: "system",
@@ -29433,6 +29546,12 @@ Object.freeze({
29433
29546
  addonId: null,
29434
29547
  access: "view"
29435
29548
  },
29549
+ "pipelineAnalytics.getGroup": {
29550
+ capName: "pipeline-analytics",
29551
+ capScope: "device",
29552
+ addonId: null,
29553
+ access: "view"
29554
+ },
29436
29555
  "pipelineAnalytics.getKeyEvents": {
29437
29556
  capName: "pipeline-analytics",
29438
29557
  capScope: "device",
@@ -29517,6 +29636,12 @@ Object.freeze({
29517
29636
  addonId: null,
29518
29637
  access: "view"
29519
29638
  },
29639
+ "pipelineAnalytics.listGroups": {
29640
+ capName: "pipeline-analytics",
29641
+ capScope: "device",
29642
+ addonId: null,
29643
+ access: "view"
29644
+ },
29520
29645
  "pipelineAnalytics.listOpsLog": {
29521
29646
  capName: "pipeline-analytics",
29522
29647
  capScope: "device",
@@ -32298,6 +32423,11 @@ Object.freeze({
32298
32423
  form: "single",
32299
32424
  optional: false
32300
32425
  }],
32426
+ "deviceManager.getBindingsBatch": [{
32427
+ name: "deviceIds",
32428
+ form: "array",
32429
+ optional: false
32430
+ }],
32301
32431
  "deviceManager.getChildren": [{
32302
32432
  name: "parentDeviceId",
32303
32433
  form: "single",
@@ -32343,6 +32473,11 @@ Object.freeze({
32343
32473
  form: "single",
32344
32474
  optional: false
32345
32475
  }],
32476
+ "deviceManager.getLinkedDevicesBatch": [{
32477
+ name: "deviceIds",
32478
+ form: "array",
32479
+ optional: false
32480
+ }],
32346
32481
  "deviceManager.getSettingsSchema": [{
32347
32482
  name: "deviceId",
32348
32483
  form: "single",
@@ -32363,6 +32498,11 @@ Object.freeze({
32363
32498
  form: "single",
32364
32499
  optional: false
32365
32500
  }],
32501
+ "deviceManager.listAll": [{
32502
+ name: "deviceIds",
32503
+ form: "array",
32504
+ optional: true
32505
+ }],
32366
32506
  "deviceManager.loadConfig": [{
32367
32507
  name: "deviceId",
32368
32508
  form: "single",
@@ -32936,6 +33076,11 @@ Object.freeze({
32936
33076
  form: "single",
32937
33077
  optional: false
32938
33078
  }],
33079
+ "pipelineAnalytics.getGroup": [{
33080
+ name: "deviceId",
33081
+ form: "single",
33082
+ optional: false
33083
+ }],
32939
33084
  "pipelineAnalytics.getKeyEvents": [{
32940
33085
  name: "deviceId",
32941
33086
  form: "single",
@@ -32991,6 +33136,11 @@ Object.freeze({
32991
33136
  form: "array",
32992
33137
  optional: false
32993
33138
  }],
33139
+ "pipelineAnalytics.listGroups": [{
33140
+ name: "deviceIds",
33141
+ form: "array",
33142
+ optional: false
33143
+ }],
32994
33144
  "pipelineAnalytics.listOpsLog": [{
32995
33145
  name: "deviceId",
32996
33146
  form: "single",
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_shared = require("./shared-UN8idVfB.js");
5
+ const require_shared = require("./shared-DSlbJzYc.js");
6
6
  let node_stream = require("node:stream");
7
7
  let webdav = require("webdav");
8
8
  //#region src/providers/webdav/webdav-config-schema.ts
@@ -1,4 +1,4 @@
1
- import { a as safeJoinRemotePath, c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as storageProviderCapability, t as createSessionId } from "./shared-BwialOVX.mjs";
1
+ import { a as safeJoinRemotePath, c as BaseAddon, i as rearmIdleAbort, o as scheduleIdleAbort, r as getRequiredBasePath, s as storageProviderCapability, t as createSessionId } from "./shared-3YApqgLa.mjs";
2
2
  import { PassThrough } from "node:stream";
3
3
  import { AuthType, createClient } from "webdav";
4
4
  //#region src/providers/webdav/webdav-config-schema.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-remote-storage",
3
- "version": "1.2.26",
3
+ "version": "1.2.27",
4
4
  "description": "Remote storage providers (SFTP, S3, WebDAV) — unifies remote backends behind the storage-provider cap",
5
5
  "keywords": [
6
6
  "camstack",