camstack 1.2.52 → 1.2.54

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.
@@ -23633,9 +23633,9 @@ var require_zod = __commonJS({
23633
23633
  }
23634
23634
  });
23635
23635
 
23636
- // ../system/dist/dist-B-mBrEz9.js
23637
- var require_dist_B_mBrEz9 = __commonJS({
23638
- "../system/dist/dist-B-mBrEz9.js"(exports) {
23636
+ // ../system/dist/dist-CImxMt5h.js
23637
+ var require_dist_CImxMt5h = __commonJS({
23638
+ "../system/dist/dist-CImxMt5h.js"(exports) {
23639
23639
  "use strict";
23640
23640
  var zod = require_zod();
23641
23641
  var EventCategory = /* @__PURE__ */ (function(EventCategory2) {
@@ -24231,6 +24231,40 @@ var require_dist_B_mBrEz9 = __commonJS({
24231
24231
  deviceSettingsSchema() {
24232
24232
  return null;
24233
24233
  }
24234
+ /**
24235
+ * INTEGRATION-LEVEL SETTINGS — declare which of this addon's global sections
24236
+ * ARE the configuration of its integration.
24237
+ *
24238
+ * Return the `ConfigSection.id`s, from {@link globalSettingsSchema}, that an
24239
+ * operator should find on the addon's integration page (System →
24240
+ * Integrations → <name>) rather than only in the cluster-wide list of every
24241
+ * addon. Empty (the default) means the addon has no integration-level
24242
+ * settings and no such surface is offered — this is opt-in, because whether
24243
+ * an addon's configuration IS its integration's configuration depends on the
24244
+ * nature of the integration.
24245
+ *
24246
+ * WHAT THIS IS NOT. It is not a scope. The selected sections keep living in
24247
+ * the ONE global schema, in the ONE addon store, written by the ONE
24248
+ * `updateGlobalSettings` path. There is deliberately no
24249
+ * `updateIntegrationSettings`: a second write path is how a surface acquires
24250
+ * a second store key, and this repo has shipped that twice (`btmPath@hub`,
24251
+ * D266). Selecting sections cannot introduce a key that selecting cannot.
24252
+ *
24253
+ * WHY IT IS A LIST OF SECTION IDS AND NOT A MARKER ON THE SECTION.
24254
+ * `ConfigFieldBase` used to carry `scope?: 'device' | 'global'` and it was
24255
+ * removed with the reason recorded at
24256
+ * `packages/types/src/interfaces/config-ui.ts:249` — *"a field's scope is
24257
+ * determined by WHICH schema it lives in, not by a field-level marker."* A
24258
+ * marker sprinkled across sections also has to borrow a field that already
24259
+ * means something else; borrowing `section.tab` put the literal word
24260
+ * "integration" into an operator-facing tab bar, because `tab` means "how to
24261
+ * GROUP this visually" and cannot also mean "where this lives" (D269
24262
+ * supersedes D268). One declaration, in one place, next to the schema whose
24263
+ * ids it names.
24264
+ */
24265
+ integrationSettingSections() {
24266
+ return [];
24267
+ }
24234
24268
  async getGlobalSettings(overlay, cap, nodeId) {
24235
24269
  const schema = this.globalSettingsSchema(cap);
24236
24270
  if (!schema) return { sections: [] };
@@ -24241,6 +24275,55 @@ var require_dist_B_mBrEz9 = __commonJS({
24241
24275
  } : projected);
24242
24276
  }
24243
24277
  /**
24278
+ * The integration-level view of this addon's settings: exactly the sections
24279
+ * named by {@link integrationSettingSections}, hydrated from the SAME store
24280
+ * `getGlobalSettings` reads, and narrowed to cluster-scoped fields.
24281
+ *
24282
+ * Returns `null` when the addon declared nothing — an addon that opts out has
24283
+ * no integration settings surface at all, rather than an empty one that reads
24284
+ * as a failed load.
24285
+ *
24286
+ * Three properties hold BY CONSTRUCTION, which is why they are here in core
24287
+ * and not in whichever UI happens to render this:
24288
+ *
24289
+ * 1. **One key.** The payload is a SUBSET of the global schema, so a field
24290
+ * shown here is the same field, with the same bare key, that the addon's
24291
+ * own page shows. There is no integration-specific writer — callers save
24292
+ * through `updateGlobalSettings` — so a second store key is unreachable,
24293
+ * not merely discouraged.
24294
+ * 2. **No node scope.** `perNode: true` fields are DROPPED. Their store key
24295
+ * is `<key>@<nodeId>` and an integration is not a node; whichever node
24296
+ * such a field silently picked would be a wrong answer for the operator
24297
+ * who opened the page (D266).
24298
+ * 3. **No silent typo.** A declared id that names no section throws. The
24299
+ * alternative — skip it — turns a rename into a surface that quietly
24300
+ * empties, which looks exactly like an addon with nothing to configure.
24301
+ */
24302
+ async getIntegrationSettings(nodeId) {
24303
+ const declared = this.integrationSettingSections();
24304
+ if (declared.length === 0) return null;
24305
+ const schema = this.globalSettingsSchema();
24306
+ if (!schema) throw new Error(`${this.constructor.name}: integrationSettingSections() names [${declared.join(", ")}] but globalSettingsSchema() returns null.`);
24307
+ const byId = new Map(schema.sections.map((section) => [section.id, section]));
24308
+ const sections = [];
24309
+ for (const id of declared) {
24310
+ const section = byId.get(id);
24311
+ if (!section) throw new Error(`${this.constructor.name}: integrationSettingSections() names unknown section "${id}". Known sections: [${[...byId.keys()].join(", ")}].`);
24312
+ const fields = dropPerNodeFields(section.fields);
24313
+ if (fields.length === 0) continue;
24314
+ sections.push({
24315
+ ...section,
24316
+ fields
24317
+ });
24318
+ }
24319
+ if (sections.length === 0) return null;
24320
+ const projected = await this.resolveGlobalStore(nodeId);
24321
+ return hydrateSchema({
24322
+ ...schema,
24323
+ sections
24324
+ }, projected);
24325
+ }
24326
+ /**
24244
24327
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
24245
24328
  * every `perNode: true` field carries THAT node's scoped value on its bare
24246
24329
  * key (absent scoped key ⇒ key absent, so the schema `default` wins — no
@@ -24540,6 +24623,33 @@ var require_dist_B_mBrEz9 = __commonJS({
24540
24623
  return {};
24541
24624
  }
24542
24625
  };
24626
+ function dropPerNodeFields(fields) {
24627
+ const kept = [];
24628
+ for (const field of fields) {
24629
+ if (field.type === "group") {
24630
+ const inner = dropPerNodeFields(field.fields);
24631
+ if (inner.length > 0) kept.push({
24632
+ ...field,
24633
+ fields: inner
24634
+ });
24635
+ continue;
24636
+ }
24637
+ if (field.type === "sub-tabs") {
24638
+ const tabs = field.tabs.map((tab) => ({
24639
+ ...tab,
24640
+ fields: dropPerNodeFields(tab.fields)
24641
+ })).filter((tab) => tab.fields.length > 0);
24642
+ if (tabs.length > 0) kept.push({
24643
+ ...field,
24644
+ tabs
24645
+ });
24646
+ continue;
24647
+ }
24648
+ if ("perNode" in field && field.perNode === true) continue;
24649
+ kept.push(field);
24650
+ }
24651
+ return kept;
24652
+ }
24543
24653
  function collectPerNodeFieldKeys(fields) {
24544
24654
  const collected = [];
24545
24655
  for (const field of fields) {
@@ -27528,6 +27638,10 @@ var require_dist_B_mBrEz9 = __commonJS({
27528
27638
  kind: "mutation",
27529
27639
  auth: "admin"
27530
27640
  }),
27641
+ getIntegrationSettings: method(zod.z.object({
27642
+ addonId: zod.z.string(),
27643
+ nodeId: zod.z.string().optional()
27644
+ }), SettingsSchemaWithValuesSchema.nullable()),
27531
27645
  getDeviceSettings: method(zod.z.object({
27532
27646
  addonId: zod.z.string(),
27533
27647
  deviceId: zod.z.number(),
@@ -32240,6 +32354,80 @@ var require_dist_B_mBrEz9 = __commonJS({
32240
32354
  /** BIG PLAN 2: declarative mount hint — read by `@camstack/system` `buildCapRouters`. */
32241
32355
  mount: { kind: "skip" }
32242
32356
  };
32357
+ var FailureReasonCountSchema = zod.z.object({
32358
+ /**
32359
+ * Why the attempt did not land, in the contributor's own vocabulary —
32360
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
32361
+ * strings that already appear in this repo's logs and, where one exists, the
32362
+ * same string the per-track `previewMissReason` records (D276): a second
32363
+ * vocabulary for the same loss would make the row and the counter
32364
+ * un-joinable.
32365
+ */
32366
+ reason: zod.z.string(),
32367
+ count: zod.z.number().int().nonnegative()
32368
+ });
32369
+ var FailureContributionSchema = zod.z.object({
32370
+ /**
32371
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
32372
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
32373
+ * `unit` free: the families are owned by different addons and a shared enum
32374
+ * is a central list that rots invisibly.
32375
+ */
32376
+ family: zod.z.string(),
32377
+ /**
32378
+ * The NUMERIC device id — the same value every log line carries as
32379
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
32380
+ * cannot name the camera must not emit the entry, because a fleet total
32381
+ * cannot answer the only question anybody asks of this surface.
32382
+ */
32383
+ deviceId: zod.z.number().int().positive(),
32384
+ /**
32385
+ * A second dimension inside the family: the model / step id for an inference
32386
+ * timeout, so "which camera AND which model" is one read. Absent when the
32387
+ * family has a single variant.
32388
+ */
32389
+ variant: zod.z.string().optional(),
32390
+ /**
32391
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
32392
+ * differencing two reads must drop the interval when it changes, because the
32393
+ * counter restarted from zero in a respawned runner. Same discipline as
32394
+ * `LoadContribution.startedAtMs`.
32395
+ */
32396
+ sinceMs: zod.z.number(),
32397
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
32398
+ atMs: zod.z.number(),
32399
+ /**
32400
+ * THE DENOMINATOR — every attempt on this path for this camera in the
32401
+ * window. A failure count published without it is the mistake this schema
32402
+ * exists to make impossible.
32403
+ */
32404
+ attempts: zod.z.number().int().nonnegative(),
32405
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
32406
+ succeeded: zod.z.number().int().nonnegative(),
32407
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
32408
+ reasons: zod.z.array(FailureReasonCountSchema).readonly()
32409
+ });
32410
+ var failureContributionCapability = {
32411
+ name: "failure-contribution",
32412
+ scope: "system",
32413
+ mode: "collection",
32414
+ internal: true,
32415
+ methods: {
32416
+ /**
32417
+ * This addon's per-camera failure counters, read live from bounded in-RAM
32418
+ * state it already keeps. Inert: no persistence, no sampling, no timer.
32419
+ *
32420
+ * READING NEVER RESETS. The counters are CUMULATIVE since `sinceMs`, and a
32421
+ * consumer that wants a rate differences two reads. A draining read would
32422
+ * make two operators with the page open each destroy half of the other's
32423
+ * numbers, and `load-contribution` already settled the same question the
32424
+ * same way for `cpuSeconds`.
32425
+ */
32426
+ list: method(zod.z.void(), zod.z.array(FailureContributionSchema).readonly())
32427
+ },
32428
+ /** In-process only — enumerated through `addons.listCapabilityProviders`. */
32429
+ mount: { kind: "skip" }
32430
+ };
32243
32431
  var LoadContributionSchema = zod.z.object({
32244
32432
  role: zod.z.enum([
32245
32433
  "decode",
@@ -36226,6 +36414,20 @@ var require_dist_B_mBrEz9 = __commonJS({
36226
36414
  * `=== true` and render nothing otherwise — never infer "no rider".
36227
36415
  */
36228
36416
  hasRider: zod.z.boolean().optional(),
36417
+ /**
36418
+ * WHY this track ended without a NATIVE best-shot tile
36419
+ * ([D276](../decisions/adr-0276-a-stand-in-tile-is-provisional-and-a-close-says-why.md)) —
36420
+ * a composed token line (`no-key-frame capture=keyframe:native-missx4`,
36421
+ * `derive-returned-null tile=standin`, …) written at close and CLEARED by
36422
+ * the late-keyFrame upgrade when a native tile lands after all. The
36423
+ * operator-facing answer to "perché manca l'immagine?" on a track whose
36424
+ * tile is a face/plate stand-in, a raster crop, or an icon.
36425
+ *
36426
+ * **Absent ≠ "missed silently"**: a row written before the column, a hub
36427
+ * that predates the field, and every track whose tile landed native all
36428
+ * omit it. Render nothing when absent.
36429
+ */
36430
+ previewMissReason: zod.z.string().optional(),
36229
36431
  ...TrackFlagFields,
36230
36432
  ...TrackRetrainFields
36231
36433
  });
@@ -47809,6 +48011,7 @@ var require_dist_B_mBrEz9 = __commonJS({
47809
48011
  channels: zod.z.array(LogChannelWindowPatchSchema).readonly().optional()
47810
48012
  });
47811
48013
  var ReportedLoadContributionSchema = LoadContributionSchema.extend({ addonId: zod.z.string() });
48014
+ var ReportedFailureContributionSchema = FailureContributionSchema.extend({ addonId: zod.z.string() });
47812
48015
  var GetLoggingSettingsInputSchema = zod.z.object({
47813
48016
  scopeNodeId: zod.z.string().optional(),
47814
48017
  /**
@@ -47918,6 +48121,28 @@ var require_dist_B_mBrEz9 = __commonJS({
47918
48121
  */
47919
48122
  getLoadContributions: method(zod.z.void(), zod.z.array(ReportedLoadContributionSchema).readonly(), { auth: "admin" }),
47920
48123
  /**
48124
+ * Every `failure-contribution` an addon on this cluster reports — per
48125
+ * camera, per reason, **with the denominator attached**.
48126
+ *
48127
+ * This is the surface the operator asked for on 2026-08-28 (*"possiamo
48128
+ * armare questi errori intanto? Così al prossimo giro ricontrolliamo tutti
48129
+ * questi punti"*). Before it, four live failure modes could only be counted
48130
+ * by grepping Loki and hand-correlating timestamps, which is exactly how a
48131
+ * 22% thumbnail gap and a 3-hour media blackout were diagnosed — twice.
48132
+ *
48133
+ * Read it as a RATIO, never as a count. `attempts` is on every entry
48134
+ * because the count on its own lies: `enrichment crop native miss` read as
48135
+ * "35x worse than yesterday" and was flat across twelve hours once divided
48136
+ * by the successes on the same path.
48137
+ *
48138
+ * The counters are CUMULATIVE since each entry's `sinceMs`. Reading does
48139
+ * not reset them, and `sinceMs` changing means the reporting runner
48140
+ * respawned — a consumer differencing two reads drops that interval.
48141
+ *
48142
+ * Admin-only: the rows name cameras and the paths that fail on them.
48143
+ */
48144
+ getFailureContributions: method(zod.z.void(), zod.z.array(ReportedFailureContributionSchema).readonly(), { auth: "admin" }),
48145
+ /**
47921
48146
  * The logging settings document — levels and armed diagnostics — resolved
47922
48147
  * for `nodeId`, or for the cluster when `nodeId` is absent.
47923
48148
  *
@@ -49322,6 +49547,7 @@ var require_dist_B_mBrEz9 = __commonJS({
49322
49547
  eventEmitterCapability,
49323
49548
  eventsCapability,
49324
49549
  faceGalleryCapability,
49550
+ failureContributionCapability,
49325
49551
  fanControlCapability,
49326
49552
  featureProbeCapability,
49327
49553
  filesystemBrowseCapability,
@@ -49658,6 +49884,12 @@ var require_dist_B_mBrEz9 = __commonJS({
49658
49884
  addonId: null,
49659
49885
  access: "view"
49660
49886
  },
49887
+ "addonSettings.getIntegrationSettings": {
49888
+ capName: "addon-settings",
49889
+ capScope: "system",
49890
+ addonId: null,
49891
+ access: "view"
49892
+ },
49661
49893
  "addonSettings.updateDeviceSettings": {
49662
49894
  capName: "addon-settings",
49663
49895
  capScope: "system",
@@ -51320,6 +51552,12 @@ var require_dist_B_mBrEz9 = __commonJS({
51320
51552
  addonId: null,
51321
51553
  access: "create"
51322
51554
  },
51555
+ "failureContribution.list": {
51556
+ capName: "failure-contribution",
51557
+ capScope: "system",
51558
+ addonId: null,
51559
+ access: "view"
51560
+ },
51323
51561
  "fanControl.setDirection": {
51324
51562
  capName: "fan-control",
51325
51563
  capScope: "device",
@@ -54626,6 +54864,12 @@ var require_dist_B_mBrEz9 = __commonJS({
54626
54864
  addonId: null,
54627
54865
  access: "create"
54628
54866
  },
54867
+ "system.getFailureContributions": {
54868
+ capName: "system",
54869
+ capScope: "system",
54870
+ addonId: null,
54871
+ access: "view"
54872
+ },
54629
54873
  "system.getLoadContributions": {
54630
54874
  capName: "system",
54631
54875
  capScope: "system",
@@ -58395,7 +58639,7 @@ var require_alerts_addon = __commonJS({
58395
58639
  [Symbol.toStringTag]: { value: "Module" }
58396
58640
  });
58397
58641
  require_chunk_Cek0wNdY();
58398
- var require_dist10 = require_dist_B_mBrEz9();
58642
+ var require_dist10 = require_dist_CImxMt5h();
58399
58643
  function selectExpired(alerts, cutoffMs) {
58400
58644
  return alerts.filter((a) => a.createdAt < cutoffMs).sort((a, b) => a.createdAt - b.createdAt).map((a) => a.id);
58401
58645
  }
@@ -59214,7 +59458,7 @@ var require_console_logging = __commonJS({
59214
59458
  [Symbol.toStringTag]: { value: "Module" }
59215
59459
  });
59216
59460
  require_chunk_Cek0wNdY();
59217
- var require_dist10 = require_dist_B_mBrEz9();
59461
+ var require_dist10 = require_dist_CImxMt5h();
59218
59462
  var require_formatter = require_formatter_DqAKDlvN();
59219
59463
  var LEVEL_RANK = {
59220
59464
  debug: 0,
@@ -59308,7 +59552,7 @@ var require_core_blocks_addon = __commonJS({
59308
59552
  "use strict";
59309
59553
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
59310
59554
  var require_chunk = require_chunk_Cek0wNdY();
59311
- var require_dist10 = require_dist_B_mBrEz9();
59555
+ var require_dist10 = require_dist_CImxMt5h();
59312
59556
  var node_crypto = __require("crypto");
59313
59557
  var node_fs_promises = __require("fs/promises");
59314
59558
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -60205,28 +60449,88 @@ var require_core_blocks = __commonJS({
60205
60449
  }
60206
60450
  });
60207
60451
 
60208
- // ../system/dist/retired-settings-keys-Davtjo5p.js
60209
- var require_retired_settings_keys_Davtjo5p = __commonJS({
60210
- "../system/dist/retired-settings-keys-Davtjo5p.js"(exports) {
60452
+ // ../system/dist/retired-settings-keys-C7gLUS3x.js
60453
+ var require_retired_settings_keys_C7gLUS3x = __commonJS({
60454
+ "../system/dist/retired-settings-keys-C7gLUS3x.js"(exports) {
60211
60455
  "use strict";
60212
- var require_dist10 = require_dist_B_mBrEz9();
60456
+ var require_dist10 = require_dist_CImxMt5h();
60213
60457
  function settingsStoreIsAuthoritativeHere(env) {
60214
60458
  const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
60215
60459
  return raw === "" || raw === "hub";
60216
60460
  }
60217
- var RETIRED_SETTINGS_KEYS = [{
60218
- namespace: "detection-pipeline",
60219
- collection: "addon-settings",
60220
- row: "root",
60221
- keys: ["pipelineSteps", "pipelineEngine"],
60222
- perNodeKeys: [
60223
- "engineRuntime",
60224
- "engineBackend",
60225
- "engineDevice",
60226
- "probedBestEngine"
60227
- ],
60228
- reason: "global step/engine seed and the per-node engine cascade \u2014 both removed; the live per-camera dispatch path never read either"
60229
- }];
60461
+ var RETIRED_SETTINGS_KEYS = [
60462
+ {
60463
+ namespace: "detection-pipeline",
60464
+ collection: "addon-settings",
60465
+ row: "root",
60466
+ keys: ["pipelineSteps", "pipelineEngine"],
60467
+ perNodeKeys: [
60468
+ "engineRuntime",
60469
+ "engineBackend",
60470
+ "engineDevice",
60471
+ "probedBestEngine"
60472
+ ],
60473
+ reason: "global step/engine seed and the per-node engine cascade \u2014 both removed; the live per-camera dispatch path never read either"
60474
+ },
60475
+ {
60476
+ namespace: "pipeline-orchestrator",
60477
+ collection: "addon-settings",
60478
+ row: "root",
60479
+ keys: [
60480
+ "enabledNodes",
60481
+ "enabledDecoderNodes",
60482
+ "enabledAudioNodes",
60483
+ "remoteSourcingNodes",
60484
+ "nativeLeaseTtlMs"
60485
+ ],
60486
+ perNodeKeys: ["backend"],
60487
+ reason: 'the pre-singleton "enabled nodes" role model (node capability now lives per node in agent-settings and is derived by NodeTopology.refresh, never read from this row), `nativeLeaseTtlMs` superseded by `nativeLeaseActivityMs`, and the decoder `backend` selector whose resolver returns DEFAULT_DECODER_BACKEND unconditionally (decoder-backend.ts)'
60488
+ },
60489
+ {
60490
+ namespace: "stream-broker",
60491
+ collection: "addon-settings",
60492
+ row: "root",
60493
+ keys: [
60494
+ "maxDecodeFps",
60495
+ "initialReconnectDelayMs",
60496
+ "maxReconnectDelayMs"
60497
+ ],
60498
+ perNodeKeys: [],
60499
+ reason: "three form knobs deleted 2026-08-28 (D272): nothing capped decode fps, and the reconnect ladder has always run on the INITIAL/MAX_RECONNECT_DELAY_MS constants"
60500
+ },
60501
+ {
60502
+ namespace: "embedding-encoder",
60503
+ collection: "addon-settings",
60504
+ row: "root",
60505
+ keys: ["runtime", "backend"],
60506
+ perNodeKeys: [],
60507
+ reason: "two selects deleted 2026-08-28 (D272); their only reference was a withTags log field, and the CUDA/CoreML options named acceleration that has never existed \u2014 embedding inference is always ONNX in the embedded Python"
60508
+ },
60509
+ {
60510
+ namespace: "audio-analyzer",
60511
+ collection: "addon-settings",
60512
+ row: "root",
60513
+ keys: ["selectedAudioModel", "probedBestAudioBackend"],
60514
+ perNodeKeys: [],
60515
+ reason: "`selectedAudioModel` deleted 2026-08-28 (D272) \u2014 createAudioPipeline takes a backend and no model id, so it never reached inference; the BARE `probedBestAudioBackend` is pre-perNode residue that projectStore already drops (no bare fallback), so no node can ever read it"
60516
+ },
60517
+ {
60518
+ namespace: "decoder-nodeav",
60519
+ collection: "addon-settings",
60520
+ row: "root",
60521
+ keys: ["probedBestHwaccel"],
60522
+ perNodeKeys: [],
60523
+ reason: "bare pre-perNode residue \u2014 the live probe result is `probedBestHwaccel@<nodeId>` and `projectStore` deliberately drops the bare key, so nothing can read it"
60524
+ },
60525
+ {
60526
+ namespace: "pipeline-analytics",
60527
+ collection: "addon-settings",
60528
+ row: "root",
60529
+ keys: ["mediaAttachPolicy"],
60530
+ perNodeKeys: [],
60531
+ reason: "deleted 2026-08-28 (D272) \u2014 absent from MediaSettingsSchema, so the value was never even resolved, let alone read"
60532
+ }
60533
+ ];
60230
60534
  var RETIRED_SETTINGS_ROWS = [
60231
60535
  {
60232
60536
  collection: "addon-settings",
@@ -62363,8 +62667,8 @@ var require_device_manager_addon = __commonJS({
62363
62667
  [Symbol.toStringTag]: { value: "Module" }
62364
62668
  });
62365
62669
  require_chunk_Cek0wNdY();
62366
- var require_dist10 = require_dist_B_mBrEz9();
62367
- var require_retired_settings_keys = require_retired_settings_keys_Davtjo5p();
62670
+ var require_dist10 = require_dist_CImxMt5h();
62671
+ var require_retired_settings_keys = require_retired_settings_keys_C7gLUS3x();
62368
62672
  var node_crypto = __require("crypto");
62369
62673
  var _camstack_types_node = require_node();
62370
62674
  var JOB_HISTORY = 20;
@@ -67113,7 +67417,7 @@ var require_hub_forwarder = __commonJS({
67113
67417
  [Symbol.toStringTag]: { value: "Module" }
67114
67418
  });
67115
67419
  require_chunk_Cek0wNdY();
67116
- var require_dist10 = require_dist_B_mBrEz9();
67420
+ var require_dist10 = require_dist_CImxMt5h();
67117
67421
  var require_formatter = require_formatter_DqAKDlvN();
67118
67422
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
67119
67423
  var HubForwarderDestination = class {
@@ -67250,7 +67554,7 @@ var require_liveness_monitor_addon = __commonJS({
67250
67554
  "use strict";
67251
67555
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
67252
67556
  require_chunk_Cek0wNdY();
67253
- var require_dist10 = require_dist_B_mBrEz9();
67557
+ var require_dist10 = require_dist_CImxMt5h();
67254
67558
  var NO_DEVICES = "liveness:no-devices";
67255
67559
  var ALL_OFFLINE = "liveness:all-devices-offline";
67256
67560
  var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
@@ -67440,7 +67744,7 @@ var require_local_auth_addon = __commonJS({
67440
67744
  [Symbol.toStringTag]: { value: "Module" }
67441
67745
  });
67442
67746
  var require_chunk = require_chunk_Cek0wNdY();
67443
- var require_dist10 = require_dist_B_mBrEz9();
67747
+ var require_dist10 = require_dist_CImxMt5h();
67444
67748
  var node_crypto = __require("crypto");
67445
67749
  node_crypto = require_chunk.__toESM(node_crypto);
67446
67750
  var crypto$1 = __require("crypto");
@@ -75253,7 +75557,7 @@ var require_loki_logging = __commonJS({
75253
75557
  [Symbol.toStringTag]: { value: "Module" }
75254
75558
  });
75255
75559
  require_chunk_Cek0wNdY();
75256
- var require_dist10 = require_dist_B_mBrEz9();
75560
+ var require_dist10 = require_dist_CImxMt5h();
75257
75561
  function sanitizeLabelName(raw) {
75258
75562
  const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
75259
75563
  return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
@@ -75818,7 +76122,7 @@ var require_native_metrics_addon = __commonJS({
75818
76122
  [Symbol.toStringTag]: { value: "Module" }
75819
76123
  });
75820
76124
  var require_chunk = require_chunk_Cek0wNdY();
75821
- var require_dist10 = require_dist_B_mBrEz9();
76125
+ var require_dist10 = require_dist_CImxMt5h();
75822
76126
  var node_fs_promises = __require("fs/promises");
75823
76127
  var node_child_process = __require("child_process");
75824
76128
  var node_util = __require("util");
@@ -77775,7 +78079,7 @@ var require_filesystem_storage_addon = __commonJS({
77775
78079
  [Symbol.toStringTag]: { value: "Module" }
77776
78080
  });
77777
78081
  var require_chunk = require_chunk_Cek0wNdY();
77778
- var require_dist10 = require_dist_B_mBrEz9();
78082
+ var require_dist10 = require_dist_CImxMt5h();
77779
78083
  var node_crypto = __require("crypto");
77780
78084
  var node_fs_promises = __require("fs/promises");
77781
78085
  var node_path = __require("path");
@@ -78891,8 +79195,8 @@ var require_sqlite_settings_addon = __commonJS({
78891
79195
  [Symbol.toStringTag]: { value: "Module" }
78892
79196
  });
78893
79197
  var require_chunk = require_chunk_Cek0wNdY();
78894
- var require_dist10 = require_dist_B_mBrEz9();
78895
- var require_retired_settings_keys = require_retired_settings_keys_Davtjo5p();
79198
+ var require_dist10 = require_dist_CImxMt5h();
79199
+ var require_retired_settings_keys = require_retired_settings_keys_C7gLUS3x();
78896
79200
  var node_crypto = __require("crypto");
78897
79201
  var node_fs = __require("fs");
78898
79202
  var node_module = __require("module");
@@ -81171,7 +81475,7 @@ var require_storage_orchestrator_addon = __commonJS({
81171
81475
  [Symbol.toStringTag]: { value: "Module" }
81172
81476
  });
81173
81477
  var require_chunk = require_chunk_Cek0wNdY();
81174
- var require_dist10 = require_dist_B_mBrEz9();
81478
+ var require_dist10 = require_dist_CImxMt5h();
81175
81479
  var node_crypto = __require("crypto");
81176
81480
  var node_fs_promises = __require("fs/promises");
81177
81481
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -83052,7 +83356,7 @@ var require_system_config_addon = __commonJS({
83052
83356
  [Symbol.toStringTag]: { value: "Module" }
83053
83357
  });
83054
83358
  require_chunk_Cek0wNdY();
83055
- var require_dist10 = require_dist_B_mBrEz9();
83359
+ var require_dist10 = require_dist_CImxMt5h();
83056
83360
  var SECTION_TITLES = {
83057
83361
  server: "Server",
83058
83362
  auth: "Authentication"
@@ -101113,7 +101417,7 @@ var require_winston_logging = __commonJS({
101113
101417
  [Symbol.toStringTag]: { value: "Module" }
101114
101418
  });
101115
101419
  var require_chunk = require_chunk_Cek0wNdY();
101116
- var require_dist10 = require_dist_B_mBrEz9();
101420
+ var require_dist10 = require_dist_CImxMt5h();
101117
101421
  var require_formatter = require_formatter_DqAKDlvN();
101118
101422
  var node_path = __require("path");
101119
101423
  node_path = require_chunk.__toESM(node_path);
@@ -103056,9 +103360,9 @@ var require_event_category_BaEgqJNv = __commonJS({
103056
103360
  }
103057
103361
  });
103058
103362
 
103059
- // ../types/dist/sleep-9d8tJRbO.js
103060
- var require_sleep_9d8tJRbO = __commonJS({
103061
- "../types/dist/sleep-9d8tJRbO.js"(exports) {
103363
+ // ../types/dist/sleep-DUxF5DdC.js
103364
+ var require_sleep_DUxF5DdC = __commonJS({
103365
+ "../types/dist/sleep-DUxF5DdC.js"(exports) {
103062
103366
  "use strict";
103063
103367
  var require_event_category = require_event_category_BaEgqJNv();
103064
103368
  var zod = require_zod();
@@ -103547,6 +103851,40 @@ var require_sleep_9d8tJRbO = __commonJS({
103547
103851
  deviceSettingsSchema() {
103548
103852
  return null;
103549
103853
  }
103854
+ /**
103855
+ * INTEGRATION-LEVEL SETTINGS — declare which of this addon's global sections
103856
+ * ARE the configuration of its integration.
103857
+ *
103858
+ * Return the `ConfigSection.id`s, from {@link globalSettingsSchema}, that an
103859
+ * operator should find on the addon's integration page (System →
103860
+ * Integrations → <name>) rather than only in the cluster-wide list of every
103861
+ * addon. Empty (the default) means the addon has no integration-level
103862
+ * settings and no such surface is offered — this is opt-in, because whether
103863
+ * an addon's configuration IS its integration's configuration depends on the
103864
+ * nature of the integration.
103865
+ *
103866
+ * WHAT THIS IS NOT. It is not a scope. The selected sections keep living in
103867
+ * the ONE global schema, in the ONE addon store, written by the ONE
103868
+ * `updateGlobalSettings` path. There is deliberately no
103869
+ * `updateIntegrationSettings`: a second write path is how a surface acquires
103870
+ * a second store key, and this repo has shipped that twice (`btmPath@hub`,
103871
+ * D266). Selecting sections cannot introduce a key that selecting cannot.
103872
+ *
103873
+ * WHY IT IS A LIST OF SECTION IDS AND NOT A MARKER ON THE SECTION.
103874
+ * `ConfigFieldBase` used to carry `scope?: 'device' | 'global'` and it was
103875
+ * removed with the reason recorded at
103876
+ * `packages/types/src/interfaces/config-ui.ts:249` — *"a field's scope is
103877
+ * determined by WHICH schema it lives in, not by a field-level marker."* A
103878
+ * marker sprinkled across sections also has to borrow a field that already
103879
+ * means something else; borrowing `section.tab` put the literal word
103880
+ * "integration" into an operator-facing tab bar, because `tab` means "how to
103881
+ * GROUP this visually" and cannot also mean "where this lives" (D269
103882
+ * supersedes D268). One declaration, in one place, next to the schema whose
103883
+ * ids it names.
103884
+ */
103885
+ integrationSettingSections() {
103886
+ return [];
103887
+ }
103550
103888
  async getGlobalSettings(overlay, cap, nodeId) {
103551
103889
  const schema = this.globalSettingsSchema(cap);
103552
103890
  if (!schema) return { sections: [] };
@@ -103557,6 +103895,55 @@ var require_sleep_9d8tJRbO = __commonJS({
103557
103895
  } : projected);
103558
103896
  }
103559
103897
  /**
103898
+ * The integration-level view of this addon's settings: exactly the sections
103899
+ * named by {@link integrationSettingSections}, hydrated from the SAME store
103900
+ * `getGlobalSettings` reads, and narrowed to cluster-scoped fields.
103901
+ *
103902
+ * Returns `null` when the addon declared nothing — an addon that opts out has
103903
+ * no integration settings surface at all, rather than an empty one that reads
103904
+ * as a failed load.
103905
+ *
103906
+ * Three properties hold BY CONSTRUCTION, which is why they are here in core
103907
+ * and not in whichever UI happens to render this:
103908
+ *
103909
+ * 1. **One key.** The payload is a SUBSET of the global schema, so a field
103910
+ * shown here is the same field, with the same bare key, that the addon's
103911
+ * own page shows. There is no integration-specific writer — callers save
103912
+ * through `updateGlobalSettings` — so a second store key is unreachable,
103913
+ * not merely discouraged.
103914
+ * 2. **No node scope.** `perNode: true` fields are DROPPED. Their store key
103915
+ * is `<key>@<nodeId>` and an integration is not a node; whichever node
103916
+ * such a field silently picked would be a wrong answer for the operator
103917
+ * who opened the page (D266).
103918
+ * 3. **No silent typo.** A declared id that names no section throws. The
103919
+ * alternative — skip it — turns a rename into a surface that quietly
103920
+ * empties, which looks exactly like an addon with nothing to configure.
103921
+ */
103922
+ async getIntegrationSettings(nodeId) {
103923
+ const declared = this.integrationSettingSections();
103924
+ if (declared.length === 0) return null;
103925
+ const schema = this.globalSettingsSchema();
103926
+ if (!schema) throw new Error(`${this.constructor.name}: integrationSettingSections() names [${declared.join(", ")}] but globalSettingsSchema() returns null.`);
103927
+ const byId = new Map(schema.sections.map((section) => [section.id, section]));
103928
+ const sections = [];
103929
+ for (const id of declared) {
103930
+ const section = byId.get(id);
103931
+ if (!section) throw new Error(`${this.constructor.name}: integrationSettingSections() names unknown section "${id}". Known sections: [${[...byId.keys()].join(", ")}].`);
103932
+ const fields = dropPerNodeFields(section.fields);
103933
+ if (fields.length === 0) continue;
103934
+ sections.push({
103935
+ ...section,
103936
+ fields
103937
+ });
103938
+ }
103939
+ if (sections.length === 0) return null;
103940
+ const projected = await this.resolveGlobalStore(nodeId);
103941
+ return hydrateSchema({
103942
+ ...schema,
103943
+ sections
103944
+ }, projected);
103945
+ }
103946
+ /**
103560
103947
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
103561
103948
  * every `perNode: true` field carries THAT node's scoped value on its bare
103562
103949
  * key (absent scoped key ⇒ key absent, so the schema `default` wins — no
@@ -103856,6 +104243,33 @@ var require_sleep_9d8tJRbO = __commonJS({
103856
104243
  return {};
103857
104244
  }
103858
104245
  };
104246
+ function dropPerNodeFields(fields) {
104247
+ const kept = [];
104248
+ for (const field of fields) {
104249
+ if (field.type === "group") {
104250
+ const inner = dropPerNodeFields(field.fields);
104251
+ if (inner.length > 0) kept.push({
104252
+ ...field,
104253
+ fields: inner
104254
+ });
104255
+ continue;
104256
+ }
104257
+ if (field.type === "sub-tabs") {
104258
+ const tabs = field.tabs.map((tab) => ({
104259
+ ...tab,
104260
+ fields: dropPerNodeFields(tab.fields)
104261
+ })).filter((tab) => tab.fields.length > 0);
104262
+ if (tabs.length > 0) kept.push({
104263
+ ...field,
104264
+ tabs
104265
+ });
104266
+ continue;
104267
+ }
104268
+ if ("perNode" in field && field.perNode === true) continue;
104269
+ kept.push(field);
104270
+ }
104271
+ return kept;
104272
+ }
103859
104273
  function collectPerNodeFieldKeys(fields) {
103860
104274
  const collected = [];
103861
104275
  for (const field of fields) {
@@ -106612,7 +107026,7 @@ var require_addon = __commonJS({
106612
107026
  "use strict";
106613
107027
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
106614
107028
  var require_event_category = require_event_category_BaEgqJNv();
106615
- var require_sleep = require_sleep_9d8tJRbO();
107029
+ var require_sleep = require_sleep_DUxF5DdC();
106616
107030
  var require_err_msg = require_err_msg_COpsHMw2();
106617
107031
  var CAP_INPUT_DEFAULTS = Object.freeze({
106618
107032
  "addons": { "getLogs": { "limit": 100 } },
@@ -106891,6 +107305,7 @@ var require_addon = __commonJS({
106891
107305
  "data-store-provider": ["histogram", "query"],
106892
107306
  "device-export": ["listExposedDevices", "listSupportedDeviceKinds"],
106893
107307
  "device-provider": ["discoverDevices", "getDevices"],
107308
+ "failure-contribution": ["list"],
106894
107309
  "llm": [
106895
107310
  "getDefaults",
106896
107311
  "getUsage",
@@ -113490,12 +113905,12 @@ var require_dist2 = __commonJS({
113490
113905
  }
113491
113906
  });
113492
113907
 
113493
- // ../system/dist/manifest-python-deps-BV_Cy99l.js
113494
- var require_manifest_python_deps_BV_Cy99l = __commonJS({
113495
- "../system/dist/manifest-python-deps-BV_Cy99l.js"(exports) {
113908
+ // ../system/dist/manifest-python-deps-BaVekjRY.js
113909
+ var require_manifest_python_deps_BaVekjRY = __commonJS({
113910
+ "../system/dist/manifest-python-deps-BaVekjRY.js"(exports) {
113496
113911
  "use strict";
113497
113912
  var require_chunk = require_chunk_Cek0wNdY();
113498
- require_dist_B_mBrEz9();
113913
+ require_dist_CImxMt5h();
113499
113914
  var node_crypto = __require("crypto");
113500
113915
  node_crypto = require_chunk.__toESM(node_crypto);
113501
113916
  var _camstack_types_node = require_node();
@@ -124610,7 +125025,7 @@ var require_dist3 = __commonJS({
124610
125025
  "use strict";
124611
125026
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
124612
125027
  var require_chunk = require_chunk_Cek0wNdY();
124613
- var require_dist10 = require_dist_B_mBrEz9();
125028
+ var require_dist10 = require_dist_CImxMt5h();
124614
125029
  var require_builtins_alerts_alerts_addon = require_alerts_addon();
124615
125030
  require_alerts();
124616
125031
  var require_formatter = require_formatter_DqAKDlvN();
@@ -124636,7 +125051,7 @@ var require_dist3 = __commonJS({
124636
125051
  var require_builtins_winston_logging_index = require_winston_logging();
124637
125052
  var require_file_data_plane = require_file_data_plane_DO8KbxCe();
124638
125053
  var require_tls$1 = require_tls_u8QCJCFE();
124639
- var require_manifest_python_deps = require_manifest_python_deps_BV_Cy99l();
125054
+ var require_manifest_python_deps = require_manifest_python_deps_BaVekjRY();
124640
125055
  var require_resource_monitor = require_resource_monitor_CdnzxBLP();
124641
125056
  var require_custom_action_registry = require_custom_action_registry_jY0NOZK8();
124642
125057
  var zod = require_zod();
@@ -205352,7 +205767,7 @@ var require_dist4 = __commonJS({
205352
205767
  "use strict";
205353
205768
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
205354
205769
  var require_event_category = require_event_category_BaEgqJNv();
205355
- var require_sleep = require_sleep_9d8tJRbO();
205770
+ var require_sleep = require_sleep_DUxF5DdC();
205356
205771
  var require_canonical_hash = require_canonical_hash_DNV8S5ET();
205357
205772
  var require_enums2 = require_enums();
205358
205773
  var require_err_msg = require_err_msg_COpsHMw2();
@@ -207228,6 +207643,97 @@ var require_dist4 = __commonJS({
207228
207643
  }
207229
207644
  return [...buckets.values()].toSorted((a, b) => a.atMs - b.atMs);
207230
207645
  }
207646
+ var MAX_REASONS_PER_KEY = 16;
207647
+ var MAX_KEYS = 1024;
207648
+ var OVERFLOW_REASON = "other";
207649
+ function counterKey(deviceId, family, variant) {
207650
+ return variant === void 0 ? `${deviceId}\0${family}` : `${deviceId}\0${family}\0${variant}`;
207651
+ }
207652
+ var FailureCounters = class {
207653
+ maxKeys;
207654
+ maxReasons;
207655
+ counters = /* @__PURE__ */ new Map();
207656
+ refused = 0;
207657
+ constructor(maxKeys = MAX_KEYS, maxReasons = 16) {
207658
+ this.maxKeys = maxKeys;
207659
+ this.maxReasons = maxReasons;
207660
+ }
207661
+ /**
207662
+ * Counters refused because {@link MAX_KEYS} was already held.
207663
+ *
207664
+ * Cumulative for the life of the instance: a bound that bit is a fact about
207665
+ * the deployment, and a surface that hid it would under-report a fleet
207666
+ * precisely when the fleet got large enough to matter.
207667
+ */
207668
+ get keysRefused() {
207669
+ return this.refused;
207670
+ }
207671
+ /** Counters currently held. */
207672
+ get size() {
207673
+ return this.counters.size;
207674
+ }
207675
+ /**
207676
+ * Fold one observation in.
207677
+ *
207678
+ * A non-positive or non-integer `deviceId` is REFUSED rather than bucketed:
207679
+ * see the module docblock — an entry that cannot name its camera is worse
207680
+ * than no entry.
207681
+ */
207682
+ note(observation, nowMs) {
207683
+ if (!Number.isInteger(observation.deviceId) || observation.deviceId <= 0) return;
207684
+ const key = counterKey(observation.deviceId, observation.family, observation.variant);
207685
+ let counter = this.counters.get(key);
207686
+ if (counter === void 0) {
207687
+ if (this.counters.size >= this.maxKeys) {
207688
+ this.refused += 1;
207689
+ return;
207690
+ }
207691
+ counter = {
207692
+ deviceId: observation.deviceId,
207693
+ family: observation.family,
207694
+ variant: observation.variant,
207695
+ sinceMs: nowMs,
207696
+ attempts: 0,
207697
+ succeeded: 0,
207698
+ reasons: /* @__PURE__ */ new Map()
207699
+ };
207700
+ this.counters.set(key, counter);
207701
+ }
207702
+ counter.attempts += 1;
207703
+ if (observation.reason === void 0) {
207704
+ counter.succeeded += 1;
207705
+ return;
207706
+ }
207707
+ const reason = counter.reasons.has(observation.reason) || counter.reasons.size < this.maxReasons ? observation.reason : OVERFLOW_REASON;
207708
+ counter.reasons.set(reason, (counter.reasons.get(reason) ?? 0) + 1);
207709
+ }
207710
+ /** Read every counter. Never mutates — see the module docblock. */
207711
+ snapshot(nowMs) {
207712
+ const out = [];
207713
+ for (const counter of this.counters.values()) out.push({
207714
+ deviceId: counter.deviceId,
207715
+ family: counter.family,
207716
+ ...counter.variant !== void 0 ? { variant: counter.variant } : {},
207717
+ sinceMs: counter.sinceMs,
207718
+ atMs: nowMs,
207719
+ attempts: counter.attempts,
207720
+ succeeded: counter.succeeded,
207721
+ reasons: [...counter.reasons.entries()].map(([reason, count]) => ({
207722
+ reason,
207723
+ count
207724
+ })).toSorted((a, b) => b.count - a.count)
207725
+ });
207726
+ return out;
207727
+ }
207728
+ /** Drop everything (host disposal). */
207729
+ clear() {
207730
+ this.counters.clear();
207731
+ }
207732
+ };
207733
+ function failureRate(sample) {
207734
+ if (sample.attempts <= 0) return null;
207735
+ return (sample.attempts - sample.succeeded) / sample.attempts;
207736
+ }
207231
207737
  var FORMAT_KEYS = [
207232
207738
  "onnx",
207233
207739
  "coreml",
@@ -209005,6 +209511,10 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
209005
209511
  kind: "mutation",
209006
209512
  auth: "admin"
209007
209513
  }),
209514
+ getIntegrationSettings: require_sleep.method(zod.z.object({
209515
+ addonId: zod.z.string(),
209516
+ nodeId: zod.z.string().optional()
209517
+ }), SettingsSchemaWithValuesSchema.nullable()),
209008
209518
  getDeviceSettings: require_sleep.method(zod.z.object({
209009
209519
  addonId: zod.z.string(),
209010
209520
  deviceId: zod.z.number(),
@@ -209284,8 +209794,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
209284
209794
  ];
209285
209795
  var DEFAULT_AUDIO_ANALYZER_CONFIG = {
209286
209796
  audioBackend: "auto",
209287
- probedBestAudioBackend: "",
209288
- selectedAudioModel: ""
209797
+ probedBestAudioBackend: ""
209289
209798
  };
209290
209799
  var audioAnalyzerCapability = {
209291
209800
  name: "audio-analyzer",
@@ -213853,6 +214362,80 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
213853
214362
  /** BIG PLAN 2: declarative mount hint — read by `@camstack/system` `buildCapRouters`. */
213854
214363
  mount: { kind: "skip" }
213855
214364
  };
214365
+ var FailureReasonCountSchema = zod.z.object({
214366
+ /**
214367
+ * Why the attempt did not land, in the contributor's own vocabulary —
214368
+ * `worker-lease-gone`, `queue-overflow`, `timeout`, `empty-read`. The same
214369
+ * strings that already appear in this repo's logs and, where one exists, the
214370
+ * same string the per-track `previewMissReason` records (D276): a second
214371
+ * vocabulary for the same loss would make the row and the counter
214372
+ * un-joinable.
214373
+ */
214374
+ reason: zod.z.string(),
214375
+ count: zod.z.number().int().nonnegative()
214376
+ });
214377
+ var FailureContributionSchema = zod.z.object({
214378
+ /**
214379
+ * The failing path — `enrichment-crop`, `inference`, `plate-ocr`,
214380
+ * `person-over-vehicle`. Free text, for the reason `load-contribution` keeps
214381
+ * `unit` free: the families are owned by different addons and a shared enum
214382
+ * is a central list that rots invisibly.
214383
+ */
214384
+ family: zod.z.string(),
214385
+ /**
214386
+ * The NUMERIC device id — the same value every log line carries as
214387
+ * `tags.deviceId`. Never nullable and never absent: a contributor that
214388
+ * cannot name the camera must not emit the entry, because a fleet total
214389
+ * cannot answer the only question anybody asks of this surface.
214390
+ */
214391
+ deviceId: zod.z.number().int().positive(),
214392
+ /**
214393
+ * A second dimension inside the family: the model / step id for an inference
214394
+ * timeout, so "which camera AND which model" is one read. Absent when the
214395
+ * family has a single variant.
214396
+ */
214397
+ variant: zod.z.string().optional(),
214398
+ /**
214399
+ * Epoch ms this counter started — the INCARNATION MARKER. A consumer
214400
+ * differencing two reads must drop the interval when it changes, because the
214401
+ * counter restarted from zero in a respawned runner. Same discipline as
214402
+ * `LoadContribution.startedAtMs`.
214403
+ */
214404
+ sinceMs: zod.z.number(),
214405
+ /** Epoch ms it was read. `atMs - sinceMs` is the interval this covers. */
214406
+ atMs: zod.z.number(),
214407
+ /**
214408
+ * THE DENOMINATOR — every attempt on this path for this camera in the
214409
+ * window. A failure count published without it is the mistake this schema
214410
+ * exists to make impossible.
214411
+ */
214412
+ attempts: zod.z.number().int().nonnegative(),
214413
+ /** Attempts that landed. `attempts - succeeded` is the loss. */
214414
+ succeeded: zod.z.number().int().nonnegative(),
214415
+ /** The loss, partitioned. Sums to `attempts - succeeded`. */
214416
+ reasons: zod.z.array(FailureReasonCountSchema).readonly()
214417
+ });
214418
+ var failureContributionCapability = {
214419
+ name: "failure-contribution",
214420
+ scope: "system",
214421
+ mode: "collection",
214422
+ internal: true,
214423
+ methods: {
214424
+ /**
214425
+ * This addon's per-camera failure counters, read live from bounded in-RAM
214426
+ * state it already keeps. Inert: no persistence, no sampling, no timer.
214427
+ *
214428
+ * READING NEVER RESETS. The counters are CUMULATIVE since `sinceMs`, and a
214429
+ * consumer that wants a rate differences two reads. A draining read would
214430
+ * make two operators with the page open each destroy half of the other's
214431
+ * numbers, and `load-contribution` already settled the same question the
214432
+ * same way for `cpuSeconds`.
214433
+ */
214434
+ list: require_sleep.method(zod.z.void(), zod.z.array(FailureContributionSchema).readonly())
214435
+ },
214436
+ /** In-process only — enumerated through `addons.listCapabilityProviders`. */
214437
+ mount: { kind: "skip" }
214438
+ };
213856
214439
  var LOAD_CONTRIBUTION_ROLES = [
213857
214440
  "decode",
213858
214441
  "transcode",
@@ -218644,6 +219227,20 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
218644
219227
  * `=== true` and render nothing otherwise — never infer "no rider".
218645
219228
  */
218646
219229
  hasRider: zod.z.boolean().optional(),
219230
+ /**
219231
+ * WHY this track ended without a NATIVE best-shot tile
219232
+ * ([D276](../decisions/adr-0276-a-stand-in-tile-is-provisional-and-a-close-says-why.md)) —
219233
+ * a composed token line (`no-key-frame capture=keyframe:native-missx4`,
219234
+ * `derive-returned-null tile=standin`, …) written at close and CLEARED by
219235
+ * the late-keyFrame upgrade when a native tile lands after all. The
219236
+ * operator-facing answer to "perché manca l'immagine?" on a track whose
219237
+ * tile is a face/plate stand-in, a raster crop, or an icon.
219238
+ *
219239
+ * **Absent ≠ "missed silently"**: a row written before the column, a hub
219240
+ * that predates the field, and every track whose tile landed native all
219241
+ * omit it. Render nothing when absent.
219242
+ */
219243
+ previewMissReason: zod.z.string().optional(),
218647
219244
  ...TrackFlagFields,
218648
219245
  ...TrackRetrainFields
218649
219246
  });
@@ -230418,6 +231015,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
230418
231015
  channels: zod.z.array(LogChannelWindowPatchSchema).readonly().optional()
230419
231016
  });
230420
231017
  var ReportedLoadContributionSchema = LoadContributionSchema.extend({ addonId: zod.z.string() });
231018
+ var ReportedFailureContributionSchema = FailureContributionSchema.extend({ addonId: zod.z.string() });
230421
231019
  var GetLoggingSettingsInputSchema = zod.z.object({
230422
231020
  scopeNodeId: zod.z.string().optional(),
230423
231021
  /**
@@ -230527,6 +231125,28 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
230527
231125
  */
230528
231126
  getLoadContributions: require_sleep.method(zod.z.void(), zod.z.array(ReportedLoadContributionSchema).readonly(), { auth: "admin" }),
230529
231127
  /**
231128
+ * Every `failure-contribution` an addon on this cluster reports — per
231129
+ * camera, per reason, **with the denominator attached**.
231130
+ *
231131
+ * This is the surface the operator asked for on 2026-08-28 (*"possiamo
231132
+ * armare questi errori intanto? Così al prossimo giro ricontrolliamo tutti
231133
+ * questi punti"*). Before it, four live failure modes could only be counted
231134
+ * by grepping Loki and hand-correlating timestamps, which is exactly how a
231135
+ * 22% thumbnail gap and a 3-hour media blackout were diagnosed — twice.
231136
+ *
231137
+ * Read it as a RATIO, never as a count. `attempts` is on every entry
231138
+ * because the count on its own lies: `enrichment crop native miss` read as
231139
+ * "35x worse than yesterday" and was flat across twelve hours once divided
231140
+ * by the successes on the same path.
231141
+ *
231142
+ * The counters are CUMULATIVE since each entry's `sinceMs`. Reading does
231143
+ * not reset them, and `sinceMs` changing means the reporting runner
231144
+ * respawned — a consumer differencing two reads drops that interval.
231145
+ *
231146
+ * Admin-only: the rows name cameras and the paths that fail on them.
231147
+ */
231148
+ getFailureContributions: require_sleep.method(zod.z.void(), zod.z.array(ReportedFailureContributionSchema).readonly(), { auth: "admin" }),
231149
+ /**
230530
231150
  * The logging settings document — levels and armed diagnostics — resolved
230531
231151
  * for `nodeId`, or for the cluster when `nodeId` is absent.
230532
231152
  *
@@ -234085,6 +234705,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
234085
234705
  eventEmitter: "event-emitter",
234086
234706
  events: "events",
234087
234707
  faceGallery: "face-gallery",
234708
+ failureContribution: "failure-contribution",
234088
234709
  fanControl: "fan-control",
234089
234710
  featureProbe: "feature-probe",
234090
234711
  filesystemBrowse: "filesystem-browse",
@@ -234409,6 +235030,10 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
234409
235030
  key: "faceGallery",
234410
235031
  name: "face-gallery"
234411
235032
  },
235033
+ {
235034
+ key: "failureContribution",
235035
+ name: "failure-contribution"
235036
+ },
234412
235037
  {
234413
235038
  key: "fanControl",
234414
235039
  name: "fan-control"
@@ -234844,6 +235469,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
234844
235469
  eventEmitterCapability,
234845
235470
  eventsCapability,
234846
235471
  faceGalleryCapability,
235472
+ failureContributionCapability,
234847
235473
  fanControlCapability,
234848
235474
  featureProbeCapability,
234849
235475
  filesystemBrowseCapability,
@@ -235180,6 +235806,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
235180
235806
  addonId: null,
235181
235807
  access: "view"
235182
235808
  },
235809
+ "addonSettings.getIntegrationSettings": {
235810
+ capName: "addon-settings",
235811
+ capScope: "system",
235812
+ addonId: null,
235813
+ access: "view"
235814
+ },
235183
235815
  "addonSettings.updateDeviceSettings": {
235184
235816
  capName: "addon-settings",
235185
235817
  capScope: "system",
@@ -236842,6 +237474,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
236842
237474
  addonId: null,
236843
237475
  access: "create"
236844
237476
  },
237477
+ "failureContribution.list": {
237478
+ capName: "failure-contribution",
237479
+ capScope: "system",
237480
+ addonId: null,
237481
+ access: "view"
237482
+ },
236845
237483
  "fanControl.setDirection": {
236846
237484
  capName: "fan-control",
236847
237485
  capScope: "device",
@@ -240148,6 +240786,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
240148
240786
  addonId: null,
240149
240787
  access: "create"
240150
240788
  },
240789
+ "system.getFailureContributions": {
240790
+ capName: "system",
240791
+ capScope: "system",
240792
+ addonId: null,
240793
+ access: "view"
240794
+ },
240151
240795
  "system.getLoadContributions": {
240152
240796
  capName: "system",
240153
240797
  capScope: "system",
@@ -240837,6 +241481,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
240837
241481
  "embedding-encoder",
240838
241482
  "events",
240839
241483
  "face-gallery",
241484
+ "failure-contribution",
240840
241485
  "fan-control",
240841
241486
  "filesystem-browse",
240842
241487
  "humidifier",
@@ -241001,6 +241646,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
241001
241646
  "device-state",
241002
241647
  "embedding-encoder",
241003
241648
  "face-gallery",
241649
+ "failure-contribution",
241004
241650
  "filesystem-browse",
241005
241651
  "integrations",
241006
241652
  "llm",
@@ -243382,7 +244028,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
243382
244028
  },
243383
244029
  addonSettings: {
243384
244030
  getGlobalSettings: (input) => dispatch("addonSettings", "getGlobalSettings", "query", input),
243385
- updateGlobalSettings: (input) => dispatch("addonSettings", "updateGlobalSettings", "mutation", input)
244031
+ updateGlobalSettings: (input) => dispatch("addonSettings", "updateGlobalSettings", "mutation", input),
244032
+ getIntegrationSettings: (input) => dispatch("addonSettings", "getIntegrationSettings", "query", input)
243386
244033
  },
243387
244034
  addonWidgets: { listWidgets: (input) => dispatch("addonWidgets", "listWidgets", "query", input) },
243388
244035
  alerts: {
@@ -243899,6 +244546,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
243899
244546
  detectSiteLocation: (input) => dispatch("system", "detectSiteLocation", "mutation", input),
243900
244547
  getRequestCensus: (input) => dispatch("system", "getRequestCensus", "query", input),
243901
244548
  getLoadContributions: (input) => dispatch("system", "getLoadContributions", "query", input),
244549
+ getFailureContributions: (input) => dispatch("system", "getFailureContributions", "query", input),
243902
244550
  getLoggingSettings: (input) => dispatch("system", "getLoggingSettings", "query", input),
243903
244551
  setLoggingSettings: (input) => dispatch("system", "setLoggingSettings", "mutation", input)
243904
244552
  },
@@ -246941,6 +247589,9 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
246941
247589
  exports.ExpressionSourceSchema = ExpressionSourceSchema;
246942
247590
  exports.FIRST_LEVEL_MACRO_CLASSES = FIRST_LEVEL_MACRO_CLASSES;
246943
247591
  exports.FULL_IMAGE_BBOX = FULL_IMAGE_BBOX;
247592
+ exports.FailureContributionSchema = FailureContributionSchema;
247593
+ exports.FailureCounters = FailureCounters;
247594
+ exports.FailureReasonCountSchema = FailureReasonCountSchema;
246944
247595
  exports.FanControlStatusSchema = FanControlStatusSchema;
246945
247596
  exports.FanDirectionSchema = FanDirectionSchema;
246946
247597
  exports.FeatureManifestSchema = FeatureManifestSchema;
@@ -247056,6 +247707,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
247056
247707
  exports.MAX_EXPRESSION_CALL_ARGS = MAX_EXPRESSION_CALL_ARGS;
247057
247708
  exports.MAX_EXPRESSION_EVAL_STEPS = MAX_EXPRESSION_EVAL_STEPS;
247058
247709
  exports.MAX_EXPRESSION_SOURCE_LENGTH = MAX_EXPRESSION_SOURCE_LENGTH;
247710
+ exports.MAX_KEYS = MAX_KEYS;
247711
+ exports.MAX_REASONS_PER_KEY = MAX_REASONS_PER_KEY;
247059
247712
  exports.MAX_SENSOR_TRIGGER_DEVICES = MAX_SENSOR_TRIGGER_DEVICES;
247060
247713
  exports.METHOD_ACCESS_MAP = METHOD_ACCESS_MAP;
247061
247714
  exports.METHOD_DEVICE_SELECTORS = METHOD_DEVICE_SELECTORS;
@@ -247226,6 +247879,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
247226
247879
  exports.OPERATOR_WRITTEN_STALE_MS = OPERATOR_WRITTEN_STALE_MS;
247227
247880
  exports.OPS_LOG_DEFAULT_LIMIT = OPS_LOG_DEFAULT_LIMIT;
247228
247881
  exports.OPS_LOG_RING_DEFAULT_MAX = OPS_LOG_RING_DEFAULT_MAX;
247882
+ exports.OVERFLOW_REASON = OVERFLOW_REASON;
247229
247883
  exports.OauthIntegrationDescriptorSchema = OauthIntegrationDescriptorSchema;
247230
247884
  exports.ObjectEventSchema = ObjectEventSchema;
247231
247885
  exports.OpsLogDomainSchema = OpsLogDomainSchema;
@@ -247347,6 +248001,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
247347
248001
  exports.RelocateMediaInputSchema = RelocateMediaInputSchema;
247348
248002
  exports.RenderedAsSchema = RenderedAsSchema;
247349
248003
  exports.ReportMotionInputSchema = ReportMotionInputSchema;
248004
+ exports.ReportedFailureContributionSchema = ReportedFailureContributionSchema;
247350
248005
  exports.ReportedLoadContributionSchema = ReportedLoadContributionSchema;
247351
248006
  exports.RequestCensusGroupSchema = RequestCensusGroupSchema;
247352
248007
  exports.RequestCensusProcedureSchema = RequestCensusProcedureSchema;
@@ -247745,6 +248400,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
247745
248400
  exports.extractNestedAddonId = extractNestedAddonId;
247746
248401
  exports.extractSourceInfoFromMetadata = extractSourceInfoFromMetadata;
247747
248402
  exports.faceGalleryCapability = faceGalleryCapability;
248403
+ exports.failureContributionCapability = failureContributionCapability;
248404
+ exports.failureRate = failureRate;
247748
248405
  exports.fanControlCapability = fanControlCapability;
247749
248406
  exports.featureProbeCapability = featureProbeCapability;
247750
248407
  exports.filesystemBrowseCapability = filesystemBrowseCapability;
@@ -405102,6 +405759,37 @@ var require_collection_preference = __commonJS({
405102
405759
  }
405103
405760
  });
405104
405761
 
405762
+ // ../../server/backend/dist/api/core/failure-contributions.js
405763
+ var require_failure_contributions = __commonJS({
405764
+ "../../server/backend/dist/api/core/failure-contributions.js"(exports) {
405765
+ "use strict";
405766
+ Object.defineProperty(exports, "__esModule", { value: true });
405767
+ exports.EMPTY_FAILURE_CONTRIBUTION_PLANE = void 0;
405768
+ exports.buildFailureContributionPlane = buildFailureContributionPlane;
405769
+ var types_1 = require_dist4();
405770
+ exports.EMPTY_FAILURE_CONTRIBUTION_PLANE = {
405771
+ contributions: async () => []
405772
+ };
405773
+ function buildFailureContributionPlane(source, onProviderError) {
405774
+ return {
405775
+ contributions: async () => {
405776
+ const out = [];
405777
+ for (const [addonId, provider] of source.entries()) {
405778
+ try {
405779
+ for (const entry of await provider.list()) {
405780
+ out.push({ ...entry, addonId });
405781
+ }
405782
+ } catch (err) {
405783
+ onProviderError?.(addonId, (0, types_1.errMsg)(err));
405784
+ }
405785
+ }
405786
+ return out;
405787
+ }
405788
+ };
405789
+ }
405790
+ }
405791
+ });
405792
+
405105
405793
  // ../../server/backend/dist/api/core/load-contributions.js
405106
405794
  var require_load_contributions = __commonJS({
405107
405795
  "../../server/backend/dist/api/core/load-contributions.js"(exports) {
@@ -406174,6 +406862,7 @@ var require_cap_providers = __commonJS({
406174
406862
  var agent_installed_packages_js_1 = require_agent_installed_packages();
406175
406863
  var http_request_census_singleton_js_1 = require_http_request_census_singleton();
406176
406864
  var collection_preference_js_1 = require_collection_preference();
406865
+ var failure_contributions_js_1 = require_failure_contributions();
406177
406866
  var load_contributions_js_1 = require_load_contributions();
406178
406867
  var logging_settings_js_1 = require_logging_settings();
406179
406868
  var request_census_settings_js_1 = require_request_census_settings();
@@ -406195,6 +406884,9 @@ var require_cap_providers = __commonJS({
406195
406884
  const loadContributions = (0, load_contributions_js_1.buildLoadContributionPlane)({ entries: () => registry?.getCollectionEntries("load-contribution") ?? [] }, (addonId, error) => {
406196
406885
  logger?.warn("load-contribution provider unreachable", { meta: { addonId, error } });
406197
406886
  });
406887
+ const failureContributions = (0, failure_contributions_js_1.buildFailureContributionPlane)({ entries: () => registry?.getCollectionEntries("failure-contribution") ?? [] }, (addonId, error) => {
406888
+ logger?.warn("failure-contribution provider unreachable", { meta: { addonId, error } });
406889
+ });
406198
406890
  const loggingSettings = new logging_settings_js_1.LoggingSettingsService({
406199
406891
  store,
406200
406892
  gate: (0, system_1.getLoggingGate)(),
@@ -406222,6 +406914,7 @@ var require_cap_providers = __commonJS({
406222
406914
  return result;
406223
406915
  },
406224
406916
  getLoadContributions: async () => loadContributions.contributions(),
406917
+ getFailureContributions: async () => failureContributions.contributions(),
406225
406918
  getRetentionConfig: async () => getRetention(registry)?.getConfig() ?? null,
406226
406919
  setRetentionConfig: async (input) => {
406227
406920
  getRetention(registry)?.setConfig(input);
@@ -412451,6 +413144,16 @@ var require_addon_settings_provider = __commonJS({
412451
413144
  }
412452
413145
  return forkedUpdate(input.addonId, "updateGlobalSettings", { patch: input.patch, ...input.nodeId ? { nodeId: input.nodeId } : {} }, input.nodeId);
412453
413146
  },
413147
+ async getIntegrationSettings(input) {
413148
+ if (gateway.isInProcess(input.addonId, input.nodeId)) {
413149
+ const addon = getAddon(input.addonId);
413150
+ if (!addon || typeof addon.getIntegrationSettings !== "function")
413151
+ return null;
413152
+ const result = await addon.getIntegrationSettings(input.nodeId);
413153
+ return result ? reshapeForOutput(result) : null;
413154
+ }
413155
+ return forkedGet(input.addonId, "getIntegrationSettings", { ...input.nodeId ? { nodeId: input.nodeId } : {} }, input.nodeId);
413156
+ },
412454
413157
  async getDeviceSettings(input) {
412455
413158
  if (gateway.isInProcess(input.addonId, input.nodeId)) {
412456
413159
  const addon = getAddon(input.addonId);