@camstack/addon-export-google 0.1.11 → 0.1.13

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.
@@ -8211,6 +8211,20 @@ var RelocateJobSchema = object({
8211
8211
  * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8212
8212
  */
8213
8213
  rowsReconciled: number().int().nonnegative().optional(),
8214
+ /**
8215
+ * Rows this run FORGOT because the file they name is not on disk.
8216
+ *
8217
+ * The mover derived the path from the row's own fields and `stat`ed it; an
8218
+ * ENOENT there is a per-path confirmation that the segment is gone (D296),
8219
+ * and the durable row is dropped through the same channel eviction uses. It
8220
+ * is reported for the same reason `rowsReconciled` is: this is a durable
8221
+ * mutation nobody asked for, and a migration that quietly erases hour rows is
8222
+ * the same failure as one that quietly skips them (D295).
8223
+ *
8224
+ * The production drain of 2026-08-30 would have reported 11 074 here — the
8225
+ * ledger claimed 5.65 GB of footage that no longer existed.
8226
+ */
8227
+ rowsForgotten: number().int().nonnegative().optional(),
8214
8228
  startedAt: number(),
8215
8229
  finishedAt: number().nullable(),
8216
8230
  error: string().nullable()
@@ -8574,6 +8588,91 @@ var RelocateResidueSchema = object({
8574
8588
  segments: number().int().nonnegative(),
8575
8589
  bytes: number().int().nonnegative()
8576
8590
  }).nullable();
8591
+ /**
8592
+ * Ask one location whether its durable hour rows describe the disk — the walk
8593
+ * (D319).
8594
+ *
8595
+ * `apply` DEFAULTS TO FALSE and that default is the product: the operator's
8596
+ * missing tool is the question, and the dry run is how they sanity-check the
8597
+ * destructive run before authorising it.
8598
+ */
8599
+ var LedgerWalkInputSchema = object({
8600
+ locationId: string().min(1),
8601
+ /** Forget the confirmed-absent rows, rather than only counting them. */
8602
+ apply: boolean().optional(),
8603
+ /** Narrow to one camera. */
8604
+ deviceId: number().int().positive().optional(),
8605
+ /** Narrow to these recording profiles; empty/absent = every profile. */
8606
+ profiles: array(string().min(1)).optional()
8607
+ });
8608
+ /** Why a whole walk did nothing. Every one leaves the ledger untouched. */
8609
+ var LedgerWalkRefusalSchema = _enum([
8610
+ "location-unknown",
8611
+ "source-writable",
8612
+ "no-ledger",
8613
+ "archive-unreadable",
8614
+ "anchor-absent",
8615
+ "anchor-unreadable",
8616
+ "anchor-moved"
8617
+ ]);
8618
+ _enum([
8619
+ "live-tail",
8620
+ "listing-error",
8621
+ "path-mismatch",
8622
+ "durable-refused"
8623
+ ]);
8624
+ /** Every skip reason, always present, always a number — so a reason that never
8625
+ * fired reports as zero rather than absent and the report shape is constant
8626
+ * between passes. Spelled out rather than `z.record` for exactly that. */
8627
+ var LedgerWalkSkipCountsSchema = object({
8628
+ "live-tail": number().int().nonnegative(),
8629
+ "listing-error": number().int().nonnegative(),
8630
+ "path-mismatch": number().int().nonnegative(),
8631
+ "durable-refused": number().int().nonnegative()
8632
+ });
8633
+ /** One camera's share of a walk, so a report names cameras and not rows. */
8634
+ var LedgerWalkDeviceReportSchema = object({
8635
+ deviceId: number().int(),
8636
+ hoursWalked: number().int().nonnegative(),
8637
+ hoursMissing: number().int().nonnegative(),
8638
+ ghostSegments: number().int().nonnegative(),
8639
+ ghostBytes: number().int().nonnegative(),
8640
+ forgottenSegments: number().int().nonnegative(),
8641
+ orphanFiles: number().int().nonnegative()
8642
+ });
8643
+ /**
8644
+ * What one walk claimed, listed, found and (only when armed) forgot.
8645
+ *
8646
+ * `archiveSegments` is the **M** and `segmentsClaimed` the **N** (D295), so a
8647
+ * walk that saw a fraction of the location is visible in its own report rather
8648
+ * than in the absence of one.
8649
+ */
8650
+ var LedgerWalkReportSchema = object({
8651
+ locationId: string(),
8652
+ applied: boolean(),
8653
+ refused: LedgerWalkRefusalSchema.nullable(),
8654
+ archiveSegments: number().int().nonnegative().nullable(),
8655
+ archiveBytes: number().int().nonnegative().nullable(),
8656
+ hoursClaimed: number().int().nonnegative(),
8657
+ hoursWalked: number().int().nonnegative(),
8658
+ hoursMissing: number().int().nonnegative(),
8659
+ /** `readdir` calls issued — the cost, stated in the unit that is paid. */
8660
+ listings: number().int().nonnegative(),
8661
+ segmentsClaimed: number().int().nonnegative(),
8662
+ ghostSegments: number().int().nonnegative(),
8663
+ ghostBytes: number().int().nonnegative(),
8664
+ ghostHoursWhole: number().int().nonnegative(),
8665
+ forgottenSegments: number().int().nonnegative(),
8666
+ forgottenBytes: number().int().nonnegative(),
8667
+ /** Files under a claimed hour that no durable row names. Never deleted. */
8668
+ orphanFiles: number().int().nonnegative(),
8669
+ orphanSample: array(string()).readonly(),
8670
+ hoursSkipped: number().int().nonnegative(),
8671
+ skippedByReason: LedgerWalkSkipCountsSchema,
8672
+ /** The walk stopped at its per-pass hour bound with claims unwalked. */
8673
+ bounded: boolean(),
8674
+ byDevice: array(LedgerWalkDeviceReportSchema).readonly()
8675
+ });
8577
8676
  /** How many rows a media pass would still act on against a given target — the
8578
8677
  * media lane's denominator AND its residue, from ONE derivation so the two can
8579
8678
  * never disagree. `null` = the count could not be taken. */
@@ -18936,13 +19035,15 @@ var ListGroupsPageSchema = object({
18936
19035
  groups: array(AnalyticsGroupRecordSchema).readonly(),
18937
19036
  nextCursor: string().nullable()
18938
19037
  });
19038
+ var KEY_EVENTS_DEFAULT_LIMIT = 50;
19039
+ var KEY_EVENTS_MAX_LIMIT = 200;
18939
19040
  var KeyEventQueryInput = object({
18940
19041
  deviceId: number(),
18941
19042
  /** Window lower bound (track firstSeen ≥ since). */
18942
19043
  since: number(),
18943
19044
  /** Window upper bound (track firstSeen ≤ until). */
18944
19045
  until: number(),
18945
- limit: number().int().min(1).max(200).default(50),
19046
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
18946
19047
  /** Drop tracks scoring below this importance. */
18947
19048
  minImportance: number().min(0).max(1).optional(),
18948
19049
  /** Restrict to a single class (e.g. 'person'). */
@@ -18964,6 +19065,32 @@ var KeyEventSchema = object({
18964
19065
  ...TrackFlagFields,
18965
19066
  ...TrackRetrainFields
18966
19067
  });
19068
+ /** `getKeyEvents`' window and filters, asked of a SET of cameras at once. */
19069
+ var KeyEventBatchQueryInput = object({
19070
+ deviceIds: array(number()).min(1).max(200),
19071
+ since: number(),
19072
+ until: number(),
19073
+ /** Applied PER CAMERA, exactly as `getKeyEvents.limit` is — never a total
19074
+ * across the set, which would let a busy camera starve a quiet one of its
19075
+ * rows and change what the merged feed contains. */
19076
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19077
+ minImportance: number().min(0).max(1).optional(),
19078
+ classFilter: string().optional()
19079
+ });
19080
+ /**
19081
+ * One camera's key events in a batch answer.
19082
+ *
19083
+ * The row exists for every requested id. `getKeyEvents` degrades to `[]` on
19084
+ * error rather than throwing, so a camera whose store read failed and one with
19085
+ * no events in the window were ALREADY indistinguishable per camera — the
19086
+ * batch does not make that worse, and the row keeps the deviceId the single
19087
+ * method's output never carried (the caller used to stamp it from the fan-out
19088
+ * key, which only worked because there was one query per camera).
19089
+ */
19090
+ var KeyEventsForDeviceSchema = object({
19091
+ deviceId: number(),
19092
+ events: array(KeyEventSchema).readonly()
19093
+ });
18967
19094
  object({
18968
19095
  trackId: string(),
18969
19096
  className: string(),
@@ -19035,6 +19162,50 @@ var EventStoreFootprintSchema = object({
19035
19162
  totalBytes: number().int(),
19036
19163
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19037
19164
  });
19165
+ /** Event-media footprint for one {@link MediaFileKind}. */
19166
+ var EventMediaKindFootprintSchema = object({
19167
+ kind: MediaFileKindEnum,
19168
+ /** Media rows of this kind. */
19169
+ rows: number().int(),
19170
+ /** Bytes on disk held by those rows. */
19171
+ bytes: number().int()
19172
+ });
19173
+ /**
19174
+ * The media footprint broken down by KIND — the axis a deletion decision
19175
+ * actually turns on.
19176
+ *
19177
+ * A byte total says how much there is; it cannot say what is safe to remove.
19178
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19179
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19180
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19181
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19182
+ * nothing else, so sizing a deletion means summing per kind.
19183
+ *
19184
+ * ## Why `unaccounted*` exists
19185
+ *
19186
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19187
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19188
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19189
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19190
+ * retired code path, or by a version that knew a kind this one does not) would
19191
+ * otherwise vanish from the total silently, and an operator would delete
19192
+ * against a denominator smaller than the disk.
19193
+ *
19194
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19195
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19196
+ */
19197
+ var EventMediaKindBreakdownSchema = object({
19198
+ /** Every media row in scope, from one unfiltered aggregate. */
19199
+ totalRows: number().int(),
19200
+ /** Every media byte in scope, from that same aggregate. */
19201
+ totalBytes: number().int(),
19202
+ /** Per-kind footprint, ordered by bytes descending. */
19203
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19204
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19205
+ unaccountedRows: number().int(),
19206
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19207
+ unaccountedBytes: number().int()
19208
+ });
19038
19209
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19039
19210
  var EventPruneCountsSchema = object({
19040
19211
  motion: number().int(),
@@ -19189,7 +19360,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19189
19360
  until: number().optional(),
19190
19361
  kinds: array(string()).optional(),
19191
19362
  limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
19192
- }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19363
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(KeyEventBatchQueryInput, array(KeyEventsForDeviceSchema).readonly()), method(object({
19193
19364
  deviceId: number(),
19194
19365
  since: number(),
19195
19366
  until: number(),
@@ -19238,6 +19409,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19238
19409
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19239
19410
  kind: "query",
19240
19411
  auth: "admin"
19412
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19413
+ kind: "query",
19414
+ auth: "admin"
19241
19415
  }), method(object({
19242
19416
  olderThanMs: number(),
19243
19417
  reason: OpsLogReasonSchema.optional()
@@ -21189,6 +21363,20 @@ method(object({
21189
21363
  error: string().optional()
21190
21364
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21191
21365
  providerId: string(),
21366
+ /**
21367
+ * The location this config is an UNSAVED edit of, when there is one.
21368
+ *
21369
+ * `listLocations` replaces every declared secret with the redaction
21370
+ * sentinel, so the edit modal's form state holds the sentinel for any
21371
+ * credential the operator did not retype — and posting that here
21372
+ * without a way to resolve it makes the provider try to authenticate
21373
+ * as `__camstack_redacted__` and report the operator's own working
21374
+ * password as wrong. Given this id, the orchestrator restores each
21375
+ * sentinel from the stored config (same rule as `upsertLocation`)
21376
+ * before dispatching. Omitted by the "Add location" wizard, where
21377
+ * every value was typed just now and nothing is stored yet.
21378
+ */
21379
+ locationId: string().optional(),
21192
21380
  config: record(string(), unknown())
21193
21381
  }), object({
21194
21382
  ok: boolean(),
@@ -26836,6 +27024,9 @@ method(object({
26836
27024
  }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
26837
27025
  kind: "query",
26838
27026
  auth: "admin"
27027
+ }), method(LedgerWalkInputSchema, LedgerWalkReportSchema, {
27028
+ kind: "mutation",
27029
+ auth: "admin"
26839
27030
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
26840
27031
  kind: "mutation",
26841
27032
  auth: "admin"
@@ -27227,7 +27418,26 @@ var SceneMonitorStatusSchema = object({
27227
27418
  monitors: array(SceneMonitorSchema),
27228
27419
  lastFetchedAt: number()
27229
27420
  });
27230
- DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({
27421
+ /**
27422
+ * One camera's row in a `listScenesBatch` answer.
27423
+ *
27424
+ * `status` is NULLABLE and the nullability is the whole point. `scene-monitor`
27425
+ * is a `defaultActive` wrapper, so every camera is asked; a camera whose
27426
+ * provider is absent (pipeline-analytics not deployed, the post-processing node
27427
+ * down) cannot answer, and that is not the same fact as a camera with no scenes
27428
+ * configured. Fanned out per camera the difference was visible — one query
27429
+ * errored while the others resolved — and a batch that returned only the rows
27430
+ * it managed would have destroyed it, silently, by making an unreachable camera
27431
+ * indistinguishable from one that answered `monitors: []`.
27432
+ *
27433
+ * So: EVERY requested deviceId gets a row. `status: null` means "this camera
27434
+ * could not be read"; `status.monitors: []` means "read, and it has none".
27435
+ */
27436
+ var SceneMonitorStatusForDeviceSchema = object({
27437
+ deviceId: number(),
27438
+ status: SceneMonitorStatusSchema.nullable()
27439
+ });
27440
+ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({ deviceIds: array(number()).min(1).max(200) }), array(SceneMonitorStatusForDeviceSchema).readonly()), method(object({
27231
27441
  deviceId: number(),
27232
27442
  label: string(),
27233
27443
  roi: MaskRectShapeSchema,
@@ -28558,6 +28768,27 @@ var CameraOccupancySnapshotSchema = object({
28558
28768
  stationaryObjects: array(StationaryObjectSchema).readonly().optional()
28559
28769
  });
28560
28770
  /**
28771
+ * One camera's row in a `getCurrentSnapshotBatch` answer.
28772
+ *
28773
+ * THREE outcomes, and the single-camera method could only express two of them
28774
+ * because `snapshot: null` was already spoken for:
28775
+ *
28776
+ * - `read: 'read'`, `snapshot` present — the live occupancy reading.
28777
+ * - `read: 'read'`, `snapshot: null` — read, and this camera has no reading
28778
+ * yet: no frame since boot, and no parked-object registry to hydrate from.
28779
+ * - `read: 'unreadable'` — the owner could not answer for this
28780
+ * camera. `snapshot` is null, and it does NOT mean "nothing parked here".
28781
+ *
28782
+ * Collapsing the last two is the failure this field exists to prevent: a
28783
+ * hydration that threw would otherwise render as an empty Stationary section,
28784
+ * which is a definite claim about a camera nobody could read.
28785
+ */
28786
+ var CameraOccupancySnapshotForDeviceSchema = object({
28787
+ deviceId: number(),
28788
+ read: _enum(["read", "unreadable"]),
28789
+ snapshot: CameraOccupancySnapshotSchema.nullable()
28790
+ });
28791
+ /**
28561
28792
  * Time-series resolution. The history methods return one bucket per
28562
28793
  * step over the requested range. Smaller resolutions cost more
28563
28794
  * memory + bandwidth; bound to discrete steps so caller cannot ask
@@ -28581,7 +28812,7 @@ var HistoryPointSchema = object({
28581
28812
  /** Object count averaged over the bucket (rounded to nearest integer). */
28582
28813
  count: number().int().nonnegative()
28583
28814
  });
28584
- DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
28815
+ DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(CameraOccupancySnapshotForDeviceSchema).readonly()), method(object({
28585
28816
  deviceId: number(),
28586
28817
  zoneId: string(),
28587
28818
  className: string().optional()
@@ -31862,6 +32093,12 @@ Object.freeze({
31862
32093
  addonId: null,
31863
32094
  access: "view"
31864
32095
  },
32096
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
32097
+ capName: "pipeline-analytics",
32098
+ capScope: "device",
32099
+ addonId: null,
32100
+ access: "view"
32101
+ },
31865
32102
  "pipelineAnalytics.getEventStoreFootprint": {
31866
32103
  capName: "pipeline-analytics",
31867
32104
  capScope: "device",
@@ -31880,6 +32117,12 @@ Object.freeze({
31880
32117
  addonId: null,
31881
32118
  access: "view"
31882
32119
  },
32120
+ "pipelineAnalytics.getKeyEventsBatch": {
32121
+ capName: "pipeline-analytics",
32122
+ capScope: "device",
32123
+ addonId: null,
32124
+ access: "view"
32125
+ },
31883
32126
  "pipelineAnalytics.getMotionEvents": {
31884
32127
  capName: "pipeline-analytics",
31885
32128
  capScope: "device",
@@ -33056,6 +33299,12 @@ Object.freeze({
33056
33299
  addonId: null,
33057
33300
  access: "view"
33058
33301
  },
33302
+ "recording.reconcileLedgerAgainstDisk": {
33303
+ capName: "recording",
33304
+ capScope: "system",
33305
+ addonId: null,
33306
+ access: "create"
33307
+ },
33059
33308
  "recording.refreshStorageLocationsForMigration": {
33060
33309
  capName: "recording",
33061
33310
  capScope: "system",
@@ -33182,6 +33431,12 @@ Object.freeze({
33182
33431
  addonId: null,
33183
33432
  access: "view"
33184
33433
  },
33434
+ "sceneMonitor.listScenesBatch": {
33435
+ capName: "scene-monitor",
33436
+ capScope: "device",
33437
+ addonId: null,
33438
+ access: "view"
33439
+ },
33185
33440
  "sceneMonitor.recheckNow": {
33186
33441
  capName: "scene-monitor",
33187
33442
  capScope: "device",
@@ -34538,6 +34793,12 @@ Object.freeze({
34538
34793
  addonId: null,
34539
34794
  access: "view"
34540
34795
  },
34796
+ "zoneAnalytics.getCurrentSnapshotBatch": {
34797
+ capName: "zone-analytics",
34798
+ capScope: "device",
34799
+ addonId: null,
34800
+ access: "view"
34801
+ },
34541
34802
  "zoneAnalytics.getUnzonedHistory": {
34542
34803
  capName: "zone-analytics",
34543
34804
  capScope: "device",
@@ -35527,6 +35788,11 @@ Object.freeze({
35527
35788
  form: "single",
35528
35789
  optional: false
35529
35790
  }],
35791
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
35792
+ name: "deviceId",
35793
+ form: "single",
35794
+ optional: true
35795
+ }],
35530
35796
  "pipelineAnalytics.getGroup": [{
35531
35797
  name: "deviceId",
35532
35798
  form: "single",
@@ -35537,6 +35803,11 @@ Object.freeze({
35537
35803
  form: "single",
35538
35804
  optional: false
35539
35805
  }],
35806
+ "pipelineAnalytics.getKeyEventsBatch": [{
35807
+ name: "deviceIds",
35808
+ form: "array",
35809
+ optional: false
35810
+ }],
35540
35811
  "pipelineAnalytics.getMotionEvents": [{
35541
35812
  name: "deviceId",
35542
35813
  form: "single",
@@ -35977,6 +36248,11 @@ Object.freeze({
35977
36248
  form: "single",
35978
36249
  optional: false
35979
36250
  }],
36251
+ "recording.reconcileLedgerAgainstDisk": [{
36252
+ name: "deviceId",
36253
+ form: "single",
36254
+ optional: true
36255
+ }],
35980
36256
  "recording.relocateFootage": [{
35981
36257
  name: "deviceId",
35982
36258
  form: "single",
@@ -36042,6 +36318,11 @@ Object.freeze({
36042
36318
  form: "single",
36043
36319
  optional: false
36044
36320
  }],
36321
+ "sceneMonitor.listScenesBatch": [{
36322
+ name: "deviceIds",
36323
+ form: "array",
36324
+ optional: false
36325
+ }],
36045
36326
  "sceneMonitor.recheckNow": [{
36046
36327
  name: "deviceId",
36047
36328
  form: "single",
@@ -36303,6 +36584,11 @@ Object.freeze({
36303
36584
  form: "single",
36304
36585
  optional: false
36305
36586
  }],
36587
+ "zoneAnalytics.getCurrentSnapshotBatch": [{
36588
+ name: "deviceIds",
36589
+ form: "array",
36590
+ optional: false
36591
+ }],
36306
36592
  "zoneAnalytics.getUnzonedHistory": [{
36307
36593
  name: "deviceId",
36308
36594
  form: "single",
@@ -8207,6 +8207,20 @@ var RelocateJobSchema = object({
8207
8207
  * in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
8208
8208
  */
8209
8209
  rowsReconciled: number().int().nonnegative().optional(),
8210
+ /**
8211
+ * Rows this run FORGOT because the file they name is not on disk.
8212
+ *
8213
+ * The mover derived the path from the row's own fields and `stat`ed it; an
8214
+ * ENOENT there is a per-path confirmation that the segment is gone (D296),
8215
+ * and the durable row is dropped through the same channel eviction uses. It
8216
+ * is reported for the same reason `rowsReconciled` is: this is a durable
8217
+ * mutation nobody asked for, and a migration that quietly erases hour rows is
8218
+ * the same failure as one that quietly skips them (D295).
8219
+ *
8220
+ * The production drain of 2026-08-30 would have reported 11 074 here — the
8221
+ * ledger claimed 5.65 GB of footage that no longer existed.
8222
+ */
8223
+ rowsForgotten: number().int().nonnegative().optional(),
8210
8224
  startedAt: number(),
8211
8225
  finishedAt: number().nullable(),
8212
8226
  error: string().nullable()
@@ -8570,6 +8584,91 @@ var RelocateResidueSchema = object({
8570
8584
  segments: number().int().nonnegative(),
8571
8585
  bytes: number().int().nonnegative()
8572
8586
  }).nullable();
8587
+ /**
8588
+ * Ask one location whether its durable hour rows describe the disk — the walk
8589
+ * (D319).
8590
+ *
8591
+ * `apply` DEFAULTS TO FALSE and that default is the product: the operator's
8592
+ * missing tool is the question, and the dry run is how they sanity-check the
8593
+ * destructive run before authorising it.
8594
+ */
8595
+ var LedgerWalkInputSchema = object({
8596
+ locationId: string().min(1),
8597
+ /** Forget the confirmed-absent rows, rather than only counting them. */
8598
+ apply: boolean().optional(),
8599
+ /** Narrow to one camera. */
8600
+ deviceId: number().int().positive().optional(),
8601
+ /** Narrow to these recording profiles; empty/absent = every profile. */
8602
+ profiles: array(string().min(1)).optional()
8603
+ });
8604
+ /** Why a whole walk did nothing. Every one leaves the ledger untouched. */
8605
+ var LedgerWalkRefusalSchema = _enum([
8606
+ "location-unknown",
8607
+ "source-writable",
8608
+ "no-ledger",
8609
+ "archive-unreadable",
8610
+ "anchor-absent",
8611
+ "anchor-unreadable",
8612
+ "anchor-moved"
8613
+ ]);
8614
+ _enum([
8615
+ "live-tail",
8616
+ "listing-error",
8617
+ "path-mismatch",
8618
+ "durable-refused"
8619
+ ]);
8620
+ /** Every skip reason, always present, always a number — so a reason that never
8621
+ * fired reports as zero rather than absent and the report shape is constant
8622
+ * between passes. Spelled out rather than `z.record` for exactly that. */
8623
+ var LedgerWalkSkipCountsSchema = object({
8624
+ "live-tail": number().int().nonnegative(),
8625
+ "listing-error": number().int().nonnegative(),
8626
+ "path-mismatch": number().int().nonnegative(),
8627
+ "durable-refused": number().int().nonnegative()
8628
+ });
8629
+ /** One camera's share of a walk, so a report names cameras and not rows. */
8630
+ var LedgerWalkDeviceReportSchema = object({
8631
+ deviceId: number().int(),
8632
+ hoursWalked: number().int().nonnegative(),
8633
+ hoursMissing: number().int().nonnegative(),
8634
+ ghostSegments: number().int().nonnegative(),
8635
+ ghostBytes: number().int().nonnegative(),
8636
+ forgottenSegments: number().int().nonnegative(),
8637
+ orphanFiles: number().int().nonnegative()
8638
+ });
8639
+ /**
8640
+ * What one walk claimed, listed, found and (only when armed) forgot.
8641
+ *
8642
+ * `archiveSegments` is the **M** and `segmentsClaimed` the **N** (D295), so a
8643
+ * walk that saw a fraction of the location is visible in its own report rather
8644
+ * than in the absence of one.
8645
+ */
8646
+ var LedgerWalkReportSchema = object({
8647
+ locationId: string(),
8648
+ applied: boolean(),
8649
+ refused: LedgerWalkRefusalSchema.nullable(),
8650
+ archiveSegments: number().int().nonnegative().nullable(),
8651
+ archiveBytes: number().int().nonnegative().nullable(),
8652
+ hoursClaimed: number().int().nonnegative(),
8653
+ hoursWalked: number().int().nonnegative(),
8654
+ hoursMissing: number().int().nonnegative(),
8655
+ /** `readdir` calls issued — the cost, stated in the unit that is paid. */
8656
+ listings: number().int().nonnegative(),
8657
+ segmentsClaimed: number().int().nonnegative(),
8658
+ ghostSegments: number().int().nonnegative(),
8659
+ ghostBytes: number().int().nonnegative(),
8660
+ ghostHoursWhole: number().int().nonnegative(),
8661
+ forgottenSegments: number().int().nonnegative(),
8662
+ forgottenBytes: number().int().nonnegative(),
8663
+ /** Files under a claimed hour that no durable row names. Never deleted. */
8664
+ orphanFiles: number().int().nonnegative(),
8665
+ orphanSample: array(string()).readonly(),
8666
+ hoursSkipped: number().int().nonnegative(),
8667
+ skippedByReason: LedgerWalkSkipCountsSchema,
8668
+ /** The walk stopped at its per-pass hour bound with claims unwalked. */
8669
+ bounded: boolean(),
8670
+ byDevice: array(LedgerWalkDeviceReportSchema).readonly()
8671
+ });
8573
8672
  /** How many rows a media pass would still act on against a given target — the
8574
8673
  * media lane's denominator AND its residue, from ONE derivation so the two can
8575
8674
  * never disagree. `null` = the count could not be taken. */
@@ -18932,13 +19031,15 @@ var ListGroupsPageSchema = object({
18932
19031
  groups: array(AnalyticsGroupRecordSchema).readonly(),
18933
19032
  nextCursor: string().nullable()
18934
19033
  });
19034
+ var KEY_EVENTS_DEFAULT_LIMIT = 50;
19035
+ var KEY_EVENTS_MAX_LIMIT = 200;
18935
19036
  var KeyEventQueryInput = object({
18936
19037
  deviceId: number(),
18937
19038
  /** Window lower bound (track firstSeen ≥ since). */
18938
19039
  since: number(),
18939
19040
  /** Window upper bound (track firstSeen ≤ until). */
18940
19041
  until: number(),
18941
- limit: number().int().min(1).max(200).default(50),
19042
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
18942
19043
  /** Drop tracks scoring below this importance. */
18943
19044
  minImportance: number().min(0).max(1).optional(),
18944
19045
  /** Restrict to a single class (e.g. 'person'). */
@@ -18960,6 +19061,32 @@ var KeyEventSchema = object({
18960
19061
  ...TrackFlagFields,
18961
19062
  ...TrackRetrainFields
18962
19063
  });
19064
+ /** `getKeyEvents`' window and filters, asked of a SET of cameras at once. */
19065
+ var KeyEventBatchQueryInput = object({
19066
+ deviceIds: array(number()).min(1).max(200),
19067
+ since: number(),
19068
+ until: number(),
19069
+ /** Applied PER CAMERA, exactly as `getKeyEvents.limit` is — never a total
19070
+ * across the set, which would let a busy camera starve a quiet one of its
19071
+ * rows and change what the merged feed contains. */
19072
+ limit: number().int().min(1).max(KEY_EVENTS_MAX_LIMIT).default(KEY_EVENTS_DEFAULT_LIMIT),
19073
+ minImportance: number().min(0).max(1).optional(),
19074
+ classFilter: string().optional()
19075
+ });
19076
+ /**
19077
+ * One camera's key events in a batch answer.
19078
+ *
19079
+ * The row exists for every requested id. `getKeyEvents` degrades to `[]` on
19080
+ * error rather than throwing, so a camera whose store read failed and one with
19081
+ * no events in the window were ALREADY indistinguishable per camera — the
19082
+ * batch does not make that worse, and the row keeps the deviceId the single
19083
+ * method's output never carried (the caller used to stamp it from the fan-out
19084
+ * key, which only worked because there was one query per camera).
19085
+ */
19086
+ var KeyEventsForDeviceSchema = object({
19087
+ deviceId: number(),
19088
+ events: array(KeyEventSchema).readonly()
19089
+ });
18963
19090
  object({
18964
19091
  trackId: string(),
18965
19092
  className: string(),
@@ -19031,6 +19158,50 @@ var EventStoreFootprintSchema = object({
19031
19158
  totalBytes: number().int(),
19032
19159
  devices: array(EventStoreDeviceFootprintSchema).readonly()
19033
19160
  });
19161
+ /** Event-media footprint for one {@link MediaFileKind}. */
19162
+ var EventMediaKindFootprintSchema = object({
19163
+ kind: MediaFileKindEnum,
19164
+ /** Media rows of this kind. */
19165
+ rows: number().int(),
19166
+ /** Bytes on disk held by those rows. */
19167
+ bytes: number().int()
19168
+ });
19169
+ /**
19170
+ * The media footprint broken down by KIND — the axis a deletion decision
19171
+ * actually turns on.
19172
+ *
19173
+ * A byte total says how much there is; it cannot say what is safe to remove.
19174
+ * The deletable set (the periodic `snapshot` filmstrip, the surplus per-edge
19175
+ * motion stills) and the keep set (`firstFrame`, rolling `lastFrame`,
19176
+ * `thumbnail`/`thumbnailSmall`, `keyFrame`/`keyFrameSmall`, the face/plate
19177
+ * buffers, gallery media, the CLIP `crop`) are distinguished by `kind` and by
19178
+ * nothing else, so sizing a deletion means summing per kind.
19179
+ *
19180
+ * ## Why `unaccounted*` exists
19181
+ *
19182
+ * `kinds` is enumerated from {@link MediaFileKindEnum} — the closed set the
19183
+ * writers use — and summed one kind at a time. `totalRows` / `totalBytes` come
19184
+ * from a SEPARATE unfiltered aggregate over the same rows, never from adding
19185
+ * `kinds` up. A row whose stored `kind` is not in the enum (written by a
19186
+ * retired code path, or by a version that knew a kind this one does not) would
19187
+ * otherwise vanish from the total silently, and an operator would delete
19188
+ * against a denominator smaller than the disk.
19189
+ *
19190
+ * `unaccountedRows` / `unaccountedBytes` are the difference. They are normally
19191
+ * zero; a non-zero value is a real finding and must be shown, not rounded away.
19192
+ */
19193
+ var EventMediaKindBreakdownSchema = object({
19194
+ /** Every media row in scope, from one unfiltered aggregate. */
19195
+ totalRows: number().int(),
19196
+ /** Every media byte in scope, from that same aggregate. */
19197
+ totalBytes: number().int(),
19198
+ /** Per-kind footprint, ordered by bytes descending. */
19199
+ kinds: array(EventMediaKindFootprintSchema).readonly(),
19200
+ /** `totalRows` minus the summed `kinds` rows — see the schema note. */
19201
+ unaccountedRows: number().int(),
19202
+ /** `totalBytes` minus the summed `kinds` bytes — see the schema note. */
19203
+ unaccountedBytes: number().int()
19204
+ });
19034
19205
  /** Per-kind counts returned by the event-prune / device-delete mutations. */
19035
19206
  var EventPruneCountsSchema = object({
19036
19207
  motion: number().int(),
@@ -19185,7 +19356,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19185
19356
  until: number().optional(),
19186
19357
  kinds: array(string()).optional(),
19187
19358
  limit: number().int().min(1).max(MAX_EVENT_QUERY_LIMIT).default(DEFAULT_EVENT_QUERY_LIMIT)
19188
- }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(object({
19359
+ }), array(SensorEventSchema).readonly()), method(KeyEventQueryInput, array(KeyEventSchema).readonly()), method(KeyEventBatchQueryInput, array(KeyEventsForDeviceSchema).readonly()), method(object({
19189
19360
  deviceId: number(),
19190
19361
  since: number(),
19191
19362
  until: number(),
@@ -19234,6 +19405,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
19234
19405
  }), TrackFlagsSchema, { kind: "mutation" }), method(object({}), EventStoreFootprintSchema, {
19235
19406
  kind: "query",
19236
19407
  auth: "admin"
19408
+ }), method(object({ deviceId: number().int().optional() }), EventMediaKindBreakdownSchema, {
19409
+ kind: "query",
19410
+ auth: "admin"
19237
19411
  }), method(object({
19238
19412
  olderThanMs: number(),
19239
19413
  reason: OpsLogReasonSchema.optional()
@@ -21185,6 +21359,20 @@ method(object({
21185
21359
  error: string().optional()
21186
21360
  }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
21187
21361
  providerId: string(),
21362
+ /**
21363
+ * The location this config is an UNSAVED edit of, when there is one.
21364
+ *
21365
+ * `listLocations` replaces every declared secret with the redaction
21366
+ * sentinel, so the edit modal's form state holds the sentinel for any
21367
+ * credential the operator did not retype — and posting that here
21368
+ * without a way to resolve it makes the provider try to authenticate
21369
+ * as `__camstack_redacted__` and report the operator's own working
21370
+ * password as wrong. Given this id, the orchestrator restores each
21371
+ * sentinel from the stored config (same rule as `upsertLocation`)
21372
+ * before dispatching. Omitted by the "Add location" wizard, where
21373
+ * every value was typed just now and nothing is stored yet.
21374
+ */
21375
+ locationId: string().optional(),
21188
21376
  config: record(string(), unknown())
21189
21377
  }), object({
21190
21378
  ok: boolean(),
@@ -26832,6 +27020,9 @@ method(object({
26832
27020
  }), method(RelocateResidueInputSchema, RelocateResidueSchema, {
26833
27021
  kind: "query",
26834
27022
  auth: "admin"
27023
+ }), method(LedgerWalkInputSchema, LedgerWalkReportSchema, {
27024
+ kind: "mutation",
27025
+ auth: "admin"
26835
27026
  }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
26836
27027
  kind: "mutation",
26837
27028
  auth: "admin"
@@ -27223,7 +27414,26 @@ var SceneMonitorStatusSchema = object({
27223
27414
  monitors: array(SceneMonitorSchema),
27224
27415
  lastFetchedAt: number()
27225
27416
  });
27226
- DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({
27417
+ /**
27418
+ * One camera's row in a `listScenesBatch` answer.
27419
+ *
27420
+ * `status` is NULLABLE and the nullability is the whole point. `scene-monitor`
27421
+ * is a `defaultActive` wrapper, so every camera is asked; a camera whose
27422
+ * provider is absent (pipeline-analytics not deployed, the post-processing node
27423
+ * down) cannot answer, and that is not the same fact as a camera with no scenes
27424
+ * configured. Fanned out per camera the difference was visible — one query
27425
+ * errored while the others resolved — and a batch that returned only the rows
27426
+ * it managed would have destroyed it, silently, by making an unreachable camera
27427
+ * indistinguishable from one that answered `monitors: []`.
27428
+ *
27429
+ * So: EVERY requested deviceId gets a row. `status: null` means "this camera
27430
+ * could not be read"; `status.monitors: []` means "read, and it has none".
27431
+ */
27432
+ var SceneMonitorStatusForDeviceSchema = object({
27433
+ deviceId: number(),
27434
+ status: SceneMonitorStatusSchema.nullable()
27435
+ });
27436
+ DeviceType.Camera, method(object({ deviceId: number() }), SceneMonitorStatusSchema), method(object({ deviceIds: array(number()).min(1).max(200) }), array(SceneMonitorStatusForDeviceSchema).readonly()), method(object({
27227
27437
  deviceId: number(),
27228
27438
  label: string(),
27229
27439
  roi: MaskRectShapeSchema,
@@ -28554,6 +28764,27 @@ var CameraOccupancySnapshotSchema = object({
28554
28764
  stationaryObjects: array(StationaryObjectSchema).readonly().optional()
28555
28765
  });
28556
28766
  /**
28767
+ * One camera's row in a `getCurrentSnapshotBatch` answer.
28768
+ *
28769
+ * THREE outcomes, and the single-camera method could only express two of them
28770
+ * because `snapshot: null` was already spoken for:
28771
+ *
28772
+ * - `read: 'read'`, `snapshot` present — the live occupancy reading.
28773
+ * - `read: 'read'`, `snapshot: null` — read, and this camera has no reading
28774
+ * yet: no frame since boot, and no parked-object registry to hydrate from.
28775
+ * - `read: 'unreadable'` — the owner could not answer for this
28776
+ * camera. `snapshot` is null, and it does NOT mean "nothing parked here".
28777
+ *
28778
+ * Collapsing the last two is the failure this field exists to prevent: a
28779
+ * hydration that threw would otherwise render as an empty Stationary section,
28780
+ * which is a definite claim about a camera nobody could read.
28781
+ */
28782
+ var CameraOccupancySnapshotForDeviceSchema = object({
28783
+ deviceId: number(),
28784
+ read: _enum(["read", "unreadable"]),
28785
+ snapshot: CameraOccupancySnapshotSchema.nullable()
28786
+ });
28787
+ /**
28557
28788
  * Time-series resolution. The history methods return one bucket per
28558
28789
  * step over the requested range. Smaller resolutions cost more
28559
28790
  * memory + bandwidth; bound to discrete steps so caller cannot ask
@@ -28577,7 +28808,7 @@ var HistoryPointSchema = object({
28577
28808
  /** Object count averaged over the bucket (rounded to nearest integer). */
28578
28809
  count: number().int().nonnegative()
28579
28810
  });
28580
- DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
28811
+ DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(CameraOccupancySnapshotForDeviceSchema).readonly()), method(object({
28581
28812
  deviceId: number(),
28582
28813
  zoneId: string(),
28583
28814
  className: string().optional()
@@ -31858,6 +32089,12 @@ Object.freeze({
31858
32089
  addonId: null,
31859
32090
  access: "view"
31860
32091
  },
32092
+ "pipelineAnalytics.getEventMediaFootprintByKind": {
32093
+ capName: "pipeline-analytics",
32094
+ capScope: "device",
32095
+ addonId: null,
32096
+ access: "view"
32097
+ },
31861
32098
  "pipelineAnalytics.getEventStoreFootprint": {
31862
32099
  capName: "pipeline-analytics",
31863
32100
  capScope: "device",
@@ -31876,6 +32113,12 @@ Object.freeze({
31876
32113
  addonId: null,
31877
32114
  access: "view"
31878
32115
  },
32116
+ "pipelineAnalytics.getKeyEventsBatch": {
32117
+ capName: "pipeline-analytics",
32118
+ capScope: "device",
32119
+ addonId: null,
32120
+ access: "view"
32121
+ },
31879
32122
  "pipelineAnalytics.getMotionEvents": {
31880
32123
  capName: "pipeline-analytics",
31881
32124
  capScope: "device",
@@ -33052,6 +33295,12 @@ Object.freeze({
33052
33295
  addonId: null,
33053
33296
  access: "view"
33054
33297
  },
33298
+ "recording.reconcileLedgerAgainstDisk": {
33299
+ capName: "recording",
33300
+ capScope: "system",
33301
+ addonId: null,
33302
+ access: "create"
33303
+ },
33055
33304
  "recording.refreshStorageLocationsForMigration": {
33056
33305
  capName: "recording",
33057
33306
  capScope: "system",
@@ -33178,6 +33427,12 @@ Object.freeze({
33178
33427
  addonId: null,
33179
33428
  access: "view"
33180
33429
  },
33430
+ "sceneMonitor.listScenesBatch": {
33431
+ capName: "scene-monitor",
33432
+ capScope: "device",
33433
+ addonId: null,
33434
+ access: "view"
33435
+ },
33181
33436
  "sceneMonitor.recheckNow": {
33182
33437
  capName: "scene-monitor",
33183
33438
  capScope: "device",
@@ -34534,6 +34789,12 @@ Object.freeze({
34534
34789
  addonId: null,
34535
34790
  access: "view"
34536
34791
  },
34792
+ "zoneAnalytics.getCurrentSnapshotBatch": {
34793
+ capName: "zone-analytics",
34794
+ capScope: "device",
34795
+ addonId: null,
34796
+ access: "view"
34797
+ },
34537
34798
  "zoneAnalytics.getUnzonedHistory": {
34538
34799
  capName: "zone-analytics",
34539
34800
  capScope: "device",
@@ -35523,6 +35784,11 @@ Object.freeze({
35523
35784
  form: "single",
35524
35785
  optional: false
35525
35786
  }],
35787
+ "pipelineAnalytics.getEventMediaFootprintByKind": [{
35788
+ name: "deviceId",
35789
+ form: "single",
35790
+ optional: true
35791
+ }],
35526
35792
  "pipelineAnalytics.getGroup": [{
35527
35793
  name: "deviceId",
35528
35794
  form: "single",
@@ -35533,6 +35799,11 @@ Object.freeze({
35533
35799
  form: "single",
35534
35800
  optional: false
35535
35801
  }],
35802
+ "pipelineAnalytics.getKeyEventsBatch": [{
35803
+ name: "deviceIds",
35804
+ form: "array",
35805
+ optional: false
35806
+ }],
35536
35807
  "pipelineAnalytics.getMotionEvents": [{
35537
35808
  name: "deviceId",
35538
35809
  form: "single",
@@ -35973,6 +36244,11 @@ Object.freeze({
35973
36244
  form: "single",
35974
36245
  optional: false
35975
36246
  }],
36247
+ "recording.reconcileLedgerAgainstDisk": [{
36248
+ name: "deviceId",
36249
+ form: "single",
36250
+ optional: true
36251
+ }],
35976
36252
  "recording.relocateFootage": [{
35977
36253
  name: "deviceId",
35978
36254
  form: "single",
@@ -36038,6 +36314,11 @@ Object.freeze({
36038
36314
  form: "single",
36039
36315
  optional: false
36040
36316
  }],
36317
+ "sceneMonitor.listScenesBatch": [{
36318
+ name: "deviceIds",
36319
+ form: "array",
36320
+ optional: false
36321
+ }],
36041
36322
  "sceneMonitor.recheckNow": [{
36042
36323
  name: "deviceId",
36043
36324
  form: "single",
@@ -36299,6 +36580,11 @@ Object.freeze({
36299
36580
  form: "single",
36300
36581
  optional: false
36301
36582
  }],
36583
+ "zoneAnalytics.getCurrentSnapshotBatch": [{
36584
+ name: "deviceIds",
36585
+ form: "array",
36586
+ optional: false
36587
+ }],
36302
36588
  "zoneAnalytics.getUnzonedHistory": [{
36303
36589
  name: "deviceId",
36304
36590
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-export-google",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Google Home export — hub-side smart-home fulfillment (SYNC / QUERY / EXECUTE / DISCONNECT) for the non-camera fleet, served over the hub's own OAuth account link. No Google credential is stored, sent or required.",
5
5
  "keywords": [
6
6
  "camstack",