@camstack/addon-pipeline-orchestrator 1.2.230 → 1.2.232

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 (26) hide show
  1. package/assets/viewer/.viewer-current-files.json +10 -10
  2. package/assets/viewer/.viewer-version +1 -1
  3. package/assets/viewer/_expo/static/js/web/Clipboard-6206d4c0d423d26e04715ebf58c315df.js +8 -0
  4. package/assets/viewer/_expo/static/js/web/Haptics-49c17326a5ec4b2b140b1449cdbb7f34.js +3 -0
  5. package/assets/viewer/_expo/static/js/web/{index-f5bda3803ae44f8b74784fe8b002bf5e.js → index-ccd23c8462e4121de32d780726261a91.js} +741 -740
  6. package/assets/viewer/_expo/static/js/web/node-fs-5b88ad96e86d7063ccdbe32db825729f.js +1 -0
  7. package/assets/viewer/_expo/static/js/web/node-path-716b5d0b761c62e0b4296f79e29f89d2.js +1 -0
  8. package/assets/viewer/embed/assets/{MaskShapeCanvas-DI4BY7W2-C0fGaeA-.js → MaskShapeCanvas-DI4BY7W2-2CDlk-HT.js} +1 -1
  9. package/assets/viewer/embed/assets/{MotionZonesSettings-7c3yiT5E-9PMziv1F.js → MotionZonesSettings-7c3yiT5E-B6oGxKku.js} +1 -1
  10. package/assets/viewer/embed/assets/{PrivacyMaskSettings-PkYs077O-DXkWrrAk.js → PrivacyMaskSettings-PkYs077O-B-RISboY.js} +1 -1
  11. package/assets/viewer/embed/assets/{SceneMonitorEditor-BvDy3x3k-B9lXS5ns.js → SceneMonitorEditor-BvDy3x3k-DBWukNQx.js} +1 -1
  12. package/assets/viewer/embed/assets/index-H9aL2Im_.js +115 -0
  13. package/assets/viewer/embed/index.html +1 -1
  14. package/assets/viewer/index.html +1 -1
  15. package/dist/_stub.js +2 -2
  16. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-Cva58zcK.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-BGdkIv4D.mjs} +2 -2
  17. package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D7D-UPVq.mjs +26 -0
  18. package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CuMONLDJ.mjs +26 -0
  19. package/dist/{hostInit-BiEedqeH.mjs → hostInit-Cm5QAJnG.mjs} +2 -2
  20. package/dist/index.js +663 -16
  21. package/dist/index.mjs +663 -16
  22. package/dist/remoteEntry.js +1 -1
  23. package/package.json +1 -1
  24. package/assets/viewer/embed/assets/index-Btt6Cej-.js +0 -115
  25. package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cm0B7Kaq.mjs +0 -26
  26. package/dist/_virtual_mf___mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-lkkSZHH0.mjs +0 -26
package/dist/index.mjs CHANGED
@@ -5363,7 +5363,7 @@ var ZodIssueCode = {
5363
5363
  var ZodFirstPartyTypeKind;
5364
5364
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5365
5365
  //#endregion
5366
- //#region ../types/dist/sleep-CiR4z7r9.mjs
5366
+ //#region ../types/dist/sleep-CaB_hL5g.mjs
5367
5367
  /**
5368
5368
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5369
5369
  * window to float samples (D455).
@@ -7956,6 +7956,8 @@ var EVENT_OWNER_TYPES = [
7956
7956
  * nothing failing until a caller asked.
7957
7957
  */
7958
7958
  var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
7959
+ /** The same list as a Zod enum, for the cap input that carries it. */
7960
+ var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
7959
7961
  new Set(EVENT_OWNER_TYPES);
7960
7962
  var EncodeProfileSchema = object({
7961
7963
  video: object({
@@ -22133,7 +22135,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
22133
22135
  ownerType: EventOwnerTypeSchema,
22134
22136
  eventId: number().int(),
22135
22137
  deviceId: number()
22136
- }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
22138
+ }), array(MediaFileInfoSchema).readonly()), method(object({
22139
+ deviceId: number(),
22140
+ ownerKind: MediaPresenceOwnerKindSchema,
22141
+ ownerIds: array(string()).max(5e3)
22142
+ }), array(string()).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
22137
22143
  kind: "mutation",
22138
22144
  auth: "admin"
22139
22145
  }), method(RebuildObjectEmbeddingsInput, RebuildObjectEmbeddingsResultSchema, {
@@ -24331,10 +24337,9 @@ var ServerBootModeSchema = _enum([
24331
24337
  * - `idle` / `checking` / `staging` — steady / in-flight registry work.
24332
24338
  * - `pending-restart` — a version is staged and the node has NOT yet
24333
24339
  * restarted onto it (still running the OLD version).
24334
- * - `awaiting-confirmation` — the node HAS restarted onto the staged version
24335
- * (it is the active probation boot) and is waiting to confirm boot-health.
24336
- * Apply/rollback are refused in this state and the node must NOT be
24337
- * manually restarted, or the probation boot auto-rolls-back.
24340
+ * - `awaiting-confirmation` — RETIRED with the probation boot (single-copy
24341
+ * collapse, 2026-07-18). Kept in the enum so an older client still parses;
24342
+ * no provider emits it.
24338
24343
  */
24339
24344
  var ServerUpdateStateSchema = _enum([
24340
24345
  "idle",
@@ -24423,12 +24428,35 @@ var ServerRollbackInfoSchema = object({
24423
24428
  var ServerPackageStatusSchema = object({
24424
24429
  /** Root package name (`@camstack/server` on the hub). */
24425
24430
  packageName: string(),
24426
- /** Version of the code the running process ACTUALLY loaded. */
24431
+ /**
24432
+ * Version of the code THIS PROCESS loaded — captured once, at boot, from the
24433
+ * package.json of the copy the code was loaded from, and never re-read. A
24434
+ * copy replaced on disk under a live process must not move this number;
24435
+ * that divergence is what `installedVersion` is for. Before 2026-09-20 this
24436
+ * was a lazy disk read that would have followed such a swap silently.
24437
+ */
24427
24438
  runningVersion: string().nullable(),
24428
24439
  /** Node.js runtime version the node's process runs on (`process.versions.node`). */
24429
24440
  nodeRuntimeVersion: string().nullable(),
24430
24441
  /** Active data-dir root version; null when booted from seed/workspace. */
24431
24442
  activeVersion: string().nullable(),
24443
+ /**
24444
+ * Version of the root package in the data-root `current/` copy RIGHT NOW —
24445
+ * what the next boot would load if nothing is staged. Normally equal to
24446
+ * `runningVersion`; differs only when the copy on disk changed under the
24447
+ * live process. `null` when there is no data-root copy (baked seed or
24448
+ * workspace boot). Optional for version skew: an older provider omits it.
24449
+ */
24450
+ installedVersion: string().nullable().optional(),
24451
+ /**
24452
+ * True when the code that would run after a restart is NOT the code running
24453
+ * now: a version is staged (`pendingVersion`), or `installedVersion` differs
24454
+ * from `runningVersion`. Derived on every read, never stored. This is the
24455
+ * one field that lets an operator or a script say "1.2.353 is installed and
24456
+ * will run after a restart" instead of reading it off a number that means
24457
+ * "installed". Optional for version skew.
24458
+ */
24459
+ restartRequired: boolean().optional(),
24432
24460
  /** N-1 version kept for rollback; null when no previous version exists. */
24433
24461
  previousVersion: string().nullable(),
24434
24462
  /** Version of the immutable baked seed closure (image fallback). */
@@ -24463,19 +24491,71 @@ var ServerPackageStatusSchema = object({
24463
24491
  });
24464
24492
  var ServerUpdateCheckResultSchema = object({
24465
24493
  packageName: string(),
24494
+ /** Same meaning as on the status payload: what THIS process loaded. */
24466
24495
  runningVersion: string().nullable(),
24467
24496
  latestVersion: string().nullable(),
24497
+ /**
24498
+ * `latestVersion` is newer than `runningVersion` AND is not the version
24499
+ * already staged on this node. A staged-and-waiting version is not "an
24500
+ * update available" — applying it again is refused (`already-staged`), and
24501
+ * a button that announced "updating…" on this boolean then got refused
24502
+ * would be lying. It is reported through `pendingVersion` instead.
24503
+ */
24468
24504
  updateAvailable: boolean(),
24469
24505
  checkedAtMs: number(),
24470
24506
  /** Non-null when the registry lookup failed (offline, bad registry, …). */
24471
- error: string().nullable()
24507
+ error: string().nullable(),
24508
+ /**
24509
+ * Version staged on this node and awaiting its restart, when one is. Before
24510
+ * 2026-09-20 a check answered `{runningVersion, updateAvailable:false}` for a
24511
+ * node whose staged closure had never booted, and the caller had no way to
24512
+ * see the difference. Optional for version skew.
24513
+ */
24514
+ pendingVersion: string().nullable().optional(),
24515
+ /** See the status payload: staged, or installed-on-disk ≠ running. Optional for skew. */
24516
+ restartRequired: boolean().optional()
24472
24517
  });
24518
+ /**
24519
+ * What an action DID — the discriminant `accepted` cannot carry. Before
24520
+ * 2026-09-20 `applyServerUpdate` answered `accepted:false` both for a refusal
24521
+ * where nothing was wrong (already current, already staged) and for a staging
24522
+ * install that died on `npm error code ECONNRESET`; the only difference was
24523
+ * prose in `message`, and an operator restarted a container on the strength of
24524
+ * it, booting the previous version.
24525
+ *
24526
+ * - `staged` — the closure is installed and validated; the node
24527
+ * restarts to apply it (`restarting:true`). NOTHING
24528
+ * runs the new code until that restart completes.
24529
+ * - `restart-scheduled` — a plain, version-preserving restart was accepted.
24530
+ * - `already-current` — the target IS the running version; nothing done.
24531
+ * - `refused` — dropped by a gate; `reason` names which.
24532
+ * - `failed` — attempted and did not land; `error` says why.
24533
+ */
24534
+ var ServerUpdateOutcomeSchema = _enum([
24535
+ "staged",
24536
+ "restart-scheduled",
24537
+ "already-current",
24538
+ "refused",
24539
+ "failed"
24540
+ ]);
24473
24541
  var ServerUpdateActionResultSchema = object({
24474
24542
  accepted: boolean(),
24475
24543
  targetVersion: string().nullable(),
24476
24544
  /** True when a graceful restart was scheduled to apply the change. */
24477
24545
  restarting: boolean(),
24478
- message: string()
24546
+ message: string(),
24547
+ /** See {@link ServerUpdateOutcomeSchema}. Optional for version skew. */
24548
+ outcome: ServerUpdateOutcomeSchema.optional(),
24549
+ /**
24550
+ * Stable, greppable cause for `refused` / `failed` (the engine's
24551
+ * `UpdateRefusalReason`, `staging-failed`, …). `null` on success.
24552
+ */
24553
+ reason: string().nullable().optional(),
24554
+ /**
24555
+ * The failure itself, verbatim enough to act on (the npm output for a
24556
+ * staging install). Non-null only for `failed`.
24557
+ */
24558
+ error: string().nullable().optional()
24479
24559
  });
24480
24560
  method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
24481
24561
  kind: "mutation",
@@ -25584,6 +25664,18 @@ method(VectorDeclareIndexInputSchema, _void(), {
25584
25664
  kind: "mutation",
25585
25665
  auth: "admin"
25586
25666
  }), method(VectorStatsInputSchema, VectorStatsResultSchema, { auth: "admin" });
25667
+ _enum([
25668
+ "queue-full",
25669
+ "camera-backoff",
25670
+ "sleeping",
25671
+ "camera-refused",
25672
+ "no-keyframe",
25673
+ "no-catalog-row",
25674
+ "unsupported",
25675
+ "unknown-device",
25676
+ "uid-missing"
25677
+ ]);
25678
+ _enum(["deferred", "final"]);
25587
25679
  var ClipSchema = object({
25588
25680
  /** Opaque, provider-namespaced id. The default provider encodes the time
25589
25681
  * window so `getClipPlayback` is self-contained (no event re-query). */
@@ -25658,29 +25750,243 @@ var ClipSchema = object({
25658
25750
  holes: array(object({
25659
25751
  startMs: number(),
25660
25752
  endMs: number()
25661
- })).optional()
25753
+ })).optional(),
25754
+ /**
25755
+ * A URL that MAY produce this clip's still, asked only for rows actually on
25756
+ * screen. The opposite of {@link ClipSchema.thumbnail}: that one is VOUCHED
25757
+ * (a JPEG is already on disk), this one is an offer. The route answers 200
25758
+ * with the image, or 204 with `x-camstack-reason` when it could not mint one
25759
+ * — a surface latches that refusal to the instant rather than retrying.
25760
+ *
25761
+ * Never both: a clip with a vouched `thumbnail` needs no mint.
25762
+ */
25763
+ thumbnailMint: string().optional(),
25764
+ /**
25765
+ * Why no still will be produced for this clip RIGHT NOW — set when the
25766
+ * provider already knows, so the surface draws the glyph and the reason
25767
+ * instead of firing a mint that cannot succeed.
25768
+ *
25769
+ * `sleeping` — a standalone battery camera; a read would be a wake (D549 4).
25770
+ * `camera-refused` — the camera answered the CoverPreview with a refusal.
25771
+ * `no-keyframe` — the window holds no decodable I-frame (an event that sits
25772
+ * inside no file is the measured case).
25773
+ * `unsupported` — this source cannot mint stills at all.
25774
+ */
25775
+ thumbnailUnavailable: object({ reason: _enum([
25776
+ "sleeping",
25777
+ "camera-refused",
25778
+ "no-keyframe",
25779
+ "unsupported"
25780
+ ]) }).optional(),
25781
+ /**
25782
+ * When the catalog this row came from was last CONFIRMED against the device.
25783
+ * Absent means "this row was read live". A persisted catalog served while a
25784
+ * camera sleeps carries the age it really has — a cached list is never drawn
25785
+ * as current (D549 13).
25786
+ */
25787
+ catalogAsOf: number().optional(),
25788
+ /**
25789
+ * The camera's OWN type strings for this clip, all of them, unmapped
25790
+ * (`md`, `people`, `dog_cat`, `sched`, …). Kept beside {@link labels}
25791
+ * because a firmware inventing a type must not vanish: the mapping into our
25792
+ * filter vocabulary is lossy on purpose and this is the lossless copy
25793
+ * (D549 20).
25794
+ */
25795
+ nativeTypes: array(string()).optional(),
25796
+ /**
25797
+ * What the subject DID — `crossline`, `intrude`, `loitering`. A behaviour
25798
+ * travels BESIDE a class, never instead of one, and the class filter ignores
25799
+ * it: "a person crossed a line" is still a person (D549 20).
25800
+ */
25801
+ behaviours: array(string()).optional(),
25802
+ /**
25803
+ * The same recording as two files — the sub twin (what the row and its
25804
+ * thumbnail are) and its main twin, matched at listing time so a quality
25805
+ * change never re-searches the camera (D549 15). `getClipPlayback`'s
25806
+ * `profile` picks between them: `low | mid` → sub, `high` → main.
25807
+ */
25808
+ streams: object({
25809
+ sub: object({
25810
+ id: string(),
25811
+ bytes: number().optional()
25812
+ }).optional(),
25813
+ main: object({
25814
+ id: string(),
25815
+ bytes: number().optional()
25816
+ }).optional()
25817
+ }).optional(),
25818
+ /** The camera says it holds a sub-stream copy of this recording. */
25819
+ supportSub: boolean().optional(),
25820
+ /**
25821
+ * Whether this row has bytes behind it. ABSENT means yes — every clip that
25822
+ * IS a file is playable, and only a source that lists EVENTS can produce a
25823
+ * row with nothing to play (the measured case: 1 of 29 hub events on 3628
25824
+ * fell inside no file). Such a row is shown, never dropped and never offered
25825
+ * as playable-then-failing (D549 19).
25826
+ */
25827
+ playable: boolean().optional(),
25828
+ /** Why {@link playable} is false, verbatim (`no-file-for-window`). */
25829
+ unplayableReason: string().optional(),
25830
+ /**
25831
+ * This clip is STILL BEING WRITTEN, so {@link ClipSchema.timeRange}`.endMs`
25832
+ * is not its end.
25833
+ *
25834
+ * Absent — the common case — means the row is closed and its `endMs` is the
25835
+ * end of the recording. Present and `true` means the source told us the file
25836
+ * has no end yet, and the `endMs` we carry is whatever the camera's index
25837
+ * entry happened to hold: on a Reolink E1 Outdoor PoE (592, measured
25838
+ * 2026-09-20) the newest file `…_192927_000000_…_0.mp4` reported an `endTime`
25839
+ * of 19:29:56 and STILL reported it eight minutes later, while the file went
25840
+ * on growing. A surface that drew 29 s there was lying about a clip that
25841
+ * plays for minutes, on the one row the operator looks at first.
25842
+ *
25843
+ * `endMs` is deliberately still a number: it is the best bound anything has
25844
+ * for a mint window or a byte fetch, and every consumer already requires it.
25845
+ * This flag says what it is WORTH, and a duration is not drawn from it.
25846
+ */
25847
+ inProgress: boolean().optional()
25662
25848
  });
25663
25849
  var ClipPlaybackSchema = object({
25664
- /** HLS master URL through the hub data-plane (Range + token in path). */
25850
+ /**
25851
+ * A media URL through the hub data-plane. {@link ClipPlaybackSchema.format}
25852
+ * says what kind — an HLS master playlist for a recording-derived clip, a
25853
+ * progressive MP4 for a native file the vendor addon muxed and serves with
25854
+ * `Range`. The URL is NOT a credential: both planes are registered
25855
+ * `access: 'authenticated'` and the hub's `/addon/<id>/<prefix>` proxy takes
25856
+ * the session cookie, so nothing is appended to it (D549 22).
25857
+ */
25665
25858
  playbackUrl: string(),
25859
+ /**
25860
+ * How to play {@link playbackUrl}. Absent means `hls` — the shape every
25861
+ * existing consumer already assumes. A player that branches on this is the
25862
+ * one change a native clip needs; a player that ignores it will hand an MP4
25863
+ * to hls.js and fail parsing it as a manifest.
25864
+ */
25865
+ format: _enum(["hls", "mp4"]).optional(),
25866
+ /**
25867
+ * Which twin was actually served. A clip that holds only one stream answers
25868
+ * with the one it has, and the surface SAYS so — a missing main twin is
25869
+ * never served silently as if it were the asked-for quality (D549 15).
25870
+ */
25871
+ served: CamProfileSchema.optional(),
25666
25872
  /** Optional LAN/remote alternates for the same clip. */
25667
25873
  playbackEndpoints: array(string()).optional(),
25668
25874
  token: string().optional()
25669
25875
  });
25876
+ /**
25877
+ * Why a source cannot answer right now — per SOURCE, never fleet-wide.
25878
+ *
25879
+ * `ok` is the only state whose clip list may be read as complete. The other
25880
+ * three exist because an empty list from a sleeping camera reads as "this
25881
+ * camera has no recordings", which is the defect this whole line of work is
25882
+ * about: a battery camera nobody woke (`sleeping`), a camera that could not be
25883
+ * reached at all (`unreachable`, also what a provider that THREW reports), a
25884
+ * camera whose SD card is not mounted (`no-storage`, measured on 640 —
25885
+ * `HddInfo mount 0`, honestly nothing to list rather than "no clips"), and a
25886
+ * camera whose OWN index disagrees with its OWN calendar (`index-empty`,
25887
+ * measured on 618: the calendar marks 3–20 September, days 6–20 list zero
25888
+ * files on both streams and both filters, with 177 GB free). That last one is
25889
+ * a defect ON THE CAMERA, and the only honest thing a provider can do is say
25890
+ * which days it asked for and got nothing — "no clips" would be a lie about a
25891
+ * card that is full of them.
25892
+ */
25893
+ var ClipSourceAvailabilitySchema = object({
25894
+ state: _enum([
25895
+ "ok",
25896
+ "sleeping",
25897
+ "unreachable",
25898
+ "no-storage",
25899
+ "index-empty"
25900
+ ]),
25901
+ /** Free text, shown verbatim. Names the camera's own refusal when there is one. */
25902
+ reason: string().optional(),
25903
+ /** When this source's catalog was last CONFIRMED. A cached list is never
25904
+ * drawn as current: the surface shows the age whenever it is older than the
25905
+ * refresh interval. */
25906
+ catalogAsOf: number().optional()
25907
+ });
25908
+ /**
25909
+ * One SOURCE of clips for a camera — a row of the picker, and the namespace
25910
+ * every clip id from it is prefixed with (`analytics`,
25911
+ * `native:reolink:onboard`, `hksv`, …).
25912
+ *
25913
+ * A provider lists the sources IT serves for that device, and answers for each
25914
+ * of them whether it can answer at all. A provider with nothing to offer on a
25915
+ * camera returns `[]` — it is not that camera's business.
25916
+ */
25917
+ var ClipSourceSchema = object({
25918
+ /** The value this source stamps on {@link ClipSchema.source}, and the prefix
25919
+ * of every clip id it mints. `getClipPlayback` routes on it. */
25920
+ source: string(),
25921
+ /** Operator-facing name of the source ("CamStack recordings", "SD card"). */
25922
+ label: string(),
25923
+ /**
25924
+ * The addon that SERVES this row.
25925
+ *
25926
+ * A surface cannot otherwise resolve a source to who answers it, and the
25927
+ * alternative — a `native:reolink:* → provider-reolink` table inside the
25928
+ * widget — is a second authority on provider identity living in the one
25929
+ * package with no business knowing it, wrong the day a third source appears
25930
+ * (D557). One addon may serve SEVERAL sources, which is why the per-device
25931
+ * switch is keyed by SOURCE and not by this (D555) — `addon-provider-reolink`
25932
+ * served a hub child two of them until the two views were measured to be one
25933
+ * store read twice (D555) and then read ONE way (D565).
25934
+ *
25935
+ * Optional for version skew only. The collection dispatcher stamps it from
25936
+ * the registry, so a row that travelled through the fan-out carries the
25937
+ * authoritative id whatever the provider filled in.
25938
+ */
25939
+ addonId: string().optional(),
25940
+ availability: ClipSourceAvailabilitySchema
25941
+ });
25670
25942
  DeviceType.Camera, method(object({
25671
25943
  deviceId: number(),
25672
25944
  since: number(),
25673
25945
  until: number(),
25674
- limit: number().int().positive().optional()
25946
+ limit: number().int().positive().optional(),
25947
+ /**
25948
+ * WHICH provider to ask — the `addonId` a {@link ClipSourceSchema} row
25949
+ * carries, never a source id and never a list.
25950
+ *
25951
+ * Absent means the device's BOUND provider, which is CamStack on every
25952
+ * camera today because that is what `deviceManager.getBindings`
25953
+ * answers with. A provider the device is not bound to is refused by
25954
+ * name rather than answered by another one (D552's
25955
+ * `rejectUnresolvedAddonPin` rule).
25956
+ *
25957
+ * It replaced `sources?: string[]`, a VIEW filter over source ids that
25958
+ * assumed the answer was a fan-out over everything a camera has. The
25959
+ * operator settled otherwise on 2026-09-20 — *"il get videoclips
25960
+ * prendere un provider opzionale, di default va su camstack"*,
25961
+ * *"l'utilizzatore è uno solo"* — so the list is asked of one provider
25962
+ * at a time and there is nothing to filter out of it.
25963
+ */
25964
+ provider: string().optional()
25675
25965
  }), array(ClipSchema).readonly(), {
25676
25966
  kind: "query",
25677
- auth: "admin"
25967
+ auth: "protected"
25968
+ }), method(object({ deviceId: number() }), array(ClipSourceSchema).readonly(), {
25969
+ kind: "query",
25970
+ auth: "protected"
25678
25971
  }), method(object({
25679
25972
  deviceId: number(),
25680
- clipId: string()
25973
+ clipId: string(),
25974
+ /**
25975
+ * Which twin to serve, on the ONE quality scale the system already has
25976
+ * (`CamProfileSchema`). `low | mid` → the sub file, `high` → the main
25977
+ * twin; both ids are already on the row so this never re-searches the
25978
+ * camera. Absent means the provider's own default (the sub file, which
25979
+ * every source is measured to hold).
25980
+ *
25981
+ * `auto` is deliberately NOT accepted here: a stored file has no
25982
+ * broker session, so the adaptive tier cannot be resolved for it. The
25983
+ * viewer resolves `auto` to a profile the same way live does, before
25984
+ * it calls (D549 15).
25985
+ */
25986
+ profile: CamProfileSchema.optional()
25681
25987
  }), ClipPlaybackSchema, {
25682
25988
  kind: "query",
25683
- auth: "admin"
25989
+ auth: "protected"
25684
25990
  });
25685
25991
  /**
25686
25992
  * Optional client-side hints sent at session creation to help the provider
@@ -37173,6 +37479,12 @@ Object.freeze({
37173
37479
  addonId: null,
37174
37480
  access: "view"
37175
37481
  },
37482
+ "pipelineAnalytics.ownersWithMedia": {
37483
+ capName: "pipeline-analytics",
37484
+ capScope: "device",
37485
+ addonId: null,
37486
+ access: "view"
37487
+ },
37176
37488
  "pipelineAnalytics.pauseForStorageMigration": {
37177
37489
  capName: "pipeline-analytics",
37178
37490
  capScope: "device",
@@ -39765,6 +40077,12 @@ Object.freeze({
39765
40077
  addonId: null,
39766
40078
  access: "view"
39767
40079
  },
40080
+ "videoclips.listSources": {
40081
+ capName: "videoclips",
40082
+ capScope: "device",
40083
+ addonId: null,
40084
+ access: "view"
40085
+ },
39768
40086
  "viewerUi.getStaticDir": {
39769
40087
  capName: "viewer-ui",
39770
40088
  capScope: "system",
@@ -41067,6 +41385,11 @@ Object.freeze({
41067
41385
  form: "single",
41068
41386
  optional: false
41069
41387
  }],
41388
+ "pipelineAnalytics.ownersWithMedia": [{
41389
+ name: "deviceId",
41390
+ form: "single",
41391
+ optional: false
41392
+ }],
41070
41393
  "pipelineAnalytics.proposeRetrainAnnotations": [{
41071
41394
  name: "deviceId",
41072
41395
  form: "single",
@@ -41773,6 +42096,11 @@ Object.freeze({
41773
42096
  form: "single",
41774
42097
  optional: false
41775
42098
  }],
42099
+ "videoclips.listSources": [{
42100
+ name: "deviceId",
42101
+ form: "single",
42102
+ optional: false
42103
+ }],
41776
42104
  "waterHeater.setAway": [{
41777
42105
  name: "deviceId",
41778
42106
  form: "single",
@@ -53007,6 +53335,289 @@ var RepeatWarnSampler = class {
53007
53335
  }
53008
53336
  };
53009
53337
  //#endregion
53338
+ //#region src/occupancy-recheck-controller.ts
53339
+ /**
53340
+ * How long a recheck registration lives once the burst was accepted. The
53341
+ * runner settles a burst at `OCCUPANCY_BURST_SAFETY_TIMEOUT_MS` (8 s) after
53342
+ * the dial; the dial itself is bounded by the decode-spawn gate. 15 s covers
53343
+ * both on this fleet and is short enough that a camera is never held for a
53344
+ * meaningful share of its 300 s cadence.
53345
+ */
53346
+ var RECHECK_BURST_WINDOW_MS = 15e3;
53347
+ /** The `reason` every orchestrator recheck carries into the runner's log. */
53348
+ var RECHECK_REASON = "orchestrator-recheck";
53349
+ /** A found burst resets the streak; two consecutive empty ones disarm. */
53350
+ function foldBurstOutcome(streak, found) {
53351
+ if (found) return {
53352
+ emptyStreak: 0,
53353
+ armed: true
53354
+ };
53355
+ const emptyStreak = streak.emptyStreak + 1;
53356
+ return {
53357
+ emptyStreak,
53358
+ armed: emptyStreak < 2
53359
+ };
53360
+ }
53361
+ /**
53362
+ * The registration a recheck rides on, derived from the SESSION config so the
53363
+ * stream, steps, zones and root detector are the camera's own.
53364
+ */
53365
+ function buildRecheckAttachConfig(session) {
53366
+ return {
53367
+ ...session,
53368
+ detectionMode: "on-motion",
53369
+ motionSources: [],
53370
+ audioMode: "disabled",
53371
+ occupancyRecheckEnabled: false,
53372
+ preRollInferenceEnabled: false
53373
+ };
53374
+ }
53375
+ var OccupancyRecheckController = class {
53376
+ deps;
53377
+ devices = /* @__PURE__ */ new Map();
53378
+ constructor(deps) {
53379
+ this.deps = deps;
53380
+ }
53381
+ /**
53382
+ * A session was dispatched: remember where and with what, cancel any pending
53383
+ * tick (the session sees the scene itself), and supersede a burst in flight
53384
+ * on the same node — the session attach replaced its registration.
53385
+ */
53386
+ noteSessionDispatched(deviceId, nodeId, config) {
53387
+ const state = this.stateFor(deviceId, nodeId, config);
53388
+ state.lastNodeId = nodeId;
53389
+ state.lastConfig = config;
53390
+ if (state.burst !== null && state.burst.nodeId === nodeId) {
53391
+ state.burst.superseded = true;
53392
+ this.deps.logger.debug("occupancy recheck: burst superseded by a session on the same node", {
53393
+ tags: {
53394
+ deviceId,
53395
+ nodeId
53396
+ },
53397
+ meta: {
53398
+ deviceId,
53399
+ nodeId
53400
+ }
53401
+ });
53402
+ }
53403
+ }
53404
+ /** A session closed: arm the cadence iff it saw a subject and the camera
53405
+ * has the recheck enabled. Re-evaluated from scratch on every close. */
53406
+ noteSessionClosed(deviceId, close) {
53407
+ const state = this.devices.get(deviceId);
53408
+ if (state === void 0) return;
53409
+ const cfg = state.lastConfig;
53410
+ const enabled = cfg.occupancyRecheckEnabled === true && (cfg.occupancyRecheckSec ?? 0) > 0;
53411
+ if (close.sawSubject) state.emptyStreak = 0;
53412
+ state.armed = state.armed || enabled && close.sawSubject;
53413
+ if (!state.armed) {
53414
+ this.deps.logger.debug("occupancy recheck: not armed", {
53415
+ tags: { deviceId },
53416
+ meta: {
53417
+ deviceId,
53418
+ enabled,
53419
+ sawSubject: close.sawSubject
53420
+ }
53421
+ });
53422
+ return;
53423
+ }
53424
+ if (state.timer === null) this.schedule(deviceId, state);
53425
+ }
53426
+ /** Every inference result the orchestrator sees; a burst result with
53427
+ * detections is what makes a burst "found". */
53428
+ noteInferenceResult(deviceId, occasion, detectionCount) {
53429
+ const state = this.devices.get(deviceId);
53430
+ if (state === void 0 || state.burst === null) return;
53431
+ if (occasion === "burst" && detectionCount > 0) state.burst.found = true;
53432
+ }
53433
+ /** The device left the orchestrator's knowledge. */
53434
+ forget(deviceId) {
53435
+ const state = this.devices.get(deviceId);
53436
+ if (state === void 0) return;
53437
+ this.cancelTimer(state);
53438
+ this.devices.delete(deviceId);
53439
+ }
53440
+ shutdown() {
53441
+ for (const state of this.devices.values()) this.cancelTimer(state);
53442
+ this.devices.clear();
53443
+ }
53444
+ stateFor(deviceId, nodeId, config) {
53445
+ let state = this.devices.get(deviceId);
53446
+ if (state === void 0) {
53447
+ state = {
53448
+ lastNodeId: nodeId,
53449
+ lastConfig: config,
53450
+ armed: false,
53451
+ emptyStreak: 0,
53452
+ timer: null,
53453
+ burst: null
53454
+ };
53455
+ this.devices.set(deviceId, state);
53456
+ }
53457
+ return state;
53458
+ }
53459
+ cancelTimer(state) {
53460
+ if (state.timer !== null) {
53461
+ clearTimeout(state.timer);
53462
+ state.timer = null;
53463
+ }
53464
+ }
53465
+ schedule(deviceId, state) {
53466
+ this.cancelTimer(state);
53467
+ const delayMs = (state.lastConfig.occupancyRecheckSec ?? 0) * 1e3;
53468
+ const timer = setTimeout(() => {
53469
+ state.timer = null;
53470
+ this.tick(deviceId).catch((err) => {
53471
+ this.deps.logger.warn("occupancy recheck: tick failed", {
53472
+ tags: { deviceId },
53473
+ meta: {
53474
+ deviceId,
53475
+ error: errMsg$1(err)
53476
+ }
53477
+ });
53478
+ });
53479
+ }, delayMs);
53480
+ timer.unref?.();
53481
+ state.timer = timer;
53482
+ }
53483
+ async tick(deviceId) {
53484
+ const state = this.devices.get(deviceId);
53485
+ if (state === void 0 || !state.armed) return;
53486
+ if (this.deps.isSessionActive(deviceId)) {
53487
+ this.deps.logger.debug("occupancy recheck: skipped — a session is active", {
53488
+ tags: { deviceId },
53489
+ meta: { deviceId }
53490
+ });
53491
+ this.schedule(deviceId, state);
53492
+ return;
53493
+ }
53494
+ if (state.burst !== null) {
53495
+ this.schedule(deviceId, state);
53496
+ return;
53497
+ }
53498
+ const nodeId = state.lastNodeId;
53499
+ const config = buildRecheckAttachConfig(state.lastConfig);
53500
+ const frames = Math.max(1, state.lastConfig.occupancyRecheckFrames ?? 4);
53501
+ const burst = {
53502
+ nodeId,
53503
+ superseded: false,
53504
+ found: false
53505
+ };
53506
+ state.burst = burst;
53507
+ try {
53508
+ await this.deps.attachOn(nodeId, config);
53509
+ } catch (err) {
53510
+ state.burst = null;
53511
+ this.deps.logger.warn("occupancy recheck: attach failed — retried next tick", {
53512
+ tags: {
53513
+ deviceId,
53514
+ nodeId
53515
+ },
53516
+ meta: {
53517
+ deviceId,
53518
+ nodeId,
53519
+ error: errMsg$1(err)
53520
+ }
53521
+ });
53522
+ this.schedule(deviceId, state);
53523
+ return;
53524
+ }
53525
+ const answer = await this.deps.requestBurstOn(nodeId, deviceId, frames, RECHECK_REASON).catch((err) => ({
53526
+ started: false,
53527
+ error: errMsg$1(err)
53528
+ }));
53529
+ if (!answer.started) {
53530
+ const refusedBecause = "refusedBecause" in answer ? answer.refusedBecause : void 0;
53531
+ const error = "error" in answer ? answer.error : void 0;
53532
+ await this.release(deviceId, state, burst);
53533
+ const asleep = refusedBecause === "battery-asleep";
53534
+ if (asleep) state.armed = false;
53535
+ this.deps.logger.info("occupancy recheck: burst refused", {
53536
+ tags: {
53537
+ deviceId,
53538
+ nodeId
53539
+ },
53540
+ meta: {
53541
+ deviceId,
53542
+ nodeId,
53543
+ refusedBecause: refusedBecause ?? null,
53544
+ error: error ?? null,
53545
+ rearmed: !asleep
53546
+ }
53547
+ });
53548
+ if (!asleep) this.schedule(deviceId, state);
53549
+ return;
53550
+ }
53551
+ this.deps.logger.info("occupancy recheck: burst dispatched", {
53552
+ tags: {
53553
+ deviceId,
53554
+ nodeId
53555
+ },
53556
+ meta: {
53557
+ deviceId,
53558
+ nodeId,
53559
+ frames,
53560
+ recheckSec: state.lastConfig.occupancyRecheckSec ?? null
53561
+ }
53562
+ });
53563
+ setTimeout(() => {
53564
+ this.settle(deviceId, state, burst).catch((err) => {
53565
+ this.deps.logger.warn("occupancy recheck: settle failed", {
53566
+ tags: {
53567
+ deviceId,
53568
+ nodeId
53569
+ },
53570
+ meta: {
53571
+ deviceId,
53572
+ nodeId,
53573
+ error: errMsg$1(err)
53574
+ }
53575
+ });
53576
+ });
53577
+ }, RECHECK_BURST_WINDOW_MS).unref?.();
53578
+ }
53579
+ async settle(deviceId, state, burst) {
53580
+ await this.release(deviceId, state, burst);
53581
+ const fold = foldBurstOutcome({ emptyStreak: state.emptyStreak }, burst.found);
53582
+ state.emptyStreak = fold.emptyStreak;
53583
+ state.armed = fold.armed;
53584
+ this.deps.logger.info("occupancy recheck: burst complete", {
53585
+ tags: {
53586
+ deviceId,
53587
+ nodeId: burst.nodeId
53588
+ },
53589
+ meta: {
53590
+ deviceId,
53591
+ nodeId: burst.nodeId,
53592
+ found: burst.found,
53593
+ emptyStreak: fold.emptyStreak,
53594
+ armed: fold.armed,
53595
+ superseded: burst.superseded
53596
+ }
53597
+ });
53598
+ if (state.armed && !this.deps.isSessionActive(deviceId)) this.schedule(deviceId, state);
53599
+ else if (state.armed) state.armed = false;
53600
+ }
53601
+ /** Detach the recheck registration unless a session took it over. */
53602
+ async release(deviceId, state, burst) {
53603
+ if (state.burst === burst) state.burst = null;
53604
+ if (burst.superseded) return;
53605
+ await this.deps.detachOn(burst.nodeId, deviceId).catch((err) => {
53606
+ this.deps.logger.warn("occupancy recheck: detach failed", {
53607
+ tags: {
53608
+ deviceId,
53609
+ nodeId: burst.nodeId
53610
+ },
53611
+ meta: {
53612
+ deviceId,
53613
+ nodeId: burst.nodeId,
53614
+ error: errMsg$1(err)
53615
+ }
53616
+ });
53617
+ });
53618
+ }
53619
+ };
53620
+ //#endregion
53010
53621
  //#region src/balancer-input-cache.ts
53011
53622
  /**
53012
53623
  * Short-TTL memo for the reads a session dispatch needs BEFORE it can choose a
@@ -53433,8 +54044,32 @@ var SessionDispatchController = class {
53433
54044
  inferenceDeviceCaps = new TtlMemo({ load: (nodeId) => this.deps.getInferenceDeviceCaps(nodeId) });
53434
54045
  /** Background refresher for {@link agentLoads} et al. Null until first use. */
53435
54046
  balancerRefreshTimer = null;
54047
+ /**
54048
+ * The occupancy recheck for session-dispatched cameras (D547). The runner's
54049
+ * own recheck lives on an `on-motion` registration no camera receives; this
54050
+ * one is armed by the session close, which only this controller sees.
54051
+ */
54052
+ recheck;
53436
54053
  constructor(deps) {
53437
54054
  this.deps = deps;
54055
+ const requestBurstOn = deps.requestBurstOn;
54056
+ if (requestBurstOn === void 0) {
54057
+ this.recheck = null;
54058
+ deps.logger.info("occupancy recheck has no burst producer on this build — no session-dispatched camera will be re-sampled", { meta: { why: "SessionDispatchControllerDeps.requestBurstOn absent" } });
54059
+ } else this.recheck = new OccupancyRecheckController({
54060
+ attachOn: (nodeId, config) => deps.attachOn(nodeId, config),
54061
+ detachOn: (nodeId, deviceId) => deps.detachOn(nodeId, deviceId),
54062
+ requestBurstOn,
54063
+ isSessionActive: (deviceId) => this.sessionRegistry.has(deviceId),
54064
+ logger: deps.logger
54065
+ });
54066
+ }
54067
+ /**
54068
+ * Every inference result the orchestrator sees, for the recheck's
54069
+ * found/empty verdict: a `'burst'` result with detections is a found burst.
54070
+ */
54071
+ noteInferenceResult(deviceId, occasion, detectionCount) {
54072
+ this.recheck?.noteInferenceResult(deviceId, occasion, detectionCount);
53438
54073
  }
53439
54074
  /**
53440
54075
  * Placement changed — this controller just attached or detached something, so
@@ -53827,6 +54462,7 @@ var SessionDispatchController = class {
53827
54462
  attachedAt = Date.now();
53828
54463
  this.deps.recordAssignment(deviceId, targetNodeId, decision.reason, decision.reason === "manual");
53829
54464
  this.sessionRegistry.register(deviceId, targetNodeId, Date.now(), deviceKey);
54465
+ this.recheck?.noteSessionDispatched(deviceId, targetNodeId, sessionConfig);
53830
54466
  log.info("detection session: dispatched on motion", {
53831
54467
  tags: {
53832
54468
  deviceId,
@@ -54003,6 +54639,7 @@ var SessionDispatchController = class {
54003
54639
  this.activeRefireCountByDevice.delete(deviceId);
54004
54640
  this.notePlacementChanged();
54005
54641
  const { openForMs, sinceSubjectSeenMs, maxHoldMs } = observed;
54642
+ this.recheck?.noteSessionClosed(deviceId, { sawSubject: sinceSubjectSeenMs !== null });
54006
54643
  const subjectPresent = sinceSubjectSeenMs !== null && sinceSubjectSeenMs < 4e3;
54007
54644
  const meta = {
54008
54645
  reason,
@@ -54084,6 +54721,7 @@ var SessionDispatchController = class {
54084
54721
  }
54085
54722
  this.deps.dropActiveDetection(deviceId);
54086
54723
  this.lastSubjectSeenAt.delete(deviceId);
54724
+ this.recheck?.forget(deviceId);
54087
54725
  });
54088
54726
  const watchNodeId = this.motionWatchByDevice.get(deviceId);
54089
54727
  if (watchNodeId) {
@@ -54119,6 +54757,7 @@ var SessionDispatchController = class {
54119
54757
  */
54120
54758
  shutdown() {
54121
54759
  this.shuttingDown = true;
54760
+ this.recheck?.shutdown();
54122
54761
  this.sessionRegistry.clear();
54123
54762
  this.motionWatchByDevice.clear();
54124
54763
  this.lastSubjectSeenAt.clear();
@@ -55113,6 +55752,11 @@ async function buildOrchestratorControllers(deps) {
55113
55752
  settingsStore,
55114
55753
  attachOn: (nodeId, config) => deps.attachOn(nodeId, config),
55115
55754
  detachOn: (nodeId, deviceId) => deps.detachOn(nodeId, deviceId),
55755
+ requestBurstOn: async (nodeId, deviceId, frames, reason) => deps.ctx().api.pipelineRunner.requestOccupancyBurst.mutate({
55756
+ deviceId,
55757
+ frames,
55758
+ reason
55759
+ }, nodePin(nodeId)),
55116
55760
  recordAssignment: (deviceId, agentNodeId, reason, pinned) => ledger.recordAssignment(deviceId, agentNodeId, reason, pinned),
55117
55761
  dropAssignment: (deviceId, emit) => ledger.dropAssignment(deviceId, emit),
55118
55762
  reconcilePlacementFromRunners: () => reconcile.reconcilePlacementFromRunners(),
@@ -55417,7 +56061,10 @@ async function buildOrchestratorControllers(deps) {
55417
56061
  handleDeviceUnregistered: (deviceId) => deps.handleDeviceUnregistered(deviceId),
55418
56062
  handleSettingsChanged: (deviceId) => deps.handleSettingsChanged(deviceId),
55419
56063
  handlePipelineCameraUpdated: (deviceId, config) => deps.handlePipelineCameraUpdated(deviceId, config),
55420
- handleInferenceResult: (payload) => deps.handleInferenceResult(payload),
56064
+ handleInferenceResult: (payload) => {
56065
+ session.noteInferenceResult(payload.deviceId, payload.occasion, payload.frame.detections?.length ?? 0);
56066
+ return deps.handleInferenceResult(payload);
56067
+ },
55421
56068
  noteMetrics: (deviceId, nodeId, metrics) => {
55422
56069
  loadShed.noteMetrics(deviceId, nodeId, metrics);
55423
56070
  nodeStress.noteMetrics(deviceId, nodeId, metrics);