@camstack/addon-provider-tuya 0.2.32 → 0.2.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +409 -30
  2. package/dist/addon.mjs +409 -30
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -8288,6 +8288,111 @@ var CameraSwitchGroupSchema = object({
8288
8288
  fetchedAt: number()
8289
8289
  });
8290
8290
  /**
8291
+ * Per-component log CHANNELS — the gate a hot path consults, and the registry
8292
+ * an addon declares its channels in.
8293
+ *
8294
+ * ## Two axes, deliberately separated
8295
+ *
8296
+ * - **DECLARATION** — which channels exist. Only the addon knows:
8297
+ * `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
8298
+ * baichuan/handshake. A hand-wired central list rots at the first addition,
8299
+ * and rots silently. So a channel is declared where it is consulted, and the
8300
+ * `log-channels` capability enumerates the declarations.
8301
+ * - **VALUE** — at which level, for which scope, until when. That stays ONE
8302
+ * thing: the logging settings document on the `system` cap. Two authorities
8303
+ * over the values is the exact defect
8304
+ * `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
8305
+ * remove; re-introducing it from the cure side would be grotesque.
8306
+ *
8307
+ * Nothing in this file reads a clock, an env var or a store. The registry is
8308
+ * a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
8309
+ * the hot path with a value somebody actually read, and by
8310
+ * {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
8311
+ * never reaches here, so it can neither disarm an armed channel nor arm a
8312
+ * disarmed one (D49).
8313
+ *
8314
+ * ## The canonical call shape
8315
+ *
8316
+ * ```ts
8317
+ * if (CH_RTP.on && CH_RTP.wants(deviceId)) {
8318
+ * CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
8319
+ * }
8320
+ * ```
8321
+ *
8322
+ * `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
8323
+ * read. Disarmed, a call site costs one load and one branch, and the `extras`
8324
+ * object literal is never constructed because it lives inside the branch. It
8325
+ * is the same shape already proven in production at `stream-broker.ts:1650`,
8326
+ * and the same discipline `LoggingGate.allowsDestination` uses for the
8327
+ * destination floor (measured at 1.93 ns/call when off).
8328
+ *
8329
+ * ## Why a channel emits at `info`
8330
+ *
8331
+ * `loki-logging.addon.ts` pins the destination default at `info` and
8332
+ * `loki-destination.ts` drops everything below it, so a line emitted at
8333
+ * `debug` never reaches Loki and the hub's in-memory ring only holds ~35
8334
+ * minutes. A diagnostic that cannot be read an hour later is worse than no
8335
+ * diagnostic, because it looks done. {@link LogChannelGate.log} therefore
8336
+ * emits at the channel's declared level, whose schema floor is `info`.
8337
+ */
8338
+ /**
8339
+ * The level a channel writes at once armed.
8340
+ *
8341
+ * `debug` is absent ON PURPOSE and not by omission: below `info` the line does
8342
+ * not leave the process for Loki, and the whole point of arming a channel is
8343
+ * to read it later.
8344
+ */
8345
+ var LogChannelLevelSchema = _enum([
8346
+ "info",
8347
+ "warn",
8348
+ "error"
8349
+ ]);
8350
+ /**
8351
+ * What an addon declares about one channel. No value, no state — a
8352
+ * declaration is inert.
8353
+ */
8354
+ var LogChannelDescriptorSchema = object({
8355
+ /**
8356
+ * Dotted `area.thing`, unique across the workspace. `area` is conventionally
8357
+ * the addon's short name so an operator reading a channel list can tell who
8358
+ * owns it without a second lookup.
8359
+ */
8360
+ name: string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
8361
+ /** One sentence: what the operator will SEE after arming it. */
8362
+ description: string().min(1),
8363
+ /** The level its lines are emitted at. Never below `info`. */
8364
+ defaultLevel: LogChannelLevelSchema,
8365
+ /**
8366
+ * Whether this channel can be narrowed to a camera.
8367
+ *
8368
+ * `true` is a PROMISE with two halves, and both must hold: the gate is
8369
+ * consulted with the numeric device id, AND every line the channel admits
8370
+ * carries `tags: { deviceId }` with that same numeric id. The second half is
8371
+ * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
8372
+ * keeps `deviceId` out of the stream labels for cardinality, so the tag in
8373
+ * the body is the only way to filter.
8374
+ *
8375
+ * A channel whose lines carry the device only in `meta` (or not at all) is
8376
+ * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
8377
+ * the operator narrows to one camera, sees nothing, and concludes the code
8378
+ * path was never taken.
8379
+ */
8380
+ perDevice: boolean()
8381
+ });
8382
+ /**
8383
+ * An armed window over one channel, as the document hands it to a mirror.
8384
+ *
8385
+ * A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
8386
+ * expires by itself, which is the one failure a boolean cannot avoid.
8387
+ */
8388
+ var LogChannelWindowSchema = object({
8389
+ channel: string().min(1),
8390
+ /** Epoch ms the window closes at. */
8391
+ armedUntilMs: number(),
8392
+ /** `null` = every camera. A non-empty list narrows to those numeric ids. */
8393
+ deviceIds: array(number().int()).readonly().nullable()
8394
+ });
8395
+ /**
8291
8396
  * Ops-log — the durable, append-only operations audit shared by the
8292
8397
  * recordings and events management surfaces.
8293
8398
  *
@@ -11908,6 +12013,35 @@ var MutationFilterSchema = object({
11908
12013
  whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
11909
12014
  whereNot: record(string(), unknown()).optional()
11910
12015
  });
12016
+ /**
12017
+ * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
12018
+ *
12019
+ * `as` names the slot in the result, so the SAME column may be asked twice with
12020
+ * two operations (`MIN(startMs)` and `MAX(startMs)` in one round trip) — which
12021
+ * a `Record<column, op>` shape could not express.
12022
+ */
12023
+ var AggregateFieldSchema = object({
12024
+ /** Result key. */
12025
+ as: string().min(1),
12026
+ /** Column to aggregate. Must be a real column of a declared collection. */
12027
+ field: string().min(1),
12028
+ op: _enum([
12029
+ "sum",
12030
+ "min",
12031
+ "max"
12032
+ ])
12033
+ });
12034
+ /**
12035
+ * `COUNT(*)` plus one number per requested field.
12036
+ *
12037
+ * `null` means NO ROW MATCHED, never zero: a `SUM` over an empty set and a sum
12038
+ * that really is 0 are different facts, and an accounting caller that renders
12039
+ * "0 bytes, oldest = 0" for "nothing here" reports a lie about a disk.
12040
+ */
12041
+ var AggregateResultSchema = object({
12042
+ count: number().int(),
12043
+ values: record(string(), number().nullable())
12044
+ });
11911
12045
  /** A single stored record: `{ id, data }`. */
11912
12046
  var SettingsRecordSchema = object({
11913
12047
  id: string(),
@@ -11992,6 +12126,11 @@ method(object({
11992
12126
  collection: string(),
11993
12127
  filter: QueryFilterSchema.optional()
11994
12128
  }), number()), method(object({
12129
+ namespace: string().optional(),
12130
+ collection: string(),
12131
+ fields: array(AggregateFieldSchema).readonly(),
12132
+ filter: QueryFilterSchema.optional()
12133
+ }), AggregateResultSchema), method(object({
11995
12134
  namespace: string().optional(),
11996
12135
  collection: string(),
11997
12136
  field: string(),
@@ -12108,6 +12247,11 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
12108
12247
  collection: string(),
12109
12248
  filter: QueryFilterSchema.optional()
12110
12249
  }), number(), { auth: "admin" }), method(object({
12250
+ namespace: string().optional(),
12251
+ collection: string(),
12252
+ fields: array(AggregateFieldSchema).readonly(),
12253
+ filter: QueryFilterSchema.optional()
12254
+ }), AggregateResultSchema, { auth: "admin" }), method(object({
12111
12255
  namespace: string().optional(),
12112
12256
  collection: string(),
12113
12257
  field: string(),
@@ -12848,24 +12992,6 @@ var deviceProviderCapability = {
12848
12992
  })
12849
12993
  }
12850
12994
  };
12851
- /**
12852
- * Device Manager capability — hub-side singleton that unifies device persistence,
12853
- * live registry access, and all management operations into a single tRPC surface.
12854
- *
12855
- * Replaces:
12856
- * - `device-persistence` capability (persistence methods absorbed here)
12857
- * - `device-management.router.ts` (deleted in Phase 2)
12858
- * - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
12859
- *
12860
- * All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
12861
- * fork into separate processes but never run on remote cluster agents. Therefore:
12862
- * - No nodeId routing needed — this is a pure hub singleton.
12863
- * - The hub's DeviceRegistry is the single source of truth for all live devices.
12864
- * - No shadow registry or cross-node aggregation required.
12865
- *
12866
- * Forked workers register devices back to the hub via `ctx.devices`
12867
- * (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
12868
- */
12869
12995
  /** One child-placement directive on a container's `childLayout`. Structurally
12870
12996
  * identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
12871
12997
  * shape for the same field. The child is identified by its re-sync-stable
@@ -13234,7 +13360,7 @@ method(object({
13234
13360
  * it answers today and the caller filters as it already does.
13235
13361
  */
13236
13362
  deviceIds: array(number()).optional()
13237
- }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
13363
+ }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ parentDeviceIds: array(number()).max(256) }), record(string(), array(DeviceInfoSchema))), method(object({ deviceId: number() }), object({
13238
13364
  mode: LinkedDevicesModeSchema,
13239
13365
  devices: array(LinkedDeviceSchema)
13240
13366
  })), method(object({ deviceIds: array(number()) }), array(LinkedDevicesForDeviceSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
@@ -13958,6 +14084,59 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13958
14084
  kind: "mutation",
13959
14085
  auth: "admin"
13960
14086
  });
14087
+ /**
14088
+ * `log-channels` — the capability an addon DECLARES its diagnostic channels
14089
+ * through. It stores nothing.
14090
+ *
14091
+ * ## Why a capability at all, and why this shape
14092
+ *
14093
+ * Which channels exist is knowledge only the addon has: `stream-broker` knows
14094
+ * webrtc/ICE/RTP, `provider-reolink` knows baichuan/handshake. A central list
14095
+ * maintained by hand rots at the first addition and rots INVISIBLY — nothing
14096
+ * fails, an operator just never sees the channel somebody added. So the list
14097
+ * is assembled from declarations at runtime.
14098
+ *
14099
+ * The shape is copied from `log-destination.cap.ts`, which already does
14100
+ * exactly this job: `mode: 'collection'`, `internal: true`,
14101
+ * `mount: { kind: 'skip' }` — no tRPC route, no generated hooks — while
14102
+ * `addons.listCapabilityProviders` still enumerates it, and the hub's
14103
+ * `CapabilityRegistry` still holds an RPC proxy per provider so a forked
14104
+ * runner's declarations reach hub-main over the transport that already exists.
14105
+ * No new UDS message, no second registry.
14106
+ *
14107
+ * ## What it deliberately does NOT own
14108
+ *
14109
+ * The VALUES — which channel is armed, for which cameras, until when — live in
14110
+ * ONE place: the logging settings document on the `system` cap
14111
+ * (`getLoggingSettings` / `setLoggingSettings`). Two authorities over the same
14112
+ * value is the defect the plan behind this work exists to remove, and
14113
+ * `setRequestCensus` was retired (D245) rather than allowed to be a second
14114
+ * one. {@link logChannelsCapability} therefore has no getter for a level, no
14115
+ * setter for a window and no persistence of any kind.
14116
+ *
14117
+ * ## Why `apply` is here even so
14118
+ *
14119
+ * The gate lives in the addon's PROCESS; the document lives in hub-main. Some
14120
+ * seam has to carry the value from the authority to the mirror, and a channel
14121
+ * that cannot be reached is precisely the dead knob this whole slice exists to
14122
+ * make impossible (D62 — `audioThresholdDbfs`, the HA entities with no source).
14123
+ * `apply` is that seam and nothing more: it writes an in-memory mirror, it
14124
+ * persists nothing, it is never the source of a value, and it is called only
14125
+ * with a set the hub actually read (D49 — a read that fails does not call it
14126
+ * at all, so no channel is silently disarmed by a bad read).
14127
+ */
14128
+ /** What `apply` reports back — enough to log, not enough to be a second state. */
14129
+ var LogChannelApplyResultSchema = object({
14130
+ /** How many declared channels are armed in this process after the call. */
14131
+ armed: number().int().min(0),
14132
+ /**
14133
+ * Names the document armed that this process does not declare. Reported
14134
+ * rather than swallowed: a name here is either a typo or an addon that has
14135
+ * not booted, and both deserve a line instead of silence.
14136
+ */
14137
+ unknown: array(string()).readonly()
14138
+ });
14139
+ method(_void(), array(LogChannelDescriptorSchema).readonly()), method(object({ windows: array(LogChannelWindowSchema).readonly() }), LogChannelApplyResultSchema, { kind: "mutation" });
13961
14140
  var LogLevelSchema = _enum([
13962
14141
  "debug",
13963
14142
  "info",
@@ -29248,17 +29427,60 @@ var SetSiteLocationInputSchema = object({
29248
29427
  longitude: number().min(-180).max(180)
29249
29428
  }).nullable();
29250
29429
  /**
29251
- * One `(procedure, user-agent, ip, principal)` tuple of the HTTP request
29430
+ * The TRANSPORT a call arrived on.
29431
+ *
29432
+ * Every counted call carries exactly one of these, and `unknown` is a PLANE
29433
+ * rather than a gap: a plane that cannot attribute a call declares it here, so
29434
+ * the call lands in a named bucket instead of vanishing. `planes` summing to
29435
+ * `procedureCalls` is what makes "the sum of the planes explains the total"
29436
+ * checkable rather than asserted.
29437
+ *
29438
+ * - `http` — the Fastify tRPC plugin (`/trpc/*`), one context per request.
29439
+ * - `ws` — `applyWSSHandler`, counted per OPERATION rather than per
29440
+ * connection; the viewer talks to the hub over `wsLink`
29441
+ * exclusively, so this is the plane the HTTP census could not see.
29442
+ * - `mesh` — the in-process `$core-caps` bridge (`createCaller`), which
29443
+ * never touches a socket and therefore never touched a census.
29444
+ * - `unknown` — counted, plane undecidable. No hook produces it today, and
29445
+ * that is exactly what its `0` asserts: every plane the hub has can name
29446
+ * itself. It is an output bucket, never a knob — a call that arrives on a
29447
+ * plane nobody instrumented lands here instead of vanishing from the total.
29448
+ */
29449
+ var TransportPlaneSchema = _enum([
29450
+ "http",
29451
+ "ws",
29452
+ "mesh",
29453
+ "unknown"
29454
+ ]);
29455
+ /**
29456
+ * Calls per plane. Every key is always present, `0` included — an absent plane
29457
+ * reads as "not instrumented", which is the one thing this census must never
29458
+ * make an operator wonder about.
29459
+ */
29460
+ var TransportPlaneCountsSchema = object({
29461
+ http: number(),
29462
+ ws: number(),
29463
+ mesh: number(),
29464
+ unknown: number()
29465
+ });
29466
+ /**
29467
+ * One `(plane, procedure, user-agent, ip, principal)` tuple of the transport
29252
29468
  * census. `principal` is the DERIVED identity (`apocaliss92 (admin)`,
29253
29469
  * `scoped:1a2b3c4d (scoped-token)`, `anonymous`) that the tRPC error log
29254
29470
  * already prints - never a token, never an `Authorization` header.
29471
+ *
29472
+ * `subscriptions` is counted APART from `calls`: a subscription is opened once
29473
+ * and lives for hours, so folding it into a call count makes one long-lived
29474
+ * stream look like a storm.
29255
29475
  */
29256
29476
  var RequestCensusGroupSchema = object({
29477
+ plane: TransportPlaneSchema,
29257
29478
  procedure: string(),
29258
29479
  userAgent: string(),
29259
29480
  ip: string(),
29260
29481
  principal: string(),
29261
29482
  calls: number(),
29483
+ subscriptions: number(),
29262
29484
  perMin: number()
29263
29485
  });
29264
29486
  /**
@@ -29271,6 +29493,14 @@ var RequestCensusGroupSchema = object({
29271
29493
  var RequestCensusProcedureSchema = object({
29272
29494
  procedure: string(),
29273
29495
  calls: number(),
29496
+ /**
29497
+ * The same total, split by transport. THIS is the row that answers the
29498
+ * question the census exists for: one look at `deviceManager.listAll` says
29499
+ * which plane carried the 4 960, without joining two log lines by eye.
29500
+ */
29501
+ planes: TransportPlaneCountsSchema,
29502
+ /** Subscription STARTS on this procedure. Never folded into `calls`. */
29503
+ subscriptions: number(),
29274
29504
  perMin: number()
29275
29505
  });
29276
29506
  /**
@@ -29298,14 +29528,45 @@ var RequestCensusStatusSchema = object({
29298
29528
  */
29299
29529
  procedureCalls: number(),
29300
29530
  /**
29531
+ * `procedureCalls` split by transport. The four keys sum to
29532
+ * `procedureCalls` by construction - {@link RequestCensusSnapshotSchema}'s
29533
+ * `planesExplainTotal` is that identity, checked rather than assumed.
29534
+ */
29535
+ planes: TransportPlaneCountsSchema,
29536
+ /**
29537
+ * True iff `planes` sums to `procedureCalls`. False means a call was counted
29538
+ * on no plane at all - which is a RESULT (a plane is missing from the
29539
+ * instrument), not a failure, and it has to be visible to be read as one.
29540
+ */
29541
+ planesExplainTotal: boolean(),
29542
+ /**
29301
29543
  * tRPC WebSocket connections opened during the window. NOT calls - the WS
29302
- * transport resolves one context per connection - but the number that says
29303
- * whether a plane this census cannot see was busy while HTTP was quiet.
29544
+ * adapter resolves one context per connection - kept because a plane's call
29545
+ * count of zero against 37 open connections says something different from a
29546
+ * plane with no connections at all.
29304
29547
  */
29305
29548
  wsConnections: number(),
29549
+ /**
29550
+ * Client frames the WS plane looked at. `wsMessages` far above
29551
+ * `planes.ws + subscriptions` means most traffic is not operations
29552
+ * (keepalives, connection params) - which is itself an answer.
29553
+ */
29554
+ wsMessages: number(),
29555
+ /**
29556
+ * Subscription STARTS across every plane, excluded from `procedureCalls` on
29557
+ * purpose: one live-events stream opened at boot and held for six hours is
29558
+ * one subscription, and counting it as a call would let a quiet plane
29559
+ * masquerade as the storm.
29560
+ */
29561
+ subscriptions: number(),
29562
+ /** `subscription.stop` frames. Starts minus stops is what is still open. */
29563
+ subscriptionStops: number(),
29306
29564
  distinctGroups: number(),
29307
- /** Calls counted in the totals whose group attribution was shed at the
29308
- * cardinality bound. */
29565
+ /**
29566
+ * Operations counted in the totals whose CALLER attribution was shed at the
29567
+ * cardinality bound. Unrelated to the `unknown` PLANE: these calls know
29568
+ * which transport they arrived on, they just lost their group row.
29569
+ */
29309
29570
  unattributedCalls: number(),
29310
29571
  procedures: array(RequestCensusProcedureSchema).readonly(),
29311
29572
  groups: array(RequestCensusGroupSchema).readonly()
@@ -29328,10 +29589,11 @@ var DiagnosticIdSchema = _enum(["request-census"]);
29328
29589
  * The layers of the level hierarchy, general → specific. The most specific
29329
29590
  * layer that carries an explicit value wins.
29330
29591
  *
29331
- * `component` is DECLARED and not yet resolvable: the per-component channels
29332
- * are a later slice of the same plan, and a `levelSource` enum that has to
29333
- * grow later would force every consumer of this document to change with it.
29334
- * Nothing returns `component` today.
29592
+ * `component` became RESOLVABLE on 2026-08-27: a component is a declared log
29593
+ * CHANNEL (`stream-broker.webrtc`, `provider-reolink.baichuan`), named by
29594
+ * `scopeComponent`. It was declared-but-dark in the first slice precisely so
29595
+ * that turning it on would not force every consumer of this document to widen
29596
+ * a `levelSource` enum — which is what has now not happened.
29335
29597
  */
29336
29598
  var LoggingScopeKindSchema = _enum([
29337
29599
  "cluster",
@@ -29358,6 +29620,14 @@ var LoggingLevelLayerSchema = object({
29358
29620
  scope: LoggingScopeKindSchema,
29359
29621
  /** The node this layer speaks for; `null` on the cluster layer. */
29360
29622
  nodeId: string().nullable(),
29623
+ /**
29624
+ * The declared channel this layer speaks for; `null` on every layer but
29625
+ * `component`. Never folded into `nodeId`: a component level is CLUSTER-WIDE
29626
+ * by design — the convention this repo settled on is one orchestrator-wide
29627
+ * setting, never per node (D52) — so a component layer that carried a node
29628
+ * would invite a per-node copy of a value that has no per-node meaning.
29629
+ */
29630
+ component: string().nullable(),
29361
29631
  /** Explicitly set here, or `null` when this layer inherits. */
29362
29632
  level: LogLevelSchema$1.nullable()
29363
29633
  });
@@ -29399,6 +29669,49 @@ var DiagnosticWindowPatchSchema = object({
29399
29669
  reportEveryMs: number().int().positive().optional()
29400
29670
  });
29401
29671
  /**
29672
+ * A channel ARMED, as the document reports it.
29673
+ *
29674
+ * `armMs` is not echoed back: what an operator needs to see is the deadline
29675
+ * and the time left, because a diagnostic left running is itself an incident
29676
+ * and "armed for 10 minutes" said an hour ago is not an answer.
29677
+ */
29678
+ var LogChannelWindowStateSchema = object({
29679
+ channel: string(),
29680
+ armed: boolean(),
29681
+ /** Epoch ms the window closes at. 0 when disarmed. */
29682
+ armedUntilMs: number(),
29683
+ /** Ms left before it expires on its own. 0 when disarmed. */
29684
+ remainingMs: number(),
29685
+ /**
29686
+ * The cameras it is narrowed to, or `null` for every camera.
29687
+ *
29688
+ * A channel declared `perDevice: false` can only ever report `null` here:
29689
+ * its lines do not carry `tags: { deviceId }`, so narrowing them would
29690
+ * produce a filter that silently matches nothing. The server REFUSES such a
29691
+ * patch rather than quietly widening it — ignoring the request would teach
29692
+ * the operator that per-camera filtering works on that channel when it does
29693
+ * not.
29694
+ */
29695
+ deviceIds: array(number().int()).readonly().nullable()
29696
+ });
29697
+ /**
29698
+ * `armMs: 0` DISARMS. Same grammar as {@link DiagnosticWindowPatchSchema}, and
29699
+ * for the same reason: a channel is a window with a deadline, never a switch.
29700
+ */
29701
+ var LogChannelWindowPatchSchema = object({
29702
+ channel: string().min(1),
29703
+ armMs: number().int().min(0),
29704
+ /**
29705
+ * Narrow to these numeric device ids. Absent or `null` = every camera.
29706
+ *
29707
+ * Numeric because the repo's own rule makes it possible: every log line
29708
+ * about a device carries `tags: { deviceId }` with the numeric id. That rule
29709
+ * was paid for with a 22% thumbnail gap and a 3-hour media blackout both
29710
+ * diagnosed by hand, and this is the first thing that collects on it.
29711
+ */
29712
+ deviceIds: array(number().int()).readonly().nullable().optional()
29713
+ });
29714
+ /**
29402
29715
  * A PATCH, and patches MERGE.
29403
29716
  *
29404
29717
  * A field absent from the patch is left exactly as it was — arming a
@@ -29417,7 +29730,14 @@ var LoggingSettingsPatchSchema = object({
29417
29730
  * Only the diagnostics NAMED here change. An armed window that is not listed
29418
29731
  * keeps running — a patch is never a full replacement.
29419
29732
  */
29420
- diagnostics: array(DiagnosticWindowPatchSchema).readonly().optional()
29733
+ diagnostics: array(DiagnosticWindowPatchSchema).readonly().optional(),
29734
+ /**
29735
+ * Only the channels NAMED here change. An armed channel that is not listed
29736
+ * keeps running — same rule as `diagnostics`, because a patch that silently
29737
+ * disarmed the channels it did not mention would make the Levels page and
29738
+ * the Diagnostics page fight over the same value.
29739
+ */
29740
+ channels: array(LogChannelWindowPatchSchema).readonly().optional()
29421
29741
  });
29422
29742
  /**
29423
29743
  * Which LAYER of the hierarchy is addressed. Absent = the cluster layer.
@@ -29430,9 +29750,22 @@ var LoggingSettingsPatchSchema = object({
29430
29750
  * authority over the whole hierarchy and answers for every layer, so the
29431
29751
  * layer selector needs a name the transport does not already own.
29432
29752
  */
29433
- var GetLoggingSettingsInputSchema = object({ scopeNodeId: string().optional() });
29753
+ var GetLoggingSettingsInputSchema = object({
29754
+ scopeNodeId: string().optional(),
29755
+ /**
29756
+ * The declared CHANNEL this document is addressed at, when the caller wants
29757
+ * the `component` layer. Absent = the node/cluster hierarchy only.
29758
+ *
29759
+ * Naming it separately rather than overloading `scopeNodeId` keeps the two
29760
+ * axes from collapsing: a component level is cluster-wide, a node level is
29761
+ * not, and one selector for both would make "which of these two did I just
29762
+ * set" unanswerable — the exact ambiguity `explicit` exists to remove.
29763
+ */
29764
+ scopeComponent: string().optional()
29765
+ });
29434
29766
  var SetLoggingSettingsInputSchema = object({
29435
29767
  scopeNodeId: string().optional(),
29768
+ scopeComponent: string().optional(),
29436
29769
  patch: LoggingSettingsPatchSchema
29437
29770
  });
29438
29771
  /**
@@ -29447,9 +29780,20 @@ var SetLoggingSettingsInputSchema = object({
29447
29780
  var LoggingSettingsStateSchema = object({
29448
29781
  /** The layer this document was read at. `null` = the cluster layer. */
29449
29782
  scopeNodeId: string().nullable(),
29783
+ /** The channel this document was read at. `null` = no component layer. */
29784
+ scopeComponent: string().nullable(),
29450
29785
  effective: LoggingEffectiveSchema,
29451
29786
  explicit: LoggingExplicitSchema,
29452
29787
  activeWindows: array(DiagnosticWindowSchema).readonly(),
29788
+ /**
29789
+ * Every channel the cluster's addons DECLARE, gathered from the
29790
+ * `log-channels` providers. Not stored anywhere: assembled per read, so a
29791
+ * channel added by a redeployed addon appears without anybody editing a
29792
+ * list, and a channel whose addon is gone stops being offered.
29793
+ */
29794
+ channels: array(LogChannelDescriptorSchema).readonly(),
29795
+ /** The channels ARMED right now, each with its deadline. */
29796
+ activeChannels: array(LogChannelWindowStateSchema).readonly(),
29453
29797
  persisted: boolean()
29454
29798
  });
29455
29799
  method(_void(), FeatureManifestSchema), method(_void(), HealthStatusSchema), method(_void(), FeatureManifestSchema), method(_void(), array(NetworkAddressSchema).readonly()), method(_void(), unknown().nullable(), { auth: "admin" }), method(record(string(), unknown()), _null(), {
@@ -32434,6 +32778,12 @@ Object.freeze({
32434
32778
  addonId: null,
32435
32779
  access: "view"
32436
32780
  },
32781
+ "dataStoreProvider.aggregate": {
32782
+ capName: "data-store-provider",
32783
+ capScope: "system",
32784
+ addonId: null,
32785
+ access: "view"
32786
+ },
32437
32787
  "dataStoreProvider.count": {
32438
32788
  capName: "data-store-provider",
32439
32789
  capScope: "system",
@@ -32848,6 +33198,12 @@ Object.freeze({
32848
33198
  addonId: null,
32849
33199
  access: "view"
32850
33200
  },
33201
+ "deviceManager.getChildrenBatch": {
33202
+ capName: "device-manager",
33203
+ capScope: "system",
33204
+ addonId: null,
33205
+ access: "view"
33206
+ },
32851
33207
  "deviceManager.getConfigSchema": {
32852
33208
  capName: "device-manager",
32853
33209
  capScope: "system",
@@ -33898,6 +34254,18 @@ Object.freeze({
33898
34254
  addonId: null,
33899
34255
  access: "create"
33900
34256
  },
34257
+ "logChannels.apply": {
34258
+ capName: "log-channels",
34259
+ capScope: "system",
34260
+ addonId: null,
34261
+ access: "create"
34262
+ },
34263
+ "logChannels.list": {
34264
+ capName: "log-channels",
34265
+ capScope: "system",
34266
+ addonId: null,
34267
+ access: "view"
34268
+ },
33901
34269
  "logDestination.query": {
33902
34270
  capName: "log-destination",
33903
34271
  capScope: "system",
@@ -36052,6 +36420,12 @@ Object.freeze({
36052
36420
  addonId: null,
36053
36421
  access: "create"
36054
36422
  },
36423
+ "settingsStore.aggregate": {
36424
+ capName: "settings-store",
36425
+ capScope: "system",
36426
+ addonId: null,
36427
+ access: "view"
36428
+ },
36055
36429
  "settingsStore.count": {
36056
36430
  capName: "settings-store",
36057
36431
  capScope: "system",
@@ -37631,6 +38005,11 @@ Object.freeze({
37631
38005
  form: "single",
37632
38006
  optional: false
37633
38007
  }],
38008
+ "deviceManager.getChildrenBatch": [{
38009
+ name: "parentDeviceIds",
38010
+ form: "array",
38011
+ optional: false
38012
+ }],
37634
38013
  "deviceManager.getConfigSchema": [{
37635
38014
  name: "deviceId",
37636
38015
  form: "single",
package/dist/addon.mjs CHANGED
@@ -8287,6 +8287,111 @@ var CameraSwitchGroupSchema = object({
8287
8287
  fetchedAt: number()
8288
8288
  });
8289
8289
  /**
8290
+ * Per-component log CHANNELS — the gate a hot path consults, and the registry
8291
+ * an addon declares its channels in.
8292
+ *
8293
+ * ## Two axes, deliberately separated
8294
+ *
8295
+ * - **DECLARATION** — which channels exist. Only the addon knows:
8296
+ * `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
8297
+ * baichuan/handshake. A hand-wired central list rots at the first addition,
8298
+ * and rots silently. So a channel is declared where it is consulted, and the
8299
+ * `log-channels` capability enumerates the declarations.
8300
+ * - **VALUE** — at which level, for which scope, until when. That stays ONE
8301
+ * thing: the logging settings document on the `system` cap. Two authorities
8302
+ * over the values is the exact defect
8303
+ * `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
8304
+ * remove; re-introducing it from the cure side would be grotesque.
8305
+ *
8306
+ * Nothing in this file reads a clock, an env var or a store. The registry is
8307
+ * a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
8308
+ * the hot path with a value somebody actually read, and by
8309
+ * {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
8310
+ * never reaches here, so it can neither disarm an armed channel nor arm a
8311
+ * disarmed one (D49).
8312
+ *
8313
+ * ## The canonical call shape
8314
+ *
8315
+ * ```ts
8316
+ * if (CH_RTP.on && CH_RTP.wants(deviceId)) {
8317
+ * CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
8318
+ * }
8319
+ * ```
8320
+ *
8321
+ * `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
8322
+ * read. Disarmed, a call site costs one load and one branch, and the `extras`
8323
+ * object literal is never constructed because it lives inside the branch. It
8324
+ * is the same shape already proven in production at `stream-broker.ts:1650`,
8325
+ * and the same discipline `LoggingGate.allowsDestination` uses for the
8326
+ * destination floor (measured at 1.93 ns/call when off).
8327
+ *
8328
+ * ## Why a channel emits at `info`
8329
+ *
8330
+ * `loki-logging.addon.ts` pins the destination default at `info` and
8331
+ * `loki-destination.ts` drops everything below it, so a line emitted at
8332
+ * `debug` never reaches Loki and the hub's in-memory ring only holds ~35
8333
+ * minutes. A diagnostic that cannot be read an hour later is worse than no
8334
+ * diagnostic, because it looks done. {@link LogChannelGate.log} therefore
8335
+ * emits at the channel's declared level, whose schema floor is `info`.
8336
+ */
8337
+ /**
8338
+ * The level a channel writes at once armed.
8339
+ *
8340
+ * `debug` is absent ON PURPOSE and not by omission: below `info` the line does
8341
+ * not leave the process for Loki, and the whole point of arming a channel is
8342
+ * to read it later.
8343
+ */
8344
+ var LogChannelLevelSchema = _enum([
8345
+ "info",
8346
+ "warn",
8347
+ "error"
8348
+ ]);
8349
+ /**
8350
+ * What an addon declares about one channel. No value, no state — a
8351
+ * declaration is inert.
8352
+ */
8353
+ var LogChannelDescriptorSchema = object({
8354
+ /**
8355
+ * Dotted `area.thing`, unique across the workspace. `area` is conventionally
8356
+ * the addon's short name so an operator reading a channel list can tell who
8357
+ * owns it without a second lookup.
8358
+ */
8359
+ name: string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
8360
+ /** One sentence: what the operator will SEE after arming it. */
8361
+ description: string().min(1),
8362
+ /** The level its lines are emitted at. Never below `info`. */
8363
+ defaultLevel: LogChannelLevelSchema,
8364
+ /**
8365
+ * Whether this channel can be narrowed to a camera.
8366
+ *
8367
+ * `true` is a PROMISE with two halves, and both must hold: the gate is
8368
+ * consulted with the numeric device id, AND every line the channel admits
8369
+ * carries `tags: { deviceId }` with that same numeric id. The second half is
8370
+ * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
8371
+ * keeps `deviceId` out of the stream labels for cardinality, so the tag in
8372
+ * the body is the only way to filter.
8373
+ *
8374
+ * A channel whose lines carry the device only in `meta` (or not at all) is
8375
+ * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
8376
+ * the operator narrows to one camera, sees nothing, and concludes the code
8377
+ * path was never taken.
8378
+ */
8379
+ perDevice: boolean()
8380
+ });
8381
+ /**
8382
+ * An armed window over one channel, as the document hands it to a mirror.
8383
+ *
8384
+ * A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
8385
+ * expires by itself, which is the one failure a boolean cannot avoid.
8386
+ */
8387
+ var LogChannelWindowSchema = object({
8388
+ channel: string().min(1),
8389
+ /** Epoch ms the window closes at. */
8390
+ armedUntilMs: number(),
8391
+ /** `null` = every camera. A non-empty list narrows to those numeric ids. */
8392
+ deviceIds: array(number().int()).readonly().nullable()
8393
+ });
8394
+ /**
8290
8395
  * Ops-log — the durable, append-only operations audit shared by the
8291
8396
  * recordings and events management surfaces.
8292
8397
  *
@@ -11907,6 +12012,35 @@ var MutationFilterSchema = object({
11907
12012
  whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
11908
12013
  whereNot: record(string(), unknown()).optional()
11909
12014
  });
12015
+ /**
12016
+ * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
12017
+ *
12018
+ * `as` names the slot in the result, so the SAME column may be asked twice with
12019
+ * two operations (`MIN(startMs)` and `MAX(startMs)` in one round trip) — which
12020
+ * a `Record<column, op>` shape could not express.
12021
+ */
12022
+ var AggregateFieldSchema = object({
12023
+ /** Result key. */
12024
+ as: string().min(1),
12025
+ /** Column to aggregate. Must be a real column of a declared collection. */
12026
+ field: string().min(1),
12027
+ op: _enum([
12028
+ "sum",
12029
+ "min",
12030
+ "max"
12031
+ ])
12032
+ });
12033
+ /**
12034
+ * `COUNT(*)` plus one number per requested field.
12035
+ *
12036
+ * `null` means NO ROW MATCHED, never zero: a `SUM` over an empty set and a sum
12037
+ * that really is 0 are different facts, and an accounting caller that renders
12038
+ * "0 bytes, oldest = 0" for "nothing here" reports a lie about a disk.
12039
+ */
12040
+ var AggregateResultSchema = object({
12041
+ count: number().int(),
12042
+ values: record(string(), number().nullable())
12043
+ });
11910
12044
  /** A single stored record: `{ id, data }`. */
11911
12045
  var SettingsRecordSchema = object({
11912
12046
  id: string(),
@@ -11991,6 +12125,11 @@ method(object({
11991
12125
  collection: string(),
11992
12126
  filter: QueryFilterSchema.optional()
11993
12127
  }), number()), method(object({
12128
+ namespace: string().optional(),
12129
+ collection: string(),
12130
+ fields: array(AggregateFieldSchema).readonly(),
12131
+ filter: QueryFilterSchema.optional()
12132
+ }), AggregateResultSchema), method(object({
11994
12133
  namespace: string().optional(),
11995
12134
  collection: string(),
11996
12135
  field: string(),
@@ -12107,6 +12246,11 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
12107
12246
  collection: string(),
12108
12247
  filter: QueryFilterSchema.optional()
12109
12248
  }), number(), { auth: "admin" }), method(object({
12249
+ namespace: string().optional(),
12250
+ collection: string(),
12251
+ fields: array(AggregateFieldSchema).readonly(),
12252
+ filter: QueryFilterSchema.optional()
12253
+ }), AggregateResultSchema, { auth: "admin" }), method(object({
12110
12254
  namespace: string().optional(),
12111
12255
  collection: string(),
12112
12256
  field: string(),
@@ -12847,24 +12991,6 @@ var deviceProviderCapability = {
12847
12991
  })
12848
12992
  }
12849
12993
  };
12850
- /**
12851
- * Device Manager capability — hub-side singleton that unifies device persistence,
12852
- * live registry access, and all management operations into a single tRPC surface.
12853
- *
12854
- * Replaces:
12855
- * - `device-persistence` capability (persistence methods absorbed here)
12856
- * - `device-management.router.ts` (deleted in Phase 2)
12857
- * - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
12858
- *
12859
- * All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
12860
- * fork into separate processes but never run on remote cluster agents. Therefore:
12861
- * - No nodeId routing needed — this is a pure hub singleton.
12862
- * - The hub's DeviceRegistry is the single source of truth for all live devices.
12863
- * - No shadow registry or cross-node aggregation required.
12864
- *
12865
- * Forked workers register devices back to the hub via `ctx.devices`
12866
- * (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
12867
- */
12868
12994
  /** One child-placement directive on a container's `childLayout`. Structurally
12869
12995
  * identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
12870
12996
  * shape for the same field. The child is identified by its re-sync-stable
@@ -13233,7 +13359,7 @@ method(object({
13233
13359
  * it answers today and the caller filters as it already does.
13234
13360
  */
13235
13361
  deviceIds: array(number()).optional()
13236
- }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
13362
+ }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ parentDeviceIds: array(number()).max(256) }), record(string(), array(DeviceInfoSchema))), method(object({ deviceId: number() }), object({
13237
13363
  mode: LinkedDevicesModeSchema,
13238
13364
  devices: array(LinkedDeviceSchema)
13239
13365
  })), method(object({ deviceIds: array(number()) }), array(LinkedDevicesForDeviceSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
@@ -13957,6 +14083,59 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13957
14083
  kind: "mutation",
13958
14084
  auth: "admin"
13959
14085
  });
14086
+ /**
14087
+ * `log-channels` — the capability an addon DECLARES its diagnostic channels
14088
+ * through. It stores nothing.
14089
+ *
14090
+ * ## Why a capability at all, and why this shape
14091
+ *
14092
+ * Which channels exist is knowledge only the addon has: `stream-broker` knows
14093
+ * webrtc/ICE/RTP, `provider-reolink` knows baichuan/handshake. A central list
14094
+ * maintained by hand rots at the first addition and rots INVISIBLY — nothing
14095
+ * fails, an operator just never sees the channel somebody added. So the list
14096
+ * is assembled from declarations at runtime.
14097
+ *
14098
+ * The shape is copied from `log-destination.cap.ts`, which already does
14099
+ * exactly this job: `mode: 'collection'`, `internal: true`,
14100
+ * `mount: { kind: 'skip' }` — no tRPC route, no generated hooks — while
14101
+ * `addons.listCapabilityProviders` still enumerates it, and the hub's
14102
+ * `CapabilityRegistry` still holds an RPC proxy per provider so a forked
14103
+ * runner's declarations reach hub-main over the transport that already exists.
14104
+ * No new UDS message, no second registry.
14105
+ *
14106
+ * ## What it deliberately does NOT own
14107
+ *
14108
+ * The VALUES — which channel is armed, for which cameras, until when — live in
14109
+ * ONE place: the logging settings document on the `system` cap
14110
+ * (`getLoggingSettings` / `setLoggingSettings`). Two authorities over the same
14111
+ * value is the defect the plan behind this work exists to remove, and
14112
+ * `setRequestCensus` was retired (D245) rather than allowed to be a second
14113
+ * one. {@link logChannelsCapability} therefore has no getter for a level, no
14114
+ * setter for a window and no persistence of any kind.
14115
+ *
14116
+ * ## Why `apply` is here even so
14117
+ *
14118
+ * The gate lives in the addon's PROCESS; the document lives in hub-main. Some
14119
+ * seam has to carry the value from the authority to the mirror, and a channel
14120
+ * that cannot be reached is precisely the dead knob this whole slice exists to
14121
+ * make impossible (D62 — `audioThresholdDbfs`, the HA entities with no source).
14122
+ * `apply` is that seam and nothing more: it writes an in-memory mirror, it
14123
+ * persists nothing, it is never the source of a value, and it is called only
14124
+ * with a set the hub actually read (D49 — a read that fails does not call it
14125
+ * at all, so no channel is silently disarmed by a bad read).
14126
+ */
14127
+ /** What `apply` reports back — enough to log, not enough to be a second state. */
14128
+ var LogChannelApplyResultSchema = object({
14129
+ /** How many declared channels are armed in this process after the call. */
14130
+ armed: number().int().min(0),
14131
+ /**
14132
+ * Names the document armed that this process does not declare. Reported
14133
+ * rather than swallowed: a name here is either a typo or an addon that has
14134
+ * not booted, and both deserve a line instead of silence.
14135
+ */
14136
+ unknown: array(string()).readonly()
14137
+ });
14138
+ method(_void(), array(LogChannelDescriptorSchema).readonly()), method(object({ windows: array(LogChannelWindowSchema).readonly() }), LogChannelApplyResultSchema, { kind: "mutation" });
13960
14139
  var LogLevelSchema = _enum([
13961
14140
  "debug",
13962
14141
  "info",
@@ -29247,17 +29426,60 @@ var SetSiteLocationInputSchema = object({
29247
29426
  longitude: number().min(-180).max(180)
29248
29427
  }).nullable();
29249
29428
  /**
29250
- * One `(procedure, user-agent, ip, principal)` tuple of the HTTP request
29429
+ * The TRANSPORT a call arrived on.
29430
+ *
29431
+ * Every counted call carries exactly one of these, and `unknown` is a PLANE
29432
+ * rather than a gap: a plane that cannot attribute a call declares it here, so
29433
+ * the call lands in a named bucket instead of vanishing. `planes` summing to
29434
+ * `procedureCalls` is what makes "the sum of the planes explains the total"
29435
+ * checkable rather than asserted.
29436
+ *
29437
+ * - `http` — the Fastify tRPC plugin (`/trpc/*`), one context per request.
29438
+ * - `ws` — `applyWSSHandler`, counted per OPERATION rather than per
29439
+ * connection; the viewer talks to the hub over `wsLink`
29440
+ * exclusively, so this is the plane the HTTP census could not see.
29441
+ * - `mesh` — the in-process `$core-caps` bridge (`createCaller`), which
29442
+ * never touches a socket and therefore never touched a census.
29443
+ * - `unknown` — counted, plane undecidable. No hook produces it today, and
29444
+ * that is exactly what its `0` asserts: every plane the hub has can name
29445
+ * itself. It is an output bucket, never a knob — a call that arrives on a
29446
+ * plane nobody instrumented lands here instead of vanishing from the total.
29447
+ */
29448
+ var TransportPlaneSchema = _enum([
29449
+ "http",
29450
+ "ws",
29451
+ "mesh",
29452
+ "unknown"
29453
+ ]);
29454
+ /**
29455
+ * Calls per plane. Every key is always present, `0` included — an absent plane
29456
+ * reads as "not instrumented", which is the one thing this census must never
29457
+ * make an operator wonder about.
29458
+ */
29459
+ var TransportPlaneCountsSchema = object({
29460
+ http: number(),
29461
+ ws: number(),
29462
+ mesh: number(),
29463
+ unknown: number()
29464
+ });
29465
+ /**
29466
+ * One `(plane, procedure, user-agent, ip, principal)` tuple of the transport
29251
29467
  * census. `principal` is the DERIVED identity (`apocaliss92 (admin)`,
29252
29468
  * `scoped:1a2b3c4d (scoped-token)`, `anonymous`) that the tRPC error log
29253
29469
  * already prints - never a token, never an `Authorization` header.
29470
+ *
29471
+ * `subscriptions` is counted APART from `calls`: a subscription is opened once
29472
+ * and lives for hours, so folding it into a call count makes one long-lived
29473
+ * stream look like a storm.
29254
29474
  */
29255
29475
  var RequestCensusGroupSchema = object({
29476
+ plane: TransportPlaneSchema,
29256
29477
  procedure: string(),
29257
29478
  userAgent: string(),
29258
29479
  ip: string(),
29259
29480
  principal: string(),
29260
29481
  calls: number(),
29482
+ subscriptions: number(),
29261
29483
  perMin: number()
29262
29484
  });
29263
29485
  /**
@@ -29270,6 +29492,14 @@ var RequestCensusGroupSchema = object({
29270
29492
  var RequestCensusProcedureSchema = object({
29271
29493
  procedure: string(),
29272
29494
  calls: number(),
29495
+ /**
29496
+ * The same total, split by transport. THIS is the row that answers the
29497
+ * question the census exists for: one look at `deviceManager.listAll` says
29498
+ * which plane carried the 4 960, without joining two log lines by eye.
29499
+ */
29500
+ planes: TransportPlaneCountsSchema,
29501
+ /** Subscription STARTS on this procedure. Never folded into `calls`. */
29502
+ subscriptions: number(),
29273
29503
  perMin: number()
29274
29504
  });
29275
29505
  /**
@@ -29297,14 +29527,45 @@ var RequestCensusStatusSchema = object({
29297
29527
  */
29298
29528
  procedureCalls: number(),
29299
29529
  /**
29530
+ * `procedureCalls` split by transport. The four keys sum to
29531
+ * `procedureCalls` by construction - {@link RequestCensusSnapshotSchema}'s
29532
+ * `planesExplainTotal` is that identity, checked rather than assumed.
29533
+ */
29534
+ planes: TransportPlaneCountsSchema,
29535
+ /**
29536
+ * True iff `planes` sums to `procedureCalls`. False means a call was counted
29537
+ * on no plane at all - which is a RESULT (a plane is missing from the
29538
+ * instrument), not a failure, and it has to be visible to be read as one.
29539
+ */
29540
+ planesExplainTotal: boolean(),
29541
+ /**
29300
29542
  * tRPC WebSocket connections opened during the window. NOT calls - the WS
29301
- * transport resolves one context per connection - but the number that says
29302
- * whether a plane this census cannot see was busy while HTTP was quiet.
29543
+ * adapter resolves one context per connection - kept because a plane's call
29544
+ * count of zero against 37 open connections says something different from a
29545
+ * plane with no connections at all.
29303
29546
  */
29304
29547
  wsConnections: number(),
29548
+ /**
29549
+ * Client frames the WS plane looked at. `wsMessages` far above
29550
+ * `planes.ws + subscriptions` means most traffic is not operations
29551
+ * (keepalives, connection params) - which is itself an answer.
29552
+ */
29553
+ wsMessages: number(),
29554
+ /**
29555
+ * Subscription STARTS across every plane, excluded from `procedureCalls` on
29556
+ * purpose: one live-events stream opened at boot and held for six hours is
29557
+ * one subscription, and counting it as a call would let a quiet plane
29558
+ * masquerade as the storm.
29559
+ */
29560
+ subscriptions: number(),
29561
+ /** `subscription.stop` frames. Starts minus stops is what is still open. */
29562
+ subscriptionStops: number(),
29305
29563
  distinctGroups: number(),
29306
- /** Calls counted in the totals whose group attribution was shed at the
29307
- * cardinality bound. */
29564
+ /**
29565
+ * Operations counted in the totals whose CALLER attribution was shed at the
29566
+ * cardinality bound. Unrelated to the `unknown` PLANE: these calls know
29567
+ * which transport they arrived on, they just lost their group row.
29568
+ */
29308
29569
  unattributedCalls: number(),
29309
29570
  procedures: array(RequestCensusProcedureSchema).readonly(),
29310
29571
  groups: array(RequestCensusGroupSchema).readonly()
@@ -29327,10 +29588,11 @@ var DiagnosticIdSchema = _enum(["request-census"]);
29327
29588
  * The layers of the level hierarchy, general → specific. The most specific
29328
29589
  * layer that carries an explicit value wins.
29329
29590
  *
29330
- * `component` is DECLARED and not yet resolvable: the per-component channels
29331
- * are a later slice of the same plan, and a `levelSource` enum that has to
29332
- * grow later would force every consumer of this document to change with it.
29333
- * Nothing returns `component` today.
29591
+ * `component` became RESOLVABLE on 2026-08-27: a component is a declared log
29592
+ * CHANNEL (`stream-broker.webrtc`, `provider-reolink.baichuan`), named by
29593
+ * `scopeComponent`. It was declared-but-dark in the first slice precisely so
29594
+ * that turning it on would not force every consumer of this document to widen
29595
+ * a `levelSource` enum — which is what has now not happened.
29334
29596
  */
29335
29597
  var LoggingScopeKindSchema = _enum([
29336
29598
  "cluster",
@@ -29357,6 +29619,14 @@ var LoggingLevelLayerSchema = object({
29357
29619
  scope: LoggingScopeKindSchema,
29358
29620
  /** The node this layer speaks for; `null` on the cluster layer. */
29359
29621
  nodeId: string().nullable(),
29622
+ /**
29623
+ * The declared channel this layer speaks for; `null` on every layer but
29624
+ * `component`. Never folded into `nodeId`: a component level is CLUSTER-WIDE
29625
+ * by design — the convention this repo settled on is one orchestrator-wide
29626
+ * setting, never per node (D52) — so a component layer that carried a node
29627
+ * would invite a per-node copy of a value that has no per-node meaning.
29628
+ */
29629
+ component: string().nullable(),
29360
29630
  /** Explicitly set here, or `null` when this layer inherits. */
29361
29631
  level: LogLevelSchema$1.nullable()
29362
29632
  });
@@ -29398,6 +29668,49 @@ var DiagnosticWindowPatchSchema = object({
29398
29668
  reportEveryMs: number().int().positive().optional()
29399
29669
  });
29400
29670
  /**
29671
+ * A channel ARMED, as the document reports it.
29672
+ *
29673
+ * `armMs` is not echoed back: what an operator needs to see is the deadline
29674
+ * and the time left, because a diagnostic left running is itself an incident
29675
+ * and "armed for 10 minutes" said an hour ago is not an answer.
29676
+ */
29677
+ var LogChannelWindowStateSchema = object({
29678
+ channel: string(),
29679
+ armed: boolean(),
29680
+ /** Epoch ms the window closes at. 0 when disarmed. */
29681
+ armedUntilMs: number(),
29682
+ /** Ms left before it expires on its own. 0 when disarmed. */
29683
+ remainingMs: number(),
29684
+ /**
29685
+ * The cameras it is narrowed to, or `null` for every camera.
29686
+ *
29687
+ * A channel declared `perDevice: false` can only ever report `null` here:
29688
+ * its lines do not carry `tags: { deviceId }`, so narrowing them would
29689
+ * produce a filter that silently matches nothing. The server REFUSES such a
29690
+ * patch rather than quietly widening it — ignoring the request would teach
29691
+ * the operator that per-camera filtering works on that channel when it does
29692
+ * not.
29693
+ */
29694
+ deviceIds: array(number().int()).readonly().nullable()
29695
+ });
29696
+ /**
29697
+ * `armMs: 0` DISARMS. Same grammar as {@link DiagnosticWindowPatchSchema}, and
29698
+ * for the same reason: a channel is a window with a deadline, never a switch.
29699
+ */
29700
+ var LogChannelWindowPatchSchema = object({
29701
+ channel: string().min(1),
29702
+ armMs: number().int().min(0),
29703
+ /**
29704
+ * Narrow to these numeric device ids. Absent or `null` = every camera.
29705
+ *
29706
+ * Numeric because the repo's own rule makes it possible: every log line
29707
+ * about a device carries `tags: { deviceId }` with the numeric id. That rule
29708
+ * was paid for with a 22% thumbnail gap and a 3-hour media blackout both
29709
+ * diagnosed by hand, and this is the first thing that collects on it.
29710
+ */
29711
+ deviceIds: array(number().int()).readonly().nullable().optional()
29712
+ });
29713
+ /**
29401
29714
  * A PATCH, and patches MERGE.
29402
29715
  *
29403
29716
  * A field absent from the patch is left exactly as it was — arming a
@@ -29416,7 +29729,14 @@ var LoggingSettingsPatchSchema = object({
29416
29729
  * Only the diagnostics NAMED here change. An armed window that is not listed
29417
29730
  * keeps running — a patch is never a full replacement.
29418
29731
  */
29419
- diagnostics: array(DiagnosticWindowPatchSchema).readonly().optional()
29732
+ diagnostics: array(DiagnosticWindowPatchSchema).readonly().optional(),
29733
+ /**
29734
+ * Only the channels NAMED here change. An armed channel that is not listed
29735
+ * keeps running — same rule as `diagnostics`, because a patch that silently
29736
+ * disarmed the channels it did not mention would make the Levels page and
29737
+ * the Diagnostics page fight over the same value.
29738
+ */
29739
+ channels: array(LogChannelWindowPatchSchema).readonly().optional()
29420
29740
  });
29421
29741
  /**
29422
29742
  * Which LAYER of the hierarchy is addressed. Absent = the cluster layer.
@@ -29429,9 +29749,22 @@ var LoggingSettingsPatchSchema = object({
29429
29749
  * authority over the whole hierarchy and answers for every layer, so the
29430
29750
  * layer selector needs a name the transport does not already own.
29431
29751
  */
29432
- var GetLoggingSettingsInputSchema = object({ scopeNodeId: string().optional() });
29752
+ var GetLoggingSettingsInputSchema = object({
29753
+ scopeNodeId: string().optional(),
29754
+ /**
29755
+ * The declared CHANNEL this document is addressed at, when the caller wants
29756
+ * the `component` layer. Absent = the node/cluster hierarchy only.
29757
+ *
29758
+ * Naming it separately rather than overloading `scopeNodeId` keeps the two
29759
+ * axes from collapsing: a component level is cluster-wide, a node level is
29760
+ * not, and one selector for both would make "which of these two did I just
29761
+ * set" unanswerable — the exact ambiguity `explicit` exists to remove.
29762
+ */
29763
+ scopeComponent: string().optional()
29764
+ });
29433
29765
  var SetLoggingSettingsInputSchema = object({
29434
29766
  scopeNodeId: string().optional(),
29767
+ scopeComponent: string().optional(),
29435
29768
  patch: LoggingSettingsPatchSchema
29436
29769
  });
29437
29770
  /**
@@ -29446,9 +29779,20 @@ var SetLoggingSettingsInputSchema = object({
29446
29779
  var LoggingSettingsStateSchema = object({
29447
29780
  /** The layer this document was read at. `null` = the cluster layer. */
29448
29781
  scopeNodeId: string().nullable(),
29782
+ /** The channel this document was read at. `null` = no component layer. */
29783
+ scopeComponent: string().nullable(),
29449
29784
  effective: LoggingEffectiveSchema,
29450
29785
  explicit: LoggingExplicitSchema,
29451
29786
  activeWindows: array(DiagnosticWindowSchema).readonly(),
29787
+ /**
29788
+ * Every channel the cluster's addons DECLARE, gathered from the
29789
+ * `log-channels` providers. Not stored anywhere: assembled per read, so a
29790
+ * channel added by a redeployed addon appears without anybody editing a
29791
+ * list, and a channel whose addon is gone stops being offered.
29792
+ */
29793
+ channels: array(LogChannelDescriptorSchema).readonly(),
29794
+ /** The channels ARMED right now, each with its deadline. */
29795
+ activeChannels: array(LogChannelWindowStateSchema).readonly(),
29452
29796
  persisted: boolean()
29453
29797
  });
29454
29798
  method(_void(), FeatureManifestSchema), method(_void(), HealthStatusSchema), method(_void(), FeatureManifestSchema), method(_void(), array(NetworkAddressSchema).readonly()), method(_void(), unknown().nullable(), { auth: "admin" }), method(record(string(), unknown()), _null(), {
@@ -32433,6 +32777,12 @@ Object.freeze({
32433
32777
  addonId: null,
32434
32778
  access: "view"
32435
32779
  },
32780
+ "dataStoreProvider.aggregate": {
32781
+ capName: "data-store-provider",
32782
+ capScope: "system",
32783
+ addonId: null,
32784
+ access: "view"
32785
+ },
32436
32786
  "dataStoreProvider.count": {
32437
32787
  capName: "data-store-provider",
32438
32788
  capScope: "system",
@@ -32847,6 +33197,12 @@ Object.freeze({
32847
33197
  addonId: null,
32848
33198
  access: "view"
32849
33199
  },
33200
+ "deviceManager.getChildrenBatch": {
33201
+ capName: "device-manager",
33202
+ capScope: "system",
33203
+ addonId: null,
33204
+ access: "view"
33205
+ },
32850
33206
  "deviceManager.getConfigSchema": {
32851
33207
  capName: "device-manager",
32852
33208
  capScope: "system",
@@ -33897,6 +34253,18 @@ Object.freeze({
33897
34253
  addonId: null,
33898
34254
  access: "create"
33899
34255
  },
34256
+ "logChannels.apply": {
34257
+ capName: "log-channels",
34258
+ capScope: "system",
34259
+ addonId: null,
34260
+ access: "create"
34261
+ },
34262
+ "logChannels.list": {
34263
+ capName: "log-channels",
34264
+ capScope: "system",
34265
+ addonId: null,
34266
+ access: "view"
34267
+ },
33900
34268
  "logDestination.query": {
33901
34269
  capName: "log-destination",
33902
34270
  capScope: "system",
@@ -36051,6 +36419,12 @@ Object.freeze({
36051
36419
  addonId: null,
36052
36420
  access: "create"
36053
36421
  },
36422
+ "settingsStore.aggregate": {
36423
+ capName: "settings-store",
36424
+ capScope: "system",
36425
+ addonId: null,
36426
+ access: "view"
36427
+ },
36054
36428
  "settingsStore.count": {
36055
36429
  capName: "settings-store",
36056
36430
  capScope: "system",
@@ -37630,6 +38004,11 @@ Object.freeze({
37630
38004
  form: "single",
37631
38005
  optional: false
37632
38006
  }],
38007
+ "deviceManager.getChildrenBatch": [{
38008
+ name: "parentDeviceIds",
38009
+ form: "array",
38010
+ optional: false
38011
+ }],
37633
38012
  "deviceManager.getConfigSchema": [{
37634
38013
  name: "deviceId",
37635
38014
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-tuya",
3
- "version": "0.2.32",
3
+ "version": "0.2.34",
4
4
  "description": "Tuya / Smart Life device-provider addon for CamStack — account-onboarded (Tuya IoT cloud fetch of device localKeys) + LOCAL DP control via the @apocaliss92/nodetuya encrypted-LAN client, exposing switch / water-heater-family kettle entities",
5
5
  "keywords": [
6
6
  "camstack",