@camstack/addon-export-hap 1.2.58 → 1.2.60

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.
@@ -8684,6 +8684,20 @@ var RelocateJobSchema = object({
8684
8684
  * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8685
8685
  */
8686
8686
  rowsReconciled: number().int().nonnegative().optional(),
8687
+ /**
8688
+ * Rows this run FORGOT because the file they name is not on disk.
8689
+ *
8690
+ * The mover derived the path from the row's own fields and `stat`ed it; an
8691
+ * ENOENT there is a per-path confirmation that the segment is gone (D296),
8692
+ * and the durable row is dropped through the same channel eviction uses. It
8693
+ * is reported for the same reason `rowsReconciled` is: this is a durable
8694
+ * mutation nobody asked for, and a migration that quietly erases hour rows is
8695
+ * the same failure as one that quietly skips them (D295).
8696
+ *
8697
+ * The production drain of 2026-08-30 would have reported 11 074 here — the
8698
+ * ledger claimed 5.65 GB of footage that no longer existed.
8699
+ */
8700
+ rowsForgotten: number().int().nonnegative().optional(),
8687
8701
  startedAt: number(),
8688
8702
  finishedAt: number().nullable(),
8689
8703
  error: string().nullable()
@@ -9047,6 +9061,91 @@ var RelocateResidueSchema = object({
9047
9061
  segments: number().int().nonnegative(),
9048
9062
  bytes: number().int().nonnegative()
9049
9063
  }).nullable();
9064
+ /**
9065
+ * Ask one location whether its durable hour rows describe the disk — the walk
9066
+ * (D319).
9067
+ *
9068
+ * `apply` DEFAULTS TO FALSE and that default is the product: the operator's
9069
+ * missing tool is the question, and the dry run is how they sanity-check the
9070
+ * destructive run before authorising it.
9071
+ */
9072
+ var LedgerWalkInputSchema = object({
9073
+ locationId: string().min(1),
9074
+ /** Forget the confirmed-absent rows, rather than only counting them. */
9075
+ apply: boolean().optional(),
9076
+ /** Narrow to one camera. */
9077
+ deviceId: number().int().positive().optional(),
9078
+ /** Narrow to these recording profiles; empty/absent = every profile. */
9079
+ profiles: array(string().min(1)).optional()
9080
+ });
9081
+ /** Why a whole walk did nothing. Every one leaves the ledger untouched. */
9082
+ var LedgerWalkRefusalSchema = _enum([
9083
+ "location-unknown",
9084
+ "source-writable",
9085
+ "no-ledger",
9086
+ "archive-unreadable",
9087
+ "anchor-absent",
9088
+ "anchor-unreadable",
9089
+ "anchor-moved"
9090
+ ]);
9091
+ _enum([
9092
+ "live-tail",
9093
+ "listing-error",
9094
+ "path-mismatch",
9095
+ "durable-refused"
9096
+ ]);
9097
+ /** Every skip reason, always present, always a number — so a reason that never
9098
+ * fired reports as zero rather than absent and the report shape is constant
9099
+ * between passes. Spelled out rather than `z.record` for exactly that. */
9100
+ var LedgerWalkSkipCountsSchema = object({
9101
+ "live-tail": number().int().nonnegative(),
9102
+ "listing-error": number().int().nonnegative(),
9103
+ "path-mismatch": number().int().nonnegative(),
9104
+ "durable-refused": number().int().nonnegative()
9105
+ });
9106
+ /** One camera's share of a walk, so a report names cameras and not rows. */
9107
+ var LedgerWalkDeviceReportSchema = object({
9108
+ deviceId: number().int(),
9109
+ hoursWalked: number().int().nonnegative(),
9110
+ hoursMissing: number().int().nonnegative(),
9111
+ ghostSegments: number().int().nonnegative(),
9112
+ ghostBytes: number().int().nonnegative(),
9113
+ forgottenSegments: number().int().nonnegative(),
9114
+ orphanFiles: number().int().nonnegative()
9115
+ });
9116
+ /**
9117
+ * What one walk claimed, listed, found and (only when armed) forgot.
9118
+ *
9119
+ * `archiveSegments` is the **M** and `segmentsClaimed` the **N** (D295), so a
9120
+ * walk that saw a fraction of the location is visible in its own report rather
9121
+ * than in the absence of one.
9122
+ */
9123
+ var LedgerWalkReportSchema = object({
9124
+ locationId: string(),
9125
+ applied: boolean(),
9126
+ refused: LedgerWalkRefusalSchema.nullable(),
9127
+ archiveSegments: number().int().nonnegative().nullable(),
9128
+ archiveBytes: number().int().nonnegative().nullable(),
9129
+ hoursClaimed: number().int().nonnegative(),
9130
+ hoursWalked: number().int().nonnegative(),
9131
+ hoursMissing: number().int().nonnegative(),
9132
+ /** `readdir` calls issued — the cost, stated in the unit that is paid. */
9133
+ listings: number().int().nonnegative(),
9134
+ segmentsClaimed: number().int().nonnegative(),
9135
+ ghostSegments: number().int().nonnegative(),
9136
+ ghostBytes: number().int().nonnegative(),
9137
+ ghostHoursWhole: number().int().nonnegative(),
9138
+ forgottenSegments: number().int().nonnegative(),
9139
+ forgottenBytes: number().int().nonnegative(),
9140
+ /** Files under a claimed hour that no durable row names. Never deleted. */
9141
+ orphanFiles: number().int().nonnegative(),
9142
+ orphanSample: array(string()).readonly(),
9143
+ hoursSkipped: number().int().nonnegative(),
9144
+ skippedByReason: LedgerWalkSkipCountsSchema,
9145
+ /** The walk stopped at its per-pass hour bound with claims unwalked. */
9146
+ bounded: boolean(),
9147
+ byDevice: array(LedgerWalkDeviceReportSchema).readonly()
9148
+ });
9050
9149
  /** How many rows a media pass would still act on against a given target — the
9051
9150
  * media lane's denominator AND its residue, from ONE derivation so the two can
9052
9151
  * never disagree. `null` = the count could not be taken. */
@@ -19455,13 +19554,15 @@ var ListGroupsPageSchema = object({
19455
19554
  groups: array(AnalyticsGroupRecordSchema).readonly(),
19456
19555
  nextCursor: string().nullable()
19457
19556
  });
19557
+ var KEY_EVENTS_DEFAULT_LIMIT = 50;
19558
+ var KEY_EVENTS_MAX_LIMIT = 200;
19458
19559
  var KeyEventQueryInput = object({
19459
19560
  deviceId: number(),
19460
19561
  /** Window lower bound (track firstSeen ≥ since). */
19461
19562
  since: number(),
19462
19563
  /** Window upper bound (track firstSeen ≤ until). */
19463
19564
  until: number(),
19464
- limit: number().int().min(1).max(200).default(50),
19565
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19465
19566
  /** Drop tracks scoring below this importance. */
19466
19567
  minImportance: number().min(0).max(1).optional(),
19467
19568
  /** Restrict to a single class (e.g. 'person'). */
@@ -19483,6 +19584,32 @@ var KeyEventSchema = object({
19483
19584
  ...TrackFlagFields,
19484
19585
  ...TrackRetrainFields
19485
19586
  });
19587
+ /** `getKeyEvents`' window and filters, asked of a SET of cameras at once. */
19588
+ var KeyEventBatchQueryInput = object({
19589
+ deviceIds: array(number()).min(1).max(200),
19590
+ since: number(),
19591
+ until: number(),
19592
+ /** Applied PER CAMERA, exactly as `getKeyEvents.limit` is — never a total
19593
+ * across the set, which would let a busy camera starve a quiet one of its
19594
+ * rows and change what the merged feed contains. */
19595
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19596
+ minImportance: number().min(0).max(1).optional(),
19597
+ classFilter: string().optional()
19598
+ });
19599
+ /**
19600
+ * One camera's key events in a batch answer.
19601
+ *
19602
+ * The row exists for every requested id. `getKeyEvents` degrades to `[]` on
19603
+ * error rather than throwing, so a camera whose store read failed and one with
19604
+ * no events in the window were ALREADY indistinguishable per camera — the
19605
+ * batch does not make that worse, and the row keeps the deviceId the single
19606
+ * method's output never carried (the caller used to stamp it from the fan-out
19607
+ * key, which only worked because there was one query per camera).
19608
+ */
19609
+ var KeyEventsForDeviceSchema = object({
19610
+ deviceId: number(),
19611
+ events: array(KeyEventSchema).readonly()
19612
+ });
19486
19613
  object({
19487
19614
  trackId: string(),
19488
19615
  className: string(),
@@ -19554,6 +19681,50 @@ var EventStoreFootprintSchema = object({
19554
19681
  totalBytes: number().int(),
19555
19682
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19556
19683
  });
19684
+ /** Event-media footprint for one {@link MediaFileKind}. */
19685
+ var EventMediaKindFootprintSchema = object({
19686
+ kind: MediaFileKindEnum,
19687
+ /** Media rows of this kind. */
19688
+ rows: number().int(),
19689
+ /** Bytes on disk held by those rows. */
19690
+ bytes: number().int()
19691
+ });
19692
+ /**
19693
+ * The media footprint broken down by KIND — the axis a deletion decision
19694
+ * actually turns on.
19695
+ *
19696
+ * A byte total says how much there is; it cannot say what is safe to remove.
19697
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19698
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19699
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19700
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19701
+ * nothing else, so sizing a deletion means summing per kind.
19702
+ *
19703
+ * ## Why `unaccounted*` exists
19704
+ *
19705
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19706
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19707
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19708
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19709
+ * retired code path, or by a version that knew a kind this one does not) would
19710
+ * otherwise vanish from the total silently, and an operator would delete
19711
+ * against a denominator smaller than the disk.
19712
+ *
19713
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19714
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19715
+ */
19716
+ var EventMediaKindBreakdownSchema = object({
19717
+ /** Every media row in scope, from one unfiltered aggregate. */
19718
+ totalRows: number().int(),
19719
+ /** Every media byte in scope, from that same aggregate. */
19720
+ totalBytes: number().int(),
19721
+ /** Per-kind footprint, ordered by bytes descending. */
19722
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19723
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19724
+ unaccountedRows: number().int(),
19725
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19726
+ unaccountedBytes: number().int()
19727
+ });
19557
19728
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19558
19729
  var EventPruneCountsSchema = object({
19559
19730
  motion: number().int(),
@@ -19708,7 +19879,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19708
19879
  until: number().optional(),
19709
19880
  kinds: array(string()).optional(),
19710
19881
  limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
19711
- }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19882
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(KeyEventBatchQueryInput, array(KeyEventsForDeviceSchema).readonly()), method(object({
19712
19883
  deviceId: number(),
19713
19884
  since: number(),
19714
19885
  until: number(),
@@ -19757,6 +19928,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19757
19928
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19758
19929
  kind: "query",
19759
19930
  auth: "admin"
19931
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19932
+ kind: "query",
19933
+ auth: "admin"
19760
19934
  }), method(object({
19761
19935
  olderThanMs: number(),
19762
19936
  reason: OpsLogReasonSchema.optional()
@@ -21708,6 +21882,20 @@ method(object({
21708
21882
  error: string().optional()
21709
21883
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21710
21884
  providerId: string(),
21885
+ /**
21886
+ * The location this config is an UNSAVED edit of, when there is one.
21887
+ *
21888
+ * `listLocations` replaces every declared secret with the redaction
21889
+ * sentinel, so the edit modal's form state holds the sentinel for any
21890
+ * credential the operator did not retype — and posting that here
21891
+ * without a way to resolve it makes the provider try to authenticate
21892
+ * as `__camstack_redacted__` and report the operator's own working
21893
+ * password as wrong. Given this id, the orchestrator restores each
21894
+ * sentinel from the stored config (same rule as `upsertLocation`)
21895
+ * before dispatching. Omitted by the "Add location" wizard, where
21896
+ * every value was typed just now and nothing is stored yet.
21897
+ */
21898
+ locationId: string().optional(),
21711
21899
  config: record(string(), unknown())
21712
21900
  }), object({
21713
21901
  ok: boolean(),
@@ -27391,6 +27579,9 @@ method(object({
27391
27579
  }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
27392
27580
  kind: "query",
27393
27581
  auth: "admin"
27582
+ }), method(LedgerWalkInputSchema, LedgerWalkReportSchema, {
27583
+ kind: "mutation",
27584
+ auth: "admin"
27394
27585
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
27395
27586
  kind: "mutation",
27396
27587
  auth: "admin"
@@ -27782,7 +27973,26 @@ var SceneMonitorStatusSchema = object({
27782
27973
  monitors: array(SceneMonitorSchema),
27783
27974
  lastFetchedAt: number()
27784
27975
  });
27785
- DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({
27976
+ /**
27977
+ * One camera's row in a `listScenesBatch` answer.
27978
+ *
27979
+ * `status` is NULLABLE and the nullability is the whole point. `scene-monitor`
27980
+ * is a `defaultActive` wrapper, so every camera is asked; a camera whose
27981
+ * provider is absent (pipeline-analytics not deployed, the post-processing node
27982
+ * down) cannot answer, and that is not the same fact as a camera with no scenes
27983
+ * configured. Fanned out per camera the difference was visible — one query
27984
+ * errored while the others resolved — and a batch that returned only the rows
27985
+ * it managed would have destroyed it, silently, by making an unreachable camera
27986
+ * indistinguishable from one that answered `monitors: []`.
27987
+ *
27988
+ * So: EVERY requested deviceId gets a row. `status: null` means "this camera
27989
+ * could not be read"; `status.monitors: []` means "read, and it has none".
27990
+ */
27991
+ var SceneMonitorStatusForDeviceSchema = object({
27992
+ deviceId: number(),
27993
+ status: SceneMonitorStatusSchema.nullable()
27994
+ });
27995
+ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({ deviceIds: array(number()).min(1).max(200) }), array(SceneMonitorStatusForDeviceSchema).readonly()), method(object({
27786
27996
  deviceId: number(),
27787
27997
  label: string(),
27788
27998
  roi: MaskRectShapeSchema,
@@ -29131,6 +29341,27 @@ var CameraOccupancySnapshotSchema = object({
29131
29341
  stationaryObjects: array(StationaryObjectSchema).readonly().optional()
29132
29342
  });
29133
29343
  /**
29344
+ * One camera's row in a `getCurrentSnapshotBatch` answer.
29345
+ *
29346
+ * THREE outcomes, and the single-camera method could only express two of them
29347
+ * because `snapshot: null` was already spoken for:
29348
+ *
29349
+ * - `read: 'read'`, `snapshot` present — the live occupancy reading.
29350
+ * - `read: 'read'`, `snapshot: null` — read, and this camera has no reading
29351
+ * yet: no frame since boot, and no parked-object registry to hydrate from.
29352
+ * - `read: 'unreadable'` — the owner could not answer for this
29353
+ * camera. `snapshot` is null, and it does NOT mean "nothing parked here".
29354
+ *
29355
+ * Collapsing the last two is the failure this field exists to prevent: a
29356
+ * hydration that threw would otherwise render as an empty Stationary section,
29357
+ * which is a definite claim about a camera nobody could read.
29358
+ */
29359
+ var CameraOccupancySnapshotForDeviceSchema = object({
29360
+ deviceId: number(),
29361
+ read: _enum(["read", "unreadable"]),
29362
+ snapshot: CameraOccupancySnapshotSchema.nullable()
29363
+ });
29364
+ /**
29134
29365
  * Time-series resolution. The history methods return one bucket per
29135
29366
  * step over the requested range. Smaller resolutions cost more
29136
29367
  * memory + bandwidth; bound to discrete steps so caller cannot ask
@@ -29154,7 +29385,7 @@ var HistoryPointSchema = object({
29154
29385
  /** Object count averaged over the bucket (rounded to nearest integer). */
29155
29386
  count: number().int().nonnegative()
29156
29387
  });
29157
- DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
29388
+ DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(CameraOccupancySnapshotForDeviceSchema).readonly()), method(object({
29158
29389
  deviceId: number(),
29159
29390
  zoneId: string(),
29160
29391
  className: string().optional()
@@ -32435,6 +32666,12 @@ Object.freeze({
32435
32666
  addonId: null,
32436
32667
  access: "view"
32437
32668
  },
32669
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
32670
+ capName: "pipeline-analytics",
32671
+ capScope: "device",
32672
+ addonId: null,
32673
+ access: "view"
32674
+ },
32438
32675
  "pipelineAnalytics.getEventStoreFootprint": {
32439
32676
  capName: "pipeline-analytics",
32440
32677
  capScope: "device",
@@ -32453,6 +32690,12 @@ Object.freeze({
32453
32690
  addonId: null,
32454
32691
  access: "view"
32455
32692
  },
32693
+ "pipelineAnalytics.getKeyEventsBatch": {
32694
+ capName: "pipeline-analytics",
32695
+ capScope: "device",
32696
+ addonId: null,
32697
+ access: "view"
32698
+ },
32456
32699
  "pipelineAnalytics.getMotionEvents": {
32457
32700
  capName: "pipeline-analytics",
32458
32701
  capScope: "device",
@@ -33629,6 +33872,12 @@ Object.freeze({
33629
33872
  addonId: null,
33630
33873
  access: "view"
33631
33874
  },
33875
+ "recording.reconcileLedgerAgainstDisk": {
33876
+ capName: "recording",
33877
+ capScope: "system",
33878
+ addonId: null,
33879
+ access: "create"
33880
+ },
33632
33881
  "recording.refreshStorageLocationsForMigration": {
33633
33882
  capName: "recording",
33634
33883
  capScope: "system",
@@ -33755,6 +34004,12 @@ Object.freeze({
33755
34004
  addonId: null,
33756
34005
  access: "view"
33757
34006
  },
34007
+ "sceneMonitor.listScenesBatch": {
34008
+ capName: "scene-monitor",
34009
+ capScope: "device",
34010
+ addonId: null,
34011
+ access: "view"
34012
+ },
33758
34013
  "sceneMonitor.recheckNow": {
33759
34014
  capName: "scene-monitor",
33760
34015
  capScope: "device",
@@ -35111,6 +35366,12 @@ Object.freeze({
35111
35366
  addonId: null,
35112
35367
  access: "view"
35113
35368
  },
35369
+ "zoneAnalytics.getCurrentSnapshotBatch": {
35370
+ capName: "zone-analytics",
35371
+ capScope: "device",
35372
+ addonId: null,
35373
+ access: "view"
35374
+ },
35114
35375
  "zoneAnalytics.getUnzonedHistory": {
35115
35376
  capName: "zone-analytics",
35116
35377
  capScope: "device",
@@ -36100,6 +36361,11 @@ Object.freeze({
36100
36361
  form: "single",
36101
36362
  optional: false
36102
36363
  }],
36364
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
36365
+ name: "deviceId",
36366
+ form: "single",
36367
+ optional: true
36368
+ }],
36103
36369
  "pipelineAnalytics.getGroup": [{
36104
36370
  name: "deviceId",
36105
36371
  form: "single",
@@ -36110,6 +36376,11 @@ Object.freeze({
36110
36376
  form: "single",
36111
36377
  optional: false
36112
36378
  }],
36379
+ "pipelineAnalytics.getKeyEventsBatch": [{
36380
+ name: "deviceIds",
36381
+ form: "array",
36382
+ optional: false
36383
+ }],
36113
36384
  "pipelineAnalytics.getMotionEvents": [{
36114
36385
  name: "deviceId",
36115
36386
  form: "single",
@@ -36550,6 +36821,11 @@ Object.freeze({
36550
36821
  form: "single",
36551
36822
  optional: false
36552
36823
  }],
36824
+ "recording.reconcileLedgerAgainstDisk": [{
36825
+ name: "deviceId",
36826
+ form: "single",
36827
+ optional: true
36828
+ }],
36553
36829
  "recording.relocateFootage": [{
36554
36830
  name: "deviceId",
36555
36831
  form: "single",
@@ -36615,6 +36891,11 @@ Object.freeze({
36615
36891
  form: "single",
36616
36892
  optional: false
36617
36893
  }],
36894
+ "sceneMonitor.listScenesBatch": [{
36895
+ name: "deviceIds",
36896
+ form: "array",
36897
+ optional: false
36898
+ }],
36618
36899
  "sceneMonitor.recheckNow": [{
36619
36900
  name: "deviceId",
36620
36901
  form: "single",
@@ -36876,6 +37157,11 @@ Object.freeze({
36876
37157
  form: "single",
36877
37158
  optional: false
36878
37159
  }],
37160
+ "zoneAnalytics.getCurrentSnapshotBatch": [{
37161
+ name: "deviceIds",
37162
+ form: "array",
37163
+ optional: false
37164
+ }],
36879
37165
  "zoneAnalytics.getUnzonedHistory": [{
36880
37166
  name: "deviceId",
36881
37167
  form: "single",
@@ -8672,6 +8672,20 @@ var RelocateJobSchema = object({
8672
8672
  * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8673
8673
  */
8674
8674
  rowsReconciled: number().int().nonnegative().optional(),
8675
+ /**
8676
+ * Rows this run FORGOT because the file they name is not on disk.
8677
+ *
8678
+ * The mover derived the path from the row's own fields and `stat`ed it; an
8679
+ * ENOENT there is a per-path confirmation that the segment is gone (D296),
8680
+ * and the durable row is dropped through the same channel eviction uses. It
8681
+ * is reported for the same reason `rowsReconciled` is: this is a durable
8682
+ * mutation nobody asked for, and a migration that quietly erases hour rows is
8683
+ * the same failure as one that quietly skips them (D295).
8684
+ *
8685
+ * The production drain of 2026-08-30 would have reported 11 074 here — the
8686
+ * ledger claimed 5.65 GB of footage that no longer existed.
8687
+ */
8688
+ rowsForgotten: number().int().nonnegative().optional(),
8675
8689
  startedAt: number(),
8676
8690
  finishedAt: number().nullable(),
8677
8691
  error: string().nullable()
@@ -9035,6 +9049,91 @@ var RelocateResidueSchema = object({
9035
9049
  segments: number().int().nonnegative(),
9036
9050
  bytes: number().int().nonnegative()
9037
9051
  }).nullable();
9052
+ /**
9053
+ * Ask one location whether its durable hour rows describe the disk — the walk
9054
+ * (D319).
9055
+ *
9056
+ * `apply` DEFAULTS TO FALSE and that default is the product: the operator's
9057
+ * missing tool is the question, and the dry run is how they sanity-check the
9058
+ * destructive run before authorising it.
9059
+ */
9060
+ var LedgerWalkInputSchema = object({
9061
+ locationId: string().min(1),
9062
+ /** Forget the confirmed-absent rows, rather than only counting them. */
9063
+ apply: boolean().optional(),
9064
+ /** Narrow to one camera. */
9065
+ deviceId: number().int().positive().optional(),
9066
+ /** Narrow to these recording profiles; empty/absent = every profile. */
9067
+ profiles: array(string().min(1)).optional()
9068
+ });
9069
+ /** Why a whole walk did nothing. Every one leaves the ledger untouched. */
9070
+ var LedgerWalkRefusalSchema = _enum([
9071
+ "location-unknown",
9072
+ "source-writable",
9073
+ "no-ledger",
9074
+ "archive-unreadable",
9075
+ "anchor-absent",
9076
+ "anchor-unreadable",
9077
+ "anchor-moved"
9078
+ ]);
9079
+ _enum([
9080
+ "live-tail",
9081
+ "listing-error",
9082
+ "path-mismatch",
9083
+ "durable-refused"
9084
+ ]);
9085
+ /** Every skip reason, always present, always a number — so a reason that never
9086
+ * fired reports as zero rather than absent and the report shape is constant
9087
+ * between passes. Spelled out rather than `z.record` for exactly that. */
9088
+ var LedgerWalkSkipCountsSchema = object({
9089
+ "live-tail": number().int().nonnegative(),
9090
+ "listing-error": number().int().nonnegative(),
9091
+ "path-mismatch": number().int().nonnegative(),
9092
+ "durable-refused": number().int().nonnegative()
9093
+ });
9094
+ /** One camera's share of a walk, so a report names cameras and not rows. */
9095
+ var LedgerWalkDeviceReportSchema = object({
9096
+ deviceId: number().int(),
9097
+ hoursWalked: number().int().nonnegative(),
9098
+ hoursMissing: number().int().nonnegative(),
9099
+ ghostSegments: number().int().nonnegative(),
9100
+ ghostBytes: number().int().nonnegative(),
9101
+ forgottenSegments: number().int().nonnegative(),
9102
+ orphanFiles: number().int().nonnegative()
9103
+ });
9104
+ /**
9105
+ * What one walk claimed, listed, found and (only when armed) forgot.
9106
+ *
9107
+ * `archiveSegments` is the **M** and `segmentsClaimed` the **N** (D295), so a
9108
+ * walk that saw a fraction of the location is visible in its own report rather
9109
+ * than in the absence of one.
9110
+ */
9111
+ var LedgerWalkReportSchema = object({
9112
+ locationId: string(),
9113
+ applied: boolean(),
9114
+ refused: LedgerWalkRefusalSchema.nullable(),
9115
+ archiveSegments: number().int().nonnegative().nullable(),
9116
+ archiveBytes: number().int().nonnegative().nullable(),
9117
+ hoursClaimed: number().int().nonnegative(),
9118
+ hoursWalked: number().int().nonnegative(),
9119
+ hoursMissing: number().int().nonnegative(),
9120
+ /** `readdir` calls issued — the cost, stated in the unit that is paid. */
9121
+ listings: number().int().nonnegative(),
9122
+ segmentsClaimed: number().int().nonnegative(),
9123
+ ghostSegments: number().int().nonnegative(),
9124
+ ghostBytes: number().int().nonnegative(),
9125
+ ghostHoursWhole: number().int().nonnegative(),
9126
+ forgottenSegments: number().int().nonnegative(),
9127
+ forgottenBytes: number().int().nonnegative(),
9128
+ /** Files under a claimed hour that no durable row names. Never deleted. */
9129
+ orphanFiles: number().int().nonnegative(),
9130
+ orphanSample: array(string()).readonly(),
9131
+ hoursSkipped: number().int().nonnegative(),
9132
+ skippedByReason: LedgerWalkSkipCountsSchema,
9133
+ /** The walk stopped at its per-pass hour bound with claims unwalked. */
9134
+ bounded: boolean(),
9135
+ byDevice: array(LedgerWalkDeviceReportSchema).readonly()
9136
+ });
9038
9137
  /** How many rows a media pass would still act on against a given target — the
9039
9138
  * media lane's denominator AND its residue, from ONE derivation so the two can
9040
9139
  * never disagree. `null` = the count could not be taken. */
@@ -19443,13 +19542,15 @@ var ListGroupsPageSchema = object({
19443
19542
  groups: array(AnalyticsGroupRecordSchema).readonly(),
19444
19543
  nextCursor: string().nullable()
19445
19544
  });
19545
+ var KEY_EVENTS_DEFAULT_LIMIT = 50;
19546
+ var KEY_EVENTS_MAX_LIMIT = 200;
19446
19547
  var KeyEventQueryInput = object({
19447
19548
  deviceId: number(),
19448
19549
  /** Window lower bound (track firstSeen ≥ since). */
19449
19550
  since: number(),
19450
19551
  /** Window upper bound (track firstSeen ≤ until). */
19451
19552
  until: number(),
19452
- limit: number().int().min(1).max(200).default(50),
19553
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19453
19554
  /** Drop tracks scoring below this importance. */
19454
19555
  minImportance: number().min(0).max(1).optional(),
19455
19556
  /** Restrict to a single class (e.g. 'person'). */
@@ -19471,6 +19572,32 @@ var KeyEventSchema = object({
19471
19572
  ...TrackFlagFields,
19472
19573
  ...TrackRetrainFields
19473
19574
  });
19575
+ /** `getKeyEvents`' window and filters, asked of a SET of cameras at once. */
19576
+ var KeyEventBatchQueryInput = object({
19577
+ deviceIds: array(number()).min(1).max(200),
19578
+ since: number(),
19579
+ until: number(),
19580
+ /** Applied PER CAMERA, exactly as `getKeyEvents.limit` is — never a total
19581
+ * across the set, which would let a busy camera starve a quiet one of its
19582
+ * rows and change what the merged feed contains. */
19583
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19584
+ minImportance: number().min(0).max(1).optional(),
19585
+ classFilter: string().optional()
19586
+ });
19587
+ /**
19588
+ * One camera's key events in a batch answer.
19589
+ *
19590
+ * The row exists for every requested id. `getKeyEvents` degrades to `[]` on
19591
+ * error rather than throwing, so a camera whose store read failed and one with
19592
+ * no events in the window were ALREADY indistinguishable per camera — the
19593
+ * batch does not make that worse, and the row keeps the deviceId the single
19594
+ * method's output never carried (the caller used to stamp it from the fan-out
19595
+ * key, which only worked because there was one query per camera).
19596
+ */
19597
+ var KeyEventsForDeviceSchema = object({
19598
+ deviceId: number(),
19599
+ events: array(KeyEventSchema).readonly()
19600
+ });
19474
19601
  object({
19475
19602
  trackId: string(),
19476
19603
  className: string(),
@@ -19542,6 +19669,50 @@ var EventStoreFootprintSchema = object({
19542
19669
  totalBytes: number().int(),
19543
19670
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19544
19671
  });
19672
+ /** Event-media footprint for one {@link MediaFileKind}. */
19673
+ var EventMediaKindFootprintSchema = object({
19674
+ kind: MediaFileKindEnum,
19675
+ /** Media rows of this kind. */
19676
+ rows: number().int(),
19677
+ /** Bytes on disk held by those rows. */
19678
+ bytes: number().int()
19679
+ });
19680
+ /**
19681
+ * The media footprint broken down by KIND — the axis a deletion decision
19682
+ * actually turns on.
19683
+ *
19684
+ * A byte total says how much there is; it cannot say what is safe to remove.
19685
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19686
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19687
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19688
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19689
+ * nothing else, so sizing a deletion means summing per kind.
19690
+ *
19691
+ * ## Why `unaccounted*` exists
19692
+ *
19693
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19694
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19695
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19696
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19697
+ * retired code path, or by a version that knew a kind this one does not) would
19698
+ * otherwise vanish from the total silently, and an operator would delete
19699
+ * against a denominator smaller than the disk.
19700
+ *
19701
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19702
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19703
+ */
19704
+ var EventMediaKindBreakdownSchema = object({
19705
+ /** Every media row in scope, from one unfiltered aggregate. */
19706
+ totalRows: number().int(),
19707
+ /** Every media byte in scope, from that same aggregate. */
19708
+ totalBytes: number().int(),
19709
+ /** Per-kind footprint, ordered by bytes descending. */
19710
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19711
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19712
+ unaccountedRows: number().int(),
19713
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19714
+ unaccountedBytes: number().int()
19715
+ });
19545
19716
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19546
19717
  var EventPruneCountsSchema = object({
19547
19718
  motion: number().int(),
@@ -19696,7 +19867,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19696
19867
  until: number().optional(),
19697
19868
  kinds: array(string()).optional(),
19698
19869
  limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
19699
- }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19870
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(KeyEventBatchQueryInput, array(KeyEventsForDeviceSchema).readonly()), method(object({
19700
19871
  deviceId: number(),
19701
19872
  since: number(),
19702
19873
  until: number(),
@@ -19745,6 +19916,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19745
19916
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19746
19917
  kind: "query",
19747
19918
  auth: "admin"
19919
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19920
+ kind: "query",
19921
+ auth: "admin"
19748
19922
  }), method(object({
19749
19923
  olderThanMs: number(),
19750
19924
  reason: OpsLogReasonSchema.optional()
@@ -21696,6 +21870,20 @@ method(object({
21696
21870
  error: string().optional()
21697
21871
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21698
21872
  providerId: string(),
21873
+ /**
21874
+ * The location this config is an UNSAVED edit of, when there is one.
21875
+ *
21876
+ * `listLocations` replaces every declared secret with the redaction
21877
+ * sentinel, so the edit modal's form state holds the sentinel for any
21878
+ * credential the operator did not retype — and posting that here
21879
+ * without a way to resolve it makes the provider try to authenticate
21880
+ * as `__camstack_redacted__` and report the operator's own working
21881
+ * password as wrong. Given this id, the orchestrator restores each
21882
+ * sentinel from the stored config (same rule as `upsertLocation`)
21883
+ * before dispatching. Omitted by the "Add location" wizard, where
21884
+ * every value was typed just now and nothing is stored yet.
21885
+ */
21886
+ locationId: string().optional(),
21699
21887
  config: record(string(), unknown())
21700
21888
  }), object({
21701
21889
  ok: boolean(),
@@ -27379,6 +27567,9 @@ method(object({
27379
27567
  }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
27380
27568
  kind: "query",
27381
27569
  auth: "admin"
27570
+ }), method(LedgerWalkInputSchema, LedgerWalkReportSchema, {
27571
+ kind: "mutation",
27572
+ auth: "admin"
27382
27573
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
27383
27574
  kind: "mutation",
27384
27575
  auth: "admin"
@@ -27770,7 +27961,26 @@ var SceneMonitorStatusSchema = object({
27770
27961
  monitors: array(SceneMonitorSchema),
27771
27962
  lastFetchedAt: number()
27772
27963
  });
27773
- DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({
27964
+ /**
27965
+ * One camera's row in a `listScenesBatch` answer.
27966
+ *
27967
+ * `status` is NULLABLE and the nullability is the whole point. `scene-monitor`
27968
+ * is a `defaultActive` wrapper, so every camera is asked; a camera whose
27969
+ * provider is absent (pipeline-analytics not deployed, the post-processing node
27970
+ * down) cannot answer, and that is not the same fact as a camera with no scenes
27971
+ * configured. Fanned out per camera the difference was visible — one query
27972
+ * errored while the others resolved — and a batch that returned only the rows
27973
+ * it managed would have destroyed it, silently, by making an unreachable camera
27974
+ * indistinguishable from one that answered `monitors: []`.
27975
+ *
27976
+ * So: EVERY requested deviceId gets a row. `status: null` means "this camera
27977
+ * could not be read"; `status.monitors: []` means "read, and it has none".
27978
+ */
27979
+ var SceneMonitorStatusForDeviceSchema = object({
27980
+ deviceId: number(),
27981
+ status: SceneMonitorStatusSchema.nullable()
27982
+ });
27983
+ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({ deviceIds: array(number()).min(1).max(200) }), array(SceneMonitorStatusForDeviceSchema).readonly()), method(object({
27774
27984
  deviceId: number(),
27775
27985
  label: string(),
27776
27986
  roi: MaskRectShapeSchema,
@@ -29119,6 +29329,27 @@ var CameraOccupancySnapshotSchema = object({
29119
29329
  stationaryObjects: array(StationaryObjectSchema).readonly().optional()
29120
29330
  });
29121
29331
  /**
29332
+ * One camera's row in a `getCurrentSnapshotBatch` answer.
29333
+ *
29334
+ * THREE outcomes, and the single-camera method could only express two of them
29335
+ * because `snapshot: null` was already spoken for:
29336
+ *
29337
+ * - `read: 'read'`, `snapshot` present — the live occupancy reading.
29338
+ * - `read: 'read'`, `snapshot: null` — read, and this camera has no reading
29339
+ * yet: no frame since boot, and no parked-object registry to hydrate from.
29340
+ * - `read: 'unreadable'` — the owner could not answer for this
29341
+ * camera. `snapshot` is null, and it does NOT mean "nothing parked here".
29342
+ *
29343
+ * Collapsing the last two is the failure this field exists to prevent: a
29344
+ * hydration that threw would otherwise render as an empty Stationary section,
29345
+ * which is a definite claim about a camera nobody could read.
29346
+ */
29347
+ var CameraOccupancySnapshotForDeviceSchema = object({
29348
+ deviceId: number(),
29349
+ read: _enum(["read", "unreadable"]),
29350
+ snapshot: CameraOccupancySnapshotSchema.nullable()
29351
+ });
29352
+ /**
29122
29353
  * Time-series resolution. The history methods return one bucket per
29123
29354
  * step over the requested range. Smaller resolutions cost more
29124
29355
  * memory + bandwidth; bound to discrete steps so caller cannot ask
@@ -29142,7 +29373,7 @@ var HistoryPointSchema = object({
29142
29373
  /** Object count averaged over the bucket (rounded to nearest integer). */
29143
29374
  count: number().int().nonnegative()
29144
29375
  });
29145
- DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
29376
+ DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(CameraOccupancySnapshotForDeviceSchema).readonly()), method(object({
29146
29377
  deviceId: number(),
29147
29378
  zoneId: string(),
29148
29379
  className: string().optional()
@@ -32423,6 +32654,12 @@ Object.freeze({
32423
32654
  addonId: null,
32424
32655
  access: "view"
32425
32656
  },
32657
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
32658
+ capName: "pipeline-analytics",
32659
+ capScope: "device",
32660
+ addonId: null,
32661
+ access: "view"
32662
+ },
32426
32663
  "pipelineAnalytics.getEventStoreFootprint": {
32427
32664
  capName: "pipeline-analytics",
32428
32665
  capScope: "device",
@@ -32441,6 +32678,12 @@ Object.freeze({
32441
32678
  addonId: null,
32442
32679
  access: "view"
32443
32680
  },
32681
+ "pipelineAnalytics.getKeyEventsBatch": {
32682
+ capName: "pipeline-analytics",
32683
+ capScope: "device",
32684
+ addonId: null,
32685
+ access: "view"
32686
+ },
32444
32687
  "pipelineAnalytics.getMotionEvents": {
32445
32688
  capName: "pipeline-analytics",
32446
32689
  capScope: "device",
@@ -33617,6 +33860,12 @@ Object.freeze({
33617
33860
  addonId: null,
33618
33861
  access: "view"
33619
33862
  },
33863
+ "recording.reconcileLedgerAgainstDisk": {
33864
+ capName: "recording",
33865
+ capScope: "system",
33866
+ addonId: null,
33867
+ access: "create"
33868
+ },
33620
33869
  "recording.refreshStorageLocationsForMigration": {
33621
33870
  capName: "recording",
33622
33871
  capScope: "system",
@@ -33743,6 +33992,12 @@ Object.freeze({
33743
33992
  addonId: null,
33744
33993
  access: "view"
33745
33994
  },
33995
+ "sceneMonitor.listScenesBatch": {
33996
+ capName: "scene-monitor",
33997
+ capScope: "device",
33998
+ addonId: null,
33999
+ access: "view"
34000
+ },
33746
34001
  "sceneMonitor.recheckNow": {
33747
34002
  capName: "scene-monitor",
33748
34003
  capScope: "device",
@@ -35099,6 +35354,12 @@ Object.freeze({
35099
35354
  addonId: null,
35100
35355
  access: "view"
35101
35356
  },
35357
+ "zoneAnalytics.getCurrentSnapshotBatch": {
35358
+ capName: "zone-analytics",
35359
+ capScope: "device",
35360
+ addonId: null,
35361
+ access: "view"
35362
+ },
35102
35363
  "zoneAnalytics.getUnzonedHistory": {
35103
35364
  capName: "zone-analytics",
35104
35365
  capScope: "device",
@@ -36088,6 +36349,11 @@ Object.freeze({
36088
36349
  form: "single",
36089
36350
  optional: false
36090
36351
  }],
36352
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
36353
+ name: "deviceId",
36354
+ form: "single",
36355
+ optional: true
36356
+ }],
36091
36357
  "pipelineAnalytics.getGroup": [{
36092
36358
  name: "deviceId",
36093
36359
  form: "single",
@@ -36098,6 +36364,11 @@ Object.freeze({
36098
36364
  form: "single",
36099
36365
  optional: false
36100
36366
  }],
36367
+ "pipelineAnalytics.getKeyEventsBatch": [{
36368
+ name: "deviceIds",
36369
+ form: "array",
36370
+ optional: false
36371
+ }],
36101
36372
  "pipelineAnalytics.getMotionEvents": [{
36102
36373
  name: "deviceId",
36103
36374
  form: "single",
@@ -36538,6 +36809,11 @@ Object.freeze({
36538
36809
  form: "single",
36539
36810
  optional: false
36540
36811
  }],
36812
+ "recording.reconcileLedgerAgainstDisk": [{
36813
+ name: "deviceId",
36814
+ form: "single",
36815
+ optional: true
36816
+ }],
36541
36817
  "recording.relocateFootage": [{
36542
36818
  name: "deviceId",
36543
36819
  form: "single",
@@ -36603,6 +36879,11 @@ Object.freeze({
36603
36879
  form: "single",
36604
36880
  optional: false
36605
36881
  }],
36882
+ "sceneMonitor.listScenesBatch": [{
36883
+ name: "deviceIds",
36884
+ form: "array",
36885
+ optional: false
36886
+ }],
36606
36887
  "sceneMonitor.recheckNow": [{
36607
36888
  name: "deviceId",
36608
36889
  form: "single",
@@ -36864,6 +37145,11 @@ Object.freeze({
36864
37145
  form: "single",
36865
37146
  optional: false
36866
37147
  }],
37148
+ "zoneAnalytics.getCurrentSnapshotBatch": [{
37149
+ name: "deviceIds",
37150
+ form: "array",
37151
+ optional: false
37152
+ }],
36867
37153
  "zoneAnalytics.getUnzonedHistory": [{
36868
37154
  name: "deviceId",
36869
37155
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-export-hap",
3
- "version": "1.2.58",
3
+ "version": "1.2.60",
4
4
  "description": "HomeKit (HAP) exporter for CamStack devices. Publishes each exposed device as its own HomeKit accessory: cameras and doorbells with SRTP streaming, HomeKit Secure Video, motion, two-way audio, PTZ and battery; switches, lights, locks and sensors through a capability→service table.",
5
5
  "keywords": [
6
6
  "camstack",