@camstack/addon-post-analysis 1.2.669 → 1.2.671

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 (16) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/{clip-model-registry-CbzBwacH.js → clip-model-registry-BhKHkBsg.js} +573 -547
  3. package/dist/{clip-model-registry-BPvijtCz.mjs → clip-model-registry-CDLYwWsU.mjs} +568 -548
  4. package/dist/embedding-encoder/index.js +1 -1
  5. package/dist/embedding-encoder/index.mjs +1 -1
  6. package/dist/pipeline-analytics/_stub.js +2 -2
  7. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-C4QK9e1e.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CRWJsnQv.mjs} +2 -2
  8. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D0SgNf77.mjs +26 -0
  9. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DFMFx1IF.mjs +26 -0
  10. package/dist/pipeline-analytics/{hostInit-CPLvPO3K.mjs → hostInit-D503xVER.mjs} +2 -2
  11. package/dist/pipeline-analytics/index.js +252 -41
  12. package/dist/pipeline-analytics/index.mjs +252 -41
  13. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  14. package/package.json +2 -2
  15. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-9HsFj_VL.mjs +0 -26
  16. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-anSYIl3S.mjs +0 -26
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_clip_model_registry = require("../clip-model-registry-CbzBwacH.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-BhKHkBsg.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_clip_model_registry.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -16773,6 +16773,68 @@ var MediaStore = class MediaStore {
16773
16773
  };
16774
16774
  }
16775
16775
  /**
16776
+ * Rows and bytes per asked storage location, the unstamped (NULL) rows, the
16777
+ * rows naming a location that was NOT asked about, and the total.
16778
+ *
16779
+ * Same statements as {@link footprintByLocation} (one aggregate over
16780
+ * `idx_media_location_size` per location, one for NULL) plus one unfiltered
16781
+ * aggregate for the total (a covering-index scan, O(rows)); the unlisted
16782
+ * remainder is the difference, relative to the ids ASKED, so a row stamped with
16783
+ * a deleted location is counted without a distinct walk only when the caller
16784
+ * asked for every live location. A read that fails THROWS — never a zero.
16785
+ */
16786
+ async censusByLocation(locationIds) {
16787
+ const fields = [{
16788
+ as: "bytes",
16789
+ field: "sizeBytes",
16790
+ op: "sum"
16791
+ }];
16792
+ const locations = [];
16793
+ let knownRows = 0;
16794
+ let knownBytes = 0;
16795
+ for (const locationId of locationIds) {
16796
+ const agg = await this.store.aggregate.query({
16797
+ collection: MEDIA_COLLECTION,
16798
+ fields: [...fields],
16799
+ filter: { where: { locationId } }
16800
+ });
16801
+ const bytes = agg.values["bytes"] ?? 0;
16802
+ locations.push({
16803
+ locationId,
16804
+ rows: agg.count,
16805
+ bytes
16806
+ });
16807
+ knownRows += agg.count;
16808
+ knownBytes += bytes;
16809
+ }
16810
+ const nullAgg = await this.store.aggregate.query({
16811
+ collection: MEDIA_COLLECTION,
16812
+ fields: [...fields],
16813
+ filter: { where: { locationId: null } }
16814
+ });
16815
+ const totalAgg = await this.store.aggregate.query({
16816
+ collection: MEDIA_COLLECTION,
16817
+ fields: [...fields]
16818
+ });
16819
+ const nullBytes = nullAgg.values["bytes"] ?? 0;
16820
+ const totalBytes = totalAgg.values["bytes"] ?? 0;
16821
+ return {
16822
+ locations,
16823
+ unstamped: {
16824
+ rows: nullAgg.count,
16825
+ bytes: nullBytes
16826
+ },
16827
+ unlisted: {
16828
+ rows: Math.max(0, totalAgg.count - knownRows - nullAgg.count),
16829
+ bytes: Math.max(0, totalBytes - knownBytes - nullBytes)
16830
+ },
16831
+ total: {
16832
+ rows: totalAgg.count,
16833
+ bytes: totalBytes
16834
+ }
16835
+ };
16836
+ }
16837
+ /**
16776
16838
  * The media footprint broken down by KIND — fleet-wide, or for one camera.
16777
16839
  *
16778
16840
  * `footprintByDevice` answers "how much is where"; this answers "how much is
@@ -52541,7 +52603,7 @@ var FaceSettingsSchema = require_clip_model_registry.object({
52541
52603
  * `resolveGlobalFaceEnabled` / `getDeviceSettingsContribution` (the field is
52542
52604
  * stripped from the per-device schema).
52543
52605
  */
52544
- enabled: require_clip_model_registry.boolean().default(true),
52606
+ faceRecognitionEnabled: require_clip_model_registry.boolean().default(true),
52545
52607
  /**
52546
52608
  * Cosine similarity (on L2-normalized embeddings) required to match. A
52547
52609
  * property of ONE model's cosine space: it moves with the default model.
@@ -52643,7 +52705,7 @@ function resolveFaceSettings(raw) {
52643
52705
  return parsed.success ? parsed.data : FACE_DEFAULTS[key];
52644
52706
  };
52645
52707
  return {
52646
- enabled: pick("enabled"),
52708
+ faceRecognitionEnabled: pick("faceRecognitionEnabled"),
52647
52709
  similarityThreshold: pick("similarityThreshold"),
52648
52710
  margin: pick("margin"),
52649
52711
  minFacePx: pick("minFacePx"),
@@ -56662,7 +56724,117 @@ async function migrateDeviceTemplateVariable(ports) {
56662
56724
  };
56663
56725
  }
56664
56726
  //#endregion
56665
- //#region src/migrations/index.ts
56727
+ //#region src/migrations/split-enabled-key.ts
56728
+ /**
56729
+ * The shared settings key `enabled` becomes two (D987, D986 §2).
56730
+ *
56731
+ * One key meant two things: "Face recognition on" and "Suppress parked objects". Toggling one
56732
+ * flipped the other. Each switch now has its own key. So today's behaviour does not change, the
56733
+ * value an install has stored under `enabled` is copied into BOTH new keys, and `enabled` is
56734
+ * deleted. That covers the cluster row (`addon-settings`) and every camera's own row
56735
+ * (`addon-device-settings`).
56736
+ *
56737
+ * A new key that already has a row is never overwritten (a seed never overwrites): it was written
56738
+ * by the build that reads it. The plan has nothing left to do once `enabled` is gone, so a rerun is
56739
+ * a no-op. A read that fails throws and nothing is written.
56740
+ */
56741
+ var PIPELINE_ANALYTICS_ID = "pipeline-analytics";
56742
+ var RETIRED_ENABLED_KEY = "enabled";
56743
+ var NEW_KEYS = ["faceRecognitionEnabled", "stationarySuppressionEnabled"];
56744
+ /** The canonical settings table: one row per key, id `<addonId>.<key>`. */
56745
+ var SETTINGS_TABLE = "addon-settings";
56746
+ /** The per-device settings table: one row per field, id `<addonId>:<deviceId>.<field>`. */
56747
+ var DEVICE_SETTINGS_TABLE = "addon-device-settings";
56748
+ function clusterEdits(stored) {
56749
+ if (!(RETIRED_ENABLED_KEY in stored)) return [];
56750
+ const value = stored[RETIRED_ENABLED_KEY];
56751
+ const rowId = (key) => `${PIPELINE_ANALYTICS_ID}.${key}`;
56752
+ const missing = NEW_KEYS.filter((key) => !(key in stored));
56753
+ const edits = [];
56754
+ if (missing.length > 0) edits.push({
56755
+ op: "setKeys",
56756
+ scope: SETTINGS_TABLE,
56757
+ values: Object.fromEntries(missing.map((key) => [rowId(key), {
56758
+ addonId: PIPELINE_ANALYTICS_ID,
56759
+ key,
56760
+ value
56761
+ }])),
56762
+ precondition: require_clip_model_registry.preconditionOf(Object.fromEntries(missing.map((key) => [rowId(key), null])))
56763
+ });
56764
+ edits.push({
56765
+ op: "deleteKeys",
56766
+ scope: SETTINGS_TABLE,
56767
+ keys: [rowId(RETIRED_ENABLED_KEY)],
56768
+ precondition: require_clip_model_registry.preconditionOf({ [rowId(RETIRED_ENABLED_KEY)]: {
56769
+ addonId: PIPELINE_ANALYTICS_ID,
56770
+ key: RETIRED_ENABLED_KEY,
56771
+ value
56772
+ } })
56773
+ });
56774
+ return edits;
56775
+ }
56776
+ async function deviceEdits(ports) {
56777
+ const ids = await ports.listDeviceIds();
56778
+ if (ids.length === 0) return [];
56779
+ const retired = await ports.readDeviceField(ids, RETIRED_ENABLED_KEY);
56780
+ if (retired.size === 0) return [];
56781
+ const carriers = [...retired.keys()];
56782
+ const existing = /* @__PURE__ */ new Map();
56783
+ for (const key of NEW_KEYS) existing.set(key, await ports.readDeviceField(carriers, key));
56784
+ const rowId = (deviceId, field) => `${PIPELINE_ANALYTICS_ID}:${deviceId}.${field}`;
56785
+ const created = {};
56786
+ const retiredRows = {};
56787
+ for (const [deviceId, value] of retired) {
56788
+ for (const key of NEW_KEYS) {
56789
+ if (existing.get(key)?.has(deviceId) === true) continue;
56790
+ created[rowId(deviceId, key)] = {
56791
+ addonId: PIPELINE_ANALYTICS_ID,
56792
+ deviceId: String(deviceId),
56793
+ key,
56794
+ value
56795
+ };
56796
+ }
56797
+ retiredRows[rowId(deviceId, RETIRED_ENABLED_KEY)] = {
56798
+ addonId: PIPELINE_ANALYTICS_ID,
56799
+ deviceId: String(deviceId),
56800
+ key: RETIRED_ENABLED_KEY,
56801
+ value
56802
+ };
56803
+ }
56804
+ const edits = [];
56805
+ const createdIds = Object.keys(created);
56806
+ if (createdIds.length > 0) edits.push({
56807
+ op: "setKeys",
56808
+ scope: DEVICE_SETTINGS_TABLE,
56809
+ values: created,
56810
+ precondition: require_clip_model_registry.preconditionOf(Object.fromEntries(createdIds.map((id) => [id, null])))
56811
+ });
56812
+ edits.push({
56813
+ op: "deleteKeys",
56814
+ scope: DEVICE_SETTINGS_TABLE,
56815
+ keys: Object.keys(retiredRows),
56816
+ precondition: require_clip_model_registry.preconditionOf(retiredRows)
56817
+ });
56818
+ return edits;
56819
+ }
56820
+ /** Copy `enabled` into both new keys (cluster row and every camera's row) and delete it. */
56821
+ async function planSplitEnabledKey(ports) {
56822
+ return { edits: [...clusterEdits(await ports.readStored([RETIRED_ENABLED_KEY, ...NEW_KEYS])), ...await deviceEdits(ports)] };
56823
+ }
56824
+ /**
56825
+ * The port's read over an addon's three-way key read: each of `keys` that has a row, raw. A key
56826
+ * the store could not answer for throws, so the migration fails instead of treating a failed read
56827
+ * as "nothing to move".
56828
+ */
56829
+ async function readRawKeys(read, keys) {
56830
+ const found = {};
56831
+ for (const key of keys) {
56832
+ const answer = await read(key);
56833
+ if (answer.kind === "unreadable") throw new Error(`${PIPELINE_ANALYTICS_ID}: the setting ${key} cannot be read (${answer.reason})`);
56834
+ if (answer.kind === "present") found[key] = answer.value;
56835
+ }
56836
+ return found;
56837
+ }
56666
56838
  /** The `pipeline-analytics` catalogue (D987): offered to the engine at the hub's boot. */
56667
56839
  var ADDON_MIGRATIONS = [{
56668
56840
  meta: {
@@ -56679,6 +56851,21 @@ var ADDON_MIGRATIONS = [{
56679
56851
  fixtureMark: "the rule name and the trigger conditions the migration does not know"
56680
56852
  },
56681
56853
  procedure: migrateDeviceTemplateVariable
56854
+ }, {
56855
+ meta: {
56856
+ id: "pipeline-analytics/2026-11-05-split-enabled-key",
56857
+ owner: PIPELINE_ANALYTICS_ID,
56858
+ level: "addon-state",
56859
+ seq: 2,
56860
+ dependsOn: [],
56861
+ store: "settings",
56862
+ scope: "cluster",
56863
+ onFailure: "block-owner",
56864
+ bodyKind: "plan",
56865
+ description: "Split the shared `enabled` setting into faceRecognitionEnabled and stationarySuppressionEnabled, copying the stored value into both, for the cluster and for every camera",
56866
+ fixtureMark: "a setting the migration does not know"
56867
+ },
56868
+ plan: planSplitEnabledKey
56682
56869
  }];
56683
56870
  //#endregion
56684
56871
  //#region src/pipeline-analytics/clip-space-plan.ts
@@ -71545,7 +71732,6 @@ Object.freeze({
71545
71732
  "addon-routes": ["getRoutes"],
71546
71733
  "addon-widgets-source": ["listWidgets"],
71547
71734
  "board-key-source": ["listSources"],
71548
- "broker": ["list", "listProviders"],
71549
71735
  "calendar-source": ["listCalendars", "listEvents"],
71550
71736
  "custom-model-registry": ["listModels"],
71551
71737
  "data-store-provider": ["histogram", "query"],
@@ -72092,7 +72278,7 @@ var SensorTransitionGate = class {
72092
72278
  var StationarySettingsSchema = require_clip_model_registry.object({
72093
72279
  /** Master switch. Off ⇒ this camera gathers no evidence and suppresses
72094
72280
  * nothing (every parked object keeps spawning normal tracks). */
72095
- enabled: require_clip_model_registry.boolean().default(true),
72281
+ stationarySuppressionEnabled: require_clip_model_registry.boolean().default(true),
72096
72282
  /** IoU at/above which a detection is the same parked object, unmoved →
72097
72283
  * suppress its spawn. `SUPPRESS_IOU`. */
72098
72284
  suppressIou: require_clip_model_registry.number().min(.3).max(.9).default(SUPPRESS_IOU),
@@ -72119,7 +72305,7 @@ function resolveStationarySettings(raw) {
72119
72305
  return parsed.success ? parsed.data : STATIONARY_DEFAULTS[key];
72120
72306
  };
72121
72307
  return {
72122
- enabled: pick("enabled"),
72308
+ stationarySuppressionEnabled: pick("stationarySuppressionEnabled"),
72123
72309
  suppressIou: pick("suppressIou"),
72124
72310
  wakeAssocIou: pick("wakeAssocIou"),
72125
72311
  sessionStillnessWindowMs: pick("sessionStillnessWindowMs"),
@@ -72530,10 +72716,10 @@ function buildDetectionSettingsSections() {
72530
72716
  fields: [
72531
72717
  {
72532
72718
  type: "boolean",
72533
- key: "enabled",
72719
+ key: "stationarySuppressionEnabled",
72534
72720
  label: "Suppress parked objects",
72535
72721
  description: "Promote a stopped object to a lightweight registry entry and suppress its detections from re-spawning tracks. Off = every parked object keeps spawning normal tracks on this camera.",
72536
- default: STATIONARY_DEFAULTS.enabled
72722
+ default: STATIONARY_DEFAULTS.stationarySuppressionEnabled
72537
72723
  },
72538
72724
  {
72539
72725
  type: "slider",
@@ -72743,8 +72929,6 @@ function retagRetentionSection(sections) {
72743
72929
  * model-substitution readout.
72744
72930
  */
72745
72931
  var SUGGESTION_THRESHOLD_READOUT_KEY = "suggestionThresholdReadout";
72746
- /** `getGlobalSettings({ cap })` scope for the face matching form. */
72747
- var FACE_GALLERY_SETTINGS_CAP = "face-gallery";
72748
72932
  var FACE_RECOGNITION_SECTION_ID = "face-recognition";
72749
72933
  /** Percent, at most one decimal, without a trailing `.0`. Cosines here are
72750
72934
  * two-decimal operator values; `36.0%` reads like false precision. */
@@ -72766,7 +72950,7 @@ function formatSuggestionThresholdReadout(similarityThreshold) {
72766
72950
  * by each device's detection-pipeline steps, so a per-device toggle would be
72767
72951
  * misleading. Keyed by section id → set of field keys to drop.
72768
72952
  */
72769
- var GLOBAL_ONLY_FIELDS = { "face-recognition": new Set(["enabled", SUGGESTION_THRESHOLD_READOUT_KEY]) };
72953
+ var GLOBAL_ONLY_FIELDS = { "face-recognition": new Set(["faceRecognitionEnabled", SUGGESTION_THRESHOLD_READOUT_KEY]) };
72770
72954
  /**
72771
72955
  * Whole sections that are cluster-wide (global page only) and must NOT appear
72772
72956
  * in a per-device contribution (D62: a per-camera field whose reader only ever
@@ -72825,8 +73009,8 @@ function stripDeviceOnlyFields(sections) {
72825
73009
  * in `BaseAddon.schema()`; the general sections below are followed by the
72826
73010
  * detection-knob sections in the original order.
72827
73011
  */
72828
- function buildGlobalSettingsSchema(cap) {
72829
- return scopeGlobalSettingsToCap({ sections: [...[
73012
+ function buildGlobalSettingsSchema() {
73013
+ return { sections: [...[
72830
73014
  {
72831
73015
  id: "cluster-post-processing",
72832
73016
  title: "Cluster post-processing",
@@ -73405,10 +73589,10 @@ function buildGlobalSettingsSchema(cap) {
73405
73589
  fields: [
73406
73590
  {
73407
73591
  type: "boolean",
73408
- key: "enabled",
73592
+ key: "faceRecognitionEnabled",
73409
73593
  label: "Face recognition on",
73410
73594
  description: "Off stops face matching for every camera. On by default.",
73411
- default: FACE_DEFAULTS.enabled
73595
+ default: FACE_DEFAULTS.faceRecognitionEnabled
73412
73596
  },
73413
73597
  {
73414
73598
  type: "number",
@@ -73503,25 +73687,22 @@ function buildGlobalSettingsSchema(cap) {
73503
73687
  }
73504
73688
  ]
73505
73689
  }
73506
- ], ...buildDetectionSettingsSections()] }, cap);
73690
+ ], ...buildDetectionSettingsSections()] };
73691
+ }
73692
+ /** The face matching form: the cluster form of `face-gallery`. */
73693
+ function buildFaceClusterSchema() {
73694
+ return { sections: buildGlobalSettingsSchema().sections.filter((section) => section.id === FACE_RECOGNITION_SECTION_ID) };
73695
+ }
73696
+ /** The cluster-wide knobs of `pipeline-analytics`: the sections a camera never overrides. */
73697
+ function buildAnalyticsClusterSchema() {
73698
+ return { sections: buildGlobalSettingsSchema().sections.filter((section) => GLOBAL_ONLY_SECTIONS.has(section.id)) };
73507
73699
  }
73508
73700
  /**
73509
- * The `cap` argument on `getGlobalSettings` is how a page asks for ONE
73510
- * capability's knobs instead of the addon's whole form. Pipeline → Cluster
73511
- * → Settings asks for `face-gallery`; System → Settings omits `cap` and
73512
- * still gets every section. An unknown cap is empty so this addon cannot
73513
- * appear on a page that meant someone else.
73701
+ * The fleet defaults of `pipeline-analytics`: every knob a camera can override on its own form,
73702
+ * the face section excluded (it is the cluster form of `face-gallery`).
73514
73703
  */
73515
- function scopeGlobalSettingsToCap(schema, cap) {
73516
- if (cap === void 0 || cap === "") return schema;
73517
- if (cap === FACE_GALLERY_SETTINGS_CAP) return {
73518
- ...schema,
73519
- sections: schema.sections.filter((s) => s.id === FACE_RECOGNITION_SECTION_ID)
73520
- };
73521
- return {
73522
- ...schema,
73523
- sections: []
73524
- };
73704
+ function buildAnalyticsDeviceDefaultsSchema() {
73705
+ return { sections: buildGlobalSettingsSchema().sections.filter((section) => section.id !== FACE_RECOGNITION_SECTION_ID && !GLOBAL_ONLY_SECTIONS.has(section.id)) };
73525
73706
  }
73526
73707
  //#endregion
73527
73708
  //#region src/pipeline-analytics/store/embedding-json-conversion.ts
@@ -124086,7 +124267,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
124086
124267
  }
124087
124268
  _faceGlobalEnabledState = null;
124088
124269
  get faceGlobalEnabledState() {
124089
- if (!this._faceGlobalEnabledState) this._faceGlobalEnabledState = this.state("enabled", FaceSettingsSchema.shape.enabled, FACE_DEFAULTS.enabled);
124270
+ if (!this._faceGlobalEnabledState) this._faceGlobalEnabledState = this.state("faceRecognitionEnabled", FaceSettingsSchema.shape.faceRecognitionEnabled, FACE_DEFAULTS.faceRecognitionEnabled);
124090
124271
  return this._faceGlobalEnabledState;
124091
124272
  }
124092
124273
  constructor() {
@@ -124463,10 +124644,19 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
124463
124644
  /** The addon's migration catalogue (D987), bound to the notification kinds. */
124464
124645
  async runTemplateMigrations() {
124465
124646
  const records = this.requireRecords();
124647
+ const settings = this.ctx.settings;
124648
+ if (settings === void 0) throw new Error("the per-device settings store is unavailable");
124466
124649
  const ports = {
124467
124650
  rules: records.kind(ncRecordKinds["rules"]),
124468
124651
  timelapseRules: records.kind(ncRecordKinds["timelapse-rules"]),
124469
- texts: records.kind(ncRecordKinds.texts)
124652
+ texts: records.kind(ncRecordKinds.texts),
124653
+ readStored: (keys) => readRawKeys((key) => this.readStoredGlobalKey(key), keys),
124654
+ listDeviceIds: () => settings.listDeviceStoreIds(),
124655
+ readDeviceField: async (deviceIds, field) => {
124656
+ const found = /* @__PURE__ */ new Map();
124657
+ for (const [deviceId, blob] of await settings.readDeviceStoreBatch(deviceIds)) if (field in blob) found.set(deviceId, blob[field]);
124658
+ return found;
124659
+ }
124470
124660
  };
124471
124661
  await this.runMigrations(require_clip_model_registry.defineAddonMigrations(ADDON_MIGRATIONS, ports));
124472
124662
  }
@@ -127033,7 +127223,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
127033
127223
  const stationarySettings = this.stationarySettingsFromCache(deviceId);
127034
127224
  const packageSettings = this.packageDropSettingsFromCache(deviceId);
127035
127225
  const packageClasses = new Set(packageSettings.packageDropClassFilter);
127036
- if (planeRunsStationary(source) && this.stationaryRegistry && this.stationaryEvidence && stationarySettings.enabled) {
127226
+ if (planeRunsStationary(source) && this.stationaryRegistry && this.stationaryEvidence && stationarySettings.stationarySuppressionEnabled) {
127037
127227
  const evidence = this.stationaryEvidence;
127038
127228
  for (const t of result.tracked) {
127039
127229
  if (packageClasses.has(t.className)) continue;
@@ -129377,7 +129567,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
129377
129567
  const registry = this.stationaryRegistry;
129378
129568
  p.setStationaryGate({ filter: (input) => {
129379
129569
  const s = this.stationarySettingsFromCache(deviceId);
129380
- if (!s.enabled) return {
129570
+ if (!s.stationarySuppressionEnabled) return {
129381
129571
  suppressedIndices: /* @__PURE__ */ new Set(),
129382
129572
  continuableIndices: /* @__PURE__ */ new Set(),
129383
129573
  confirmed: [],
@@ -132734,6 +132924,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
132734
132924
  } });
132735
132925
  return { jobId };
132736
132926
  }
132927
+ /** Stored media per storage location (see the cap): indexed aggregates, no walk. */
132928
+ async getMediaLocationCensus(input) {
132929
+ return this.requireMediaStore().censusByLocation(input.locationIds);
132930
+ }
132737
132931
  /**
132738
132932
  * How many media / retrain rows still carry no `locationId`.
132739
132933
  *
@@ -132937,8 +133131,25 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
132937
133131
  });
132938
133132
  return await readWinningBlob(candidates, pick, readBlob);
132939
133133
  }
132940
- globalSettingsSchema(cap) {
132941
- return this.schema(buildGlobalSettingsSchema(cap));
133134
+ scopedSettingsCapabilities() {
133135
+ return ["pipeline-analytics", "face-gallery"];
133136
+ }
133137
+ /**
133138
+ * D986 M4. The face matching form is the cluster form of `face-gallery`; the cluster-wide knobs
133139
+ * (post-processing node, index maintenance, device alerts, prune windows) are the cluster form
133140
+ * of `pipeline-analytics`. A page asks for ONE capability's form, and System → Settings asks for
133141
+ * all of them. The schemas are static; the only live value (the derived suggestion floor) is
133142
+ * injected into the hydrate blob by `getGlobalSettings`.
133143
+ */
133144
+ async clusterSettingsSchema(cap) {
133145
+ if (cap === "face-gallery") return this.schema(buildFaceClusterSchema());
133146
+ if (cap === "pipeline-analytics") return this.schema(buildAnalyticsClusterSchema());
133147
+ return null;
133148
+ }
133149
+ /** The knobs a camera can override: stored as the fleet default, resolved per camera. */
133150
+ async deviceDefaultsSchema(cap) {
133151
+ if (cap === "pipeline-analytics") return this.schema(buildAnalyticsDeviceDefaultsSchema());
133152
+ return null;
132942
133153
  }
132943
133154
  /**
132944
133155
  * The only reason this is overridden: the face suggestion floor is DERIVED
@@ -132952,11 +133163,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
132952
133163
  * implies and has nothing to contradict it with. Everything else about this
132953
133164
  * call is `BaseAddon`'s.
132954
133165
  *
132955
- * (Fix round, L3.) `globalSettingsSchema()` is reused, untouched, by
133166
+ * (Fix round, L3.) `buildGlobalSettingsSchema()` is reused, untouched, by
132956
133167
  * `getDeviceSettingsContribution` below to build the PER-DEVICE form —
132957
133168
  * `trackingTape` must stay there. So the strip happens here, on the result
132958
133169
  * THIS method actually hands to the global settings page, never inside
132959
- * `globalSettingsSchema()` itself, which would also remove it from the
133170
+ * `buildGlobalSettingsSchema()` itself, which would also remove it from the
132960
133171
  * per-device form that shares the same base sections.
132961
133172
  */
132962
133173
  async getGlobalSettings(overlay, cap, nodeId) {
@@ -132978,7 +133189,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
132978
133189
  }
132979
133190
  async getDeviceSettingsContribution(input) {
132980
133191
  if (!await this.isCameraDevice(input.deviceId)) return null;
132981
- const schema = this.globalSettingsSchema();
133192
+ const schema = buildGlobalSettingsSchema();
132982
133193
  const raw = await this.readDeviceBlob(input.deviceId);
132983
133194
  const globalRaw = await this.rawGlobalStoreMirror.resolve();
132984
133195
  const baseSections = schema ? hydrateWithInheritedGlobals({