camstack 1.2.48 → 1.2.49

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-D3lqzV40.js
23637
- var require_dist_D3lqzV40 = __commonJS({
23638
- "../system/dist/dist-D3lqzV40.js"(exports) {
23636
+ // ../system/dist/dist-BVU5JADq.js
23637
+ var require_dist_BVU5JADq = __commonJS({
23638
+ "../system/dist/dist-BVU5JADq.js"(exports) {
23639
23639
  "use strict";
23640
23640
  var zod = require_zod();
23641
23641
  var EventCategory = /* @__PURE__ */ (function(EventCategory2) {
@@ -25904,6 +25904,46 @@ var require_dist_D3lqzV40 = __commonJS({
25904
25904
  function logLevelAtMost(level, threshold) {
25905
25905
  return LOG_LEVEL_RANK[level] <= LOG_LEVEL_RANK[threshold];
25906
25906
  }
25907
+ var LogChannelLevelSchema = zod.z.enum([
25908
+ "info",
25909
+ "warn",
25910
+ "error"
25911
+ ]);
25912
+ var LogChannelDescriptorSchema = zod.z.object({
25913
+ /**
25914
+ * Dotted `area.thing`, unique across the workspace. `area` is conventionally
25915
+ * the addon's short name so an operator reading a channel list can tell who
25916
+ * owns it without a second lookup.
25917
+ */
25918
+ name: zod.z.string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
25919
+ /** One sentence: what the operator will SEE after arming it. */
25920
+ description: zod.z.string().min(1),
25921
+ /** The level its lines are emitted at. Never below `info`. */
25922
+ defaultLevel: LogChannelLevelSchema,
25923
+ /**
25924
+ * Whether this channel can be narrowed to a camera.
25925
+ *
25926
+ * `true` is a PROMISE with two halves, and both must hold: the gate is
25927
+ * consulted with the numeric device id, AND every line the channel admits
25928
+ * carries `tags: { deviceId }` with that same numeric id. The second half is
25929
+ * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
25930
+ * keeps `deviceId` out of the stream labels for cardinality, so the tag in
25931
+ * the body is the only way to filter.
25932
+ *
25933
+ * A channel whose lines carry the device only in `meta` (or not at all) is
25934
+ * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
25935
+ * the operator narrows to one camera, sees nothing, and concludes the code
25936
+ * path was never taken.
25937
+ */
25938
+ perDevice: zod.z.boolean()
25939
+ });
25940
+ var LogChannelWindowSchema = zod.z.object({
25941
+ channel: zod.z.string().min(1),
25942
+ /** Epoch ms the window closes at. */
25943
+ armedUntilMs: zod.z.number(),
25944
+ /** `null` = every camera. A non-empty list narrows to those numeric ids. */
25945
+ deviceIds: zod.z.array(zod.z.number().int()).readonly().nullable()
25946
+ });
25907
25947
  var OpsLogDomainSchema = zod.z.enum(["recording", "events"]);
25908
25948
  var OpsLogOpSchema = zod.z.enum([
25909
25949
  "prune",
@@ -29461,6 +29501,21 @@ var require_dist_D3lqzV40 = __commonJS({
29461
29501
  whereBetween: zod.z.record(zod.z.string(), zod.z.tuple([zod.z.unknown(), zod.z.unknown()])).optional(),
29462
29502
  whereNot: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
29463
29503
  });
29504
+ var AggregateFieldSchema = zod.z.object({
29505
+ /** Result key. */
29506
+ as: zod.z.string().min(1),
29507
+ /** Column to aggregate. Must be a real column of a declared collection. */
29508
+ field: zod.z.string().min(1),
29509
+ op: zod.z.enum([
29510
+ "sum",
29511
+ "min",
29512
+ "max"
29513
+ ])
29514
+ });
29515
+ var AggregateResultSchema = zod.z.object({
29516
+ count: zod.z.number().int(),
29517
+ values: zod.z.record(zod.z.string(), zod.z.number().nullable())
29518
+ });
29464
29519
  var SettingsRecordSchema = zod.z.object({
29465
29520
  id: zod.z.string(),
29466
29521
  data: zod.z.record(zod.z.string(), zod.z.unknown())
@@ -29588,6 +29643,32 @@ var require_dist_D3lqzV40 = __commonJS({
29588
29643
  collection: zod.z.string(),
29589
29644
  filter: QueryFilterSchema.optional()
29590
29645
  }), zod.z.number()),
29646
+ /**
29647
+ * `COUNT(*)` and one `SUM` / `MIN` / `MAX` per requested field, in ONE
29648
+ * statement, over the rows `filter` selects.
29649
+ *
29650
+ * Exists because "how much is there" was being answered by materialising
29651
+ * "what is there". The recorder's storage-pressure sweep asked its in-RAM
29652
+ * footage index for bytes/count/oldest/newest across a set of storage
29653
+ * locations twice a minute, and the only way to answer that from a map is
29654
+ * to visit every row — 7.1 M of them on the live hub, ~15 M row visits a
29655
+ * minute on the main thread, which is also why the whole archive had to
29656
+ * stay resident to be visited. The question is a sum; nothing needs to be
29657
+ * materialised to answer it.
29658
+ *
29659
+ * **The engine REFUSES a field it cannot serve**, exactly as
29660
+ * `query.columns` does and unlike a PREDICATE, which is skipped when
29661
+ * unresolvable. A dropped predicate over-matches and the caller sees extra
29662
+ * rows; a dropped aggregate returns a NUMBER that is wrong and looks
29663
+ * exactly like a real one. That asymmetry is what this repo has already
29664
+ * paid for once in `count`.
29665
+ */
29666
+ aggregate: method(zod.z.object({
29667
+ namespace: zod.z.string().optional(),
29668
+ collection: zod.z.string(),
29669
+ fields: zod.z.array(AggregateFieldSchema).readonly(),
29670
+ filter: QueryFilterSchema.optional()
29671
+ }), AggregateResultSchema),
29591
29672
  /** Grouped counts per ((field-origin)/bucketSize) bucket, filtered. */
29592
29673
  histogram: method(zod.z.object({
29593
29674
  namespace: zod.z.string().optional(),
@@ -29746,6 +29827,15 @@ var require_dist_D3lqzV40 = __commonJS({
29746
29827
  collection: zod.z.string(),
29747
29828
  filter: QueryFilterSchema.optional()
29748
29829
  }), zod.z.number(), { auth: "admin" }),
29830
+ /** `COUNT(*)` plus one SUM/MIN/MAX per field, in one statement. Mirror of
29831
+ * `settings-store.aggregate` — see it for why an unresolvable field is
29832
+ * refused rather than dropped. */
29833
+ aggregate: method(zod.z.object({
29834
+ namespace: zod.z.string().optional(),
29835
+ collection: zod.z.string(),
29836
+ fields: zod.z.array(AggregateFieldSchema).readonly(),
29837
+ filter: QueryFilterSchema.optional()
29838
+ }), AggregateResultSchema, { auth: "admin" }),
29749
29839
  /** Grouped counts per ((field-origin)/bucketSize) bucket, filtered. */
29750
29840
  histogram: method(zod.z.object({
29751
29841
  namespace: zod.z.string().optional(),
@@ -30857,6 +30947,39 @@ var require_dist_D3lqzV40 = __commonJS({
30857
30947
  /** List children of a parent device (by parent numeric id). */
30858
30948
  getChildren: method(zod.z.object({ parentDeviceId: zod.z.number() }), zod.z.array(DeviceInfoSchema)),
30859
30949
  /**
30950
+ * `getChildren` for a NAMED SET of parents, in one call.
30951
+ *
30952
+ * The accessory reconcile in `device-cap-proxy.ts` asks this question once
30953
+ * per registered device — every `BaseDevice` inherits a
30954
+ * `getAccessoryChildren()` that returns `[]`, so even a leaf accessory
30955
+ * pays a round-trip to learn it has nothing to prune. Measured on the live
30956
+ * hub 2026-08-27 over a 120-second boot window, fleet of 1 017 devices:
30957
+ * `DeviceRowStore.list < DeviceRowStore.listByParent < getChildren` at
30958
+ * **1 024 calls** returning **919 rows in total** — 1 024 RPCs and 1 024
30959
+ * indexed scans to move less than one row each. `listByParentMany`
30960
+ * collapses the scans; this collapses the RPCs.
30961
+ *
30962
+ * Keyed by parent id as a STRING — a JSON object cannot key by number
30963
+ * (same reason as `getDeviceStatusAggregateBatch`). The per-parent value
30964
+ * is exactly what `getChildren` returns for that parent.
30965
+ *
30966
+ * A parent with no children — or one the fleet does not know — is ABSENT
30967
+ * from the record, never an invented empty row: the same contract as
30968
+ * `DeviceRowStore.getMany`/`listByParentMany`. An EMPTY `parentDeviceIds`
30969
+ * reads nothing at all rather than degrading to "every device".
30970
+ *
30971
+ * `parentDeviceIds` is capped at {@link DEVICE_CHILDREN_BATCH_MAX} — see
30972
+ * that constant for why. A caller with more parents than that sends more
30973
+ * than one call; it never sends one pathological one.
30974
+ *
30975
+ * Version skew: this is a NEW method, not a new field on `getChildren`, so
30976
+ * a hub that predates it answers NOT_FOUND rather than silently stripping
30977
+ * an unknown input key and answering a DIFFERENT question. The kernel-side
30978
+ * loader degrades to per-parent `getChildren` on that error — see
30979
+ * `children-batch-loader.ts`.
30980
+ */
30981
+ getChildrenBatch: method(zod.z.object({ parentDeviceIds: zod.z.array(zod.z.number()).max(256) }), zod.z.record(zod.z.string(), zod.z.array(DeviceInfoSchema))),
30982
+ /**
30860
30983
  * Resolve the devices LINKED to a camera — the single policy authority
30861
30984
  * both consumers call (viewer devices panel + pipeline-analytics event
30862
30985
  * kinds/ingest). Device-tree children are ALWAYS included; mode 'auto'
@@ -31901,6 +32024,38 @@ var require_dist_D3lqzV40 = __commonJS({
31901
32024
  })
31902
32025
  }
31903
32026
  };
32027
+ var LogChannelApplyResultSchema = zod.z.object({
32028
+ /** How many declared channels are armed in this process after the call. */
32029
+ armed: zod.z.number().int().min(0),
32030
+ /**
32031
+ * Names the document armed that this process does not declare. Reported
32032
+ * rather than swallowed: a name here is either a typo or an addon that has
32033
+ * not booted, and both deserve a line instead of silence.
32034
+ */
32035
+ unknown: zod.z.array(zod.z.string()).readonly()
32036
+ });
32037
+ var logChannelsCapability = {
32038
+ name: "log-channels",
32039
+ scope: "system",
32040
+ mode: "collection",
32041
+ internal: true,
32042
+ methods: {
32043
+ /** The channels this addon declares. Inert: no value, no state. */
32044
+ list: method(zod.z.void(), zod.z.array(LogChannelDescriptorSchema).readonly()),
32045
+ /**
32046
+ * Refresh this process's mirror from the document's FULL set of armed
32047
+ * windows.
32048
+ *
32049
+ * Full and not incremental on purpose: the document is the authority, so a
32050
+ * channel it does not name is disarmed here. An incremental apply would
32051
+ * let a disarm get lost in transit and leave a channel running that
32052
+ * nobody can see is running.
32053
+ */
32054
+ apply: method(zod.z.object({ windows: zod.z.array(LogChannelWindowSchema).readonly() }), LogChannelApplyResultSchema, { kind: "mutation" })
32055
+ },
32056
+ /** In-process only — enumerated through `addons.listCapabilityProviders`. */
32057
+ mount: { kind: "skip" }
32058
+ };
31904
32059
  var LogLevelSchema = zod.z.enum([
31905
32060
  "debug",
31906
32061
  "info",
@@ -47226,6 +47381,14 @@ var require_dist_D3lqzV40 = __commonJS({
47226
47381
  scope: LoggingScopeKindSchema,
47227
47382
  /** The node this layer speaks for; `null` on the cluster layer. */
47228
47383
  nodeId: zod.z.string().nullable(),
47384
+ /**
47385
+ * The declared channel this layer speaks for; `null` on every layer but
47386
+ * `component`. Never folded into `nodeId`: a component level is CLUSTER-WIDE
47387
+ * by design — the convention this repo settled on is one orchestrator-wide
47388
+ * setting, never per node (D52) — so a component layer that carried a node
47389
+ * would invite a per-node copy of a value that has no per-node meaning.
47390
+ */
47391
+ component: zod.z.string().nullable(),
47229
47392
  /** Explicitly set here, or `null` when this layer inherits. */
47230
47393
  level: LogLevelSchema$1.nullable()
47231
47394
  });
@@ -47251,6 +47414,38 @@ var require_dist_D3lqzV40 = __commonJS({
47251
47414
  /** Cadence of the diagnostic's aggregated report line. Clamped by the server. */
47252
47415
  reportEveryMs: zod.z.number().int().positive().optional()
47253
47416
  });
47417
+ var LogChannelWindowStateSchema = zod.z.object({
47418
+ channel: zod.z.string(),
47419
+ armed: zod.z.boolean(),
47420
+ /** Epoch ms the window closes at. 0 when disarmed. */
47421
+ armedUntilMs: zod.z.number(),
47422
+ /** Ms left before it expires on its own. 0 when disarmed. */
47423
+ remainingMs: zod.z.number(),
47424
+ /**
47425
+ * The cameras it is narrowed to, or `null` for every camera.
47426
+ *
47427
+ * A channel declared `perDevice: false` can only ever report `null` here:
47428
+ * its lines do not carry `tags: { deviceId }`, so narrowing them would
47429
+ * produce a filter that silently matches nothing. The server REFUSES such a
47430
+ * patch rather than quietly widening it — ignoring the request would teach
47431
+ * the operator that per-camera filtering works on that channel when it does
47432
+ * not.
47433
+ */
47434
+ deviceIds: zod.z.array(zod.z.number().int()).readonly().nullable()
47435
+ });
47436
+ var LogChannelWindowPatchSchema = zod.z.object({
47437
+ channel: zod.z.string().min(1),
47438
+ armMs: zod.z.number().int().min(0),
47439
+ /**
47440
+ * Narrow to these numeric device ids. Absent or `null` = every camera.
47441
+ *
47442
+ * Numeric because the repo's own rule makes it possible: every log line
47443
+ * about a device carries `tags: { deviceId }` with the numeric id. That rule
47444
+ * was paid for with a 22% thumbnail gap and a 3-hour media blackout both
47445
+ * diagnosed by hand, and this is the first thing that collects on it.
47446
+ */
47447
+ deviceIds: zod.z.array(zod.z.number().int()).readonly().nullable().optional()
47448
+ });
47254
47449
  var LoggingSettingsPatchSchema = zod.z.object({
47255
47450
  /**
47256
47451
  * Absent leaves the level untouched. `null` CLEARS the explicit value at the
@@ -47261,19 +47456,50 @@ var require_dist_D3lqzV40 = __commonJS({
47261
47456
  * Only the diagnostics NAMED here change. An armed window that is not listed
47262
47457
  * keeps running — a patch is never a full replacement.
47263
47458
  */
47264
- diagnostics: zod.z.array(DiagnosticWindowPatchSchema).readonly().optional()
47459
+ diagnostics: zod.z.array(DiagnosticWindowPatchSchema).readonly().optional(),
47460
+ /**
47461
+ * Only the channels NAMED here change. An armed channel that is not listed
47462
+ * keeps running — same rule as `diagnostics`, because a patch that silently
47463
+ * disarmed the channels it did not mention would make the Levels page and
47464
+ * the Diagnostics page fight over the same value.
47465
+ */
47466
+ channels: zod.z.array(LogChannelWindowPatchSchema).readonly().optional()
47467
+ });
47468
+ var GetLoggingSettingsInputSchema = zod.z.object({
47469
+ scopeNodeId: zod.z.string().optional(),
47470
+ /**
47471
+ * The declared CHANNEL this document is addressed at, when the caller wants
47472
+ * the `component` layer. Absent = the node/cluster hierarchy only.
47473
+ *
47474
+ * Naming it separately rather than overloading `scopeNodeId` keeps the two
47475
+ * axes from collapsing: a component level is cluster-wide, a node level is
47476
+ * not, and one selector for both would make "which of these two did I just
47477
+ * set" unanswerable — the exact ambiguity `explicit` exists to remove.
47478
+ */
47479
+ scopeComponent: zod.z.string().optional()
47265
47480
  });
47266
- var GetLoggingSettingsInputSchema = zod.z.object({ scopeNodeId: zod.z.string().optional() });
47267
47481
  var SetLoggingSettingsInputSchema = zod.z.object({
47268
47482
  scopeNodeId: zod.z.string().optional(),
47483
+ scopeComponent: zod.z.string().optional(),
47269
47484
  patch: LoggingSettingsPatchSchema
47270
47485
  });
47271
47486
  var LoggingSettingsStateSchema = zod.z.object({
47272
47487
  /** The layer this document was read at. `null` = the cluster layer. */
47273
47488
  scopeNodeId: zod.z.string().nullable(),
47489
+ /** The channel this document was read at. `null` = no component layer. */
47490
+ scopeComponent: zod.z.string().nullable(),
47274
47491
  effective: LoggingEffectiveSchema,
47275
47492
  explicit: LoggingExplicitSchema,
47276
47493
  activeWindows: zod.z.array(DiagnosticWindowSchema).readonly(),
47494
+ /**
47495
+ * Every channel the cluster's addons DECLARE, gathered from the
47496
+ * `log-channels` providers. Not stored anywhere: assembled per read, so a
47497
+ * channel added by a redeployed addon appears without anybody editing a
47498
+ * list, and a channel whose addon is gone stops being offered.
47499
+ */
47500
+ channels: zod.z.array(LogChannelDescriptorSchema).readonly(),
47501
+ /** The channels ARMED right now, each with its deadline. */
47502
+ activeChannels: zod.z.array(LogChannelWindowStateSchema).readonly(),
47277
47503
  persisted: zod.z.boolean()
47278
47504
  });
47279
47505
  var systemCapability = {
@@ -48752,6 +48978,7 @@ var require_dist_D3lqzV40 = __commonJS({
48752
48978
  llmRuntimeCapability,
48753
48979
  localNetworkCapability,
48754
48980
  lockControlCapability,
48981
+ logChannelsCapability,
48755
48982
  logDestinationCapability,
48756
48983
  loginMethodCapability,
48757
48984
  mediaPlayerCapability,
@@ -49718,6 +49945,12 @@ var require_dist_D3lqzV40 = __commonJS({
49718
49945
  addonId: null,
49719
49946
  access: "view"
49720
49947
  },
49948
+ "dataStoreProvider.aggregate": {
49949
+ capName: "data-store-provider",
49950
+ capScope: "system",
49951
+ addonId: null,
49952
+ access: "view"
49953
+ },
49721
49954
  "dataStoreProvider.count": {
49722
49955
  capName: "data-store-provider",
49723
49956
  capScope: "system",
@@ -50132,6 +50365,12 @@ var require_dist_D3lqzV40 = __commonJS({
50132
50365
  addonId: null,
50133
50366
  access: "view"
50134
50367
  },
50368
+ "deviceManager.getChildrenBatch": {
50369
+ capName: "device-manager",
50370
+ capScope: "system",
50371
+ addonId: null,
50372
+ access: "view"
50373
+ },
50135
50374
  "deviceManager.getConfigSchema": {
50136
50375
  capName: "device-manager",
50137
50376
  capScope: "system",
@@ -51182,6 +51421,18 @@ var require_dist_D3lqzV40 = __commonJS({
51182
51421
  addonId: null,
51183
51422
  access: "create"
51184
51423
  },
51424
+ "logChannels.apply": {
51425
+ capName: "log-channels",
51426
+ capScope: "system",
51427
+ addonId: null,
51428
+ access: "create"
51429
+ },
51430
+ "logChannels.list": {
51431
+ capName: "log-channels",
51432
+ capScope: "system",
51433
+ addonId: null,
51434
+ access: "view"
51435
+ },
51185
51436
  "logDestination.query": {
51186
51437
  capName: "log-destination",
51187
51438
  capScope: "system",
@@ -53336,6 +53587,12 @@ var require_dist_D3lqzV40 = __commonJS({
53336
53587
  addonId: null,
53337
53588
  access: "create"
53338
53589
  },
53590
+ "settingsStore.aggregate": {
53591
+ capName: "settings-store",
53592
+ capScope: "system",
53593
+ addonId: null,
53594
+ access: "view"
53595
+ },
53339
53596
  "settingsStore.count": {
53340
53597
  capName: "settings-store",
53341
53598
  capScope: "system",
@@ -54915,6 +55172,11 @@ var require_dist_D3lqzV40 = __commonJS({
54915
55172
  form: "single",
54916
55173
  optional: false
54917
55174
  }],
55175
+ "deviceManager.getChildrenBatch": [{
55176
+ name: "parentDeviceIds",
55177
+ form: "array",
55178
+ optional: false
55179
+ }],
54918
55180
  "deviceManager.getConfigSchema": [{
54919
55181
  name: "deviceId",
54920
55182
  form: "single",
@@ -57730,7 +57992,7 @@ var require_alerts_addon = __commonJS({
57730
57992
  [Symbol.toStringTag]: { value: "Module" }
57731
57993
  });
57732
57994
  require_chunk_Cek0wNdY();
57733
- var require_dist10 = require_dist_D3lqzV40();
57995
+ var require_dist10 = require_dist_BVU5JADq();
57734
57996
  function selectExpired(alerts, cutoffMs) {
57735
57997
  return alerts.filter((a) => a.createdAt < cutoffMs).sort((a, b) => a.createdAt - b.createdAt).map((a) => a.id);
57736
57998
  }
@@ -58549,7 +58811,7 @@ var require_console_logging = __commonJS({
58549
58811
  [Symbol.toStringTag]: { value: "Module" }
58550
58812
  });
58551
58813
  require_chunk_Cek0wNdY();
58552
- var require_dist10 = require_dist_D3lqzV40();
58814
+ var require_dist10 = require_dist_BVU5JADq();
58553
58815
  var require_formatter = require_formatter_DqAKDlvN();
58554
58816
  var LEVEL_RANK = {
58555
58817
  debug: 0,
@@ -58643,7 +58905,7 @@ var require_core_blocks_addon = __commonJS({
58643
58905
  "use strict";
58644
58906
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
58645
58907
  var require_chunk = require_chunk_Cek0wNdY();
58646
- var require_dist10 = require_dist_D3lqzV40();
58908
+ var require_dist10 = require_dist_BVU5JADq();
58647
58909
  var node_crypto = __require("crypto");
58648
58910
  var node_fs_promises = __require("fs/promises");
58649
58911
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -59540,11 +59802,11 @@ var require_core_blocks = __commonJS({
59540
59802
  }
59541
59803
  });
59542
59804
 
59543
- // ../system/dist/retired-settings-keys-OtUQtEbq.js
59544
- var require_retired_settings_keys_OtUQtEbq = __commonJS({
59545
- "../system/dist/retired-settings-keys-OtUQtEbq.js"(exports) {
59805
+ // ../system/dist/retired-settings-keys-_PLI9w0k.js
59806
+ var require_retired_settings_keys_PLI9w0k = __commonJS({
59807
+ "../system/dist/retired-settings-keys-_PLI9w0k.js"(exports) {
59546
59808
  "use strict";
59547
- var require_dist10 = require_dist_D3lqzV40();
59809
+ var require_dist10 = require_dist_BVU5JADq();
59548
59810
  function settingsStoreIsAuthoritativeHere(env) {
59549
59811
  const raw = (env["CAMSTACK_ROLE"] ?? "").trim().toLowerCase();
59550
59812
  return raw === "" || raw === "hub";
@@ -61587,8 +61849,8 @@ var require_device_manager_addon = __commonJS({
61587
61849
  [Symbol.toStringTag]: { value: "Module" }
61588
61850
  });
61589
61851
  require_chunk_Cek0wNdY();
61590
- var require_dist10 = require_dist_D3lqzV40();
61591
- var require_retired_settings_keys = require_retired_settings_keys_OtUQtEbq();
61852
+ var require_dist10 = require_dist_BVU5JADq();
61853
+ var require_retired_settings_keys = require_retired_settings_keys_PLI9w0k();
61592
61854
  var node_crypto = __require("crypto");
61593
61855
  var _camstack_types_node = require_node();
61594
61856
  var JOB_HISTORY = 20;
@@ -62750,30 +63012,16 @@ var require_device_manager_addon = __commonJS({
62750
63012
  ...sourceInfoGetDevice !== void 0 ? { sourceInfo: sourceInfoGetDevice } : {}
62751
63013
  };
62752
63014
  }
62753
- async function getChildren(pctx, input) {
62754
- const { parentDeviceId } = input;
62755
- let ownerAddonId = null;
62756
- if (pctx.registry) {
62757
- if (pctx.registry.getById(parentDeviceId)) ownerAddonId = pctx.registry.getAddonId(parentDeviceId);
62758
- }
62759
- if (!ownerAddonId) {
62760
- const persisted = await pctx.metaStore.resolvePersistedById(parentDeviceId);
62761
- if (!persisted) return [];
62762
- ownerAddonId = persisted.addonId;
62763
- }
63015
+ async function projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, liveChildren) {
62764
63016
  const results = [];
62765
63017
  const seen = /* @__PURE__ */ new Set();
62766
- const childRows = await pctx.metaStore.rows.listByParent(parentDeviceId);
62767
63018
  const rowById = /* @__PURE__ */ new Map();
62768
63019
  for (const row of childRows) rowById.set(row.meta.id, row);
62769
- if (pctx.registry) {
62770
- const liveChildren = pctx.registry.getChildren(parentDeviceId);
62771
- for (const device of liveChildren) {
62772
- const key = String(device.id);
62773
- const row = rowById.get(device.id);
62774
- results.push(toDeviceInfo(ownerAddonId, device, row?.metadata ?? null, row?.meta ?? null));
62775
- seen.add(key);
62776
- }
63020
+ for (const device of liveChildren) {
63021
+ const key = String(device.id);
63022
+ const row = rowById.get(device.id);
63023
+ results.push(toDeviceInfo(ownerAddonId, device, row?.metadata ?? null, row?.meta ?? null));
63024
+ seen.add(key);
62777
63025
  }
62778
63026
  for (const row of childRows) {
62779
63027
  const m = row.meta;
@@ -62809,6 +63057,58 @@ var require_device_manager_addon = __commonJS({
62809
63057
  }
62810
63058
  return results;
62811
63059
  }
63060
+ function liveOwnerOf(pctx, parentDeviceId) {
63061
+ if (!pctx.registry) return null;
63062
+ if (!pctx.registry.getById(parentDeviceId)) return null;
63063
+ return pctx.registry.getAddonId(parentDeviceId);
63064
+ }
63065
+ async function getChildren(pctx, input) {
63066
+ const { parentDeviceId } = input;
63067
+ let ownerAddonId = liveOwnerOf(pctx, parentDeviceId);
63068
+ if (!ownerAddonId) {
63069
+ const persisted = await pctx.metaStore.resolvePersistedById(parentDeviceId);
63070
+ if (!persisted) return [];
63071
+ ownerAddonId = persisted.addonId;
63072
+ }
63073
+ const childRows = await pctx.metaStore.rows.listByParent(parentDeviceId);
63074
+ const liveChildren = pctx.registry?.getChildren(parentDeviceId) ?? [];
63075
+ return [...await projectChildren(pctx, parentDeviceId, ownerAddonId, childRows, liveChildren)];
63076
+ }
63077
+ async function getChildrenBatch(pctx, input) {
63078
+ const parentIds = [...new Set(input.parentDeviceIds)];
63079
+ const out = {};
63080
+ if (parentIds.length === 0) return out;
63081
+ const owners = /* @__PURE__ */ new Map();
63082
+ const unownedIds = [];
63083
+ for (const parentId of parentIds) {
63084
+ const live = liveOwnerOf(pctx, parentId);
63085
+ if (live !== null) owners.set(parentId, live);
63086
+ else unownedIds.push(parentId);
63087
+ }
63088
+ if (unownedIds.length > 0) {
63089
+ const rows = await pctx.metaStore.rows.getMany(unownedIds);
63090
+ for (const [parentId, row] of rows) owners.set(parentId, row.meta.addonId);
63091
+ }
63092
+ const requested = new Set(parentIds);
63093
+ const rowsByParent = await pctx.metaStore.rows.listByParentMany([...owners.keys()]);
63094
+ const liveByParent = /* @__PURE__ */ new Map();
63095
+ for (const device of pctx.registry?.getAll() ?? []) {
63096
+ const parentId = device.parentDeviceId;
63097
+ if (parentId === null || parentId === void 0) continue;
63098
+ if (!requested.has(parentId)) continue;
63099
+ const bucket = liveByParent.get(parentId);
63100
+ if (bucket === void 0) liveByParent.set(parentId, [device]);
63101
+ else bucket.push(device);
63102
+ }
63103
+ for (const [parentId, ownerAddonId] of owners) {
63104
+ const childRows = rowsByParent.get(parentId) ?? [];
63105
+ const liveChildren = liveByParent.get(parentId) ?? [];
63106
+ if (childRows.length === 0 && liveChildren.length === 0) continue;
63107
+ const projected = await projectChildren(pctx, parentId, ownerAddonId, childRows, liveChildren);
63108
+ if (projected.length > 0) out[String(parentId)] = [...projected];
63109
+ }
63110
+ return out;
63111
+ }
62812
63112
  async function getStreamSources(pctx, input) {
62813
63113
  const { deviceId } = input;
62814
63114
  if (pctx.registry) {
@@ -64255,16 +64555,17 @@ var require_device_manager_addon = __commonJS({
64255
64555
  async function collectDescendants(metaStore, rootId) {
64256
64556
  const out = [];
64257
64557
  const visited = /* @__PURE__ */ new Set([rootId]);
64258
- const queue = [rootId];
64259
- while (queue.length > 0) {
64260
- const parentId = queue.shift();
64261
- if (parentId === void 0) break;
64262
- for (const row of await metaStore.rows.listByParent(parentId)) {
64558
+ let frontier = [rootId];
64559
+ while (frontier.length > 0) {
64560
+ const byParent = await metaStore.rows.listByParentMany(frontier);
64561
+ const next = [];
64562
+ for (const parentId of frontier) for (const row of byParent.get(parentId) ?? []) {
64263
64563
  if (visited.has(row.meta.id)) continue;
64264
64564
  visited.add(row.meta.id);
64265
64565
  out.push(row.meta);
64266
- queue.push(row.meta.id);
64566
+ next.push(row.meta.id);
64267
64567
  }
64568
+ frontier = next;
64268
64569
  }
64269
64570
  return out;
64270
64571
  }
@@ -65323,6 +65624,46 @@ var require_device_manager_addon = __commonJS({
65323
65624
  limit: DEVICE_ROWS_FLEET_LIMIT
65324
65625
  });
65325
65626
  }
65627
+ /**
65628
+ * Direct children of MANY devices, in ONE query.
65629
+ *
65630
+ * `listByParent` called once per parent is the same shape `get` was before
65631
+ * {@link getMany}: an indexed lookup this store makes cheap, repeated until
65632
+ * it isn't. Measured on the live hub 2026-08-27, 120-second boot window:
65633
+ * `DeviceRowStore.list < DeviceRowStore.listByParent < getChildren` at
65634
+ * **1 024 calls** — one per device — for 919 rows in total. The BFS in
65635
+ * `device-meta-actions.ts#collectDescendants` walked a container tree the
65636
+ * same way, one query per node.
65637
+ *
65638
+ * `whereIn` on the indexed `parentDeviceId` column collapses a level of that
65639
+ * walk into a single statement. Contract, verbatim from {@link getMany}: an
65640
+ * EMPTY `parentDeviceIds` reads nothing at all rather than degrading to
65641
+ * "everything", and a parent with no children — or one the fleet does not
65642
+ * know — is simply ABSENT from the map, never an invented empty row.
65643
+ */
65644
+ async listByParentMany(parentDeviceIds) {
65645
+ const out = /* @__PURE__ */ new Map();
65646
+ const unique = [...new Set(parentDeviceIds)];
65647
+ if (unique.length === 0) return out;
65648
+ const rows = await this.list({
65649
+ whereIn: { parentDeviceId: unique },
65650
+ orderBy: {
65651
+ field: "deviceId",
65652
+ direction: "asc"
65653
+ },
65654
+ limit: DEVICE_ROWS_FLEET_LIMIT
65655
+ });
65656
+ const byParent = /* @__PURE__ */ new Map();
65657
+ for (const row of rows) {
65658
+ const parent = row.meta.parentDeviceId;
65659
+ if (parent === null || parent === void 0) continue;
65660
+ const bucket = byParent.get(parent);
65661
+ if (bucket === void 0) byParent.set(parent, [row]);
65662
+ else bucket.push(row);
65663
+ }
65664
+ for (const [parent, bucket] of byParent) out.set(parent, bucket);
65665
+ return out;
65666
+ }
65326
65667
  /** How many devices the fleet holds. Used to tell "empty store" from "gone device". */
65327
65668
  async count() {
65328
65669
  await this.declare();
@@ -66056,6 +66397,7 @@ var require_device_manager_addon = __commonJS({
66056
66397
  listAll: (input) => listAll(pctx, input),
66057
66398
  getDevice: (input) => getDevice(pctx, input),
66058
66399
  getChildren: (input) => getChildren(pctx, input),
66400
+ getChildrenBatch: (input) => getChildrenBatch(pctx, input),
66059
66401
  getLinkedDevices: (input) => getLinkedDevices(pctx, input),
66060
66402
  getLinkedDevicesBatch: (input) => getLinkedDevicesBatch(pctx, input),
66061
66403
  getDeviceSettingsContribution: (input) => buildLinkedDevicesContribution(pctx, input.deviceId),
@@ -66257,7 +66599,7 @@ var require_hub_forwarder = __commonJS({
66257
66599
  [Symbol.toStringTag]: { value: "Module" }
66258
66600
  });
66259
66601
  require_chunk_Cek0wNdY();
66260
- var require_dist10 = require_dist_D3lqzV40();
66602
+ var require_dist10 = require_dist_BVU5JADq();
66261
66603
  var require_formatter = require_formatter_DqAKDlvN();
66262
66604
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
66263
66605
  var HubForwarderDestination = class {
@@ -66394,7 +66736,7 @@ var require_liveness_monitor_addon = __commonJS({
66394
66736
  "use strict";
66395
66737
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
66396
66738
  require_chunk_Cek0wNdY();
66397
- var require_dist10 = require_dist_D3lqzV40();
66739
+ var require_dist10 = require_dist_BVU5JADq();
66398
66740
  var NO_DEVICES = "liveness:no-devices";
66399
66741
  var ALL_OFFLINE = "liveness:all-devices-offline";
66400
66742
  var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
@@ -66584,7 +66926,7 @@ var require_local_auth_addon = __commonJS({
66584
66926
  [Symbol.toStringTag]: { value: "Module" }
66585
66927
  });
66586
66928
  var require_chunk = require_chunk_Cek0wNdY();
66587
- var require_dist10 = require_dist_D3lqzV40();
66929
+ var require_dist10 = require_dist_BVU5JADq();
66588
66930
  var node_crypto = __require("crypto");
66589
66931
  node_crypto = require_chunk.__toESM(node_crypto);
66590
66932
  var crypto$1 = __require("crypto");
@@ -74268,7 +74610,7 @@ var require_loki_logging = __commonJS({
74268
74610
  [Symbol.toStringTag]: { value: "Module" }
74269
74611
  });
74270
74612
  require_chunk_Cek0wNdY();
74271
- var require_dist10 = require_dist_D3lqzV40();
74613
+ var require_dist10 = require_dist_BVU5JADq();
74272
74614
  function sanitizeLabelName(raw) {
74273
74615
  const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
74274
74616
  return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
@@ -74833,7 +75175,7 @@ var require_native_metrics_addon = __commonJS({
74833
75175
  [Symbol.toStringTag]: { value: "Module" }
74834
75176
  });
74835
75177
  var require_chunk = require_chunk_Cek0wNdY();
74836
- var require_dist10 = require_dist_D3lqzV40();
75178
+ var require_dist10 = require_dist_BVU5JADq();
74837
75179
  var node_child_process = __require("child_process");
74838
75180
  var node_util = __require("util");
74839
75181
  var node_os = __require("os");
@@ -75775,7 +76117,7 @@ var require_filesystem_storage_addon = __commonJS({
75775
76117
  [Symbol.toStringTag]: { value: "Module" }
75776
76118
  });
75777
76119
  var require_chunk = require_chunk_Cek0wNdY();
75778
- var require_dist10 = require_dist_D3lqzV40();
76120
+ var require_dist10 = require_dist_BVU5JADq();
75779
76121
  var node_crypto = __require("crypto");
75780
76122
  var node_fs_promises = __require("fs/promises");
75781
76123
  var node_path = __require("path");
@@ -76891,8 +77233,8 @@ var require_sqlite_settings_addon = __commonJS({
76891
77233
  [Symbol.toStringTag]: { value: "Module" }
76892
77234
  });
76893
77235
  var require_chunk = require_chunk_Cek0wNdY();
76894
- var require_dist10 = require_dist_D3lqzV40();
76895
- var require_retired_settings_keys = require_retired_settings_keys_OtUQtEbq();
77236
+ var require_dist10 = require_dist_BVU5JADq();
77237
+ var require_retired_settings_keys = require_retired_settings_keys_PLI9w0k();
76896
77238
  var node_crypto = __require("crypto");
76897
77239
  var node_fs = __require("fs");
76898
77240
  var node_module = __require("module");
@@ -77733,6 +78075,46 @@ var require_sqlite_settings_addon = __commonJS({
77733
78075
  params
77734
78076
  }, () => this.getDb().prepare(sql).get(...params))?.cnt ?? 0;
77735
78077
  }
78078
+ /**
78079
+ * `COUNT(*)` and one SUM/MIN/MAX per requested field, in ONE statement.
78080
+ *
78081
+ * Result slots are aliased POSITIONALLY (`a0`, `a1`, …) and mapped back by
78082
+ * index: `as` is caller text and must never reach SQL as an identifier, and
78083
+ * the column name is validated through {@link fieldExprFor} exactly like a
78084
+ * projection — an unresolvable field THROWS rather than being dropped, because
78085
+ * a missing aggregate comes back as a number that looks real.
78086
+ *
78087
+ * `select` mode on the filter, so this agrees with `query` and `count` about
78088
+ * which rows it is talking about.
78089
+ */
78090
+ async aggregate({ namespace, collection, fields, filter }) {
78091
+ const scoped = this.scopedName(namespace, collection);
78092
+ const decl = this.requireDeclared(scoped);
78093
+ const shape = this.shapeOf(decl);
78094
+ const selects = ['COUNT(*) AS "n"'];
78095
+ fields.forEach((f, i) => {
78096
+ const col = fieldExprFor(f.field, shape);
78097
+ if (col === null) throw new UnsafeFilterError(`aggregate cannot read "${f.field}" on "${scoped}" \u2014 it is not a column of this collection, and answering without it would return a number that looks real`);
78098
+ selects.push(`${f.op.toUpperCase()}(${col}) AS "a${i}"`);
78099
+ });
78100
+ const { whereSql, params } = compileFilter$1(filter, shape, "select", (v) => this.serializeColumnValue(v));
78101
+ const sql = `SELECT ${selects.join(", ")} FROM "${scoped}"${whereSql}`;
78102
+ const row = this.measured({
78103
+ op: "aggregate",
78104
+ collection: scoped,
78105
+ sql,
78106
+ params
78107
+ }, () => this.getDb().prepare(sql).get(...params));
78108
+ const values = {};
78109
+ fields.forEach((f, i) => {
78110
+ const raw = row?.[`a${i}`];
78111
+ values[f.as] = typeof raw === "number" && Number.isFinite(raw) ? raw : null;
78112
+ });
78113
+ return {
78114
+ count: row?.["n"] ?? 0,
78115
+ values
78116
+ };
78117
+ }
77736
78118
  async histogram({ namespace, collection, field, bucketSize, origin, filter }) {
77737
78119
  const scoped = this.scopedName(namespace, collection);
77738
78120
  const decl = this.requireDeclared(scoped);
@@ -79086,7 +79468,7 @@ var require_storage_orchestrator_addon = __commonJS({
79086
79468
  [Symbol.toStringTag]: { value: "Module" }
79087
79469
  });
79088
79470
  var require_chunk = require_chunk_Cek0wNdY();
79089
- var require_dist10 = require_dist_D3lqzV40();
79471
+ var require_dist10 = require_dist_BVU5JADq();
79090
79472
  var node_crypto = __require("crypto");
79091
79473
  var node_fs_promises = __require("fs/promises");
79092
79474
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -79655,6 +80037,7 @@ var require_storage_orchestrator_addon = __commonJS({
79655
80037
  deleteWhere: async (input) => (await engine()).deleteWhere(input),
79656
80038
  updateWhere: async (input) => (await engine()).updateWhere(input),
79657
80039
  count: async (input) => (await engine()).count(input),
80040
+ aggregate: async (input) => (await engine()).aggregate(input),
79658
80041
  histogram: async (input) => (await engine()).histogram(input),
79659
80042
  isEmpty: async (input) => (await engine()).isEmpty(input),
79660
80043
  declareCollection: async (input) => (await engine()).declareCollection(input)
@@ -80965,7 +81348,7 @@ var require_system_config_addon = __commonJS({
80965
81348
  [Symbol.toStringTag]: { value: "Module" }
80966
81349
  });
80967
81350
  require_chunk_Cek0wNdY();
80968
- var require_dist10 = require_dist_D3lqzV40();
81351
+ var require_dist10 = require_dist_BVU5JADq();
80969
81352
  var SECTION_TITLES = {
80970
81353
  server: "Server",
80971
81354
  auth: "Authentication"
@@ -99026,7 +99409,7 @@ var require_winston_logging = __commonJS({
99026
99409
  [Symbol.toStringTag]: { value: "Module" }
99027
99410
  });
99028
99411
  var require_chunk = require_chunk_Cek0wNdY();
99029
- var require_dist10 = require_dist_D3lqzV40();
99412
+ var require_dist10 = require_dist_BVU5JADq();
99030
99413
  var require_formatter = require_formatter_DqAKDlvN();
99031
99414
  var node_path = __require("path");
99032
99415
  node_path = require_chunk.__toESM(node_path);
@@ -104814,6 +105197,7 @@ var require_addon = __commonJS({
104814
105197
  "listProfiles",
104815
105198
  "listRuntimeNodes"
104816
105199
  ],
105200
+ "log-channels": ["list"],
104817
105201
  "log-destination": ["query"],
104818
105202
  "login-method": ["getLoginMethods"],
104819
105203
  "mqtt-broker": ["listBrokers"],
@@ -111401,11 +111785,12 @@ var require_dist2 = __commonJS({
111401
111785
  }
111402
111786
  });
111403
111787
 
111404
- // ../system/dist/manifest-python-deps-B_mCU6gz.js
111405
- var require_manifest_python_deps_B_mCU6gz = __commonJS({
111406
- "../system/dist/manifest-python-deps-B_mCU6gz.js"(exports) {
111788
+ // ../system/dist/manifest-python-deps-GjlyPjm0.js
111789
+ var require_manifest_python_deps_GjlyPjm0 = __commonJS({
111790
+ "../system/dist/manifest-python-deps-GjlyPjm0.js"(exports) {
111407
111791
  "use strict";
111408
111792
  var require_chunk = require_chunk_Cek0wNdY();
111793
+ require_dist_BVU5JADq();
111409
111794
  var node_crypto = __require("crypto");
111410
111795
  node_crypto = require_chunk.__toESM(node_crypto);
111411
111796
  var _camstack_types_node = require_node();
@@ -112479,6 +112864,105 @@ var require_manifest_python_deps_B_mCU6gz = __commonJS({
112479
112864
  });
112480
112865
  return true;
112481
112866
  }
112867
+ function parseChildRows(value) {
112868
+ if (!Array.isArray(value)) return null;
112869
+ const rows = [];
112870
+ for (const entry of value) {
112871
+ if (typeof entry !== "object" || entry === null) return null;
112872
+ const id = Reflect.get(entry, "id");
112873
+ const stableId = Reflect.get(entry, "stableId");
112874
+ const name = Reflect.get(entry, "name");
112875
+ const linkDeviceId = Reflect.get(entry, "linkDeviceId");
112876
+ if (typeof id !== "number" || typeof stableId !== "string" || typeof name !== "string") return null;
112877
+ rows.push({
112878
+ id,
112879
+ stableId,
112880
+ name,
112881
+ ...typeof linkDeviceId === "number" || linkDeviceId === null ? { linkDeviceId } : {}
112882
+ });
112883
+ }
112884
+ return rows;
112885
+ }
112886
+ function parseChildrenByParent(value) {
112887
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
112888
+ const out = /* @__PURE__ */ new Map();
112889
+ for (const [key, entry] of Object.entries(value)) {
112890
+ const parentDeviceId = Number(key);
112891
+ if (!Number.isInteger(parentDeviceId)) return null;
112892
+ const rows = parseChildRows(entry);
112893
+ if (rows === null) return null;
112894
+ out.set(parentDeviceId, rows);
112895
+ }
112896
+ return out;
112897
+ }
112898
+ function looksLikeUnknownMethod(err) {
112899
+ const lowered = (err instanceof Error ? err.message : String(err)).toLowerCase();
112900
+ return lowered.includes("not_found") || lowered.includes("no procedure") || lowered.includes("unknown method") || lowered.includes("no such method") || lowered.includes("not available");
112901
+ }
112902
+ function createChildrenBatchLoader(opts) {
112903
+ const maxBatch = Math.max(1, opts.maxBatch ?? 256);
112904
+ let pending = [];
112905
+ let flushScheduled = false;
112906
+ let flushing = false;
112907
+ let batchUnsupported = false;
112908
+ const answerOneByOne = async (waiters) => {
112909
+ await Promise.all(waiters.map(async (waiter) => {
112910
+ try {
112911
+ waiter.resolve(await opts.fetchOne(waiter.parentDeviceId));
112912
+ } catch (err) {
112913
+ waiter.reject(err);
112914
+ }
112915
+ }));
112916
+ };
112917
+ const runBatch = async (waiters) => {
112918
+ const parentDeviceIds = [...new Set(waiters.map((w) => w.parentDeviceId))];
112919
+ try {
112920
+ const byParent = await opts.fetchBatch(parentDeviceIds);
112921
+ for (const waiter of waiters) waiter.resolve(byParent.get(waiter.parentDeviceId) ?? []);
112922
+ } catch (err) {
112923
+ const unknown = looksLikeUnknownMethod(err);
112924
+ if (unknown) batchUnsupported = true;
112925
+ opts.logger.warn(unknown ? "device-manager.getChildrenBatch is not available on this hub \u2014 falling back to per-parent getChildren for the process lifetime" : "device-manager.getChildrenBatch failed \u2014 answering this batch per parent", { meta: {
112926
+ parents: parentDeviceIds.length,
112927
+ error: err instanceof Error ? err.message : String(err)
112928
+ } });
112929
+ await answerOneByOne(waiters);
112930
+ }
112931
+ };
112932
+ const flush = async () => {
112933
+ flushing = true;
112934
+ try {
112935
+ while (pending.length > 0) {
112936
+ const batch = pending.slice(0, maxBatch);
112937
+ pending = pending.slice(maxBatch);
112938
+ if (batchUnsupported) await answerOneByOne(batch);
112939
+ else await runBatch(batch);
112940
+ }
112941
+ } finally {
112942
+ flushing = false;
112943
+ }
112944
+ };
112945
+ const scheduleFlush = () => {
112946
+ if (flushScheduled || flushing) return;
112947
+ flushScheduled = true;
112948
+ setTimeout(() => {
112949
+ flushScheduled = false;
112950
+ flush().catch((err) => {
112951
+ opts.logger.warn("children batch flush threw", { meta: { error: err instanceof Error ? err.message : String(err) } });
112952
+ });
112953
+ }, 0);
112954
+ };
112955
+ return { load(parentDeviceId) {
112956
+ return new Promise((resolve, reject) => {
112957
+ pending.push({
112958
+ parentDeviceId,
112959
+ resolve,
112960
+ reject
112961
+ });
112962
+ scheduleFlush();
112963
+ });
112964
+ } };
112965
+ }
112482
112966
  var ACCESSORY_SPAWN_CONCURRENCY = 12;
112483
112967
  async function runBounded(items, limit, worker2) {
112484
112968
  const effectiveLimit = Math.max(1, Math.min(limit, items.length));
@@ -112686,6 +113170,19 @@ var require_manifest_python_deps_B_mCU6gz = __commonJS({
112686
113170
  api
112687
113171
  });
112688
113172
  let selfApi;
113173
+ const childrenLoader = createChildrenBatchLoader({
113174
+ logger: opts.logger,
113175
+ fetchBatch: async (parentDeviceIds) => {
113176
+ const parsed = parseChildrenByParent(await callDeviceManager(api, "getChildrenBatch", { parentDeviceIds }));
113177
+ if (parsed === null) throw new Error("getChildrenBatch returned a shape this kernel cannot read");
113178
+ return parsed;
113179
+ },
113180
+ fetchOne: async (parentDeviceId) => {
113181
+ const parsed = parseChildRows(await callDeviceManager(api, "getChildren", { parentDeviceId }));
113182
+ if (parsed === null) throw new Error("getChildren returned a shape this kernel cannot read");
113183
+ return parsed;
113184
+ }
113185
+ });
112689
113186
  const deviceRebuildFactories = /* @__PURE__ */ new Map();
112690
113187
  const buildContext = (stableId, id, parentDeviceId = null, initialRuntimeState = {}, persistedConfig = {}, deviceMeta = null) => {
112691
113188
  let runtimeStateRef = null;
@@ -113097,7 +113594,7 @@ var require_manifest_python_deps_B_mCU6gz = __commonJS({
113097
113594
  return;
113098
113595
  }
113099
113596
  try {
113100
- const currentChildren = await callDeviceManager(api, "getChildren", { parentDeviceId: device.id });
113597
+ const currentChildren = await childrenLoader.load(device.id);
113101
113598
  for (const child of currentChildren) {
113102
113599
  if (child.linkDeviceId !== device.id) continue;
113103
113600
  if (expected.has(child.stableId)) continue;
@@ -122274,7 +122771,7 @@ var require_dist3 = __commonJS({
122274
122771
  "use strict";
122275
122772
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
122276
122773
  var require_chunk = require_chunk_Cek0wNdY();
122277
- var require_dist10 = require_dist_D3lqzV40();
122774
+ var require_dist10 = require_dist_BVU5JADq();
122278
122775
  var require_builtins_alerts_alerts_addon = require_alerts_addon();
122279
122776
  require_alerts();
122280
122777
  var require_formatter = require_formatter_DqAKDlvN();
@@ -122300,7 +122797,7 @@ var require_dist3 = __commonJS({
122300
122797
  var require_builtins_winston_logging_index = require_winston_logging();
122301
122798
  var require_file_data_plane = require_file_data_plane_DO8KbxCe();
122302
122799
  var require_tls$1 = require_tls_u8QCJCFE();
122303
- var require_manifest_python_deps = require_manifest_python_deps_B_mCU6gz();
122800
+ var require_manifest_python_deps = require_manifest_python_deps_GjlyPjm0();
122304
122801
  var require_resource_monitor = require_resource_monitor_CdnzxBLP();
122305
122802
  var require_custom_action_registry = require_custom_action_registry_jY0NOZK8();
122306
122803
  var zod = require_zod();
@@ -203840,6 +204337,253 @@ var require_dist4 = __commonJS({
203840
204337
  function logLevelAtMost(level, threshold) {
203841
204338
  return LOG_LEVEL_RANK[level] <= LOG_LEVEL_RANK[threshold];
203842
204339
  }
204340
+ var LogChannelLevelSchema = zod.z.enum([
204341
+ "info",
204342
+ "warn",
204343
+ "error"
204344
+ ]);
204345
+ var LogChannelDescriptorSchema = zod.z.object({
204346
+ /**
204347
+ * Dotted `area.thing`, unique across the workspace. `area` is conventionally
204348
+ * the addon's short name so an operator reading a channel list can tell who
204349
+ * owns it without a second lookup.
204350
+ */
204351
+ name: zod.z.string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
204352
+ /** One sentence: what the operator will SEE after arming it. */
204353
+ description: zod.z.string().min(1),
204354
+ /** The level its lines are emitted at. Never below `info`. */
204355
+ defaultLevel: LogChannelLevelSchema,
204356
+ /**
204357
+ * Whether this channel can be narrowed to a camera.
204358
+ *
204359
+ * `true` is a PROMISE with two halves, and both must hold: the gate is
204360
+ * consulted with the numeric device id, AND every line the channel admits
204361
+ * carries `tags: { deviceId }` with that same numeric id. The second half is
204362
+ * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
204363
+ * keeps `deviceId` out of the stream labels for cardinality, so the tag in
204364
+ * the body is the only way to filter.
204365
+ *
204366
+ * A channel whose lines carry the device only in `meta` (or not at all) is
204367
+ * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
204368
+ * the operator narrows to one camera, sees nothing, and concludes the code
204369
+ * path was never taken.
204370
+ */
204371
+ perDevice: zod.z.boolean()
204372
+ });
204373
+ var LogChannelWindowSchema = zod.z.object({
204374
+ channel: zod.z.string().min(1),
204375
+ /** Epoch ms the window closes at. */
204376
+ armedUntilMs: zod.z.number(),
204377
+ /** `null` = every camera. A non-empty list narrows to those numeric ids. */
204378
+ deviceIds: zod.z.array(zod.z.number().int()).readonly().nullable()
204379
+ });
204380
+ var LogChannelGate = class {
204381
+ descriptor;
204382
+ /**
204383
+ * HOT PATH GUARD. A plain data FIELD, and it must stay one.
204384
+ *
204385
+ * `log-channel.spec.ts` asserts the property descriptor has no getter and
204386
+ * booby-traps the device set, so turning this into an accessor — or reading
204387
+ * anything before it — fails the spec instead of taxing every line the
204388
+ * process emits.
204389
+ */
204390
+ on = false;
204391
+ /** `null` while armed for every camera. Never read while `on` is false. */
204392
+ devices = null;
204393
+ level;
204394
+ closesAtMs = 0;
204395
+ constructor(descriptor) {
204396
+ this.descriptor = descriptor;
204397
+ this.level = descriptor.defaultLevel;
204398
+ }
204399
+ /** Epoch ms this channel disarms itself at. 0 when disarmed. */
204400
+ get armedUntilMs() {
204401
+ return this.on ? this.closesAtMs : 0;
204402
+ }
204403
+ /**
204404
+ * Does this channel want a line about `deviceId`?
204405
+ *
204406
+ * Call it only behind `gate.on &&`. On its own it is still correct — the
204407
+ * guard is repeated inside — but the point of the prefix is that a disarmed
204408
+ * channel must not pay the call at all.
204409
+ */
204410
+ wants(deviceId) {
204411
+ if (!this.on) return false;
204412
+ return this.devices === null || this.devices.has(deviceId);
204413
+ }
204414
+ /**
204415
+ * Emit one line on this channel, at the channel's declared level.
204416
+ *
204417
+ * The channel name is added as `tags.logChannel` so LogQL can select the
204418
+ * channel without matching on the message text, and whatever `tags` the
204419
+ * caller passed — `deviceId` above all — is preserved.
204420
+ */
204421
+ log(logger, message, extras) {
204422
+ if (!this.on) return;
204423
+ const tags = {
204424
+ ...extras.tags,
204425
+ logChannel: this.descriptor.name
204426
+ };
204427
+ const line = {
204428
+ ...extras,
204429
+ tags
204430
+ };
204431
+ if (this.level === "error") logger.error(message, line);
204432
+ else if (this.level === "warn") logger.warn(message, line);
204433
+ else logger.info(message, line);
204434
+ }
204435
+ /**
204436
+ * Arm (or RE-arm, restarting) this channel. Off the hot path only.
204437
+ *
204438
+ * An empty `deviceIds` list is treated as "every camera" rather than "no
204439
+ * camera": a window that matches nothing is indistinguishable from a
204440
+ * disarmed one, and the operator who asked for it would wait for lines that
204441
+ * can never come.
204442
+ */
204443
+ arm(window2) {
204444
+ const ids = window2.deviceIds;
204445
+ this.devices = ids === null || ids.length === 0 ? null : new Set(ids);
204446
+ this.closesAtMs = window2.armedUntilMs;
204447
+ this.on = true;
204448
+ }
204449
+ /** Disarm. Off the hot path only. */
204450
+ disarm() {
204451
+ this.on = false;
204452
+ this.devices = null;
204453
+ this.closesAtMs = 0;
204454
+ }
204455
+ };
204456
+ var LogChannelRegistry = class {
204457
+ gates = /* @__PURE__ */ new Map();
204458
+ /**
204459
+ * Declare a channel and get its gate.
204460
+ *
204461
+ * A duplicate name throws. Two declarations of one name is a programming
204462
+ * error, not a merge: the operator would arm one and the other would stay
204463
+ * dark, which is the dead-knob shape (D62) with an extra step.
204464
+ */
204465
+ declare(descriptor) {
204466
+ const parsed = LogChannelDescriptorSchema.parse(descriptor);
204467
+ if (this.gates.get(parsed.name) !== void 0) throw new Error(`log channel "${parsed.name}" is already declared in this process \u2014 two declarations of one name is a programming error, not a merge`);
204468
+ const gate = new LogChannelGate(parsed);
204469
+ this.gates.set(parsed.name, gate);
204470
+ return gate;
204471
+ }
204472
+ /** The declarations, sorted by name so a list is stable to read and diff. */
204473
+ list() {
204474
+ return [...this.gates.values()].map((gate) => gate.descriptor).sort((a, b) => a.name.localeCompare(b.name));
204475
+ }
204476
+ /** The gate for a declared channel, or `undefined`. */
204477
+ gate(name) {
204478
+ return this.gates.get(name);
204479
+ }
204480
+ /**
204481
+ * Apply the FULL set of armed windows. Off the hot path.
204482
+ *
204483
+ * Full, not incremental, and that is the whole design: the document is the
204484
+ * authority, so a channel the document does not name is disarmed here. An
204485
+ * incremental apply would let a disarm get lost in transit and leave a
204486
+ * channel running that nobody can see is running.
204487
+ *
204488
+ * A window already past its deadline is ignored rather than armed — a
204489
+ * restore that re-armed an expired window would make a forgotten diagnostic
204490
+ * immortal across restarts.
204491
+ *
204492
+ * Returns the names it could not place, so the caller can log them: a
204493
+ * channel named in the document that this process does not declare is
204494
+ * either a typo or an addon that has not booted yet, and both deserve a
204495
+ * line rather than silence.
204496
+ */
204497
+ apply(windows, nowMs) {
204498
+ const wanted = /* @__PURE__ */ new Map();
204499
+ const unknown = [];
204500
+ for (const window2 of windows) {
204501
+ if (window2.armedUntilMs <= nowMs) continue;
204502
+ if (!this.gates.has(window2.channel)) {
204503
+ unknown.push(window2.channel);
204504
+ continue;
204505
+ }
204506
+ wanted.set(window2.channel, window2);
204507
+ }
204508
+ for (const [name, gate] of this.gates) {
204509
+ const window2 = wanted.get(name);
204510
+ if (window2 === void 0) gate.disarm();
204511
+ else gate.arm(window2);
204512
+ }
204513
+ return unknown;
204514
+ }
204515
+ /**
204516
+ * Disarm whatever has run out. Called on a timer, NEVER from a log path — a
204517
+ * diagnostic that adds a `Date.now()` to the path it is measuring measures
204518
+ * itself.
204519
+ *
204520
+ * Returns the names it closed, so the caller can write the one line that
204521
+ * says a window ended and stops "it went quiet" from reading as "the branch
204522
+ * was not taken".
204523
+ */
204524
+ tick(nowMs) {
204525
+ const closed = [];
204526
+ for (const [name, gate] of this.gates) if (gate.on && gate.armedUntilMs <= nowMs) {
204527
+ gate.disarm();
204528
+ closed.push(name);
204529
+ }
204530
+ return closed;
204531
+ }
204532
+ /** The channels armed right now, as the document would describe them. */
204533
+ armed() {
204534
+ const out = [];
204535
+ for (const [name, gate] of this.gates) if (gate.on) out.push({
204536
+ channel: name,
204537
+ armedUntilMs: gate.armedUntilMs,
204538
+ deviceIds: null
204539
+ });
204540
+ return out;
204541
+ }
204542
+ };
204543
+ var instance = null;
204544
+ function getLogChannelRegistry() {
204545
+ instance ??= new LogChannelRegistry();
204546
+ return instance;
204547
+ }
204548
+ function declareLogChannel(descriptor) {
204549
+ return getLogChannelRegistry().declare(descriptor);
204550
+ }
204551
+ function __resetLogChannelRegistryForTests() {
204552
+ instance = null;
204553
+ }
204554
+ var LOG_CHANNEL_TICK_MS = 5e3;
204555
+ function createLogChannelsProvider(logger, options = {}) {
204556
+ const registry = getLogChannelRegistry();
204557
+ const now = options.now ?? Date.now;
204558
+ const tickMs = options.tickMs ?? 5e3;
204559
+ const timer = setInterval(() => {
204560
+ const closed = registry.tick(now());
204561
+ for (const name of closed) logger.info("log channel window closed", {
204562
+ tags: { logChannel: name },
204563
+ meta: { channel: name }
204564
+ });
204565
+ }, tickMs);
204566
+ timer.unref?.();
204567
+ return {
204568
+ list: () => registry.list(),
204569
+ apply: (input) => {
204570
+ const unknown = registry.apply(input.windows, now());
204571
+ const armed = registry.armed();
204572
+ logger.info("log channels applied", { meta: {
204573
+ armed: armed.map((window2) => window2.channel),
204574
+ unknown,
204575
+ declared: registry.list().length
204576
+ } });
204577
+ return {
204578
+ armed: armed.length,
204579
+ unknown
204580
+ };
204581
+ },
204582
+ stop: () => {
204583
+ clearInterval(timer);
204584
+ }
204585
+ };
204586
+ }
203843
204587
  var OpsLogDomainSchema = zod.z.enum(["recording", "events"]);
203844
204588
  var OpsLogOpSchema = zod.z.enum([
203845
204589
  "prune",
@@ -208416,6 +209160,21 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
208416
209160
  whereBetween: zod.z.record(zod.z.string(), zod.z.tuple([zod.z.unknown(), zod.z.unknown()])).optional(),
208417
209161
  whereNot: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
208418
209162
  });
209163
+ var AggregateFieldSchema = zod.z.object({
209164
+ /** Result key. */
209165
+ as: zod.z.string().min(1),
209166
+ /** Column to aggregate. Must be a real column of a declared collection. */
209167
+ field: zod.z.string().min(1),
209168
+ op: zod.z.enum([
209169
+ "sum",
209170
+ "min",
209171
+ "max"
209172
+ ])
209173
+ });
209174
+ var AggregateResultSchema = zod.z.object({
209175
+ count: zod.z.number().int(),
209176
+ values: zod.z.record(zod.z.string(), zod.z.number().nullable())
209177
+ });
208419
209178
  var SettingsRecordSchema = zod.z.object({
208420
209179
  id: zod.z.string(),
208421
209180
  data: zod.z.record(zod.z.string(), zod.z.unknown())
@@ -208543,6 +209302,32 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
208543
209302
  collection: zod.z.string(),
208544
209303
  filter: QueryFilterSchema.optional()
208545
209304
  }), zod.z.number()),
209305
+ /**
209306
+ * `COUNT(*)` and one `SUM` / `MIN` / `MAX` per requested field, in ONE
209307
+ * statement, over the rows `filter` selects.
209308
+ *
209309
+ * Exists because "how much is there" was being answered by materialising
209310
+ * "what is there". The recorder's storage-pressure sweep asked its in-RAM
209311
+ * footage index for bytes/count/oldest/newest across a set of storage
209312
+ * locations twice a minute, and the only way to answer that from a map is
209313
+ * to visit every row — 7.1 M of them on the live hub, ~15 M row visits a
209314
+ * minute on the main thread, which is also why the whole archive had to
209315
+ * stay resident to be visited. The question is a sum; nothing needs to be
209316
+ * materialised to answer it.
209317
+ *
209318
+ * **The engine REFUSES a field it cannot serve**, exactly as
209319
+ * `query.columns` does and unlike a PREDICATE, which is skipped when
209320
+ * unresolvable. A dropped predicate over-matches and the caller sees extra
209321
+ * rows; a dropped aggregate returns a NUMBER that is wrong and looks
209322
+ * exactly like a real one. That asymmetry is what this repo has already
209323
+ * paid for once in `count`.
209324
+ */
209325
+ aggregate: require_sleep.method(zod.z.object({
209326
+ namespace: zod.z.string().optional(),
209327
+ collection: zod.z.string(),
209328
+ fields: zod.z.array(AggregateFieldSchema).readonly(),
209329
+ filter: QueryFilterSchema.optional()
209330
+ }), AggregateResultSchema),
208546
209331
  /** Grouped counts per ((field-origin)/bucketSize) bucket, filtered. */
208547
209332
  histogram: require_sleep.method(zod.z.object({
208548
209333
  namespace: zod.z.string().optional(),
@@ -208701,6 +209486,15 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
208701
209486
  collection: zod.z.string(),
208702
209487
  filter: QueryFilterSchema.optional()
208703
209488
  }), zod.z.number(), { auth: "admin" }),
209489
+ /** `COUNT(*)` plus one SUM/MIN/MAX per field, in one statement. Mirror of
209490
+ * `settings-store.aggregate` — see it for why an unresolvable field is
209491
+ * refused rather than dropped. */
209492
+ aggregate: require_sleep.method(zod.z.object({
209493
+ namespace: zod.z.string().optional(),
209494
+ collection: zod.z.string(),
209495
+ fields: zod.z.array(AggregateFieldSchema).readonly(),
209496
+ filter: QueryFilterSchema.optional()
209497
+ }), AggregateResultSchema, { auth: "admin" }),
208704
209498
  /** Grouped counts per ((field-origin)/bucketSize) bucket, filtered. */
208705
209499
  histogram: require_sleep.method(zod.z.object({
208706
209500
  namespace: zod.z.string().optional(),
@@ -209415,6 +210209,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
209415
210209
  })
209416
210210
  }
209417
210211
  };
210212
+ var DEVICE_CHILDREN_BATCH_MAX = 256;
209418
210213
  var ChildLayoutEntrySchema = zod.z.object({
209419
210214
  childKey: zod.z.string(),
209420
210215
  section: zod.z.string(),
@@ -209896,6 +210691,39 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
209896
210691
  /** List children of a parent device (by parent numeric id). */
209897
210692
  getChildren: require_sleep.method(zod.z.object({ parentDeviceId: zod.z.number() }), zod.z.array(DeviceInfoSchema)),
209898
210693
  /**
210694
+ * `getChildren` for a NAMED SET of parents, in one call.
210695
+ *
210696
+ * The accessory reconcile in `device-cap-proxy.ts` asks this question once
210697
+ * per registered device — every `BaseDevice` inherits a
210698
+ * `getAccessoryChildren()` that returns `[]`, so even a leaf accessory
210699
+ * pays a round-trip to learn it has nothing to prune. Measured on the live
210700
+ * hub 2026-08-27 over a 120-second boot window, fleet of 1 017 devices:
210701
+ * `DeviceRowStore.list < DeviceRowStore.listByParent < getChildren` at
210702
+ * **1 024 calls** returning **919 rows in total** — 1 024 RPCs and 1 024
210703
+ * indexed scans to move less than one row each. `listByParentMany`
210704
+ * collapses the scans; this collapses the RPCs.
210705
+ *
210706
+ * Keyed by parent id as a STRING — a JSON object cannot key by number
210707
+ * (same reason as `getDeviceStatusAggregateBatch`). The per-parent value
210708
+ * is exactly what `getChildren` returns for that parent.
210709
+ *
210710
+ * A parent with no children — or one the fleet does not know — is ABSENT
210711
+ * from the record, never an invented empty row: the same contract as
210712
+ * `DeviceRowStore.getMany`/`listByParentMany`. An EMPTY `parentDeviceIds`
210713
+ * reads nothing at all rather than degrading to "every device".
210714
+ *
210715
+ * `parentDeviceIds` is capped at {@link DEVICE_CHILDREN_BATCH_MAX} — see
210716
+ * that constant for why. A caller with more parents than that sends more
210717
+ * than one call; it never sends one pathological one.
210718
+ *
210719
+ * Version skew: this is a NEW method, not a new field on `getChildren`, so
210720
+ * a hub that predates it answers NOT_FOUND rather than silently stripping
210721
+ * an unknown input key and answering a DIFFERENT question. The kernel-side
210722
+ * loader degrades to per-parent `getChildren` on that error — see
210723
+ * `children-batch-loader.ts`.
210724
+ */
210725
+ getChildrenBatch: require_sleep.method(zod.z.object({ parentDeviceIds: zod.z.array(zod.z.number()).max(256) }), zod.z.record(zod.z.string(), zod.z.array(DeviceInfoSchema))),
210726
+ /**
209899
210727
  * Resolve the devices LINKED to a camera — the single policy authority
209900
210728
  * both consumers call (viewer devices panel + pipeline-analytics event
209901
210729
  * kinds/ingest). Device-tree children are ALWAYS included; mode 'auto'
@@ -210947,6 +211775,38 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
210947
211775
  })
210948
211776
  }
210949
211777
  };
211778
+ var LogChannelApplyResultSchema = zod.z.object({
211779
+ /** How many declared channels are armed in this process after the call. */
211780
+ armed: zod.z.number().int().min(0),
211781
+ /**
211782
+ * Names the document armed that this process does not declare. Reported
211783
+ * rather than swallowed: a name here is either a typo or an addon that has
211784
+ * not booted, and both deserve a line instead of silence.
211785
+ */
211786
+ unknown: zod.z.array(zod.z.string()).readonly()
211787
+ });
211788
+ var logChannelsCapability = {
211789
+ name: "log-channels",
211790
+ scope: "system",
211791
+ mode: "collection",
211792
+ internal: true,
211793
+ methods: {
211794
+ /** The channels this addon declares. Inert: no value, no state. */
211795
+ list: require_sleep.method(zod.z.void(), zod.z.array(LogChannelDescriptorSchema).readonly()),
211796
+ /**
211797
+ * Refresh this process's mirror from the document's FULL set of armed
211798
+ * windows.
211799
+ *
211800
+ * Full and not incremental on purpose: the document is the authority, so a
211801
+ * channel it does not name is disarmed here. An incremental apply would
211802
+ * let a disarm get lost in transit and leave a channel running that
211803
+ * nobody can see is running.
211804
+ */
211805
+ apply: require_sleep.method(zod.z.object({ windows: zod.z.array(LogChannelWindowSchema).readonly() }), LogChannelApplyResultSchema, { kind: "mutation" })
211806
+ },
211807
+ /** In-process only — enumerated through `addons.listCapabilityProviders`. */
211808
+ mount: { kind: "skip" }
211809
+ };
210950
211810
  var LogLevelSchema = zod.z.enum([
210951
211811
  "debug",
210952
211812
  "info",
@@ -227266,6 +228126,14 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
227266
228126
  scope: LoggingScopeKindSchema,
227267
228127
  /** The node this layer speaks for; `null` on the cluster layer. */
227268
228128
  nodeId: zod.z.string().nullable(),
228129
+ /**
228130
+ * The declared channel this layer speaks for; `null` on every layer but
228131
+ * `component`. Never folded into `nodeId`: a component level is CLUSTER-WIDE
228132
+ * by design — the convention this repo settled on is one orchestrator-wide
228133
+ * setting, never per node (D52) — so a component layer that carried a node
228134
+ * would invite a per-node copy of a value that has no per-node meaning.
228135
+ */
228136
+ component: zod.z.string().nullable(),
227269
228137
  /** Explicitly set here, or `null` when this layer inherits. */
227270
228138
  level: LogLevelSchema$1.nullable()
227271
228139
  });
@@ -227291,6 +228159,38 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
227291
228159
  /** Cadence of the diagnostic's aggregated report line. Clamped by the server. */
227292
228160
  reportEveryMs: zod.z.number().int().positive().optional()
227293
228161
  });
228162
+ var LogChannelWindowStateSchema = zod.z.object({
228163
+ channel: zod.z.string(),
228164
+ armed: zod.z.boolean(),
228165
+ /** Epoch ms the window closes at. 0 when disarmed. */
228166
+ armedUntilMs: zod.z.number(),
228167
+ /** Ms left before it expires on its own. 0 when disarmed. */
228168
+ remainingMs: zod.z.number(),
228169
+ /**
228170
+ * The cameras it is narrowed to, or `null` for every camera.
228171
+ *
228172
+ * A channel declared `perDevice: false` can only ever report `null` here:
228173
+ * its lines do not carry `tags: { deviceId }`, so narrowing them would
228174
+ * produce a filter that silently matches nothing. The server REFUSES such a
228175
+ * patch rather than quietly widening it — ignoring the request would teach
228176
+ * the operator that per-camera filtering works on that channel when it does
228177
+ * not.
228178
+ */
228179
+ deviceIds: zod.z.array(zod.z.number().int()).readonly().nullable()
228180
+ });
228181
+ var LogChannelWindowPatchSchema = zod.z.object({
228182
+ channel: zod.z.string().min(1),
228183
+ armMs: zod.z.number().int().min(0),
228184
+ /**
228185
+ * Narrow to these numeric device ids. Absent or `null` = every camera.
228186
+ *
228187
+ * Numeric because the repo's own rule makes it possible: every log line
228188
+ * about a device carries `tags: { deviceId }` with the numeric id. That rule
228189
+ * was paid for with a 22% thumbnail gap and a 3-hour media blackout both
228190
+ * diagnosed by hand, and this is the first thing that collects on it.
228191
+ */
228192
+ deviceIds: zod.z.array(zod.z.number().int()).readonly().nullable().optional()
228193
+ });
227294
228194
  var LoggingSettingsPatchSchema = zod.z.object({
227295
228195
  /**
227296
228196
  * Absent leaves the level untouched. `null` CLEARS the explicit value at the
@@ -227301,19 +228201,50 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
227301
228201
  * Only the diagnostics NAMED here change. An armed window that is not listed
227302
228202
  * keeps running — a patch is never a full replacement.
227303
228203
  */
227304
- diagnostics: zod.z.array(DiagnosticWindowPatchSchema).readonly().optional()
228204
+ diagnostics: zod.z.array(DiagnosticWindowPatchSchema).readonly().optional(),
228205
+ /**
228206
+ * Only the channels NAMED here change. An armed channel that is not listed
228207
+ * keeps running — same rule as `diagnostics`, because a patch that silently
228208
+ * disarmed the channels it did not mention would make the Levels page and
228209
+ * the Diagnostics page fight over the same value.
228210
+ */
228211
+ channels: zod.z.array(LogChannelWindowPatchSchema).readonly().optional()
228212
+ });
228213
+ var GetLoggingSettingsInputSchema = zod.z.object({
228214
+ scopeNodeId: zod.z.string().optional(),
228215
+ /**
228216
+ * The declared CHANNEL this document is addressed at, when the caller wants
228217
+ * the `component` layer. Absent = the node/cluster hierarchy only.
228218
+ *
228219
+ * Naming it separately rather than overloading `scopeNodeId` keeps the two
228220
+ * axes from collapsing: a component level is cluster-wide, a node level is
228221
+ * not, and one selector for both would make "which of these two did I just
228222
+ * set" unanswerable — the exact ambiguity `explicit` exists to remove.
228223
+ */
228224
+ scopeComponent: zod.z.string().optional()
227305
228225
  });
227306
- var GetLoggingSettingsInputSchema = zod.z.object({ scopeNodeId: zod.z.string().optional() });
227307
228226
  var SetLoggingSettingsInputSchema = zod.z.object({
227308
228227
  scopeNodeId: zod.z.string().optional(),
228228
+ scopeComponent: zod.z.string().optional(),
227309
228229
  patch: LoggingSettingsPatchSchema
227310
228230
  });
227311
228231
  var LoggingSettingsStateSchema = zod.z.object({
227312
228232
  /** The layer this document was read at. `null` = the cluster layer. */
227313
228233
  scopeNodeId: zod.z.string().nullable(),
228234
+ /** The channel this document was read at. `null` = no component layer. */
228235
+ scopeComponent: zod.z.string().nullable(),
227314
228236
  effective: LoggingEffectiveSchema,
227315
228237
  explicit: LoggingExplicitSchema,
227316
228238
  activeWindows: zod.z.array(DiagnosticWindowSchema).readonly(),
228239
+ /**
228240
+ * Every channel the cluster's addons DECLARE, gathered from the
228241
+ * `log-channels` providers. Not stored anywhere: assembled per read, so a
228242
+ * channel added by a redeployed addon appears without anybody editing a
228243
+ * list, and a channel whose addon is gone stops being offered.
228244
+ */
228245
+ channels: zod.z.array(LogChannelDescriptorSchema).readonly(),
228246
+ /** The channels ARMED right now, each with its deadline. */
228247
+ activeChannels: zod.z.array(LogChannelWindowStateSchema).readonly(),
227317
228248
  persisted: zod.z.boolean()
227318
228249
  });
227319
228250
  var systemCapability = {
@@ -230945,6 +231876,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
230945
231876
  llmRuntime: "llm-runtime",
230946
231877
  localNetwork: "local-network",
230947
231878
  lockControl: "lock-control",
231879
+ logChannels: "log-channels",
230948
231880
  logDestination: "log-destination",
230949
231881
  loginMethod: "login-method",
230950
231882
  mediaPlayer: "media-player",
@@ -231315,6 +232247,10 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
231315
232247
  key: "lockControl",
231316
232248
  name: "lock-control"
231317
232249
  },
232250
+ {
232251
+ key: "logChannels",
232252
+ name: "log-channels"
232253
+ },
231318
232254
  {
231319
232255
  key: "logDestination",
231320
232256
  name: "log-destination"
@@ -231694,6 +232630,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
231694
232630
  llmRuntimeCapability,
231695
232631
  localNetworkCapability,
231696
232632
  lockControlCapability,
232633
+ logChannelsCapability,
231697
232634
  logDestinationCapability,
231698
232635
  loginMethodCapability,
231699
232636
  mediaPlayerCapability,
@@ -232660,6 +233597,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
232660
233597
  addonId: null,
232661
233598
  access: "view"
232662
233599
  },
233600
+ "dataStoreProvider.aggregate": {
233601
+ capName: "data-store-provider",
233602
+ capScope: "system",
233603
+ addonId: null,
233604
+ access: "view"
233605
+ },
232663
233606
  "dataStoreProvider.count": {
232664
233607
  capName: "data-store-provider",
232665
233608
  capScope: "system",
@@ -233074,6 +234017,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
233074
234017
  addonId: null,
233075
234018
  access: "view"
233076
234019
  },
234020
+ "deviceManager.getChildrenBatch": {
234021
+ capName: "device-manager",
234022
+ capScope: "system",
234023
+ addonId: null,
234024
+ access: "view"
234025
+ },
233077
234026
  "deviceManager.getConfigSchema": {
233078
234027
  capName: "device-manager",
233079
234028
  capScope: "system",
@@ -234124,6 +235073,18 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
234124
235073
  addonId: null,
234125
235074
  access: "create"
234126
235075
  },
235076
+ "logChannels.apply": {
235077
+ capName: "log-channels",
235078
+ capScope: "system",
235079
+ addonId: null,
235080
+ access: "create"
235081
+ },
235082
+ "logChannels.list": {
235083
+ capName: "log-channels",
235084
+ capScope: "system",
235085
+ addonId: null,
235086
+ access: "view"
235087
+ },
234127
235088
  "logDestination.query": {
234128
235089
  capName: "log-destination",
234129
235090
  capScope: "system",
@@ -236278,6 +237239,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
236278
237239
  addonId: null,
236279
237240
  access: "create"
236280
237241
  },
237242
+ "settingsStore.aggregate": {
237243
+ capName: "settings-store",
237244
+ capScope: "system",
237245
+ addonId: null,
237246
+ access: "view"
237247
+ },
236281
237248
  "settingsStore.count": {
236282
237249
  capName: "settings-store",
236283
237250
  capScope: "system",
@@ -237626,6 +238593,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
237626
238593
  "llm-runtime",
237627
238594
  "local-network",
237628
238595
  "lock-control",
238596
+ "log-channels",
237629
238597
  "log-destination",
237630
238598
  "login-method",
237631
238599
  "media-player",
@@ -237782,6 +238750,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
237782
238750
  "llm",
237783
238751
  "llm-runtime",
237784
238752
  "local-network",
238753
+ "log-channels",
237785
238754
  "log-destination",
237786
238755
  "login-method",
237787
238756
  "mesh-network",
@@ -238109,6 +239078,11 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
238109
239078
  form: "single",
238110
239079
  optional: false
238111
239080
  }],
239081
+ "deviceManager.getChildrenBatch": [{
239082
+ name: "parentDeviceIds",
239083
+ form: "array",
239084
+ optional: false
239085
+ }],
238112
239086
  "deviceManager.getConfigSchema": [{
238113
239087
  name: "deviceId",
238114
239088
  form: "single",
@@ -239583,6 +240557,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
239583
240557
  "deviceManager.getBindings",
239584
240558
  "deviceManager.getBindingsBatch",
239585
240559
  "deviceManager.getChildren",
240560
+ "deviceManager.getChildrenBatch",
239586
240561
  "deviceManager.getConfigSchema",
239587
240562
  "deviceManager.getDevice",
239588
240563
  "deviceManager.getDeviceAggregate",
@@ -240275,6 +241250,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
240275
241250
  listPersistedByAddon: (input) => dispatch("deviceManager", "listPersistedByAddon", "query", input),
240276
241251
  listAll: (input) => dispatch("deviceManager", "listAll", "query", input),
240277
241252
  getChildren: (input) => dispatch("deviceManager", "getChildren", "query", input),
241253
+ getChildrenBatch: (input) => dispatch("deviceManager", "getChildrenBatch", "query", input),
240278
241254
  getLinkedDevicesBatch: (input) => dispatch("deviceManager", "getLinkedDevicesBatch", "query", input),
240279
241255
  removeByIntegration: (input) => dispatch("deviceManager", "removeByIntegration", "mutation", input),
240280
241256
  getBindingsBatch: (input) => dispatch("deviceManager", "getBindingsBatch", "query", input),
@@ -240589,6 +241565,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
240589
241565
  deleteWhere: (input) => dispatch("settingsStore", "deleteWhere", "mutation", input),
240590
241566
  updateWhere: (input) => dispatch("settingsStore", "updateWhere", "mutation", input),
240591
241567
  count: (input) => dispatch("settingsStore", "count", "query", input),
241568
+ aggregate: (input) => dispatch("settingsStore", "aggregate", "query", input),
240592
241569
  histogram: (input) => dispatch("settingsStore", "histogram", "query", input),
240593
241570
  isEmpty: (input) => dispatch("settingsStore", "isEmpty", "query", input),
240594
241571
  declareCollection: (input) => dispatch("settingsStore", "declareCollection", "mutation", input)
@@ -243598,6 +244575,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
243598
244575
  exports.DETECTION_PIPELINE_CAP_NAME = DETECTION_PIPELINE_CAP_NAME;
243599
244576
  exports.DEVICE_BACKEND_TO_FORMAT = DEVICE_BACKEND_TO_FORMAT;
243600
244577
  exports.DEVICE_CAP_NAMES = DEVICE_CAP_NAMES;
244578
+ exports.DEVICE_CHILDREN_BATCH_MAX = DEVICE_CHILDREN_BATCH_MAX;
243601
244579
  exports.DEVICE_PROFILES = DEVICE_PROFILES;
243602
244580
  exports.DEVICE_SCOPED_CAPS = require_sleep.DEVICE_SCOPED_CAPS;
243603
244581
  exports.DEVICE_SETTINGS_CONTRIBUTION_METHODS = require_sleep.DEVICE_SETTINGS_CONTRIBUTION_METHODS;
@@ -243748,6 +244726,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
243748
244726
  exports.IntercomStatusSchema = IntercomStatusSchema;
243749
244727
  exports.KNOWN_CAP_NAMES = KNOWN_CAP_NAMES;
243750
244728
  exports.KeyEventSchema = KeyEventSchema;
244729
+ exports.LOG_CHANNEL_TICK_MS = LOG_CHANNEL_TICK_MS;
243751
244730
  exports.LOG_LEVEL_RANK = LOG_LEVEL_RANK;
243752
244731
  exports.LabelAttributionSchema = LabelAttributionSchema;
243753
244732
  exports.LabelDefinitionSchema = LabelDefinitionSchema;
@@ -243783,6 +244762,14 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
243783
244762
  exports.LocationStatSchema = LocationStatSchema;
243784
244763
  exports.LockControlStatusSchema = LockControlStatusSchema;
243785
244764
  exports.LockStateSchema = LockStateSchema;
244765
+ exports.LogChannelApplyResultSchema = LogChannelApplyResultSchema;
244766
+ exports.LogChannelDescriptorSchema = LogChannelDescriptorSchema;
244767
+ exports.LogChannelGate = LogChannelGate;
244768
+ exports.LogChannelLevelSchema = LogChannelLevelSchema;
244769
+ exports.LogChannelRegistry = LogChannelRegistry;
244770
+ exports.LogChannelWindowPatchSchema = LogChannelWindowPatchSchema;
244771
+ exports.LogChannelWindowSchema = LogChannelWindowSchema;
244772
+ exports.LogChannelWindowStateSchema = LogChannelWindowStateSchema;
243786
244773
  exports.LogEntrySchema = LogEntrySchema;
243787
244774
  exports.LogLevelSchema = LogLevelSchema;
243788
244775
  exports.LogStreamEntrySchema = LogStreamEntrySchema;
@@ -244333,6 +245320,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244333
245320
  exports.ZoneRulesArraySchema = ZoneRulesArraySchema;
244334
245321
  exports.ZoneSchema = ZoneSchema;
244335
245322
  exports.ZoneScopeBreakdownSchema = ZoneScopeBreakdownSchema;
245323
+ exports.__resetLogChannelRegistryForTests = __resetLogChannelRegistryForTests;
244336
245324
  exports.accessoriesCapability = accessoriesCapability;
244337
245325
  exports.accessoryStableId = accessoryStableId;
244338
245326
  exports.addonPagesCapability = addonPagesCapability;
@@ -244429,6 +245417,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244429
245417
  exports.createExpressionScope = createExpressionScope;
244430
245418
  exports.createHwAccelCache = createHwAccelCache;
244431
245419
  exports.createLazyTrpcSource = require_sleep.createLazyTrpcSource;
245420
+ exports.createLogChannelsProvider = createLogChannelsProvider;
244432
245421
  exports.createMirrorSource = require_sleep.createMirrorSource;
244433
245422
  exports.createRuntimeStateBridge = createRuntimeStateBridge;
244434
245423
  exports.createSliceHandle = require_sleep.createSliceHandle;
@@ -244438,6 +245427,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244438
245427
  exports.dataStoreProviderCapability = dataStoreProviderCapability;
244439
245428
  exports.dayNightCapability = dayNightCapability;
244440
245429
  exports.declarationOwnerNodeId = declarationOwnerNodeId;
245430
+ exports.declareLogChannel = declareLogChannel;
244441
245431
  exports.decodeVectorBase64 = decodeVectorBase64;
244442
245432
  exports.decoderCapability = decoderCapability;
244443
245433
  exports.defaultDeliveryForSection = defaultDeliveryForSection;
@@ -244500,6 +245490,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244500
245490
  exports.getAudioMacroClassIds = getAudioMacroClassIds;
244501
245491
  exports.getByPath = getByPath;
244502
245492
  exports.getCapsByProviderKind = getCapsByProviderKind;
245493
+ exports.getLogChannelRegistry = getLogChannelRegistry;
244503
245494
  exports.getTaxonomyEntry = getTaxonomyEntry;
244504
245495
  exports.hasMotionTrigger = hasMotionTrigger;
244505
245496
  exports.hfModelUrl = hfModelUrl;
@@ -244553,6 +245544,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
244553
245544
  exports.locationSimilarity = locationSimilarity;
244554
245545
  exports.lockControlCapability = lockControlCapability;
244555
245546
  exports.logBannerArgs = require_canonical_hash.logBannerArgs;
245547
+ exports.logChannelsCapability = logChannelsCapability;
244556
245548
  exports.logDestinationCapability = logDestinationCapability;
244557
245549
  exports.logLevelAtMost = logLevelAtMost;
244558
245550
  exports.loginMethodCapability = loginMethodCapability;
@@ -401844,17 +402836,55 @@ var require_logging_settings = __commonJS({
401844
402836
  "../../server/backend/dist/api/core/logging-settings.js"(exports) {
401845
402837
  "use strict";
401846
402838
  Object.defineProperty(exports, "__esModule", { value: true });
401847
- exports.LoggingSettingsService = exports.HUB_NODE_ID = exports.LOGGING_SETTINGS_KEY = void 0;
402839
+ exports.LoggingSettingsService = exports.MAX_CHANNEL_WINDOW_MS = exports.EMPTY_LOG_CHANNEL_PLANE = exports.HUB_NODE_ID = exports.LOGGING_SETTINGS_KEY = void 0;
402840
+ exports.buildLogChannelPlane = buildLogChannelPlane;
401848
402841
  exports.toLoggingSettingsRecord = toLoggingSettingsRecord;
401849
402842
  exports.resolveLevel = resolveLevel;
401850
402843
  exports.describeLayers = describeLayers;
401851
402844
  exports.mergeLevel = mergeLevel;
402845
+ exports.validateChannelPatch = validateChannelPatch;
402846
+ exports.mergeChannelWindows = mergeChannelWindows;
402847
+ exports.liveChannelWindows = liveChannelWindows;
402848
+ exports.describeChannelWindows = describeChannelWindows;
401852
402849
  var types_1 = require_dist4();
401853
402850
  var system_1 = require_dist3();
401854
402851
  exports.LOGGING_SETTINGS_KEY = "logging-settings";
401855
402852
  var SYSTEM_SETTINGS_COLLECTION = "system-settings";
401856
402853
  exports.HUB_NODE_ID = "hub";
401857
402854
  var LOG_LEVELS = ["debug", "info", "warn", "error"];
402855
+ exports.EMPTY_LOG_CHANNEL_PLANE = {
402856
+ declarations: async () => [],
402857
+ apply: async () => {
402858
+ }
402859
+ };
402860
+ function buildLogChannelPlane(source, onProviderError) {
402861
+ return {
402862
+ declarations: async () => {
402863
+ const seen = /* @__PURE__ */ new Map();
402864
+ for (const [addonId, provider] of source.entries()) {
402865
+ try {
402866
+ for (const descriptor of await provider.list()) {
402867
+ if (!seen.has(descriptor.name))
402868
+ seen.set(descriptor.name, descriptor);
402869
+ }
402870
+ } catch (err) {
402871
+ onProviderError?.(addonId, "list", (0, types_1.errMsg)(err));
402872
+ }
402873
+ }
402874
+ return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
402875
+ },
402876
+ apply: async (windows) => {
402877
+ for (const [addonId, provider] of source.entries()) {
402878
+ try {
402879
+ await provider.apply({ windows });
402880
+ } catch (err) {
402881
+ onProviderError?.(addonId, "apply", (0, types_1.errMsg)(err));
402882
+ }
402883
+ }
402884
+ }
402885
+ };
402886
+ }
402887
+ exports.MAX_CHANNEL_WINDOW_MS = 30 * 6e4;
401858
402888
  var LoggingSettingsService = class {
401859
402889
  store;
401860
402890
  gate;
@@ -401862,6 +402892,7 @@ var require_logging_settings = __commonJS({
401862
402892
  localNodeId;
401863
402893
  logger;
401864
402894
  now;
402895
+ channels;
401865
402896
  constructor(deps) {
401866
402897
  this.store = deps.store;
401867
402898
  this.gate = deps.gate;
@@ -401869,6 +402900,7 @@ var require_logging_settings = __commonJS({
401869
402900
  this.localNodeId = deps.localNodeId ?? exports.HUB_NODE_ID;
401870
402901
  this.logger = deps.logger;
401871
402902
  this.now = deps.now ?? Date.now;
402903
+ this.channels = deps.channels ?? exports.EMPTY_LOG_CHANNEL_PLANE;
401872
402904
  }
401873
402905
  /**
401874
402906
  * The document as it stands, resolved for `nodeId` (absent = the cluster).
@@ -401877,9 +402909,9 @@ var require_logging_settings = __commonJS({
401877
402909
  * and, crucially, does not touch the mirror. What the operator sees is
401878
402910
  * honestly labelled as un-persisted; what the hub does is unchanged.
401879
402911
  */
401880
- async get(scopeNodeId) {
402912
+ async get(scopeNodeId, scopeComponent) {
401881
402913
  const record = await this.read();
401882
- return this.describe(record, scopeNodeId ?? null);
402914
+ return this.describe(record, scopeNodeId ?? null, scopeComponent ?? null);
401883
402915
  }
401884
402916
  /**
401885
402917
  * Apply a patch. Fields absent from it are untouched.
@@ -401889,28 +402921,47 @@ var require_logging_settings = __commonJS({
401889
402921
  * must still give the operator a live measurement, and must say so rather
401890
402922
  * than reporting a durable arm that is not durable.
401891
402923
  */
401892
- async set(patch, scopeNodeId) {
402924
+ async set(patch, scopeNodeId, scopeComponent) {
401893
402925
  await this.applyDiagnostics(patch);
401894
402926
  const scope = scopeNodeId ?? null;
402927
+ const component = scopeComponent ?? null;
401895
402928
  const stored = await this.read();
401896
402929
  const levelChanged = patch.level !== void 0;
401897
- let persistedLevel = true;
402930
+ const channelsChanged = patch.channels !== void 0;
402931
+ let persisted = true;
402932
+ let next = stored ?? {};
401898
402933
  if (levelChanged) {
401899
- const next = mergeLevel(stored ?? {}, scope, patch.level ?? null, this.now());
401900
- persistedLevel = await this.write(next);
402934
+ next = mergeLevel(next, scope, component, patch.level ?? null, this.now());
402935
+ }
402936
+ if (channelsChanged) {
402937
+ const declared = await this.channels.declarations();
402938
+ const rejected = validateChannelPatch(patch.channels ?? [], declared);
402939
+ if (rejected.length > 0) {
402940
+ this.logger?.warn("log channel patch refused - nothing was armed or disarmed", {
402941
+ meta: { reasons: rejected }
402942
+ });
402943
+ throw new Error(`log channel patch refused: ${rejected.join("; ")}`);
402944
+ }
402945
+ next = mergeChannelWindows(next, patch.channels ?? [], this.now());
402946
+ }
402947
+ if (levelChanged || channelsChanged) {
402948
+ persisted = await this.write(next);
401901
402949
  this.refreshMirror(next);
402950
+ await this.pushChannels(next);
401902
402951
  this.logger?.info("logging settings written", {
401903
402952
  meta: {
401904
- scope: scope === null ? "cluster" : "node",
402953
+ scope: component !== null ? "component" : scope === null ? "cluster" : "node",
401905
402954
  nodeId: scope,
401906
- level: patch.level ?? null,
401907
- persisted: persistedLevel
402955
+ component,
402956
+ level: levelChanged ? patch.level ?? null : void 0,
402957
+ channels: (patch.channels ?? []).map((c) => `${c.channel}:${c.armMs}`),
402958
+ persisted
401908
402959
  }
401909
402960
  });
401910
- const state = await this.describe(next, scope);
401911
- return { ...state, persisted: state.persisted && persistedLevel };
402961
+ const state = await this.describe(next, scope, component);
402962
+ return { ...state, persisted: state.persisted && persisted };
401912
402963
  }
401913
- return this.describe(stored, scope);
402964
+ return this.describe(stored, scope, component);
401914
402965
  }
401915
402966
  /**
401916
402967
  * Re-establish the mirror and re-arm any diagnostic window that outlived the
@@ -401923,8 +402974,10 @@ var require_logging_settings = __commonJS({
401923
402974
  */
401924
402975
  async restore() {
401925
402976
  const record = await this.read();
401926
- if (record !== null)
402977
+ if (record !== null) {
401927
402978
  this.refreshMirror(record);
402979
+ await this.pushChannels(record);
402980
+ }
401928
402981
  await this.requestCensus.restore();
401929
402982
  }
401930
402983
  // ── internals ─────────────────────────────────────────────────────
@@ -401942,16 +402995,55 @@ var require_logging_settings = __commonJS({
401942
402995
  });
401943
402996
  }
401944
402997
  }
401945
- async describe(record, scopeNodeId) {
401946
- const resolved = resolveLevel(record ?? {}, scopeNodeId);
402998
+ /**
402999
+ * Push the live windows to every declared gate.
403000
+ *
403001
+ * A failure here is logged and swallowed: the document is written and the
403002
+ * operator is told what it says, and a runner that could not be reached
403003
+ * simply keeps the mirror it has. Throwing would turn one unreachable addon
403004
+ * into a failed write for the whole cluster.
403005
+ */
403006
+ async pushChannels(record) {
403007
+ try {
403008
+ await this.channels.apply(liveChannelWindows(record, this.now()));
403009
+ } catch (err) {
403010
+ this.logger?.warn("log channel windows written but NOT pushed to every gate", {
403011
+ meta: { error: (0, types_1.errMsg)(err) }
403012
+ });
403013
+ }
403014
+ }
403015
+ async describe(record, scopeNodeId, scopeComponent) {
403016
+ const resolved = resolveLevel(record ?? {}, scopeNodeId, scopeComponent);
401947
403017
  return {
401948
403018
  scopeNodeId,
403019
+ scopeComponent,
401949
403020
  effective: { level: resolved.level, levelSource: resolved.source },
401950
- explicit: { layers: describeLayers(record ?? {}, scopeNodeId) },
403021
+ explicit: { layers: describeLayers(record ?? {}, scopeNodeId, scopeComponent) },
401951
403022
  activeWindows: await this.describeWindows(),
403023
+ channels: await this.declaredChannels(),
403024
+ activeChannels: describeChannelWindows(record ?? {}, this.now()),
401952
403025
  persisted: record !== null
401953
403026
  };
401954
403027
  }
403028
+ /**
403029
+ * The declarations, assembled per read.
403030
+ *
403031
+ * Not cached and not stored: a channel added by a redeployed addon has to
403032
+ * appear without anybody editing a list, and a channel whose addon is gone
403033
+ * has to stop being offered. A read that throws yields the EMPTY list and
403034
+ * says so — "unknown" is the truth about a set of addons nobody reached, and
403035
+ * it changes no armed window (D49/D224).
403036
+ */
403037
+ async declaredChannels() {
403038
+ try {
403039
+ return await this.channels.declarations();
403040
+ } catch (err) {
403041
+ this.logger?.warn("log channel declarations unreadable - reporting none", {
403042
+ meta: { error: (0, types_1.errMsg)(err) }
403043
+ });
403044
+ return [];
403045
+ }
403046
+ }
401955
403047
  /**
401956
403048
  * The armed windows, with their deadline.
401957
403049
  *
@@ -402015,14 +403107,21 @@ var require_logging_settings = __commonJS({
402015
403107
  return {};
402016
403108
  const clusterLevel = asLogLevel(Reflect.get(value, "clusterLevel"));
402017
403109
  const nodeLevels = asNodeLevels(Reflect.get(value, "nodeLevels"));
403110
+ const componentLevels = asNodeLevels(Reflect.get(value, "componentLevels"));
403111
+ const channelWindows = asChannelWindows(Reflect.get(value, "channelWindows"));
402018
403112
  const updatedAt = Reflect.get(value, "updatedAt");
402019
403113
  return {
402020
403114
  ...clusterLevel !== null ? { clusterLevel } : {},
402021
403115
  ...nodeLevels !== null ? { nodeLevels } : {},
403116
+ ...componentLevels !== null ? { componentLevels } : {},
403117
+ ...channelWindows !== null ? { channelWindows } : {},
402022
403118
  ...typeof updatedAt === "number" && Number.isFinite(updatedAt) ? { updatedAt } : {}
402023
403119
  };
402024
403120
  }
402025
- function resolveLevel(record, nodeId) {
403121
+ function resolveLevel(record, nodeId, component = null) {
403122
+ const componentLevel = component === null ? void 0 : record.componentLevels?.[component];
403123
+ if (componentLevel !== void 0)
403124
+ return { level: componentLevel, source: "component" };
402026
403125
  const nodeLevel = nodeId === null ? void 0 : record.nodeLevels?.[nodeId];
402027
403126
  if (nodeLevel !== void 0)
402028
403127
  return { level: nodeLevel, source: "node" };
@@ -402030,17 +403129,37 @@ var require_logging_settings = __commonJS({
402030
403129
  return { level: record.clusterLevel, source: "cluster" };
402031
403130
  return { level: system_1.DEFAULT_LOG_LEVEL, source: "default" };
402032
403131
  }
402033
- function describeLayers(record, nodeId) {
402034
- const cluster = {
402035
- scope: "cluster",
402036
- nodeId: null,
402037
- level: record.clusterLevel ?? null
402038
- };
402039
- if (nodeId === null)
402040
- return [cluster];
402041
- return [cluster, { scope: "node", nodeId, level: record.nodeLevels?.[nodeId] ?? null }];
403132
+ function describeLayers(record, nodeId, component = null) {
403133
+ const layers = [
403134
+ { scope: "cluster", nodeId: null, component: null, level: record.clusterLevel ?? null }
403135
+ ];
403136
+ if (nodeId !== null) {
403137
+ layers.push({
403138
+ scope: "node",
403139
+ nodeId,
403140
+ component: null,
403141
+ level: record.nodeLevels?.[nodeId] ?? null
403142
+ });
403143
+ }
403144
+ if (component !== null) {
403145
+ layers.push({
403146
+ scope: "component",
403147
+ nodeId: null,
403148
+ component,
403149
+ level: record.componentLevels?.[component] ?? null
403150
+ });
403151
+ }
403152
+ return layers;
402042
403153
  }
402043
- function mergeLevel(record, nodeId, next, updatedAt) {
403154
+ function mergeLevel(record, nodeId, component, next, updatedAt) {
403155
+ if (component !== null) {
403156
+ const { [component]: _clearedComponent, ...otherComponents } = record.componentLevels ?? {};
403157
+ const componentLevels = {
403158
+ ...otherComponents,
403159
+ ...next !== null ? { [component]: next } : {}
403160
+ };
403161
+ return { ...record, componentLevels, updatedAt };
403162
+ }
402044
403163
  if (nodeId === null) {
402045
403164
  const { clusterLevel: _dropped, ...rest } = record;
402046
403165
  return { ...rest, ...next !== null ? { clusterLevel: next } : {}, updatedAt };
@@ -402056,6 +403175,81 @@ var require_logging_settings = __commonJS({
402056
403175
  updatedAt
402057
403176
  };
402058
403177
  }
403178
+ function validateChannelPatch(patches, declared) {
403179
+ const byName = new Map(declared.map((d) => [d.name, d]));
403180
+ const reasons = [];
403181
+ for (const patch of patches) {
403182
+ const descriptor = byName.get(patch.channel);
403183
+ if (descriptor === void 0) {
403184
+ const known = [...byName.keys()].sort().slice(0, 8).join(", ");
403185
+ reasons.push(`"${patch.channel}" is not declared by any addon on this cluster` + (known.length > 0 ? ` (declared: ${known})` : ""));
403186
+ continue;
403187
+ }
403188
+ const ids = patch.deviceIds;
403189
+ if (ids !== void 0 && ids !== null && ids.length > 0 && !descriptor.perDevice) {
403190
+ reasons.push(`"${patch.channel}" is declared perDevice: false \u2014 its lines carry no tags.deviceId, so narrowing it to a camera would match nothing`);
403191
+ }
403192
+ }
403193
+ return reasons;
403194
+ }
403195
+ function mergeChannelWindows(record, patches, nowMs) {
403196
+ const windows = { ...record.channelWindows };
403197
+ for (const patch of patches) {
403198
+ if (patch.armMs <= 0) {
403199
+ delete windows[patch.channel];
403200
+ continue;
403201
+ }
403202
+ const ids = patch.deviceIds;
403203
+ windows[patch.channel] = {
403204
+ armedUntilMs: nowMs + Math.min(patch.armMs, exports.MAX_CHANNEL_WINDOW_MS),
403205
+ deviceIds: ids === void 0 || ids === null || ids.length === 0 ? null : [...ids]
403206
+ };
403207
+ }
403208
+ return { ...record, channelWindows: windows, updatedAt: nowMs };
403209
+ }
403210
+ function liveChannelWindows(record, nowMs) {
403211
+ const out = [];
403212
+ for (const [channel, window2] of Object.entries(record.channelWindows ?? {})) {
403213
+ if (window2.armedUntilMs <= nowMs)
403214
+ continue;
403215
+ out.push({ channel, armedUntilMs: window2.armedUntilMs, deviceIds: window2.deviceIds });
403216
+ }
403217
+ return out.sort((a, b) => a.channel.localeCompare(b.channel));
403218
+ }
403219
+ function describeChannelWindows(record, nowMs) {
403220
+ const out = [];
403221
+ for (const [channel, window2] of Object.entries(record.channelWindows ?? {})) {
403222
+ const armed = window2.armedUntilMs > nowMs;
403223
+ out.push({
403224
+ channel,
403225
+ armed,
403226
+ armedUntilMs: armed ? window2.armedUntilMs : 0,
403227
+ remainingMs: armed ? window2.armedUntilMs - nowMs : 0,
403228
+ deviceIds: armed ? window2.deviceIds : null
403229
+ });
403230
+ }
403231
+ return out.sort((a, b) => a.channel.localeCompare(b.channel));
403232
+ }
403233
+ function asChannelWindows(value) {
403234
+ if (typeof value !== "object" || value === null)
403235
+ return null;
403236
+ const out = {};
403237
+ for (const [key, raw] of Object.entries(value)) {
403238
+ if (typeof raw !== "object" || raw === null)
403239
+ continue;
403240
+ const armedUntilMs = Reflect.get(raw, "armedUntilMs");
403241
+ if (typeof armedUntilMs !== "number" || !Number.isFinite(armedUntilMs))
403242
+ continue;
403243
+ out[key] = { armedUntilMs, deviceIds: asDeviceIds(Reflect.get(raw, "deviceIds")) };
403244
+ }
403245
+ return out;
403246
+ }
403247
+ function asDeviceIds(value) {
403248
+ if (!Array.isArray(value))
403249
+ return null;
403250
+ const ids = value.filter((id) => typeof id === "number" && Number.isInteger(id));
403251
+ return ids.length > 0 ? ids : null;
403252
+ }
402059
403253
  function asLogLevel(value) {
402060
403254
  return typeof value === "string" && isLogLevel(value) ? value : null;
402061
403255
  }
@@ -402692,10 +403886,14 @@ var require_cap_providers = __commonJS({
402692
403886
  store,
402693
403887
  ...logger !== void 0 ? { logger } : {}
402694
403888
  });
403889
+ const channels = (0, logging_settings_js_1.buildLogChannelPlane)({ entries: () => registry?.getCollectionEntries("log-channels") ?? [] }, (addonId, phase, error) => {
403890
+ logger?.warn("log-channels provider unreachable", { meta: { addonId, phase, error } });
403891
+ });
402695
403892
  const loggingSettings = new logging_settings_js_1.LoggingSettingsService({
402696
403893
  store,
402697
403894
  gate: (0, system_1.getLoggingGate)(),
402698
403895
  requestCensus,
403896
+ channels,
402699
403897
  ...logger !== void 0 ? { logger } : {}
402700
403898
  });
402701
403899
  return {
@@ -402729,8 +403927,8 @@ var require_cap_providers = __commonJS({
402729
403927
  setSiteLocation: async (input) => siteLocation.set(input),
402730
403928
  detectSiteLocation: async () => siteLocation.detect(),
402731
403929
  getRequestCensus: async () => requestCensus.status(),
402732
- getLoggingSettings: async (input) => loggingSettings.get(input.scopeNodeId),
402733
- setLoggingSettings: async (input) => loggingSettings.set(input.patch, input.scopeNodeId)
403930
+ getLoggingSettings: async (input) => loggingSettings.get(input.scopeNodeId, input.scopeComponent),
403931
+ setLoggingSettings: async (input) => loggingSettings.set(input.patch, input.scopeNodeId, input.scopeComponent)
402734
403932
  };
402735
403933
  }
402736
403934
  function buildSiteLocationService(registry, logger) {
@@ -405277,6 +406475,7 @@ var require_device_config_secret_redaction = __commonJS({
405277
406475
  exports.NON_ADMIN_CONFIG_REDACTED_METHODS = exports.REDACTED_SECRET = void 0;
405278
406476
  exports.isSecretConfigKey = isSecretConfigKey;
405279
406477
  exports.redactDeviceInfoSecrets = redactDeviceInfoSecrets;
406478
+ exports.redactDeviceInfoRecordSecrets = redactDeviceInfoRecordSecrets;
405280
406479
  exports.redactSettingsSections = redactSettingsSections;
405281
406480
  exports.redactSettingsAggregate = redactSettingsAggregate;
405282
406481
  exports.redactConfigEntries = redactConfigEntries;
@@ -405314,6 +406513,14 @@ var require_device_config_secret_redaction = __commonJS({
405314
406513
  return data;
405315
406514
  return { ...data, config: redactRecord(config) };
405316
406515
  }
406516
+ function redactDeviceInfoRecordSecrets(data) {
406517
+ if (!isRecord(data))
406518
+ return data;
406519
+ const out = {};
406520
+ for (const [key, value] of Object.entries(data))
406521
+ out[key] = redactDeviceInfoSecrets(value);
406522
+ return out;
406523
+ }
405317
406524
  function redactField(field) {
405318
406525
  if (!isRecord(field))
405319
406526
  return field;
@@ -405372,6 +406579,7 @@ var require_device_config_secret_redaction = __commonJS({
405372
406579
  ["deviceManager.getDevice", redactDeviceInfoSecrets],
405373
406580
  ["deviceManager.listAll", redactDeviceInfoSecrets],
405374
406581
  ["deviceManager.getChildren", redactDeviceInfoSecrets],
406582
+ ["deviceManager.getChildrenBatch", redactDeviceInfoRecordSecrets],
405375
406583
  ["deviceManager.getDeviceSettingsAggregate", redactSettingsSections],
405376
406584
  ["deviceManager.getSettingsSchema", redactSettingsSections],
405377
406585
  ["deviceManager.getDeviceAggregate", redactSettingsAggregate],
@@ -408962,6 +410170,234 @@ var require_addon_settings_provider = __commonJS({
408962
410170
  }
408963
410171
  });
408964
410172
 
410173
+ // ../../server/backend/dist/core/addon/single-flight-refresh.js
410174
+ var require_single_flight_refresh = __commonJS({
410175
+ "../../server/backend/dist/core/addon/single-flight-refresh.js"(exports) {
410176
+ "use strict";
410177
+ Object.defineProperty(exports, "__esModule", { value: true });
410178
+ exports.createSingleFlightRefresh = createSingleFlightRefresh;
410179
+ function widenScope(a, b) {
410180
+ if (a === null || b === null)
410181
+ return null;
410182
+ const out = new Set(a);
410183
+ for (const id of b)
410184
+ out.add(id);
410185
+ return out;
410186
+ }
410187
+ function createSingleFlightRefresh(run) {
410188
+ let inFlight = null;
410189
+ let pendingScope;
410190
+ let pendingPromise = null;
410191
+ let releasePending = () => {
410192
+ };
410193
+ const start = (scope) => {
410194
+ const active = (async () => {
410195
+ try {
410196
+ await run(scope);
410197
+ } finally {
410198
+ inFlight = null;
410199
+ if (pendingScope !== void 0) {
410200
+ const next = pendingScope;
410201
+ pendingScope = void 0;
410202
+ const release = releasePending;
410203
+ releasePending = () => {
410204
+ };
410205
+ pendingPromise = null;
410206
+ void start(next).finally(release);
410207
+ }
410208
+ }
410209
+ })();
410210
+ inFlight = active;
410211
+ return active;
410212
+ };
410213
+ return {
410214
+ request: async (scope) => {
410215
+ if (scope !== null && scope.size === 0)
410216
+ return;
410217
+ if (inFlight === null) {
410218
+ await start(scope);
410219
+ return;
410220
+ }
410221
+ pendingScope = pendingScope === void 0 ? scope : widenScope(pendingScope, scope);
410222
+ if (pendingPromise === null) {
410223
+ pendingPromise = new Promise((resolve) => {
410224
+ releasePending = resolve;
410225
+ });
410226
+ }
410227
+ const wait = pendingPromise;
410228
+ if (wait !== null)
410229
+ await wait;
410230
+ }
410231
+ };
410232
+ }
410233
+ }
410234
+ });
410235
+
410236
+ // ../../server/backend/dist/core/addon/device-meta-mirror.js
410237
+ var require_device_meta_mirror = __commonJS({
410238
+ "../../server/backend/dist/core/addon/device-meta-mirror.js"(exports) {
410239
+ "use strict";
410240
+ Object.defineProperty(exports, "__esModule", { value: true });
410241
+ exports.DeviceMetaMirror = exports.MIRROR_IRRELEVANT_META_FIELDS = void 0;
410242
+ exports.classifyDeviceEvent = classifyDeviceEvent;
410243
+ var types_1 = require_dist4();
410244
+ var single_flight_refresh_js_1 = require_single_flight_refresh();
410245
+ exports.MIRROR_IRRELEVANT_META_FIELDS = /* @__PURE__ */ new Set([
410246
+ "name",
410247
+ "disabled",
410248
+ "metadata",
410249
+ "display",
410250
+ "role",
410251
+ "integrationId",
410252
+ "linkDeviceId",
410253
+ "primaryChildEntityId",
410254
+ "childLayout"
410255
+ ]);
410256
+ function classifyDeviceEvent(data) {
410257
+ if (data === null || typeof data !== "object")
410258
+ return { kind: "fleet" };
410259
+ const field = Reflect.get(data, "field");
410260
+ if (typeof field === "string" && exports.MIRROR_IRRELEVANT_META_FIELDS.has(field)) {
410261
+ return { kind: "none" };
410262
+ }
410263
+ const deviceId = Reflect.get(data, "deviceId");
410264
+ if (typeof deviceId !== "number" || !Number.isFinite(deviceId))
410265
+ return { kind: "fleet" };
410266
+ return { kind: "device", deviceId };
410267
+ }
410268
+ var MAX_ANCESTOR_HOPS = 8;
410269
+ var DeviceMetaMirror = class {
410270
+ read;
410271
+ live;
410272
+ logger;
410273
+ parents = /* @__PURE__ */ new Map();
410274
+ meta = /* @__PURE__ */ new Map();
410275
+ gate;
410276
+ constructor(read, live, logger) {
410277
+ this.read = read;
410278
+ this.live = live;
410279
+ this.logger = logger;
410280
+ this.gate = (0, single_flight_refresh_js_1.createSingleFlightRefresh)((scope) => this.refresh(scope));
410281
+ }
410282
+ /** Ask for a refresh covering exactly what the event changed. */
410283
+ onDeviceEvent(data) {
410284
+ const scope = classifyDeviceEvent(data);
410285
+ if (scope.kind === "none")
410286
+ return;
410287
+ void this.gate.request(scope.kind === "fleet" ? null : /* @__PURE__ */ new Set([scope.deviceId]));
410288
+ }
410289
+ /** Warm (or re-warm) the whole fleet — the boot sweep, and the D8 reconcile
410290
+ * that repairs anything a dropped event would have left behind. */
410291
+ async refreshAll() {
410292
+ await this.gate.request(null);
410293
+ }
410294
+ /** Parent of a device: mirror first (covers forked devices), then the live
410295
+ * hub registry (covers a hub-local device before the first warm). */
410296
+ parentOf(deviceId) {
410297
+ const mirrored = this.parents.get(deviceId);
410298
+ if (mirrored !== void 0)
410299
+ return mirrored;
410300
+ return this.live.parentDeviceId(deviceId);
410301
+ }
410302
+ /** Persisted `DeviceType` string, or null when unknown. */
410303
+ typeOf(deviceId) {
410304
+ const mirrored = this.meta.get(deviceId);
410305
+ if (mirrored !== void 0)
410306
+ return mirrored.type;
410307
+ return this.live.type(deviceId);
410308
+ }
410309
+ /** Persisted operator `location` label, or null when unset/unknown. */
410310
+ locationOf(deviceId) {
410311
+ const mirrored = this.meta.get(deviceId);
410312
+ if (mirrored !== void 0)
410313
+ return mirrored.location;
410314
+ return this.live.location(deviceId);
410315
+ }
410316
+ /** The whole mirrored fleet — fuels fleet-wide selector expansion. */
410317
+ list() {
410318
+ const out = [];
410319
+ for (const [id, m] of this.meta) {
410320
+ out.push({
410321
+ id,
410322
+ type: m.type,
410323
+ location: m.location,
410324
+ parentDeviceId: this.parents.get(id) ?? null
410325
+ });
410326
+ }
410327
+ return out;
410328
+ }
410329
+ /** Ancestor chain (parent, grandparent, …), bounded. Empty for a top-level
410330
+ * device or one the hub has never heard of. */
410331
+ ancestorsOf(deviceId) {
410332
+ const out = [];
410333
+ let current = deviceId;
410334
+ for (let hop = 0; hop < MAX_ANCESTOR_HOPS; hop++) {
410335
+ const parent = this.parentOf(current);
410336
+ if (parent === null || parent === current)
410337
+ break;
410338
+ out.push(parent);
410339
+ current = parent;
410340
+ }
410341
+ return out;
410342
+ }
410343
+ async refresh(scope) {
410344
+ try {
410345
+ const ids = scope === null ? null : [...scope];
410346
+ const rows = await this.read(ids);
410347
+ if (!Array.isArray(rows))
410348
+ return;
410349
+ const nextParents = /* @__PURE__ */ new Map();
410350
+ const nextMeta = /* @__PURE__ */ new Map();
410351
+ for (const row of rows) {
410352
+ if (row === null || typeof row !== "object")
410353
+ continue;
410354
+ const id = Reflect.get(row, "id");
410355
+ if (typeof id !== "number")
410356
+ continue;
410357
+ const parent = Reflect.get(row, "parentDeviceId");
410358
+ if (typeof parent === "number")
410359
+ nextParents.set(id, parent);
410360
+ const type = Reflect.get(row, "type");
410361
+ const location = Reflect.get(row, "location");
410362
+ nextMeta.set(id, {
410363
+ type: typeof type === "string" ? type : "",
410364
+ location: typeof location === "string" ? location : null
410365
+ });
410366
+ }
410367
+ if (ids === null) {
410368
+ this.parents.clear();
410369
+ for (const [k, v] of nextParents)
410370
+ this.parents.set(k, v);
410371
+ this.meta.clear();
410372
+ for (const [k, v] of nextMeta)
410373
+ this.meta.set(k, v);
410374
+ return;
410375
+ }
410376
+ for (const id of ids) {
410377
+ const m = nextMeta.get(id);
410378
+ if (m === void 0) {
410379
+ this.parents.delete(id);
410380
+ this.meta.delete(id);
410381
+ continue;
410382
+ }
410383
+ this.meta.set(id, m);
410384
+ const parent = nextParents.get(id);
410385
+ if (parent === void 0)
410386
+ this.parents.delete(id);
410387
+ else
410388
+ this.parents.set(id, parent);
410389
+ }
410390
+ } catch (err) {
410391
+ this.logger.debug("device-meta mirror refresh failed \u2014 keeping previous", {
410392
+ meta: { error: (0, types_1.errMsg)(err), scope: scope === null ? "fleet" : scope.size }
410393
+ });
410394
+ }
410395
+ }
410396
+ };
410397
+ exports.DeviceMetaMirror = DeviceMetaMirror;
410398
+ }
410399
+ });
410400
+
408965
410401
  // ../../server/backend/dist/core/addon/integration-visibility.js
408966
410402
  var require_integration_visibility = __commonJS({
408967
410403
  "../../server/backend/dist/core/addon/integration-visibility.js"(exports) {
@@ -409221,58 +410657,6 @@ var require_runner_spawn_fanout = __commonJS({
409221
410657
  }
409222
410658
  });
409223
410659
 
409224
- // ../../server/backend/dist/core/addon/single-flight-refresh.js
409225
- var require_single_flight_refresh = __commonJS({
409226
- "../../server/backend/dist/core/addon/single-flight-refresh.js"(exports) {
409227
- "use strict";
409228
- Object.defineProperty(exports, "__esModule", { value: true });
409229
- exports.createSingleFlightRefresh = createSingleFlightRefresh;
409230
- function createSingleFlightRefresh(run) {
409231
- let inFlight = null;
409232
- let pending = false;
409233
- let pendingPromise = null;
409234
- let releasePending = () => {
409235
- };
409236
- const start = () => {
409237
- const active = (async () => {
409238
- try {
409239
- await run();
409240
- } finally {
409241
- inFlight = null;
409242
- if (pending) {
409243
- pending = false;
409244
- const release = releasePending;
409245
- releasePending = () => {
409246
- };
409247
- pendingPromise = null;
409248
- void start().finally(release);
409249
- }
409250
- }
409251
- })();
409252
- inFlight = active;
409253
- return active;
409254
- };
409255
- return {
409256
- request: async () => {
409257
- if (inFlight === null) {
409258
- await start();
409259
- return;
409260
- }
409261
- if (!pending) {
409262
- pending = true;
409263
- pendingPromise = new Promise((resolve) => {
409264
- releasePending = resolve;
409265
- });
409266
- }
409267
- const wait = pendingPromise;
409268
- if (wait !== null)
409269
- await wait;
409270
- }
409271
- };
409272
- }
409273
- }
409274
- });
409275
-
409276
410660
  // ../../server/backend/dist/core/addon/addon-registry.service.js
409277
410661
  var require_addon_registry_service = __commonJS({
409278
410662
  "../../server/backend/dist/core/addon/addon-registry.service.js"(exports) {
@@ -409329,13 +410713,13 @@ var require_addon_registry_service = __commonJS({
409329
410713
  var addon_call_gateway_js_1 = require_addon_call_gateway();
409330
410714
  var addon_row_manifest_1 = require_addon_row_manifest();
409331
410715
  var addon_settings_provider_js_1 = require_addon_settings_provider();
410716
+ var device_meta_mirror_js_1 = require_device_meta_mirror();
409332
410717
  var integration_visibility_js_1 = require_integration_visibility();
409333
410718
  var package_dir_utils_1 = require_package_dir_utils();
409334
410719
  var prune_misplaced_addons_js_1 = require_prune_misplaced_addons();
409335
410720
  var require_cache_js_1 = require_require_cache();
409336
410721
  var runner_convergence_1 = require_runner_convergence();
409337
410722
  var runner_spawn_fanout_js_1 = require_runner_spawn_fanout();
409338
- var single_flight_refresh_js_1 = require_single_flight_refresh();
409339
410723
  function shouldEvictMissingOnDisk(entry, id, onDiskIds) {
409340
410724
  return entry.source === "installed" && entry.packageName !== "@camstack/system" && !onDiskIds.has(id);
409341
410725
  }
@@ -409542,6 +410926,14 @@ var require_addon_registry_service = __commonJS({
409542
410926
  this.streamProbe = streamProbe;
409543
410927
  this.logger = this.loggingService.createLogger("AddonRegistry");
409544
410928
  this.addonLoader = new system_1.AddonLoader(this.loggingService.createLogger("AddonLoader"));
410929
+ this.deviceMirror = new device_meta_mirror_js_1.DeviceMetaMirror(async (deviceIds) => this.getBrokerApi().deviceManager.listAll.query({
410930
+ projection: "slim",
410931
+ ...deviceIds === null ? {} : { deviceIds: [...deviceIds] }
410932
+ }), {
410933
+ parentDeviceId: (deviceId) => this.deviceRegistry.getById(deviceId)?.parentDeviceId ?? null,
410934
+ type: (deviceId) => this.deviceRegistry.getById(deviceId)?.type ?? null,
410935
+ location: (deviceId) => this.deviceRegistry.getById(deviceId)?.location ?? null
410936
+ }, this.logger);
409545
410937
  this.healthMonitor = new system_1.AddonHealthMonitor({
409546
410938
  eventBus: this.eventBusService,
409547
410939
  logger: this.loggingService.createLogger("AddonHealthMonitor"),
@@ -410103,56 +411495,22 @@ var require_addon_registry_service = __commonJS({
410103
411495
  // fleet — forked devices included. This is a hub-process, SYNCHRONOUSLY
410104
411496
  // readable mirror of that parentage, refreshed OFF the request path on every
410105
411497
  // device-meta lifecycle event — never a per-call DB query.
410106
- deviceParentMirror = /* @__PURE__ */ new Map();
410107
- /**
410108
- * Hub-process mirror of each device's persisted `type` + `location`, warmed
410109
- * from the same `listAll({projection:'slim'})` sweep as the parent mirror.
410110
- * Backs the v3 scope-model `types` / `locations` selectors: the enforcement
410111
- * matcher resolves a selector against a device's type/location synchronously,
410112
- * off the request path (D49). Empty for a device the hub has never heard of.
410113
- */
410114
- deviceMetaMirror = /* @__PURE__ */ new Map();
410115
- /** Parent of a device: the persisted mirror first (covers forked devices),
410116
- * falling back to the live hub registry (covers a hub-local device before the
410117
- * first mirror warm). null when top-level or unknown. */
410118
- parentOfDevice(deviceId) {
410119
- const mirrored = this.deviceParentMirror.get(deviceId);
410120
- if (mirrored !== void 0)
410121
- return mirrored;
410122
- return this.deviceRegistry.getById(deviceId)?.parentDeviceId ?? null;
410123
- }
411498
+ //
411499
+ // The mirror itself, and the rule that decides how much of the fleet one
411500
+ // event may re-read, live in `device-meta-mirror.ts` with the 4 904-call /
411501
+ // 4 987 368-row boot census that made the rule necessary.
411502
+ deviceMirror;
410124
411503
  /** Persisted `DeviceType` string of a device, or null when unknown. Mirror
410125
411504
  * first (covers forked devices), then the live hub registry. */
410126
- getPersistedType = (deviceId) => {
410127
- const mirrored = this.deviceMetaMirror.get(deviceId);
410128
- if (mirrored !== void 0)
410129
- return mirrored.type;
410130
- return this.deviceRegistry.getById(deviceId)?.type ?? null;
410131
- };
411505
+ getPersistedType = (deviceId) => this.deviceMirror.typeOf(deviceId);
410132
411506
  /** Persisted operator `location` label of a device, or null when unset/unknown. */
410133
- getPersistedLocation = (deviceId) => {
410134
- const mirrored = this.deviceMetaMirror.get(deviceId);
410135
- if (mirrored !== void 0)
410136
- return mirrored.location;
410137
- return this.deviceRegistry.getById(deviceId)?.location ?? null;
410138
- };
411507
+ getPersistedLocation = (deviceId) => this.deviceMirror.locationOf(deviceId);
410139
411508
  /**
410140
411509
  * The whole persisted fleet, slim — id + type + location + parentDeviceId.
410141
411510
  * Fuels the FLEET-wide selector expansion (response projection + `auth.me`
410142
411511
  * counts). Read synchronously off the mirror, never a per-call DB query.
410143
411512
  */
410144
- getPersistedDeviceList = () => {
410145
- const out = [];
410146
- for (const [id, m] of this.deviceMetaMirror) {
410147
- out.push({
410148
- id,
410149
- type: m.type,
410150
- location: m.location,
410151
- parentDeviceId: this.deviceParentMirror.get(id) ?? null
410152
- });
410153
- }
410154
- return out;
410155
- };
411513
+ getPersistedDeviceList = () => this.deviceMirror.list();
410156
411514
  /**
410157
411515
  * Ancestor chain (parent, grandparent, …) of a device, bounded to 8 hops
410158
411516
  * (defence-in-depth against a corrupt registry cycle). Synchronous — it is
@@ -410160,80 +411518,31 @@ var require_addon_registry_service = __commonJS({
410160
411518
  * a FORKED camera covers its accessory children. Empty for a top-level device
410161
411519
  * or one the hub has never heard of.
410162
411520
  */
410163
- getPersistedAncestors = (deviceId) => {
410164
- const out = [];
410165
- let current = deviceId;
410166
- for (let hop = 0; hop < 8; hop++) {
410167
- const parent = this.parentOfDevice(current);
410168
- if (parent == null || parent === current)
410169
- break;
410170
- out.push(parent);
410171
- current = parent;
410172
- }
410173
- return out;
410174
- };
410175
- /** One full-fleet rebuild in flight, at most one queued behind it. Every
410176
- * event below asks for the SAME total rebuild, so running it once per event
410177
- * was ~1 000 rebuilds of a 974-row mirror on a boot — each one a 625 KB read
410178
- * and parse on hub-main's event loop. No timer: the first event still
410179
- * refreshes immediately, because this mirror backs scope enforcement.
410180
- * See `single-flight-refresh.ts`. */
410181
- deviceMirrorGate = (0, single_flight_refresh_js_1.createSingleFlightRefresh)(() => this.refreshDeviceParentMirror());
410182
- /** Subscribe the mirror to every device-meta lifecycle event, and warm it once
410183
- * the addon set (device-manager included) is up. */
411521
+ getPersistedAncestors = (deviceId) => this.deviceMirror.ancestorsOf(deviceId);
411522
+ /**
411523
+ * Subscribe the mirror to every device-meta lifecycle event, and warm it once
411524
+ * the addon set (device-manager included) is up.
411525
+ *
411526
+ * The device events ask for a SCOPED refresh — the one device they name, or
411527
+ * nothing at all when they name a field the mirror does not hold.
411528
+ * `SystemAddonsReady` is the only fleet-wide read, and it is also the D8
411529
+ * reconcile: whatever a dropped event would have left stale, the warm
411530
+ * repairs.
411531
+ */
410184
411532
  wireDeviceParentMirror() {
410185
- const refresh = () => {
410186
- void this.deviceMirrorGate.request();
410187
- };
410188
411533
  for (const category of [
410189
411534
  types_1.EventCategory.DeviceMetaChanged,
410190
411535
  types_1.EventCategory.DeviceRegistered,
410191
411536
  types_1.EventCategory.DeviceUnregistered,
410192
- types_1.EventCategory.DeviceProvisioned,
410193
- types_1.EventCategory.SystemAddonsReady
411537
+ types_1.EventCategory.DeviceProvisioned
410194
411538
  ]) {
410195
- this.eventBusService.subscribe({ category }, refresh);
410196
- }
410197
- }
410198
- /** Rebuild the parent mirror from the device-manager's persisted fleet
410199
- * (`listAll`, slim projection — no config blob). Off the request path; a
410200
- * failed read KEEPS the previous mirror (D49 — a read that fails changes
410201
- * nothing, and must never look like an unbind that destroys inheritance). */
410202
- async refreshDeviceParentMirror() {
410203
- try {
410204
- const api = this.getBrokerApi();
410205
- const rows = await api.deviceManager.listAll.query({ projection: "slim" });
410206
- if (!Array.isArray(rows))
410207
- return;
410208
- const nextParents = /* @__PURE__ */ new Map();
410209
- const nextMeta = /* @__PURE__ */ new Map();
410210
- for (const row of rows) {
410211
- if (row === null || typeof row !== "object")
410212
- continue;
410213
- const id = Reflect.get(row, "id");
410214
- if (typeof id !== "number")
410215
- continue;
410216
- const parent = Reflect.get(row, "parentDeviceId");
410217
- if (typeof parent === "number")
410218
- nextParents.set(id, parent);
410219
- const type = Reflect.get(row, "type");
410220
- const location = Reflect.get(row, "location");
410221
- nextMeta.set(id, {
410222
- type: typeof type === "string" ? type : "",
410223
- location: typeof location === "string" ? location : null
410224
- });
410225
- }
410226
- this.deviceParentMirror.clear();
410227
- for (const [k, v] of nextParents)
410228
- this.deviceParentMirror.set(k, v);
410229
- this.deviceMetaMirror.clear();
410230
- for (const [k, v] of nextMeta)
410231
- this.deviceMetaMirror.set(k, v);
410232
- } catch (err) {
410233
- this.logger.debug("device-parent mirror refresh failed \u2014 keeping previous", {
410234
- meta: { error: (0, types_1.errMsg)(err) }
411539
+ this.eventBusService.subscribe({ category }, (event) => {
411540
+ this.deviceMirror.onDeviceEvent(event.data);
410235
411541
  });
410236
411542
  }
411543
+ this.eventBusService.subscribe({ category: types_1.EventCategory.SystemAddonsReady }, () => {
411544
+ void this.deviceMirror.refreshAll();
411545
+ });
410237
411546
  }
410238
411547
  /** Load persisted collection disabled-lists from settings-store into the registry */
410239
411548
  loadCollectionPreferences() {