@camstack/addon-cloudflare 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.
@@ -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
  }
@@ -8095,6 +8105,12 @@ var ModelVariantGroupSchema = object({
8095
8105
  */
8096
8106
  resolution: number().int().positive().optional()
8097
8107
  });
8108
+ var ModelProviderIdSchema = _enum([
8109
+ "camstack",
8110
+ "frigate",
8111
+ "scrypted",
8112
+ "custom"
8113
+ ]);
8098
8114
  var ModelCatalogEntrySchema = object({
8099
8115
  id: string(),
8100
8116
  name: string(),
@@ -8192,6 +8208,12 @@ var ModelCatalogEntrySchema = object({
8192
8208
  */
8193
8209
  group: ModelVariantGroupSchema.optional(),
8194
8210
  /**
8211
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8212
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8213
+ * persisted before this field existed (`inferModelProvider` fills those).
8214
+ */
8215
+ provider: ModelProviderIdSchema.optional(),
8216
+ /**
8195
8217
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8196
8218
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8197
8219
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -14404,12 +14426,15 @@ var NcOccupancyConditionSchema = object({
14404
14426
  * there is no second switch that can disagree with the first and every rule
14405
14427
  * authored before the decision migrates for free (`audioModeOf`):
14406
14428
  *
14407
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14408
- * classifier labels with one of them. No window, no percentage:
14409
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14410
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14411
- * the analyzer's (`classificationMinScore`, per device) a label only
14412
- * reaches this condition if the classifier was already confident enough.
14429
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14430
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14431
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14432
+ * frames is the wrong question for a classifier that labels 1–3 frames
14433
+ * per episode. The count window is the brake that drops a single-frame
14434
+ * false positive; the rule's own `throttle` cooldown is the other. The
14435
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14436
+ * per device) — a label only reaches this condition if the classifier was
14437
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14413
14438
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14414
14439
  * the condition: at least `hitPercent`% of the samples over
14415
14440
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14436,14 +14461,22 @@ var NcOccupancyConditionSchema = object({
14436
14461
  * an operator who typed `dog` mean the same thing.
14437
14462
  */
14438
14463
  var NcAudioConditionSchema = object({
14439
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14464
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14440
14465
  labels: array(string().min(1)).min(1).optional(),
14441
14466
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14442
14467
  dbThreshold: number().min(-96).max(0).optional(),
14443
14468
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14444
14469
  hitPercent: number().int().min(1).max(100).default(60),
14445
14470
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14446
- samplingSeconds: number().int().min(1).max(300).default(10)
14471
+ samplingSeconds: number().int().min(1).max(300).default(10),
14472
+ /**
14473
+ * LABEL MODE: how many labelled frames must land inside
14474
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14475
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14476
+ */
14477
+ confirmHits: number().int().min(1).max(20).optional(),
14478
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14479
+ confirmWindowSec: number().int().min(1).max(60).optional()
14447
14480
  });
14448
14481
  /**
14449
14482
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -16817,6 +16850,46 @@ var RecentTracksPageSchema = object({
16817
16850
  /** Cursor for the next page, or null when this page is the last. */
16818
16851
  nextCursor: string().nullable()
16819
16852
  });
16853
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
16854
+ var LIST_GROUPS_MAX_LIMIT = 100;
16855
+ var AnalyticsGroupRecordSchema = object({
16856
+ id: string(),
16857
+ deviceId: number().int(),
16858
+ openedAt: number().int(),
16859
+ closedAt: number().int(),
16860
+ timestamp: number().int(),
16861
+ memberCount: number().int(),
16862
+ memberTrackIds: array(string()).readonly(),
16863
+ className: string(),
16864
+ classes: array(string()).readonly(),
16865
+ /** Relative event-media path, or null when the group has no picture yet. */
16866
+ mediaUrl: string().nullable(),
16867
+ singleton: boolean()
16868
+ });
16869
+ var AnalyticsGroupMemberSchema = object({
16870
+ trackId: string(),
16871
+ deviceId: number().int(),
16872
+ className: string(),
16873
+ firstSeen: number().int(),
16874
+ lastSeen: number().int(),
16875
+ mediaUrl: string().nullable()
16876
+ });
16877
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
16878
+ var ListGroupsQueryInput = object({
16879
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
16880
+ deviceIds: array(number()),
16881
+ /** Window lower bound on `closedAt` (inclusive). */
16882
+ since: number().optional(),
16883
+ /** Window upper bound on `openedAt` (inclusive). */
16884
+ until: number().optional(),
16885
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
16886
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
16887
+ cursor: string().optional()
16888
+ });
16889
+ var ListGroupsPageSchema = object({
16890
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
16891
+ nextCursor: string().nullable()
16892
+ });
16820
16893
  var KeyEventQueryInput = object({
16821
16894
  deviceId: number(),
16822
16895
  /** Window lower bound (track firstSeen ≥ since). */
@@ -16892,7 +16965,9 @@ var TrackCascadeCountsSchema = object({
16892
16965
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
16893
16966
  plates: number().int(),
16894
16967
  /** Per-track CLIP search vectors removed (best-effort). */
16895
- embeddings: number().int()
16968
+ embeddings: number().int(),
16969
+ /** Group membership + group rows removed with their last member (best-effort). */
16970
+ groups: number().int()
16896
16971
  });
16897
16972
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
16898
16973
  var DiskReconcileCountsSchema = object({
@@ -17038,7 +17113,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17038
17113
  * stationary registry). Default false: the timeline lists passages,
17039
17114
  * not parking records (operator decision, 2026-08-15). */
17040
17115
  includeStationary: boolean().optional()
17041
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17116
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17117
+ deviceId: number(),
17118
+ groupId: string().min(1)
17119
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17042
17120
  kind: "mutation",
17043
17121
  auth: "admin"
17044
17122
  }), 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({
@@ -17348,7 +17426,8 @@ var PipelineModelOptionSchema = object({
17348
17426
  sizeMB: number()
17349
17427
  })),
17350
17428
  group: ModelVariantGroupSchema.optional(),
17351
- legacy: boolean().optional()
17429
+ legacy: boolean().optional(),
17430
+ provider: ModelProviderIdSchema.optional()
17352
17431
  });
17353
17432
  var ConfigFieldBridge = custom();
17354
17433
  var PipelineAddonSchemaSchema = object({
@@ -23955,7 +24034,12 @@ var PlateInfoSchema = object({
23955
24034
  plateBbox: BoundingBoxSchema.optional(),
23956
24035
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
23957
24036
  keyFrameMediaKey: string().optional(),
23958
- base64: string().optional()
24037
+ base64: string().optional(),
24038
+ /**
24039
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
24040
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
24041
+ */
24042
+ cropUrl: string().optional()
23959
24043
  });
23960
24044
  var MediaFileLiteSchema = object({
23961
24045
  key: string(),
@@ -29425,6 +29509,12 @@ Object.freeze({
29425
29509
  addonId: null,
29426
29510
  access: "view"
29427
29511
  },
29512
+ "pipelineAnalytics.getGroup": {
29513
+ capName: "pipeline-analytics",
29514
+ capScope: "device",
29515
+ addonId: null,
29516
+ access: "view"
29517
+ },
29428
29518
  "pipelineAnalytics.getKeyEvents": {
29429
29519
  capName: "pipeline-analytics",
29430
29520
  capScope: "device",
@@ -29509,6 +29599,12 @@ Object.freeze({
29509
29599
  addonId: null,
29510
29600
  access: "view"
29511
29601
  },
29602
+ "pipelineAnalytics.listGroups": {
29603
+ capName: "pipeline-analytics",
29604
+ capScope: "device",
29605
+ addonId: null,
29606
+ access: "view"
29607
+ },
29512
29608
  "pipelineAnalytics.listOpsLog": {
29513
29609
  capName: "pipeline-analytics",
29514
29610
  capScope: "device",
@@ -32928,6 +33024,11 @@ Object.freeze({
32928
33024
  form: "single",
32929
33025
  optional: false
32930
33026
  }],
33027
+ "pipelineAnalytics.getGroup": [{
33028
+ name: "deviceId",
33029
+ form: "single",
33030
+ optional: false
33031
+ }],
32931
33032
  "pipelineAnalytics.getKeyEvents": [{
32932
33033
  name: "deviceId",
32933
33034
  form: "single",
@@ -32983,6 +33084,11 @@ Object.freeze({
32983
33084
  form: "array",
32984
33085
  optional: false
32985
33086
  }],
33087
+ "pipelineAnalytics.listGroups": [{
33088
+ name: "deviceIds",
33089
+ form: "array",
33090
+ optional: false
33091
+ }],
32986
33092
  "pipelineAnalytics.listOpsLog": [{
32987
33093
  name: "deviceId",
32988
33094
  form: "single",
@@ -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
  }
@@ -8095,6 +8105,12 @@ var ModelVariantGroupSchema = object({
8095
8105
  */
8096
8106
  resolution: number().int().positive().optional()
8097
8107
  });
8108
+ var ModelProviderIdSchema = _enum([
8109
+ "camstack",
8110
+ "frigate",
8111
+ "scrypted",
8112
+ "custom"
8113
+ ]);
8098
8114
  var ModelCatalogEntrySchema = object({
8099
8115
  id: string(),
8100
8116
  name: string(),
@@ -8192,6 +8208,12 @@ var ModelCatalogEntrySchema = object({
8192
8208
  */
8193
8209
  group: ModelVariantGroupSchema.optional(),
8194
8210
  /**
8211
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
8212
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
8213
+ * persisted before this field existed (`inferModelProvider` fills those).
8214
+ */
8215
+ provider: ModelProviderIdSchema.optional(),
8216
+ /**
8195
8217
  * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8196
8218
  * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8197
8219
  * labels already ARE the CamStack macros (Scrypted identity map).
@@ -14404,12 +14426,15 @@ var NcOccupancyConditionSchema = object({
14404
14426
  * there is no second switch that can disagree with the first and every rule
14405
14427
  * authored before the decision migrates for free (`audioModeOf`):
14406
14428
  *
14407
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
14408
- * classifier labels with one of them. No window, no percentage:
14409
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
14410
- * `throttle` cooldown is the only brake. The per-label confidence floor is
14411
- * the analyzer's (`classificationMinScore`, per device) a label only
14412
- * reaches this condition if the classifier was already confident enough.
14429
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
14430
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
14431
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
14432
+ * frames is the wrong question for a classifier that labels 1–3 frames
14433
+ * per episode. The count window is the brake that drops a single-frame
14434
+ * false positive; the rule's own `throttle` cooldown is the other. The
14435
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
14436
+ * per device) — a label only reaches this condition if the classifier was
14437
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
14413
14438
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
14414
14439
  * the condition: at least `hitPercent`% of the samples over
14415
14440
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -14436,14 +14461,22 @@ var NcOccupancyConditionSchema = object({
14436
14461
  * an operator who typed `dog` mean the same thing.
14437
14462
  */
14438
14463
  var NcAudioConditionSchema = object({
14439
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
14464
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
14440
14465
  labels: array(string().min(1)).min(1).optional(),
14441
14466
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
14442
14467
  dbThreshold: number().min(-96).max(0).optional(),
14443
14468
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
14444
14469
  hitPercent: number().int().min(1).max(100).default(60),
14445
14470
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
14446
- samplingSeconds: number().int().min(1).max(300).default(10)
14471
+ samplingSeconds: number().int().min(1).max(300).default(10),
14472
+ /**
14473
+ * LABEL MODE: how many labelled frames must land inside
14474
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
14475
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
14476
+ */
14477
+ confirmHits: number().int().min(1).max(20).optional(),
14478
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
14479
+ confirmWindowSec: number().int().min(1).max(60).optional()
14447
14480
  });
14448
14481
  /**
14449
14482
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -16817,6 +16850,46 @@ var RecentTracksPageSchema = object({
16817
16850
  /** Cursor for the next page, or null when this page is the last. */
16818
16851
  nextCursor: string().nullable()
16819
16852
  });
16853
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
16854
+ var LIST_GROUPS_MAX_LIMIT = 100;
16855
+ var AnalyticsGroupRecordSchema = object({
16856
+ id: string(),
16857
+ deviceId: number().int(),
16858
+ openedAt: number().int(),
16859
+ closedAt: number().int(),
16860
+ timestamp: number().int(),
16861
+ memberCount: number().int(),
16862
+ memberTrackIds: array(string()).readonly(),
16863
+ className: string(),
16864
+ classes: array(string()).readonly(),
16865
+ /** Relative event-media path, or null when the group has no picture yet. */
16866
+ mediaUrl: string().nullable(),
16867
+ singleton: boolean()
16868
+ });
16869
+ var AnalyticsGroupMemberSchema = object({
16870
+ trackId: string(),
16871
+ deviceId: number().int(),
16872
+ className: string(),
16873
+ firstSeen: number().int(),
16874
+ lastSeen: number().int(),
16875
+ mediaUrl: string().nullable()
16876
+ });
16877
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: array(AnalyticsGroupMemberSchema).readonly() });
16878
+ var ListGroupsQueryInput = object({
16879
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
16880
+ deviceIds: array(number()),
16881
+ /** Window lower bound on `closedAt` (inclusive). */
16882
+ since: number().optional(),
16883
+ /** Window upper bound on `openedAt` (inclusive). */
16884
+ until: number().optional(),
16885
+ limit: number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
16886
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
16887
+ cursor: string().optional()
16888
+ });
16889
+ var ListGroupsPageSchema = object({
16890
+ groups: array(AnalyticsGroupRecordSchema).readonly(),
16891
+ nextCursor: string().nullable()
16892
+ });
16820
16893
  var KeyEventQueryInput = object({
16821
16894
  deviceId: number(),
16822
16895
  /** Window lower bound (track firstSeen ≥ since). */
@@ -16892,7 +16965,9 @@ var TrackCascadeCountsSchema = object({
16892
16965
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
16893
16966
  plates: number().int(),
16894
16967
  /** Per-track CLIP search vectors removed (best-effort). */
16895
- embeddings: number().int()
16968
+ embeddings: number().int(),
16969
+ /** Group membership + group rows removed with their last member (best-effort). */
16970
+ groups: number().int()
16896
16971
  });
16897
16972
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
16898
16973
  var DiskReconcileCountsSchema = object({
@@ -17038,7 +17113,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17038
17113
  * stationary registry). Default false: the timeline lists passages,
17039
17114
  * not parking records (operator decision, 2026-08-15). */
17040
17115
  includeStationary: boolean().optional()
17041
- }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
17116
+ }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(ListGroupsQueryInput, ListGroupsPageSchema), method(object({
17117
+ deviceId: number(),
17118
+ groupId: string().min(1)
17119
+ }), AnalyticsGroupDetailSchema.nullable()), method(object({ deviceId: number() }), _void(), {
17042
17120
  kind: "mutation",
17043
17121
  auth: "admin"
17044
17122
  }), 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({
@@ -17348,7 +17426,8 @@ var PipelineModelOptionSchema = object({
17348
17426
  sizeMB: number()
17349
17427
  })),
17350
17428
  group: ModelVariantGroupSchema.optional(),
17351
- legacy: boolean().optional()
17429
+ legacy: boolean().optional(),
17430
+ provider: ModelProviderIdSchema.optional()
17352
17431
  });
17353
17432
  var ConfigFieldBridge = custom();
17354
17433
  var PipelineAddonSchemaSchema = object({
@@ -23955,7 +24034,12 @@ var PlateInfoSchema = object({
23955
24034
  plateBbox: BoundingBoxSchema.optional(),
23956
24035
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
23957
24036
  keyFrameMediaKey: string().optional(),
23958
- base64: string().optional()
24037
+ base64: string().optional(),
24038
+ /**
24039
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
24040
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
24041
+ */
24042
+ cropUrl: string().optional()
23959
24043
  });
23960
24044
  var MediaFileLiteSchema = object({
23961
24045
  key: string(),
@@ -29425,6 +29509,12 @@ Object.freeze({
29425
29509
  addonId: null,
29426
29510
  access: "view"
29427
29511
  },
29512
+ "pipelineAnalytics.getGroup": {
29513
+ capName: "pipeline-analytics",
29514
+ capScope: "device",
29515
+ addonId: null,
29516
+ access: "view"
29517
+ },
29428
29518
  "pipelineAnalytics.getKeyEvents": {
29429
29519
  capName: "pipeline-analytics",
29430
29520
  capScope: "device",
@@ -29509,6 +29599,12 @@ Object.freeze({
29509
29599
  addonId: null,
29510
29600
  access: "view"
29511
29601
  },
29602
+ "pipelineAnalytics.listGroups": {
29603
+ capName: "pipeline-analytics",
29604
+ capScope: "device",
29605
+ addonId: null,
29606
+ access: "view"
29607
+ },
29512
29608
  "pipelineAnalytics.listOpsLog": {
29513
29609
  capName: "pipeline-analytics",
29514
29610
  capScope: "device",
@@ -32928,6 +33024,11 @@ Object.freeze({
32928
33024
  form: "single",
32929
33025
  optional: false
32930
33026
  }],
33027
+ "pipelineAnalytics.getGroup": [{
33028
+ name: "deviceId",
33029
+ form: "single",
33030
+ optional: false
33031
+ }],
32931
33032
  "pipelineAnalytics.getKeyEvents": [{
32932
33033
  name: "deviceId",
32933
33034
  form: "single",
@@ -32983,6 +33084,11 @@ Object.freeze({
32983
33084
  form: "array",
32984
33085
  optional: false
32985
33086
  }],
33087
+ "pipelineAnalytics.listGroups": [{
33088
+ name: "deviceIds",
33089
+ form: "array",
33090
+ optional: false
33091
+ }],
32986
33092
  "pipelineAnalytics.listOpsLog": [{
32987
33093
  name: "deviceId",
32988
33094
  form: "single",
@@ -21,7 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
- const require_dist = require("../dist-CbCaiEpw.js");
24
+ const require_dist = require("../dist-B3KpK67G.js");
25
25
  let node_crypto = require("node:crypto");
26
26
  let node_path = require("node:path");
27
27
  node_path = __toESM(node_path);
@@ -758,6 +758,24 @@ var cloudflareTunnelActions = require_dist.defineCustomActions({
758
758
  })).readonly() }))
759
759
  });
760
760
  //#endregion
761
+ //#region src/tunnel/auto-start.ts
762
+ /**
763
+ * Whether cloudflare-tunnel may spawn `cloudflared` at boot / config-reload.
764
+ *
765
+ * Constructor defaults are `mode: 'quick'`. A settings store that has not
766
+ * answered therefore looks exactly like "the operator wants a trycloudflare
767
+ * URL". Auto-starting that is how a named hostname (the one the notification
768
+ * centre still mints) went Cloudflare 530: the named tunnel never connected.
769
+ *
770
+ * An empty store that DID answer is a real choice — a fresh install still
771
+ * auto-starts quick. Only "never answered" is refused.
772
+ */
773
+ function shouldAutoStartTunnel(input) {
774
+ if (!input.settingsStoreReady) return false;
775
+ if (input.mode === "quick") return true;
776
+ return input.hasCustomToken && input.hasCustomHostname;
777
+ }
778
+ //#endregion
761
779
  //#region src/tunnel/cloudflare-tunnel.addon.ts
762
780
  /**
763
781
  * Cloudflare Tunnel — exposes CamStack via Cloudflare's network.
@@ -882,6 +900,10 @@ var CloudflareTunnelAddon = class extends require_dist.BaseAddon {
882
900
  }
883
901
  });
884
902
  });
903
+ else if (!this.settingsStoreReady) this.ctx.logger.warn("boot auto-start skipped — settings store did not answer; defaults are not an operator choice", { tags: {
904
+ topic: "tunnel",
905
+ phase: "auto-start-skipped"
906
+ } });
885
907
  return {
886
908
  providers: [{
887
909
  capability: require_dist.networkAccessCapability,
@@ -903,8 +925,12 @@ var CloudflareTunnelAddon = class extends require_dist.BaseAddon {
903
925
  * auto-start and by `onConfigChanged` to decide whether a change
904
926
  * warrants a respawn even when no instance was previously running. */
905
927
  isRunCapable() {
906
- if (this.config.mode === "quick") return true;
907
- return !!this.config.customTunnelToken && !!this.config.customHostname;
928
+ return shouldAutoStartTunnel({
929
+ settingsStoreReady: this.settingsStoreReady,
930
+ mode: this.config.mode,
931
+ hasCustomToken: !!this.config.customTunnelToken,
932
+ hasCustomHostname: !!this.config.customHostname
933
+ });
908
934
  }
909
935
  async onShutdown() {
910
936
  if (this.service) {
@@ -1,4 +1,4 @@
1
- import { a as BaseAddon, c as boolean, d as number, f as object, m as EventCategory, n as defineCustomActions, o as _enum, p as string, r as networkAccessCapability, s as array, t as customAction, u as literal } from "../dist-DnohT9Id.mjs";
1
+ import { a as BaseAddon, c as boolean, d as number, f as object, m as EventCategory, n as defineCustomActions, o as _enum, p as string, r as networkAccessCapability, s as array, t as customAction, u as literal } from "../dist-BgYOkp3_.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as path from "node:path";
4
4
  import { spawn } from "node:child_process";
@@ -731,6 +731,24 @@ var cloudflareTunnelActions = defineCustomActions({
731
731
  })).readonly() }))
732
732
  });
733
733
  //#endregion
734
+ //#region src/tunnel/auto-start.ts
735
+ /**
736
+ * Whether cloudflare-tunnel may spawn `cloudflared` at boot / config-reload.
737
+ *
738
+ * Constructor defaults are `mode: 'quick'`. A settings store that has not
739
+ * answered therefore looks exactly like "the operator wants a trycloudflare
740
+ * URL". Auto-starting that is how a named hostname (the one the notification
741
+ * centre still mints) went Cloudflare 530: the named tunnel never connected.
742
+ *
743
+ * An empty store that DID answer is a real choice — a fresh install still
744
+ * auto-starts quick. Only "never answered" is refused.
745
+ */
746
+ function shouldAutoStartTunnel(input) {
747
+ if (!input.settingsStoreReady) return false;
748
+ if (input.mode === "quick") return true;
749
+ return input.hasCustomToken && input.hasCustomHostname;
750
+ }
751
+ //#endregion
734
752
  //#region src/tunnel/cloudflare-tunnel.addon.ts
735
753
  /**
736
754
  * Cloudflare Tunnel — exposes CamStack via Cloudflare's network.
@@ -855,6 +873,10 @@ var CloudflareTunnelAddon = class extends BaseAddon {
855
873
  }
856
874
  });
857
875
  });
876
+ else if (!this.settingsStoreReady) this.ctx.logger.warn("boot auto-start skipped — settings store did not answer; defaults are not an operator choice", { tags: {
877
+ topic: "tunnel",
878
+ phase: "auto-start-skipped"
879
+ } });
858
880
  return {
859
881
  providers: [{
860
882
  capability: networkAccessCapability,
@@ -876,8 +898,12 @@ var CloudflareTunnelAddon = class extends BaseAddon {
876
898
  * auto-start and by `onConfigChanged` to decide whether a change
877
899
  * warrants a respawn even when no instance was previously running. */
878
900
  isRunCapable() {
879
- if (this.config.mode === "quick") return true;
880
- return !!this.config.customTunnelToken && !!this.config.customHostname;
901
+ return shouldAutoStartTunnel({
902
+ settingsStoreReady: this.settingsStoreReady,
903
+ mode: this.config.mode,
904
+ hasCustomToken: !!this.config.customTunnelToken,
905
+ hasCustomHostname: !!this.config.customHostname
906
+ });
881
907
  }
882
908
  async onShutdown() {
883
909
  if (this.service) {
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_dist = require("../dist-CbCaiEpw.js");
2
+ const require_dist = require("../dist-B3KpK67G.js");
3
3
  //#region src/turn/cloudflare-turn.ts
4
4
  /**
5
5
  * Cloudflare returns ICE servers in several flavours depending on which
@@ -1,4 +1,4 @@
1
- import { a as BaseAddon, c as boolean, d as number, f as object, i as turnProviderCapability, l as discriminatedUnion, n as defineCustomActions, p as string, t as customAction, u as literal } from "../dist-DnohT9Id.mjs";
1
+ import { a as BaseAddon, c as boolean, d as number, f as object, i as turnProviderCapability, l as discriminatedUnion, n as defineCustomActions, p as string, t as customAction, u as literal } from "../dist-BgYOkp3_.mjs";
2
2
  //#region src/turn/cloudflare-turn.ts
3
3
  /**
4
4
  * Cloudflare returns ICE servers in several flavours depending on which
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-cloudflare",
3
- "version": "1.2.26",
3
+ "version": "1.2.27",
4
4
  "description": "Cloudflare bundle — Tunnel (network-access) + TURN relay (turn-provider). Multi-entry npm package shipping 2 addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",