@camstack/addon-post-analysis 1.2.292 → 1.2.294
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.
- package/dist/{clip-model-registry-CTY5co6P.js → clip-model-registry-BVhju940.js} +4062 -3694
- package/dist/{clip-model-registry-Cudz--Ql.mjs → clip-model-registry-Bz5BzfjI.mjs} +4063 -3695
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +2 -2
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CBHxwCSj.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-4K3NP0yl.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-T_INdcV8.mjs +26 -0
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Bs5QFgmh.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-DSKT2dE4.mjs → hostInit-B47ONgvZ.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +268 -128
- package/dist/pipeline-analytics/index.mjs +268 -128
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +2 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-2OwTV_D2.mjs +0 -26
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DAAKQ3sC.mjs +0 -26
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_clip_model_registry = require("../clip-model-registry-
|
|
5
|
+
const require_clip_model_registry = require("../clip-model-registry-BVhju940.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_clip_model_registry.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -67587,15 +67587,17 @@ function resolveMediaSettings(raw) {
|
|
|
67587
67587
|
* `recording` became a device collection on 2026-09-24 (D625): a camera may
|
|
67588
67588
|
* have several sources of recorded coverage and every read names one. Every
|
|
67589
67589
|
* read in this addon wants the same one — the footage OUR recorder wrote,
|
|
67590
|
-
* because everything downstream of it (a recorded clip cut, a timelapse
|
|
67591
|
-
*
|
|
67592
|
-
*
|
|
67590
|
+
* because everything downstream of it (a recorded clip cut, a timelapse) is
|
|
67591
|
+
* about bytes we hold and can export. The retention sweep's earliest-footage
|
|
67592
|
+
* floor is NOT asked here any more: it is one number the archive's ledger
|
|
67593
|
+
* already folds, and asking it as coverage from the epoch walked every hour
|
|
67594
|
+
* since 1970 (D674, `retention/archive-footage-floor.ts`). A camera's own SD card is not an answer to any of those questions.
|
|
67593
67595
|
*
|
|
67594
67596
|
* It also owns the `read: 'unreadable'` rule, in one place: a read that FAILED
|
|
67595
67597
|
* throws here rather than arriving downstream as an empty range list. Every
|
|
67596
67598
|
* caller of this module treats "no ranges" as "recording is off for this
|
|
67597
|
-
* camera" and acts on it — dropping the attachment, skipping the render
|
|
67598
|
-
*
|
|
67599
|
+
* camera" and acts on it — dropping the attachment, skipping the render —
|
|
67600
|
+
* so folding a failed read into that shape would
|
|
67599
67601
|
* make an unreachable recorder look exactly like a camera nobody records
|
|
67600
67602
|
* (D393). The callers already handle a throw and log it as a read failure.
|
|
67601
67603
|
*/
|
|
@@ -67614,6 +67616,248 @@ async function ourRecordingAvailability(api, input) {
|
|
|
67614
67616
|
profilesWithFootage: answer.profilesWithFootage
|
|
67615
67617
|
};
|
|
67616
67618
|
}
|
|
67619
|
+
//#endregion
|
|
67620
|
+
//#region src/pipeline-analytics/retention-policy.ts
|
|
67621
|
+
/**
|
|
67622
|
+
* Analytics retention policy — **ONE window per device** (D62).
|
|
67623
|
+
*
|
|
67624
|
+
* A device has one retention window, and everything scoped to that device is
|
|
67625
|
+
* deleted on it: the track (which cascades to its object events, their child
|
|
67626
|
+
* crops, its media, its unassigned face/plate rows and crops, and its CLIP
|
|
67627
|
+
* vector) AND the trackless roots (motion, audio, sensor events). The only
|
|
67628
|
+
* thing outside it is the ops log, which is global and longer than any device
|
|
67629
|
+
* window because it records what retention did — including for devices that no
|
|
67630
|
+
* longer exist.
|
|
67631
|
+
*
|
|
67632
|
+
* Two modes:
|
|
67633
|
+
*
|
|
67634
|
+
* - `follow-recordings` (the DEFAULT): the window is the instant of the oldest
|
|
67635
|
+
* segment still on disk. That follows the recorder's age retention AND its
|
|
67636
|
+
* disk-pressure eviction automatically, with no second policy to keep in
|
|
67637
|
+
* sync. A camera with no footage at all (recording off / brand new) falls
|
|
67638
|
+
* back to `trackRetentionDays`, so analytics stay bounded either way.
|
|
67639
|
+
*
|
|
67640
|
+
* - `custom`: `trackRetentionDays`, and nothing else. `0` = keep forever, and
|
|
67641
|
+
* that is the ONE way retention is off for a device.
|
|
67642
|
+
*
|
|
67643
|
+
* ## Why there is only one number here
|
|
67644
|
+
*
|
|
67645
|
+
* There used to be four: `trackRetentionDays` plus a day window per event
|
|
67646
|
+
* family (motion / object / audio). [D61](../../../../docs/decisions/adr-0061.md)
|
|
67647
|
+
* made object events, faces, plates, crops and vectors TRACK-OWNED — they leave
|
|
67648
|
+
* through the cascade and by no other path — which silently disconnected
|
|
67649
|
+
* `retentionObjectDays` from the thing it was named after. An operator could set
|
|
67650
|
+
* object retention to 30 days, the form would save it, and the events would die
|
|
67651
|
+
* with their track at 7. A knob that decides nothing is worse than a missing
|
|
67652
|
+
* one: it reads as a decision that was made.
|
|
67653
|
+
*
|
|
67654
|
+
* So the per-family windows are gone (D62), not merely ignored, and the
|
|
67655
|
+
* surviving surface is ENUMERATED in {@link RETENTION_SETTING_KEYS} so
|
|
67656
|
+
* `scripts/check-retention-settings-consumed.ts` can fail the build the next
|
|
67657
|
+
* time a knob and its reader come apart.
|
|
67658
|
+
*
|
|
67659
|
+
* Follow-mode floor: nothing younger than {@link FOLLOW_FLOOR_DAYS} is ever
|
|
67660
|
+
* pruned, even when footage is younger (a camera that STARTED recording
|
|
67661
|
+
* yesterday must not nuke its whole history back to the first segment).
|
|
67662
|
+
*/
|
|
67663
|
+
var DAY_MS$1 = 1440 * 60 * 1e3;
|
|
67664
|
+
/**
|
|
67665
|
+
* Per-device analytics retention settings as stored in the device blob.
|
|
67666
|
+
* `retentionMode` absent = follow-recordings (the default).
|
|
67667
|
+
*/
|
|
67668
|
+
var RetentionSettingsSchema = require_clip_model_registry.object({
|
|
67669
|
+
retentionMode: require_clip_model_registry._enum(["follow-recordings", "custom"]).default("follow-recordings"),
|
|
67670
|
+
trackRetentionDays: require_clip_model_registry.number().min(0).default(7)
|
|
67671
|
+
});
|
|
67672
|
+
/** The default retention window, in days. The form's default reads from here so
|
|
67673
|
+
* the schema is the single source. */
|
|
67674
|
+
var TRACK_RETENTION_DEFAULT_DAYS = RetentionSettingsSchema.parse({}).trackRetentionDays;
|
|
67675
|
+
/** Resolve the settings from a raw device-store blob — invalid or missing
|
|
67676
|
+
* fields fall back to defaults, never throw. */
|
|
67677
|
+
function resolveRetentionSettings(raw) {
|
|
67678
|
+
const parsed = RetentionSettingsSchema.safeParse(raw);
|
|
67679
|
+
if (parsed.success) return parsed.data;
|
|
67680
|
+
const shape = RetentionSettingsSchema.shape;
|
|
67681
|
+
const defaults = RetentionSettingsSchema.parse({});
|
|
67682
|
+
const mode = shape.retentionMode.safeParse(raw["retentionMode"]);
|
|
67683
|
+
const days = shape.trackRetentionDays.safeParse(raw["trackRetentionDays"]);
|
|
67684
|
+
return {
|
|
67685
|
+
retentionMode: mode.success ? mode.data : defaults.retentionMode,
|
|
67686
|
+
trackRetentionDays: days.success ? days.data : defaults.trackRetentionDays
|
|
67687
|
+
};
|
|
67688
|
+
}
|
|
67689
|
+
/**
|
|
67690
|
+
* Follow-mode cutoff: the oldest retained footage instant, floored so anything
|
|
67691
|
+
* younger than {@link FOLLOW_FLOOR_DAYS} survives. `null` footage (none on
|
|
67692
|
+
* disk) → `null`, and the caller falls back to the custom window.
|
|
67693
|
+
*/
|
|
67694
|
+
function followCutoffMs(nowMs, earliestFootageMs) {
|
|
67695
|
+
if (earliestFootageMs === null) return null;
|
|
67696
|
+
return Math.min(earliestFootageMs, nowMs - 7 * DAY_MS$1);
|
|
67697
|
+
}
|
|
67698
|
+
/** Compute the effective cutoff for one device at `nowMs`. */
|
|
67699
|
+
function resolveRetentionCutoffs(settings, nowMs, earliestFootageMs) {
|
|
67700
|
+
if (settings.retentionMode === "follow-recordings") {
|
|
67701
|
+
const cutoff = followCutoffMs(nowMs, earliestFootageMs);
|
|
67702
|
+
if (cutoff !== null) return {
|
|
67703
|
+
cutoffMs: cutoff,
|
|
67704
|
+
effectiveMode: "follow-recordings"
|
|
67705
|
+
};
|
|
67706
|
+
}
|
|
67707
|
+
return {
|
|
67708
|
+
cutoffMs: settings.trackRetentionDays <= 0 ? null : nowMs - settings.trackRetentionDays * DAY_MS$1,
|
|
67709
|
+
effectiveMode: "custom"
|
|
67710
|
+
};
|
|
67711
|
+
}
|
|
67712
|
+
//#endregion
|
|
67713
|
+
//#region src/pipeline-analytics/retention/archive-footage-floor.ts
|
|
67714
|
+
/**
|
|
67715
|
+
* How long one archive answer serves every camera. The ledger's floor moves
|
|
67716
|
+
* only when retention deletes an hour (minutes-to-days granularity), and the
|
|
67717
|
+
* sweep it feeds runs every 5 min — the same 10 min the walk was cached for.
|
|
67718
|
+
*/
|
|
67719
|
+
var FOOTAGE_FLOOR_TTL_MS = 10 * 6e4;
|
|
67720
|
+
/**
|
|
67721
|
+
* How long a FAILED read is reused before it is asked again. Short, so the
|
|
67722
|
+
* first sweep after the recorder answers is a real one; long enough that one
|
|
67723
|
+
* pass over ~24 cameras is one failed call, not twenty-four.
|
|
67724
|
+
*/
|
|
67725
|
+
var FOOTAGE_FLOOR_RETRY_MS = 6e4;
|
|
67726
|
+
/** What a follow-recordings camera gets when its floor could not be read:
|
|
67727
|
+
* retention OFF for this pass — nothing is deleted on a guess. */
|
|
67728
|
+
var UNREADABLE_FLOOR_CUTOFFS = {
|
|
67729
|
+
cutoffMs: null,
|
|
67730
|
+
effectiveMode: "follow-recordings",
|
|
67731
|
+
floorUnreadable: true
|
|
67732
|
+
};
|
|
67733
|
+
var NO_FOOTAGE = {
|
|
67734
|
+
kind: "read",
|
|
67735
|
+
oldestMs: null
|
|
67736
|
+
};
|
|
67737
|
+
/**
|
|
67738
|
+
* Turn one `getStorageUsage` answer into per-camera floors — or say why it
|
|
67739
|
+
* cannot be.
|
|
67740
|
+
*
|
|
67741
|
+
* - `devicesRead !== 'archive'`: the rows are UNKNOWN (`unavailable`: no
|
|
67742
|
+
* ledger, a ledger not loaded yet, no location resolved) or come from a
|
|
67743
|
+
* recorder predating D418, whose rows are RESIDENCY. No floor may be read.
|
|
67744
|
+
* - `complete !== true`: the ledger load was cut at its row cap, or the
|
|
67745
|
+
* recorder predates the field. A camera ABSENT from such an answer may hold
|
|
67746
|
+
* footage, and a PRESENT row's `oldestMs` is the oldest of a fraction (the
|
|
67747
|
+
* load keeps the newest hours) — too young, i.e. a delete. So the whole
|
|
67748
|
+
* answer is unreadable, not only the absent cameras.
|
|
67749
|
+
* - A row whose `oldestMs` is ABSENT comes from a recorder predating the
|
|
67750
|
+
* field — unknown for that camera, never "no footage".
|
|
67751
|
+
* - A camera with no row in a COMPLETE answer, or a row whose `oldestMs` is
|
|
67752
|
+
* `null`: the archive holds no footage for it — the custom window applies.
|
|
67753
|
+
*/
|
|
67754
|
+
function footageFloorsOf(usage) {
|
|
67755
|
+
if (usage.devicesRead !== "archive") return `recordingArchive.getStorageUsage answered devicesRead=${usage.devicesRead ?? "absent"} — the per-camera rows are not the archive`;
|
|
67756
|
+
if (usage.complete !== true) return `recordingArchive.getStorageUsage answered complete=${String(usage.complete ?? "absent")} — the per-camera rows may be a fraction of the archive`;
|
|
67757
|
+
const floors = /* @__PURE__ */ new Map();
|
|
67758
|
+
for (const row of usage.devices) floors.set(row.deviceId, row.oldestMs === void 0 ? {
|
|
67759
|
+
kind: "unreadable",
|
|
67760
|
+
reason: "the recorder omitted oldestMs for this camera"
|
|
67761
|
+
} : {
|
|
67762
|
+
kind: "read",
|
|
67763
|
+
oldestMs: row.oldestMs
|
|
67764
|
+
});
|
|
67765
|
+
return floors;
|
|
67766
|
+
}
|
|
67767
|
+
/**
|
|
67768
|
+
* Per-camera floors, one archive call per {@link FOOTAGE_FLOOR_TTL_MS} for the
|
|
67769
|
+
* whole fleet, single-flight.
|
|
67770
|
+
*/
|
|
67771
|
+
var ArchiveFootageFloors = class {
|
|
67772
|
+
deps;
|
|
67773
|
+
answer = null;
|
|
67774
|
+
inFlight = null;
|
|
67775
|
+
/** Cameras whose sweep is currently skipped — one line per episode, not per
|
|
67776
|
+
* pass: two sweeps × every camera × every 5 min would be a flood. */
|
|
67777
|
+
skipped = /* @__PURE__ */ new Set();
|
|
67778
|
+
/** Is the archive currently unreadable? The read-failure line is said once
|
|
67779
|
+
* per EPISODE (and its end once), not once per ~sweep per runner. */
|
|
67780
|
+
readFailing = false;
|
|
67781
|
+
constructor(deps) {
|
|
67782
|
+
this.deps = deps;
|
|
67783
|
+
}
|
|
67784
|
+
nowMs() {
|
|
67785
|
+
return this.deps.now?.() ?? Date.now();
|
|
67786
|
+
}
|
|
67787
|
+
async floorOf(deviceId) {
|
|
67788
|
+
const answer = await this.current();
|
|
67789
|
+
if ("unreadable" in answer) return {
|
|
67790
|
+
kind: "unreadable",
|
|
67791
|
+
reason: answer.unreadable
|
|
67792
|
+
};
|
|
67793
|
+
return answer.floors.get(deviceId) ?? NO_FOOTAGE;
|
|
67794
|
+
}
|
|
67795
|
+
/**
|
|
67796
|
+
* The per-device effective cutoffs (mode + footage → numbers). A follow
|
|
67797
|
+
* camera whose floor is unreadable is NOT swept this pass, and that is logged.
|
|
67798
|
+
*/
|
|
67799
|
+
async cutoffsFor(deviceId, settings, nowMs) {
|
|
67800
|
+
if (settings.retentionMode !== "follow-recordings") return resolveRetentionCutoffs(settings, nowMs, null);
|
|
67801
|
+
const floor = await this.floorOf(deviceId);
|
|
67802
|
+
if (floor.kind === "unreadable") {
|
|
67803
|
+
if (!this.skipped.has(deviceId)) {
|
|
67804
|
+
this.skipped.add(deviceId);
|
|
67805
|
+
this.deps.logger.warn("retention: follow-recordings floor unreadable — this camera is not swept until the archive answers", {
|
|
67806
|
+
tags: { deviceId },
|
|
67807
|
+
meta: { reason: floor.reason }
|
|
67808
|
+
});
|
|
67809
|
+
}
|
|
67810
|
+
return UNREADABLE_FLOOR_CUTOFFS;
|
|
67811
|
+
}
|
|
67812
|
+
if (this.skipped.delete(deviceId)) this.deps.logger.info("retention: follow-recordings floor readable again — sweeping", {
|
|
67813
|
+
tags: { deviceId },
|
|
67814
|
+
meta: { oldestMs: floor.oldestMs }
|
|
67815
|
+
});
|
|
67816
|
+
return resolveRetentionCutoffs(settings, nowMs, floor.oldestMs);
|
|
67817
|
+
}
|
|
67818
|
+
async current() {
|
|
67819
|
+
const held = this.answer;
|
|
67820
|
+
const now = this.nowMs();
|
|
67821
|
+
if (held !== null) {
|
|
67822
|
+
const ttl = "unreadable" in held ? FOOTAGE_FLOOR_RETRY_MS : FOOTAGE_FLOOR_TTL_MS;
|
|
67823
|
+
if (now - held.atMs < ttl) return held;
|
|
67824
|
+
}
|
|
67825
|
+
this.inFlight ??= this.read().finally(() => {
|
|
67826
|
+
this.inFlight = null;
|
|
67827
|
+
});
|
|
67828
|
+
return this.inFlight;
|
|
67829
|
+
}
|
|
67830
|
+
async read() {
|
|
67831
|
+
let answer;
|
|
67832
|
+
try {
|
|
67833
|
+
const floors = footageFloorsOf(await this.deps.api.recordingArchive.getStorageUsage.query({}));
|
|
67834
|
+
answer = typeof floors === "string" ? {
|
|
67835
|
+
atMs: this.nowMs(),
|
|
67836
|
+
unreadable: floors
|
|
67837
|
+
} : {
|
|
67838
|
+
atMs: this.nowMs(),
|
|
67839
|
+
floors
|
|
67840
|
+
};
|
|
67841
|
+
} catch (err) {
|
|
67842
|
+
answer = {
|
|
67843
|
+
atMs: this.nowMs(),
|
|
67844
|
+
unreadable: `recordingArchive.getStorageUsage failed: ${err instanceof Error ? err.message : String(err)}`
|
|
67845
|
+
};
|
|
67846
|
+
}
|
|
67847
|
+
if ("unreadable" in answer && !this.readFailing) {
|
|
67848
|
+
this.readFailing = true;
|
|
67849
|
+
this.deps.logger.warn("retention: the archive floor could not be read — follow-recordings cameras are not swept on a guess", { meta: {
|
|
67850
|
+
reason: answer.unreadable,
|
|
67851
|
+
retryMs: FOOTAGE_FLOOR_RETRY_MS
|
|
67852
|
+
} });
|
|
67853
|
+
} else if (!("unreadable" in answer) && this.readFailing) {
|
|
67854
|
+
this.readFailing = false;
|
|
67855
|
+
this.deps.logger.info("retention: the archive floor is readable again", { meta: { cameras: answer.floors.size } });
|
|
67856
|
+
}
|
|
67857
|
+
this.answer = answer;
|
|
67858
|
+
return answer;
|
|
67859
|
+
}
|
|
67860
|
+
};
|
|
67617
67861
|
function toDetectionBbox(bbox) {
|
|
67618
67862
|
return {
|
|
67619
67863
|
x: bbox.x,
|
|
@@ -79042,7 +79286,7 @@ async function sweepTracklessRoots(deps) {
|
|
|
79042
79286
|
const cutoffs = await deps.resolveCutoffs(deviceId, deps.nowMs);
|
|
79043
79287
|
const cutoffMs = cutoffs.cutoffMs;
|
|
79044
79288
|
if (cutoffMs === null) {
|
|
79045
|
-
deps.onRetentionDisabled(deviceId);
|
|
79289
|
+
if (cutoffs.floorUnreadable !== true) deps.onRetentionDisabled(deviceId);
|
|
79046
79290
|
continue;
|
|
79047
79291
|
}
|
|
79048
79292
|
const events = await deps.evictTracklessEvents({
|
|
@@ -79064,99 +79308,6 @@ async function sweepTracklessRoots(deps) {
|
|
|
79064
79308
|
}
|
|
79065
79309
|
}
|
|
79066
79310
|
//#endregion
|
|
79067
|
-
//#region src/pipeline-analytics/retention-policy.ts
|
|
79068
|
-
/**
|
|
79069
|
-
* Analytics retention policy — **ONE window per device** (D62).
|
|
79070
|
-
*
|
|
79071
|
-
* A device has one retention window, and everything scoped to that device is
|
|
79072
|
-
* deleted on it: the track (which cascades to its object events, their child
|
|
79073
|
-
* crops, its media, its unassigned face/plate rows and crops, and its CLIP
|
|
79074
|
-
* vector) AND the trackless roots (motion, audio, sensor events). The only
|
|
79075
|
-
* thing outside it is the ops log, which is global and longer than any device
|
|
79076
|
-
* window because it records what retention did — including for devices that no
|
|
79077
|
-
* longer exist.
|
|
79078
|
-
*
|
|
79079
|
-
* Two modes:
|
|
79080
|
-
*
|
|
79081
|
-
* - `follow-recordings` (the DEFAULT): the window is the instant of the oldest
|
|
79082
|
-
* segment still on disk. That follows the recorder's age retention AND its
|
|
79083
|
-
* disk-pressure eviction automatically, with no second policy to keep in
|
|
79084
|
-
* sync. A camera with no footage at all (recording off / brand new) falls
|
|
79085
|
-
* back to `trackRetentionDays`, so analytics stay bounded either way.
|
|
79086
|
-
*
|
|
79087
|
-
* - `custom`: `trackRetentionDays`, and nothing else. `0` = keep forever, and
|
|
79088
|
-
* that is the ONE way retention is off for a device.
|
|
79089
|
-
*
|
|
79090
|
-
* ## Why there is only one number here
|
|
79091
|
-
*
|
|
79092
|
-
* There used to be four: `trackRetentionDays` plus a day window per event
|
|
79093
|
-
* family (motion / object / audio). [D61](../../../../docs/decisions/adr-0061.md)
|
|
79094
|
-
* made object events, faces, plates, crops and vectors TRACK-OWNED — they leave
|
|
79095
|
-
* through the cascade and by no other path — which silently disconnected
|
|
79096
|
-
* `retentionObjectDays` from the thing it was named after. An operator could set
|
|
79097
|
-
* object retention to 30 days, the form would save it, and the events would die
|
|
79098
|
-
* with their track at 7. A knob that decides nothing is worse than a missing
|
|
79099
|
-
* one: it reads as a decision that was made.
|
|
79100
|
-
*
|
|
79101
|
-
* So the per-family windows are gone (D62), not merely ignored, and the
|
|
79102
|
-
* surviving surface is ENUMERATED in {@link RETENTION_SETTING_KEYS} so
|
|
79103
|
-
* `scripts/check-retention-settings-consumed.ts` can fail the build the next
|
|
79104
|
-
* time a knob and its reader come apart.
|
|
79105
|
-
*
|
|
79106
|
-
* Follow-mode floor: nothing younger than {@link FOLLOW_FLOOR_DAYS} is ever
|
|
79107
|
-
* pruned, even when footage is younger (a camera that STARTED recording
|
|
79108
|
-
* yesterday must not nuke its whole history back to the first segment).
|
|
79109
|
-
*/
|
|
79110
|
-
var DAY_MS$1 = 1440 * 60 * 1e3;
|
|
79111
|
-
/**
|
|
79112
|
-
* Per-device analytics retention settings as stored in the device blob.
|
|
79113
|
-
* `retentionMode` absent = follow-recordings (the default).
|
|
79114
|
-
*/
|
|
79115
|
-
var RetentionSettingsSchema = require_clip_model_registry.object({
|
|
79116
|
-
retentionMode: require_clip_model_registry._enum(["follow-recordings", "custom"]).default("follow-recordings"),
|
|
79117
|
-
trackRetentionDays: require_clip_model_registry.number().min(0).default(7)
|
|
79118
|
-
});
|
|
79119
|
-
/** The default retention window, in days. The form's default reads from here so
|
|
79120
|
-
* the schema is the single source. */
|
|
79121
|
-
var TRACK_RETENTION_DEFAULT_DAYS = RetentionSettingsSchema.parse({}).trackRetentionDays;
|
|
79122
|
-
/** Resolve the settings from a raw device-store blob — invalid or missing
|
|
79123
|
-
* fields fall back to defaults, never throw. */
|
|
79124
|
-
function resolveRetentionSettings(raw) {
|
|
79125
|
-
const parsed = RetentionSettingsSchema.safeParse(raw);
|
|
79126
|
-
if (parsed.success) return parsed.data;
|
|
79127
|
-
const shape = RetentionSettingsSchema.shape;
|
|
79128
|
-
const defaults = RetentionSettingsSchema.parse({});
|
|
79129
|
-
const mode = shape.retentionMode.safeParse(raw["retentionMode"]);
|
|
79130
|
-
const days = shape.trackRetentionDays.safeParse(raw["trackRetentionDays"]);
|
|
79131
|
-
return {
|
|
79132
|
-
retentionMode: mode.success ? mode.data : defaults.retentionMode,
|
|
79133
|
-
trackRetentionDays: days.success ? days.data : defaults.trackRetentionDays
|
|
79134
|
-
};
|
|
79135
|
-
}
|
|
79136
|
-
/**
|
|
79137
|
-
* Follow-mode cutoff: the oldest retained footage instant, floored so anything
|
|
79138
|
-
* younger than {@link FOLLOW_FLOOR_DAYS} survives. `null` footage (none on
|
|
79139
|
-
* disk) → `null`, and the caller falls back to the custom window.
|
|
79140
|
-
*/
|
|
79141
|
-
function followCutoffMs(nowMs, earliestFootageMs) {
|
|
79142
|
-
if (earliestFootageMs === null) return null;
|
|
79143
|
-
return Math.min(earliestFootageMs, nowMs - 7 * DAY_MS$1);
|
|
79144
|
-
}
|
|
79145
|
-
/** Compute the effective cutoff for one device at `nowMs`. */
|
|
79146
|
-
function resolveRetentionCutoffs(settings, nowMs, earliestFootageMs) {
|
|
79147
|
-
if (settings.retentionMode === "follow-recordings") {
|
|
79148
|
-
const cutoff = followCutoffMs(nowMs, earliestFootageMs);
|
|
79149
|
-
if (cutoff !== null) return {
|
|
79150
|
-
cutoffMs: cutoff,
|
|
79151
|
-
effectiveMode: "follow-recordings"
|
|
79152
|
-
};
|
|
79153
|
-
}
|
|
79154
|
-
return {
|
|
79155
|
-
cutoffMs: settings.trackRetentionDays <= 0 ? null : nowMs - settings.trackRetentionDays * DAY_MS$1,
|
|
79156
|
-
effectiveMode: "custom"
|
|
79157
|
-
};
|
|
79158
|
-
}
|
|
79159
|
-
//#endregion
|
|
79160
79311
|
//#region src/pipeline-analytics/runtime/binding-cache.ts
|
|
79161
79312
|
var BindingCache = class {
|
|
79162
79313
|
api;
|
|
@@ -95410,37 +95561,26 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
95410
95561
|
await this.trackCloser.sweep();
|
|
95411
95562
|
await this.summarySealer?.sweep();
|
|
95412
95563
|
}
|
|
95413
|
-
/**
|
|
95414
|
-
*
|
|
95415
|
-
*
|
|
95416
|
-
*
|
|
95417
|
-
|
|
95418
|
-
|
|
95419
|
-
|
|
95420
|
-
|
|
95421
|
-
|
|
95422
|
-
|
|
95423
|
-
|
|
95424
|
-
deviceId,
|
|
95425
|
-
fromMs: 0,
|
|
95426
|
-
toMs: Date.now()
|
|
95427
|
-
});
|
|
95428
|
-
let min = Number.POSITIVE_INFINITY;
|
|
95429
|
-
for (const r of res.ranges) if (r.startMs < min) min = r.startMs;
|
|
95430
|
-
value = Number.isFinite(min) ? min : null;
|
|
95431
|
-
} catch {
|
|
95432
|
-
value = null;
|
|
95433
|
-
}
|
|
95434
|
-
this.earliestFootageCache.set(deviceId, {
|
|
95435
|
-
at: Date.now(),
|
|
95436
|
-
value
|
|
95564
|
+
/**
|
|
95565
|
+
* The follow-recordings floor per camera, read from the ARCHIVE's hour ledger
|
|
95566
|
+
* (`recordingArchive.getStorageUsage`, one call for every camera) — never an
|
|
95567
|
+
* availability walk from the epoch, which cost the recorder 2.2 GB and an OOM
|
|
95568
|
+
* (D674). LAZY: `ctx` is not assigned when field initialisers run.
|
|
95569
|
+
*/
|
|
95570
|
+
footageFloorsInstance = null;
|
|
95571
|
+
get footageFloors() {
|
|
95572
|
+
this.footageFloorsInstance ??= new ArchiveFootageFloors({
|
|
95573
|
+
api: this.ctx.api,
|
|
95574
|
+
logger: this.ctx.logger
|
|
95437
95575
|
});
|
|
95438
|
-
return
|
|
95576
|
+
return this.footageFloorsInstance;
|
|
95439
95577
|
}
|
|
95440
|
-
/** The per-device effective cutoffs (mode + footage → numbers).
|
|
95578
|
+
/** The per-device effective cutoffs (mode + footage → numbers). A
|
|
95579
|
+
* follow-recordings camera whose floor could not be read gets NO cutoff this
|
|
95580
|
+
* pass (logged in `ArchiveFootageFloors`), never the custom window. */
|
|
95441
95581
|
async deviceRetentionCutoffs(deviceId, nowMs) {
|
|
95442
|
-
const
|
|
95443
|
-
return
|
|
95582
|
+
const raw = await this.readDeviceBlob(deviceId);
|
|
95583
|
+
return this.footageFloors.cutoffsFor(deviceId, resolveRetentionSettings(raw), nowMs);
|
|
95444
95584
|
}
|
|
95445
95585
|
/**
|
|
95446
95586
|
* The devices one retention pass covers — see `retention/retention-scope.ts`
|