camstack 1.2.21 → 1.2.22
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.
|
@@ -15626,6 +15626,7 @@ var RecordingConfigSchema = external_exports.object({
|
|
|
15626
15626
|
scrubThumbnails: ScrubThumbnailPresetSchema.optional()
|
|
15627
15627
|
}).strict();
|
|
15628
15628
|
var RelocateJobStateSchema = external_exports.enum([
|
|
15629
|
+
"queued",
|
|
15629
15630
|
"running",
|
|
15630
15631
|
"done",
|
|
15631
15632
|
"failed",
|
|
@@ -15658,6 +15659,15 @@ var RelocateFootageInputSchema = external_exports.object({
|
|
|
15658
15659
|
/** Limits relocation to the logical profile class. Omit only for the
|
|
15659
15660
|
* pre-orchestration compatibility path. */
|
|
15660
15661
|
footageClass: RelocateFootageClassSchema.optional(),
|
|
15662
|
+
/** Scope the move to ONE camera. Absent = every camera on the source, which
|
|
15663
|
+
* is what a whole-disk drain means. The rebalance path always sets it: its
|
|
15664
|
+
* unit is a (camera, profile) pile, not a disk. */
|
|
15665
|
+
deviceId: external_exports.number().int().optional(),
|
|
15666
|
+
/** Scope the move to specific segment profiles (`high` / `mid` / `low`).
|
|
15667
|
+
* Finer than `footageClass`, which cannot separate high from mid — and the
|
|
15668
|
+
* placement plan assigns those two independently, so a rebalance that could
|
|
15669
|
+
* only say "recordings" would move footage the plan never asked to move. */
|
|
15670
|
+
profiles: external_exports.array(external_exports.string()).optional(),
|
|
15661
15671
|
/** Copy throttle in MB/s (default 40) — the drain is a background chore,
|
|
15662
15672
|
* never allowed to starve live writers. */
|
|
15663
15673
|
throttleMbps: external_exports.number().min(1).max(1e3).optional()
|
|
@@ -15748,6 +15758,21 @@ var StorageLocationSchema = external_exports.object({
|
|
|
15748
15758
|
nodeId: external_exports.string().optional(),
|
|
15749
15759
|
isDefault: external_exports.boolean().default(false),
|
|
15750
15760
|
isSystem: external_exports.boolean().default(false),
|
|
15761
|
+
/**
|
|
15762
|
+
* Operator opt-in: whether consumers that BALANCE across several locations
|
|
15763
|
+
* of a type may write here. Recordings reads it today; event media and
|
|
15764
|
+
* backups are the next consumers, which is why the flag lives on the
|
|
15765
|
+
* location rather than in any one addon's store — nothing has to be
|
|
15766
|
+
* extended to add the next consumer.
|
|
15767
|
+
*
|
|
15768
|
+
* OPTIONAL, and ABSENT MEANS ACTIVE. Every location persisted before the
|
|
15769
|
+
* flag existed reads back with no flag and keeps working exactly as before;
|
|
15770
|
+
* that is the whole compat story, and it is why no migration ships with it.
|
|
15771
|
+
* A newly CREATED sibling is stamped `false` by the orchestrator (creating a
|
|
15772
|
+
* disk must not silently start writing to it); the default of a type is
|
|
15773
|
+
* always stamped `true`.
|
|
15774
|
+
*/
|
|
15775
|
+
enabled: external_exports.boolean().optional(),
|
|
15751
15776
|
/** COMPUTED at read time by the orchestrator (statfs of the backing volume
|
|
15752
15777
|
* for node-local locations it can reach) — never persisted, absent when the
|
|
15753
15778
|
* volume is remote/unreachable. The single capacity truth every UI reads. */
|
|
@@ -20642,9 +20667,9 @@ var filesystemBrowseCapability = {
|
|
|
20642
20667
|
mode: "singleton",
|
|
20643
20668
|
internal: true,
|
|
20644
20669
|
methods: {
|
|
20645
|
-
/** The
|
|
20670
|
+
/** The roots `createDir` is sandboxed to on this node. Browsing is not confined to them. */
|
|
20646
20671
|
listAllowedRoots: method(external_exports.void(), external_exports.array(external_exports.string()).readonly(), { auth: "admin" }),
|
|
20647
|
-
/** Immediate subdirectories of `path`
|
|
20672
|
+
/** Immediate subdirectories of any absolute `path` + free/total bytes. Read-only. */
|
|
20648
20673
|
browse: method(external_exports.object({ path: external_exports.string() }), BrowseResultSchema, { auth: "admin" }),
|
|
20649
20674
|
/** Create a subdirectory (within an allowed root). Returns its absolute path. */
|
|
20650
20675
|
createDir: method(external_exports.object({ path: external_exports.string() }), external_exports.object({ path: external_exports.string() }), {
|
|
@@ -22359,18 +22384,46 @@ var NcDeliverySchema = external_exports.enum([
|
|
|
22359
22384
|
"system-event"
|
|
22360
22385
|
]);
|
|
22361
22386
|
var NcSystemEventKindSchema = external_exports.enum([
|
|
22362
|
-
"
|
|
22363
|
-
"
|
|
22387
|
+
"device-online",
|
|
22388
|
+
"device-offline",
|
|
22389
|
+
"device-disabled",
|
|
22390
|
+
"device-enabled",
|
|
22364
22391
|
"stream-online",
|
|
22365
22392
|
"stream-offline",
|
|
22366
22393
|
"node-online",
|
|
22367
22394
|
"node-offline",
|
|
22368
22395
|
"addon-update-available",
|
|
22369
|
-
"server-update-available"
|
|
22396
|
+
"server-update-available",
|
|
22397
|
+
"alarm-triggered",
|
|
22398
|
+
"alarm-armed",
|
|
22399
|
+
"alarm-disarmed",
|
|
22400
|
+
"camera-online",
|
|
22401
|
+
"camera-offline",
|
|
22402
|
+
"camera-disabled",
|
|
22403
|
+
"camera-enabled"
|
|
22370
22404
|
]);
|
|
22405
|
+
var NC_LEGACY_SYSTEM_EVENT_KINDS = /* @__PURE__ */ new Set([
|
|
22406
|
+
"camera-online",
|
|
22407
|
+
"camera-offline",
|
|
22408
|
+
"camera-disabled",
|
|
22409
|
+
"camera-enabled"
|
|
22410
|
+
]);
|
|
22411
|
+
var NC_AUTHORABLE_SYSTEM_EVENT_KINDS = NcSystemEventKindSchema.options.filter((kind) => !NC_LEGACY_SYSTEM_EVENT_KINDS.has(kind));
|
|
22371
22412
|
var NcSystemEventConditionSchema = external_exports.object({
|
|
22372
22413
|
kinds: external_exports.array(NcSystemEventKindSchema).min(1),
|
|
22373
22414
|
deviceIds: external_exports.array(external_exports.number().int()).min(1).optional(),
|
|
22415
|
+
/**
|
|
22416
|
+
* Narrow a `device-*` kind to these device TYPES (`DeviceType` values —
|
|
22417
|
+
* `camera`, `switch`, `sensor`, `container`, …). Absent = every type, which
|
|
22418
|
+
* is what a liveness rule means when nobody said otherwise.
|
|
22419
|
+
*
|
|
22420
|
+
* This is where "only my cameras" is expressed, and it lives on the rule for
|
|
22421
|
+
* one reason: the intake cannot know which devices this household cares
|
|
22422
|
+
* about, and a producer-side filter is one no operator can change. Fails
|
|
22423
|
+
* CLOSED — a subject whose device type is unknown (an id the device mirror
|
|
22424
|
+
* does not carry) matches no `deviceTypes` list.
|
|
22425
|
+
*/
|
|
22426
|
+
deviceTypes: external_exports.array(external_exports.string().min(1)).min(1).optional(),
|
|
22374
22427
|
nodeIds: external_exports.array(external_exports.string().min(1)).min(1).optional(),
|
|
22375
22428
|
packageNames: external_exports.array(external_exports.string().min(1)).min(1).optional()
|
|
22376
22429
|
});
|
|
@@ -22406,6 +22459,16 @@ var NcOccupancyConditionSchema = external_exports.object({
|
|
|
22406
22459
|
count: external_exports.number().int().min(0).default(1),
|
|
22407
22460
|
sustainSeconds: external_exports.number().int().min(0).max(3600).default(15)
|
|
22408
22461
|
});
|
|
22462
|
+
var NcAudioConditionSchema = external_exports.object({
|
|
22463
|
+
/** Audio macro labels; absent = any sound (level-only rule). */
|
|
22464
|
+
labels: external_exports.array(external_exports.string().min(1)).min(1).optional(),
|
|
22465
|
+
/** Level floor in dBFS (negative-going, `0` = full scale); absent = any level. */
|
|
22466
|
+
dbThreshold: external_exports.number().min(-96).max(0).optional(),
|
|
22467
|
+
/** Percentage of the window's samples that must be hits (1–100). */
|
|
22468
|
+
hitPercent: external_exports.number().int().min(1).max(100).default(60),
|
|
22469
|
+
/** Length of the sampling window in seconds. */
|
|
22470
|
+
samplingSeconds: external_exports.number().int().min(1).max(300).default(10)
|
|
22471
|
+
});
|
|
22409
22472
|
var NcCrossingSchema = external_exports.enum([
|
|
22410
22473
|
"enter",
|
|
22411
22474
|
"exit",
|
|
@@ -22604,7 +22667,33 @@ var NcConditionsSchema = external_exports.object({
|
|
|
22604
22667
|
* threshold and holds for `sustainSeconds`. Fail-closed on missing
|
|
22605
22668
|
* substrate (no snapshot / missing zone). See {@link NcOccupancyCondition}.
|
|
22606
22669
|
*/
|
|
22607
|
-
occupancy: NcOccupancyConditionSchema.optional()
|
|
22670
|
+
occupancy: NcOccupancyConditionSchema.optional(),
|
|
22671
|
+
/**
|
|
22672
|
+
* IMMEDIATE only. Sustained-sound matcher — `hitPercent` of the samples in a
|
|
22673
|
+
* `samplingSeconds` window clear the optional `dbThreshold` and carry one of
|
|
22674
|
+
* the optional `labels`. Fail-closed on missing substrate (no audio samples,
|
|
22675
|
+
* a window that is not full yet, neither filter given). See
|
|
22676
|
+
* {@link NcAudioCondition}.
|
|
22677
|
+
*
|
|
22678
|
+
* Presence of this key is what makes a rule an AUDIO rule: the engine fires
|
|
22679
|
+
* it ONLY on a confirmed audio window, and a rule carrying it never fires on
|
|
22680
|
+
* a detection, a track or a device event (the same fail-closed pairing
|
|
22681
|
+
* `occupancy` has with the `device-event` trigger). That is how audio labels
|
|
22682
|
+
* leave `classes`: an audio rule names its sounds HERE, and the legacy path
|
|
22683
|
+
* (an `immediate` rule naming an `audio-*` class, one notification per
|
|
22684
|
+
* classified sample) stays exactly as it was for rules that already use it.
|
|
22685
|
+
*
|
|
22686
|
+
* NOT in {@link NC_CONDITION_CATALOG} yet, and that is the sequencing rule
|
|
22687
|
+
* rather than an oversight: the viewer mirrors the descriptor enums BY HAND
|
|
22688
|
+
* (`camstack/src/data/notification-center.ts`, guarded by
|
|
22689
|
+
* `scripts/check-viewer-condition-mirror.ts`) and its rule editor STRIPS the
|
|
22690
|
+
* condition fields it does not know when a rule is saved from the phone.
|
|
22691
|
+
* Publishing an editor for a condition the app cannot round-trip is how an
|
|
22692
|
+
* operator loses a rule's conditions by opening it — so the descriptor, the
|
|
22693
|
+
* admin widget and the viewer mirror land together (P2 + P3), and only then
|
|
22694
|
+
* does an audio rule become authorable.
|
|
22695
|
+
*/
|
|
22696
|
+
audio: NcAudioConditionSchema.optional()
|
|
22608
22697
|
});
|
|
22609
22698
|
var NcRuleTargetSchema = external_exports.object({
|
|
22610
22699
|
/** `notification-output` Target id. */
|
|
@@ -22681,6 +22770,44 @@ var NcThrottleSchema = external_exports.object({
|
|
|
22681
22770
|
*/
|
|
22682
22771
|
granularity: NcThrottleGranularitySchema.optional()
|
|
22683
22772
|
});
|
|
22773
|
+
var NC_CONFIRM_MIN_TIMEOUT_MS = 1e3;
|
|
22774
|
+
var NC_CONFIRM_MAX_TIMEOUT_MS = 2e4;
|
|
22775
|
+
var NC_CONFIRM_DEFAULT_TIMEOUT_MS = 8e3;
|
|
22776
|
+
var NcConfirmExpectSchema = external_exports.object({
|
|
22777
|
+
op: external_exports.enum([
|
|
22778
|
+
">=",
|
|
22779
|
+
">",
|
|
22780
|
+
"<=",
|
|
22781
|
+
"<",
|
|
22782
|
+
"=="
|
|
22783
|
+
]),
|
|
22784
|
+
count: external_exports.number().int().min(0).max(1e3)
|
|
22785
|
+
});
|
|
22786
|
+
var NcConfirmSchema = external_exports.object({
|
|
22787
|
+
/** Off unless asked for. An absent `confirm` and `enabled:false` are the
|
|
22788
|
+
* same thing, and both mean "deliver exactly as before". */
|
|
22789
|
+
enabled: external_exports.boolean().default(false),
|
|
22790
|
+
/** Explicit vision profile; absent = the `purpose:'vision'` cluster default. */
|
|
22791
|
+
profileId: external_exports.string().optional(),
|
|
22792
|
+
/**
|
|
22793
|
+
* The operator's question, in his own words. Absent = a question derived
|
|
22794
|
+
* from the rule (its class and its expectation).
|
|
22795
|
+
*
|
|
22796
|
+
* NEVER composed with text READ OUT OF THE FRAME. The model reads OSD
|
|
22797
|
+
* banners, signage and plates as instructions if you let them reach the
|
|
22798
|
+
* prompt — proven live — so the authoritative contract stays in the system
|
|
22799
|
+
* turn and only rule-authored words land here.
|
|
22800
|
+
*/
|
|
22801
|
+
prompt: external_exports.string().max(1e3).optional(),
|
|
22802
|
+
/** Fire only when the model's count satisfies this. Absent = the model's
|
|
22803
|
+
* own boolean verdict decides. */
|
|
22804
|
+
expect: NcConfirmExpectSchema.optional(),
|
|
22805
|
+
timeoutMs: external_exports.number().int().min(NC_CONFIRM_MIN_TIMEOUT_MS).max(NC_CONFIRM_MAX_TIMEOUT_MS).default(NC_CONFIRM_DEFAULT_TIMEOUT_MS),
|
|
22806
|
+
/** What a timeout / unreachable model MEANS. `fire` (default) = fail-open. */
|
|
22807
|
+
onTimeout: external_exports.enum(["fire", "suppress"]).default("fire"),
|
|
22808
|
+
/** Longest edge the judged image is downscaled to before it is sent. */
|
|
22809
|
+
maxImagePx: external_exports.number().int().min(64).max(2048).default(448)
|
|
22810
|
+
});
|
|
22684
22811
|
var NcRuleInputSchema = external_exports.object({
|
|
22685
22812
|
name: external_exports.string().min(1).max(200),
|
|
22686
22813
|
enabled: external_exports.boolean().default(true),
|
|
@@ -22740,9 +22867,45 @@ var NcRuleInputSchema = external_exports.object({
|
|
|
22740
22867
|
* `{ cap: 'alarm-panel', method: 'arm', args: { mode: 'away' } }`, the same
|
|
22741
22868
|
* shape as every other actuation.
|
|
22742
22869
|
*/
|
|
22743
|
-
actions: NcRuleActionsSchema.optional()
|
|
22870
|
+
actions: NcRuleActionsSchema.optional(),
|
|
22871
|
+
/**
|
|
22872
|
+
* AI CONFIRM — see {@link NcConfirmSchema}. `.optional()`, never defaulted:
|
|
22873
|
+
* a rule that predates the gate must keep delivering byte-for-byte as it
|
|
22874
|
+
* did, and absent is the only way to say that without a migration.
|
|
22875
|
+
*/
|
|
22876
|
+
confirm: NcConfirmSchema.optional()
|
|
22877
|
+
});
|
|
22878
|
+
var NcRulePatchSchema = NcRuleInputSchema.partial().extend({
|
|
22879
|
+
disabledTargetIds: external_exports.array(external_exports.string()).optional(),
|
|
22880
|
+
/**
|
|
22881
|
+
* `.partial()` DOES NOT REMOVE A FIELD'S `.default()`.
|
|
22882
|
+
*
|
|
22883
|
+
* It makes the key optional to SUPPLY; the parse still materialises the
|
|
22884
|
+
* default when the key is absent. And `NcRuleStore.update` merges with
|
|
22885
|
+
* `{ ...existing, ...patch }`, so a materialised key OVERWRITES the stored
|
|
22886
|
+
* one — which made every partial edit destructive:
|
|
22887
|
+
*
|
|
22888
|
+
* nc.updateRule({ throttle }) → conditions reset to `{}`
|
|
22889
|
+
* nc.setRuleTargetEnabled(...) → conditions reset to `{}`
|
|
22890
|
+
* setEnabled(ruleId, false) → conditions reset to `{}`
|
|
22891
|
+
*
|
|
22892
|
+
* A rule scoped to one camera and one zone silently became a rule that
|
|
22893
|
+
* matches EVERY event on EVERY camera, and lost its `media` policy
|
|
22894
|
+
* (zoneCrop / gif / clip / frame) and its `priority` at the same time. Seen
|
|
22895
|
+
* live on 2026-08-12: a rule scoped to device 617 fired on 590 and 615
|
|
22896
|
+
* within a minute of a two-field patch.
|
|
22897
|
+
*
|
|
22898
|
+
* So every defaulted field is re-declared here WITHOUT its default. The
|
|
22899
|
+
* inner defaults still apply when the caller DOES send the key — `{}` for
|
|
22900
|
+
* conditions remains a real instruction ("clear them") — and only the
|
|
22901
|
+
* absent key is now genuinely absent.
|
|
22902
|
+
*/
|
|
22903
|
+
enabled: external_exports.boolean().optional(),
|
|
22904
|
+
conditions: NcConditionsSchema.optional(),
|
|
22905
|
+
media: NcMediaPolicySchema.optional(),
|
|
22906
|
+
throttle: NcThrottleSchema.optional(),
|
|
22907
|
+
priority: external_exports.number().int().min(1).max(5).optional()
|
|
22744
22908
|
});
|
|
22745
|
-
var NcRulePatchSchema = NcRuleInputSchema.partial().extend({ disabledTargetIds: external_exports.array(external_exports.string()).optional() });
|
|
22746
22909
|
var NcRuleSchema = NcRuleInputSchema.extend({
|
|
22747
22910
|
id: external_exports.string(),
|
|
22748
22911
|
/** userId of the admin who created the rule (server-stamped caller). */
|
|
@@ -22985,6 +23148,15 @@ var NcAlarmSettingsSchema = external_exports.object({
|
|
|
22985
23148
|
announceTargets: external_exports.array(external_exports.string().min(1)).max(16)
|
|
22986
23149
|
});
|
|
22987
23150
|
var NcAlarmSettingsPatchSchema = NcAlarmSettingsSchema.partial();
|
|
23151
|
+
var NcAlarmSkipReasonSchema = external_exports.enum([
|
|
23152
|
+
"muted",
|
|
23153
|
+
"detection-off",
|
|
23154
|
+
"offline"
|
|
23155
|
+
]);
|
|
23156
|
+
var NcAlarmSkippedDeviceSchema = external_exports.object({
|
|
23157
|
+
deviceId: external_exports.number().int(),
|
|
23158
|
+
reason: NcAlarmSkipReasonSchema
|
|
23159
|
+
});
|
|
22988
23160
|
var NcAlarmModeCoverageSchema = external_exports.object({
|
|
22989
23161
|
mode: AlarmArmModeSchema,
|
|
22990
23162
|
/** Enabled rules gated on `armed_<mode>`. Zero means the mode does nothing. */
|
|
@@ -22992,7 +23164,18 @@ var NcAlarmModeCoverageSchema = external_exports.object({
|
|
|
22992
23164
|
/** At least one covering rule has no device scope, so the mode covers all. */
|
|
22993
23165
|
allDevices: external_exports.boolean(),
|
|
22994
23166
|
/** Ids named by the covering rules. A SUBSET when `allDevices` is true. */
|
|
22995
|
-
deviceIds: external_exports.array(external_exports.number().int())
|
|
23167
|
+
deviceIds: external_exports.array(external_exports.number().int()),
|
|
23168
|
+
/**
|
|
23169
|
+
* Devices this mode NAMES but cannot actually arm, each with the switch that
|
|
23170
|
+
* excludes it.
|
|
23171
|
+
*
|
|
23172
|
+
* "Away armed — 12 cameras" is a promise, and a muted camera among those
|
|
23173
|
+
* twelve makes it false in exactly the way nobody notices until an incident.
|
|
23174
|
+
* Defaulted to `[]` so a coverage answer computed before this field existed
|
|
23175
|
+
* still parses as "nothing known to be skipped" rather than failing the whole
|
|
23176
|
+
* alarm tab.
|
|
23177
|
+
*/
|
|
23178
|
+
skippedDevices: external_exports.array(NcAlarmSkippedDeviceSchema).default([])
|
|
22996
23179
|
});
|
|
22997
23180
|
var NcAlarmConfigSchema = external_exports.object({
|
|
22998
23181
|
/**
|
|
@@ -26110,9 +26293,16 @@ var CameraStatusSchema = external_exports.object({
|
|
|
26110
26293
|
audio: CameraAudioStatusSchema.nullable(),
|
|
26111
26294
|
recording: CameraRecordingStatusSchema.nullable(),
|
|
26112
26295
|
/**
|
|
26113
|
-
* Per-camera
|
|
26296
|
+
* Per-camera functions an OPERATOR has turned off
|
|
26114
26297
|
* ([D61](../../../../docs/decisions/adr-0067.md)).
|
|
26115
26298
|
*
|
|
26299
|
+
* Composed from the AUTHORITIES themselves — the wrapper bindings,
|
|
26300
|
+
* `RecordingConfig.enabled`, the notification mute, the broker's audio
|
|
26301
|
+
* policy, the camera's own microphone — via `composeSwitchedOff`, not from
|
|
26302
|
+
* the deprecated `getCameraSwitches` group ([D113](../../../../docs/decisions/adr-0113.md)).
|
|
26303
|
+
* The badge outlives the control panel: the panel was a convenience, this is
|
|
26304
|
+
* the difference between a camera being off and a camera being dead.
|
|
26305
|
+
*
|
|
26116
26306
|
* This is the difference between DISABLED and BROKEN. A camera whose
|
|
26117
26307
|
* `detection` block reports zero fps and whose `switchedOff` contains
|
|
26118
26308
|
* `'object-detection'` was switched off by a person; the same camera with an
|
|
@@ -26539,6 +26729,10 @@ var pipelineOrchestratorCapability = {
|
|
|
26539
26729
|
agentNodeId: external_exports.string().optional()
|
|
26540
26730
|
}), CameraPipelineConfigSchema),
|
|
26541
26731
|
/**
|
|
26732
|
+
* @deprecated The aggregate switch group is being withdrawn
|
|
26733
|
+
* ([D113](../../../../docs/decisions/adr-0113.md)). Build nothing new on
|
|
26734
|
+
* this pair; read the authority directly.
|
|
26735
|
+
*
|
|
26542
26736
|
* The whole per-camera function switch group, DERIVED — never a stored
|
|
26543
26737
|
* list ([D61](../../../../docs/decisions/adr-0067.md)).
|
|
26544
26738
|
*
|
|
@@ -26552,9 +26746,23 @@ var pipelineOrchestratorCapability = {
|
|
|
26552
26746
|
* `auth: 'view'` deliberately — a NON-admin must be able to see that a
|
|
26553
26747
|
* camera is quiet because somebody switched it off. Only the mutation is
|
|
26554
26748
|
* admin-gated.
|
|
26749
|
+
*
|
|
26750
|
+
* **Removal plan.** It stays and it KEEPS WORKING while shipped viewers
|
|
26751
|
+
* (v1.0.305) and the admin UI still call it — removing it now is a broken
|
|
26752
|
+
* app on a device nobody can redeploy from here. It is served by a thin
|
|
26753
|
+
* shim over the same authorities (`camera-switch-service.ts`), so the
|
|
26754
|
+
* behaviour of the pair is the behaviour of the authorities by
|
|
26755
|
+
* construction. It is deleted once every surface reaches its own
|
|
26756
|
+
* component's options and the last caller is gone. Nothing on this server
|
|
26757
|
+
* reads it: `CameraStatus.switchedOff` is composed from the authorities
|
|
26758
|
+
* directly via `composeSwitchedOff`.
|
|
26555
26759
|
*/
|
|
26556
26760
|
getCameraSwitches: method(external_exports.object({ deviceId: external_exports.number() }), CameraSwitchGroupSchema),
|
|
26557
26761
|
/**
|
|
26762
|
+
* @deprecated See {@link getCameraSwitches}. Write the authority — the
|
|
26763
|
+
* wrapper binding, `RecordingConfig.enabled`, the notification mute — not
|
|
26764
|
+
* this ([D113](../../../../docs/decisions/adr-0113.md)).
|
|
26765
|
+
*
|
|
26558
26766
|
* Flip ONE switch, routed to its existing authority.
|
|
26559
26767
|
*
|
|
26560
26768
|
* Never writes a parallel map: `recording` patches `RecordingConfig.enabled`
|
|
@@ -26932,24 +27140,28 @@ var snapshotCapability = {
|
|
|
26932
27140
|
*
|
|
26933
27141
|
* `getSnapshotOverview` is cache-only by contract: it answers from whatever
|
|
26934
27142
|
* the wrapper happens to hold and never captures. Under D93 the client
|
|
26935
|
-
* versions its image URL on that answer, and an image REQUEST
|
|
26936
|
-
*
|
|
26937
|
-
*
|
|
26938
|
-
*
|
|
26939
|
-
*
|
|
26940
|
-
*
|
|
26941
|
-
*
|
|
26942
|
-
*
|
|
27143
|
+
* versions its image URL on that answer, and an image REQUEST was the only
|
|
27144
|
+
* demand signal. Both of those are satisfiable by the client's own image
|
|
27145
|
+
* cache — `expo-image` is URL-keyed and never revalidates — so a URL painted
|
|
27146
|
+
* in a previous session comes off disk with no network, no demand, and no
|
|
27147
|
+
* capture. Measured on the live hub: reopening after two minutes idle
|
|
27148
|
+
* painted 15 of 16 tiles at **168 s old** with zero HTTP requests, and the
|
|
27149
|
+
* fleet only recovered because a later poll happened to observe a different
|
|
27150
|
+
* identity.
|
|
26943
27151
|
*
|
|
26944
27152
|
* ## The two properties that fix it
|
|
26945
27153
|
*
|
|
26946
27154
|
* **It is an RPC, so no client cache can answer it.** The demand signal
|
|
26947
|
-
* always reaches the wrapper. This method therefore
|
|
26948
|
-
*
|
|
26949
|
-
*
|
|
26950
|
-
*
|
|
26951
|
-
*
|
|
26952
|
-
*
|
|
27155
|
+
* always reaches the wrapper. This method therefore CAPTURES, where
|
|
27156
|
+
* `getSnapshotOverview` must never (D93) — the distinction is not "one is
|
|
27157
|
+
* newer" but that the overview poll is app-wide (a capturing overview would
|
|
27158
|
+
* dial every camera on the install) while this is called by a rendered
|
|
27159
|
+
* surface naming the tiles it is actually painting, at the width it is
|
|
27160
|
+
* painting them.
|
|
27161
|
+
*
|
|
27162
|
+
* Since 2026-08-11 this is the ONLY thing that refreshes a snapshot: the
|
|
27163
|
+
* server-side keep-warm loop was removed (operator directive — on-demand,
|
|
27164
|
+
* always), so a camera nobody is looking at costs nothing at all.
|
|
26953
27165
|
*
|
|
26954
27166
|
* **It waits, briefly and boundedly, for the capture it triggered.** The
|
|
26955
27167
|
* returned `capturedAt` is the frame the link will serve, not the frame the
|
|
@@ -32987,7 +33199,19 @@ var RecordingManifestSchema = external_exports.object({
|
|
|
32987
33199
|
});
|
|
32988
33200
|
var RecordingDeviceUsageSchema = external_exports.object({
|
|
32989
33201
|
deviceId: external_exports.number(),
|
|
32990
|
-
usedBytes: external_exports.number()
|
|
33202
|
+
usedBytes: external_exports.number(),
|
|
33203
|
+
/**
|
|
33204
|
+
* Start of this camera's OLDEST indexed segment, across every profile and
|
|
33205
|
+
* location — the "Oldest footage" column in Recordings → Storage, and the
|
|
33206
|
+
* only honest answer to "is retention actually holding?" per camera.
|
|
33207
|
+
*
|
|
33208
|
+
* `null` = the camera has no footage. OPTIONAL because a recorder that
|
|
33209
|
+
* predates this field omits it entirely, and a hub whose types carry the
|
|
33210
|
+
* field must keep validating that older provider's payload: the framework
|
|
33211
|
+
* (types) and the addon ship on different trains, and the addon is usually
|
|
33212
|
+
* the later of the two.
|
|
33213
|
+
*/
|
|
33214
|
+
oldestMs: external_exports.number().nullable().optional()
|
|
32991
33215
|
});
|
|
32992
33216
|
var RecordingLocationUsageSchema = external_exports.object({
|
|
32993
33217
|
/** StorageLocation id; null for the legacy/degraded single-root fallback. */
|
|
@@ -33010,6 +33234,42 @@ var RecordingStorageUsageSchema = external_exports.object({
|
|
|
33010
33234
|
devices: external_exports.array(RecordingDeviceUsageSchema),
|
|
33011
33235
|
locations: external_exports.array(RecordingLocationUsageSchema)
|
|
33012
33236
|
});
|
|
33237
|
+
var RecordingRebalanceMoveSchema = external_exports.object({
|
|
33238
|
+
deviceId: external_exports.number(),
|
|
33239
|
+
profile: external_exports.string(),
|
|
33240
|
+
fromLocationId: external_exports.string(),
|
|
33241
|
+
toLocationId: external_exports.string(),
|
|
33242
|
+
bytes: external_exports.number(),
|
|
33243
|
+
files: external_exports.number().int()
|
|
33244
|
+
});
|
|
33245
|
+
var RecordingRebalanceSkipReasonSchema = external_exports.enum([
|
|
33246
|
+
"unassigned",
|
|
33247
|
+
"target-not-writable",
|
|
33248
|
+
"below-threshold",
|
|
33249
|
+
"no-headroom"
|
|
33250
|
+
]);
|
|
33251
|
+
var RecordingRebalanceSkipSchema = external_exports.object({
|
|
33252
|
+
deviceId: external_exports.number(),
|
|
33253
|
+
profile: external_exports.string(),
|
|
33254
|
+
fromLocationId: external_exports.string(),
|
|
33255
|
+
/** The location the plan wants; null when the camera has no assignment. */
|
|
33256
|
+
toLocationId: external_exports.string().nullable(),
|
|
33257
|
+
bytes: external_exports.number(),
|
|
33258
|
+
reason: RecordingRebalanceSkipReasonSchema
|
|
33259
|
+
});
|
|
33260
|
+
var RecordingRebalancePlanSchema = external_exports.object({
|
|
33261
|
+
moves: external_exports.array(RecordingRebalanceMoveSchema),
|
|
33262
|
+
skipped: external_exports.array(RecordingRebalanceSkipSchema),
|
|
33263
|
+
bytesToMove: external_exports.number(),
|
|
33264
|
+
/** Relocate job ids enqueued. Always empty for the plan (dry-run) call. */
|
|
33265
|
+
jobIds: external_exports.array(external_exports.string())
|
|
33266
|
+
});
|
|
33267
|
+
var RecordingRebalanceInputSchema = external_exports.object({
|
|
33268
|
+
/** Copy throttle in MB/s (default 40) — a rebalance is a background chore. */
|
|
33269
|
+
throttleMbps: external_exports.number().min(1).max(1e3).optional(),
|
|
33270
|
+
/** Ignore piles smaller than this (default 1 GB). */
|
|
33271
|
+
minMoveGb: external_exports.number().min(0).optional()
|
|
33272
|
+
});
|
|
33013
33273
|
var LocateSegmentResultSchema = external_exports.discriminatedUnion("kind", [external_exports.object({
|
|
33014
33274
|
kind: external_exports.literal("segment"),
|
|
33015
33275
|
startMs: external_exports.number(),
|
|
@@ -33235,13 +33495,59 @@ var recordingCapability = {
|
|
|
33235
33495
|
cancelStorageMigrationMove: method(external_exports.object({ jobId: external_exports.string() }), external_exports.object({ cancelled: external_exports.boolean() }), {
|
|
33236
33496
|
kind: "mutation",
|
|
33237
33497
|
auth: "admin"
|
|
33498
|
+
}),
|
|
33499
|
+
/**
|
|
33500
|
+
* Move footage between locations — the OPERATOR's mover, scoped to one
|
|
33501
|
+
* camera (and optionally to specific profiles) rather than a whole disk.
|
|
33502
|
+
* Queued FIFO behind the single-flight engine, so arming several is safe.
|
|
33503
|
+
*/
|
|
33504
|
+
relocateFootage: method(RelocateFootageInputSchema, external_exports.object({ jobId: external_exports.string() }), {
|
|
33505
|
+
kind: "mutation",
|
|
33506
|
+
auth: "admin"
|
|
33507
|
+
}),
|
|
33508
|
+
/** Every relocate job this recorder knows about, newest first (in RAM: the
|
|
33509
|
+
* move is resumable, so a lost list costs nothing but the display). */
|
|
33510
|
+
listRelocateJobs: method(external_exports.object({}), external_exports.array(RelocateJobSchema).readonly(), {
|
|
33511
|
+
kind: "query",
|
|
33512
|
+
auth: "admin"
|
|
33513
|
+
}),
|
|
33514
|
+
/** Cancel a running or queued relocate job. A queued job never runs. */
|
|
33515
|
+
cancelRelocateJob: method(external_exports.object({ jobId: external_exports.string() }), external_exports.object({ cancelled: external_exports.boolean() }), {
|
|
33516
|
+
kind: "mutation",
|
|
33517
|
+
auth: "admin"
|
|
33518
|
+
}),
|
|
33519
|
+
/** What a rebalance WOULD move, and what it would refuse. Moves nothing. */
|
|
33520
|
+
planStorageRebalance: method(RecordingRebalanceInputSchema, RecordingRebalancePlanSchema, {
|
|
33521
|
+
kind: "query",
|
|
33522
|
+
auth: "admin"
|
|
33523
|
+
}),
|
|
33524
|
+
/** Arm the rebalance: enqueue one relocate job per planned move. */
|
|
33525
|
+
startStorageRebalance: method(RecordingRebalanceInputSchema, RecordingRebalancePlanSchema, {
|
|
33526
|
+
kind: "mutation",
|
|
33527
|
+
auth: "admin"
|
|
33238
33528
|
})
|
|
33239
33529
|
}
|
|
33240
33530
|
};
|
|
33241
33531
|
var ExportSpeedSchema = external_exports.number().min(0.25).max(32);
|
|
33532
|
+
var ExportDenseRangeSchema = external_exports.object({
|
|
33533
|
+
fromSec: external_exports.number().nonnegative(),
|
|
33534
|
+
toSec: external_exports.number().nonnegative()
|
|
33535
|
+
}).refine((r) => r.toSec > r.fromSec, { message: "dense range must have toSec > fromSec" });
|
|
33536
|
+
var ExportDenseSchema = external_exports.object({
|
|
33537
|
+
everyMs: external_exports.number().int().positive(),
|
|
33538
|
+
ranges: external_exports.array(ExportDenseRangeSchema).min(1).max(200)
|
|
33539
|
+
});
|
|
33242
33540
|
var ExportTimelapseSchema = external_exports.object({
|
|
33243
33541
|
everyMs: external_exports.number().int().positive(),
|
|
33244
|
-
outputFps: external_exports.number().int().min(1).max(60).optional()
|
|
33542
|
+
outputFps: external_exports.number().int().min(1).max(60).optional(),
|
|
33543
|
+
/** Optional second, FASTER rate over the intervals that matter. */
|
|
33544
|
+
dense: ExportDenseSchema.optional()
|
|
33545
|
+
}).superRefine((v, ctx) => {
|
|
33546
|
+
if (v.dense !== void 0 && v.dense.everyMs >= v.everyMs) ctx.addIssue({
|
|
33547
|
+
code: external_exports.ZodIssueCode.custom,
|
|
33548
|
+
message: "dense.everyMs must be strictly smaller than the base everyMs",
|
|
33549
|
+
path: ["dense", "everyMs"]
|
|
33550
|
+
});
|
|
33245
33551
|
});
|
|
33246
33552
|
var ExportOptionsSchema = external_exports.object({
|
|
33247
33553
|
speed: ExportSpeedSchema.optional(),
|
|
@@ -33291,8 +33597,16 @@ var ExportDownloadSchema = external_exports.object({
|
|
|
33291
33597
|
url: external_exports.string(),
|
|
33292
33598
|
endpoints: external_exports.array(external_exports.string())
|
|
33293
33599
|
});
|
|
33600
|
+
var RECORDING_EXPORT_MAX_READ_BYTES = 50 * 1024 * 1024;
|
|
33601
|
+
var ExportBytesSchema = external_exports.object({
|
|
33602
|
+
base64: external_exports.string(),
|
|
33603
|
+
contentType: external_exports.string(),
|
|
33604
|
+
/** Suggested filename, extension included. */
|
|
33605
|
+
name: external_exports.string(),
|
|
33606
|
+
bytes: external_exports.number().int().nonnegative()
|
|
33607
|
+
});
|
|
33294
33608
|
var recordingExportCapability = {
|
|
33295
|
-
name: "
|
|
33609
|
+
name: "recording-export",
|
|
33296
33610
|
scope: "system",
|
|
33297
33611
|
mode: "singleton",
|
|
33298
33612
|
methods: {
|
|
@@ -33330,6 +33644,27 @@ var recordingExportCapability = {
|
|
|
33330
33644
|
getDownloadUrl: method(external_exports.object({ exportId: external_exports.string() }), ExportDownloadSchema, {
|
|
33331
33645
|
kind: "query",
|
|
33332
33646
|
auth: "protected"
|
|
33647
|
+
}),
|
|
33648
|
+
/**
|
|
33649
|
+
* The finished file's BYTES, base64, for a caller that must republish them
|
|
33650
|
+
* somewhere a session-less fetcher can reach.
|
|
33651
|
+
*
|
|
33652
|
+
* `getDownloadUrl` is the right answer for a human: the download route is
|
|
33653
|
+
* served `access: 'authenticated'`, which a browser satisfies and a
|
|
33654
|
+
* notifier BACKEND does not. It answers a RELATIVE path, so it is not even
|
|
33655
|
+
* a URL an outside fetcher could try. This method exists for the one case
|
|
33656
|
+
* that needs the other thing — a scheduled timelapse whose video has to
|
|
33657
|
+
* become a public attachment on the notification artifact plane.
|
|
33658
|
+
*
|
|
33659
|
+
* Deliberately narrow: `ready` only (a queued, rendering, failed, expired
|
|
33660
|
+
* or deleted export has no file, and answering "0 bytes" for one is how a
|
|
33661
|
+
* caller ships an empty attachment), still inside its lifetime, and under
|
|
33662
|
+
* {@link RECORDING_EXPORT_MAX_READ_BYTES}. Every refusal throws with the
|
|
33663
|
+
* reason — none of them is silent.
|
|
33664
|
+
*/
|
|
33665
|
+
readExportBytes: method(external_exports.object({ exportId: external_exports.string() }), ExportBytesSchema, {
|
|
33666
|
+
kind: "query",
|
|
33667
|
+
auth: "protected"
|
|
33333
33668
|
})
|
|
33334
33669
|
}
|
|
33335
33670
|
};
|
|
@@ -38757,6 +39092,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
38757
39092
|
addonId: null,
|
|
38758
39093
|
access: "create"
|
|
38759
39094
|
},
|
|
39095
|
+
"recording.cancelRelocateJob": {
|
|
39096
|
+
capName: "recording",
|
|
39097
|
+
capScope: "system",
|
|
39098
|
+
addonId: null,
|
|
39099
|
+
access: "create"
|
|
39100
|
+
},
|
|
38760
39101
|
"recording.cancelStorageMigrationMove": {
|
|
38761
39102
|
capName: "recording",
|
|
38762
39103
|
capScope: "system",
|
|
@@ -38811,6 +39152,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
38811
39152
|
addonId: null,
|
|
38812
39153
|
access: "view"
|
|
38813
39154
|
},
|
|
39155
|
+
"recording.listRelocateJobs": {
|
|
39156
|
+
capName: "recording",
|
|
39157
|
+
capScope: "system",
|
|
39158
|
+
addonId: null,
|
|
39159
|
+
access: "view"
|
|
39160
|
+
},
|
|
38814
39161
|
"recording.locateSegment": {
|
|
38815
39162
|
capName: "recording",
|
|
38816
39163
|
capScope: "system",
|
|
@@ -38823,6 +39170,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
38823
39170
|
addonId: null,
|
|
38824
39171
|
access: "create"
|
|
38825
39172
|
},
|
|
39173
|
+
"recording.planStorageRebalance": {
|
|
39174
|
+
capName: "recording",
|
|
39175
|
+
capScope: "system",
|
|
39176
|
+
addonId: null,
|
|
39177
|
+
access: "view"
|
|
39178
|
+
},
|
|
38826
39179
|
"recording.pruneFootage": {
|
|
38827
39180
|
capName: "recording",
|
|
38828
39181
|
capScope: "system",
|
|
@@ -38847,6 +39200,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
38847
39200
|
addonId: null,
|
|
38848
39201
|
access: "create"
|
|
38849
39202
|
},
|
|
39203
|
+
"recording.relocateFootage": {
|
|
39204
|
+
capName: "recording",
|
|
39205
|
+
capScope: "system",
|
|
39206
|
+
addonId: null,
|
|
39207
|
+
access: "create"
|
|
39208
|
+
},
|
|
38850
39209
|
"recording.renderClip": {
|
|
38851
39210
|
capName: "recording",
|
|
38852
39211
|
capScope: "system",
|
|
@@ -38883,38 +39242,50 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
38883
39242
|
addonId: null,
|
|
38884
39243
|
access: "create"
|
|
38885
39244
|
},
|
|
39245
|
+
"recording.startStorageRebalance": {
|
|
39246
|
+
capName: "recording",
|
|
39247
|
+
capScope: "system",
|
|
39248
|
+
addonId: null,
|
|
39249
|
+
access: "create"
|
|
39250
|
+
},
|
|
38886
39251
|
"recordingExport.cancelExport": {
|
|
38887
|
-
capName: "
|
|
39252
|
+
capName: "recording-export",
|
|
38888
39253
|
capScope: "system",
|
|
38889
39254
|
addonId: null,
|
|
38890
39255
|
access: "create"
|
|
38891
39256
|
},
|
|
38892
39257
|
"recordingExport.createExport": {
|
|
38893
|
-
capName: "
|
|
39258
|
+
capName: "recording-export",
|
|
38894
39259
|
capScope: "system",
|
|
38895
39260
|
addonId: null,
|
|
38896
39261
|
access: "create"
|
|
38897
39262
|
},
|
|
38898
39263
|
"recordingExport.deleteExport": {
|
|
38899
|
-
capName: "
|
|
39264
|
+
capName: "recording-export",
|
|
38900
39265
|
capScope: "system",
|
|
38901
39266
|
addonId: null,
|
|
38902
39267
|
access: "delete"
|
|
38903
39268
|
},
|
|
38904
39269
|
"recordingExport.getDownloadUrl": {
|
|
38905
|
-
capName: "
|
|
39270
|
+
capName: "recording-export",
|
|
38906
39271
|
capScope: "system",
|
|
38907
39272
|
addonId: null,
|
|
38908
39273
|
access: "view"
|
|
38909
39274
|
},
|
|
38910
39275
|
"recordingExport.getExport": {
|
|
38911
|
-
capName: "
|
|
39276
|
+
capName: "recording-export",
|
|
38912
39277
|
capScope: "system",
|
|
38913
39278
|
addonId: null,
|
|
38914
39279
|
access: "view"
|
|
38915
39280
|
},
|
|
38916
39281
|
"recordingExport.listExports": {
|
|
38917
|
-
capName: "
|
|
39282
|
+
capName: "recording-export",
|
|
39283
|
+
capScope: "system",
|
|
39284
|
+
addonId: null,
|
|
39285
|
+
access: "view"
|
|
39286
|
+
},
|
|
39287
|
+
"recordingExport.readExportBytes": {
|
|
39288
|
+
capName: "recording-export",
|
|
38918
39289
|
capScope: "system",
|
|
38919
39290
|
addonId: null,
|
|
38920
39291
|
access: "view"
|
|
@@ -40267,6 +40638,12 @@ var CadenceSecField = external_exports.number().int().min(2).max(3600);
|
|
|
40267
40638
|
var FramerateField = external_exports.number().int().min(1).max(60);
|
|
40268
40639
|
var TargetsField = external_exports.array(NcRuleTargetSchema).min(1);
|
|
40269
40640
|
var PriorityField = external_exports.number().int().min(1).max(5);
|
|
40641
|
+
var TIMELAPSE_DENSE_FLOOR_SEC = 0.1;
|
|
40642
|
+
var DenseCadenceSecField = external_exports.number().min(TIMELAPSE_DENSE_FLOOR_SEC).max(3600);
|
|
40643
|
+
var MinDwellSecField = external_exports.number().min(0).max(60);
|
|
40644
|
+
var PreviewTextField = external_exports.string().max(200);
|
|
40645
|
+
var PreviewModeField = external_exports.enum(["image", "gif"]);
|
|
40646
|
+
var ReportClassesField = external_exports.array(external_exports.string().min(1).max(40)).max(20);
|
|
40270
40647
|
var TimelapseRuleInputSchema = external_exports.object({
|
|
40271
40648
|
name: NameField,
|
|
40272
40649
|
enabled: external_exports.boolean().default(true),
|
|
@@ -40282,9 +40659,30 @@ var TimelapseRuleInputSchema = external_exports.object({
|
|
|
40282
40659
|
cadenceSec: CadenceSecField.default(15),
|
|
40283
40660
|
/** Output frames per second of the assembled mp4 (predecessor parity). */
|
|
40284
40661
|
framerate: FramerateField.default(10),
|
|
40662
|
+
/**
|
|
40663
|
+
* Explicit dense cadence — see {@link DenseCadenceSecField}. Absent ⇒ derived
|
|
40664
|
+
* as `max(1s, cadenceSec / 30)`, which is what every rule written before this
|
|
40665
|
+
* field gets.
|
|
40666
|
+
*/
|
|
40667
|
+
denseCadenceSec: DenseCadenceSecField.optional(),
|
|
40668
|
+
/** Output-seconds guarantee per detection range — see {@link MinDwellSecField}. */
|
|
40669
|
+
minDwellSec: MinDwellSecField.optional(),
|
|
40285
40670
|
/** `notification-output` targets the finished video/thumbnail is sent to. */
|
|
40286
40671
|
targets: TargetsField,
|
|
40287
40672
|
template: TimelapseTemplateSchema.optional(),
|
|
40673
|
+
/**
|
|
40674
|
+
* Caption on the notification's PREVIEW FRAME — see {@link PreviewTextField}
|
|
40675
|
+
* and {@link DEFAULT_TIMELAPSE_PREVIEW_TEXT}.
|
|
40676
|
+
*
|
|
40677
|
+
* Deliberately NOT part of {@link TimelapseTemplateSchema}: that object is
|
|
40678
|
+
* the notification's title/body, and clearing it (`template: null`) must not
|
|
40679
|
+
* silently clear the caption too.
|
|
40680
|
+
*/
|
|
40681
|
+
previewText: PreviewTextField.optional(),
|
|
40682
|
+
/** Still or animation — see {@link PreviewModeField}. */
|
|
40683
|
+
previewMode: PreviewModeField.default("image"),
|
|
40684
|
+
/** Classes the notification counts — see {@link ReportClassesField}. */
|
|
40685
|
+
reportClasses: ReportClassesField.optional(),
|
|
40288
40686
|
/** Canonical notification priority ordinal (1..5); per-target overridable. */
|
|
40289
40687
|
priority: PriorityField.default(3)
|
|
40290
40688
|
});
|
|
@@ -40295,8 +40693,13 @@ var TimelapseRulePatchSchema = external_exports.object({
|
|
|
40295
40693
|
schedule: NcScheduleSchema.optional(),
|
|
40296
40694
|
cadenceSec: CadenceSecField.optional(),
|
|
40297
40695
|
framerate: FramerateField.optional(),
|
|
40696
|
+
denseCadenceSec: DenseCadenceSecField.optional(),
|
|
40697
|
+
minDwellSec: MinDwellSecField.optional(),
|
|
40298
40698
|
targets: TargetsField.optional(),
|
|
40299
40699
|
template: TimelapseTemplateSchema.nullable().optional(),
|
|
40700
|
+
previewText: PreviewTextField.optional(),
|
|
40701
|
+
previewMode: PreviewModeField.optional(),
|
|
40702
|
+
reportClasses: ReportClassesField.optional(),
|
|
40300
40703
|
priority: PriorityField.optional()
|
|
40301
40704
|
});
|
|
40302
40705
|
var TimelapseRuleSchema = TimelapseRuleInputSchema.extend({
|
|
@@ -40308,10 +40711,28 @@ var TimelapseRuleSchema = TimelapseRuleInputSchema.extend({
|
|
|
40308
40711
|
*/
|
|
40309
40712
|
ownerUserId: external_exports.string().optional(),
|
|
40310
40713
|
/**
|
|
40311
|
-
* Epoch-ms of the
|
|
40312
|
-
*
|
|
40714
|
+
* Epoch-ms of the NEWEST successful generation across every camera of this
|
|
40715
|
+
* rule. What a UI shows, and the compatibility floor for
|
|
40716
|
+
* {@link readTimelapseGeneratedAt}. Absent = never generated.
|
|
40313
40717
|
*/
|
|
40314
40718
|
lastGeneratedAt: external_exports.number().optional(),
|
|
40719
|
+
/**
|
|
40720
|
+
* PER-CAMERA generation state, keyed by `String(deviceId)` — the
|
|
40721
|
+
* re-generation guard's real durable state.
|
|
40722
|
+
*
|
|
40723
|
+
* One rule covers several cameras and each renders its own video, so a rule
|
|
40724
|
+
* -wide stamp is wrong in the direction that DESTROYS work: camera A
|
|
40725
|
+
* succeeding at 06:05 tells camera B, whose render failed, that it is
|
|
40726
|
+
* already done — and B's night is gone for good, because the window will not
|
|
40727
|
+
* come back.
|
|
40728
|
+
*
|
|
40729
|
+
* ADDITIVE, so the migration is free: a row written before this field simply
|
|
40730
|
+
* has no map, and {@link readTimelapseGeneratedAt} falls back to
|
|
40731
|
+
* {@link TimelapseRuleSchema.shape.lastGeneratedAt}. Reading an old row as
|
|
40732
|
+
* "never generated" would re-render and re-notify every camera of every rule
|
|
40733
|
+
* once, on the deploy that shipped the map.
|
|
40734
|
+
*/
|
|
40735
|
+
generatedByDevice: external_exports.record(external_exports.string(), external_exports.number()).optional(),
|
|
40315
40736
|
/** userId of the caller who created the rule (server-stamped). */
|
|
40316
40737
|
createdBy: external_exports.string(),
|
|
40317
40738
|
createdAt: external_exports.number(),
|