@camstack/addon-pipeline 1.2.106 → 1.2.115
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/{addon-utils-BEzVJ0xg.js → addon-utils-C0ht2KPO.js} +51 -15
- package/dist/{addon-utils-A2S9D7pu.mjs → addon-utils-CgBCF-mE.mjs} +50 -14
- package/dist/audio-analyzer/index.js +3 -3
- package/dist/audio-analyzer/index.mjs +3 -3
- package/dist/detection-pipeline/index.js +198 -38
- package/dist/detection-pipeline/index.mjs +197 -37
- package/dist/{dist-BSeR_hVv.js → dist-BqIbYW1A.js} +290 -17
- package/dist/{dist-lITI1Bn5.mjs → dist-DGQkkWc6.mjs} +273 -18
- package/dist/{event-loop-stall-monitor-0VvjTRDP.mjs → event-loop-stall-monitor-D5jK5v4Z.mjs} +223 -2
- package/dist/{event-loop-stall-monitor-BNsp9k8r.js → event-loop-stall-monitor-ylMGpz48.js} +234 -1
- package/dist/{lazy-sharp-CBohM8D8.js → lazy-sharp-cOsy7l_P.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +915 -107
- package/dist/pipeline-runner/index.mjs +914 -106
- package/dist/{process-memory-C33u3tQz.js → process-memory-45juuHpN.js} +1 -1
- package/dist/{process-memory-CEKVKfwg.mjs → process-memory-BtT7WfbJ.mjs} +1 -1
- package/dist/recorder/index.js +187 -47
- package/dist/recorder/index.mjs +187 -47
- package/dist/session-decode/decode-worker-child.js +30 -10
- package/dist/session-decode/decode-worker-child.mjs +29 -9
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-w2XLD3uI.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C_SFFNGT.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B1LDFHY9.mjs +26 -0
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DsIi4G5Q.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BAMyzCWw.mjs} +1 -1
- package/dist/stream-broker/{hostInit-B-h24cDy.mjs → hostInit-CG28NvlV.mjs} +3 -3
- package/dist/stream-broker/index.js +279 -198
- package/dist/stream-broker/index.mjs +279 -198
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-CiKfal2P.js → worker-protocol-CIrqGbpG.js} +6 -3
- package/dist/{worker-protocol-BDiL8TBm.mjs → worker-protocol-DyocTIx_.mjs} +6 -3
- package/package.json +1 -1
- package/python/inference_pool.py +4 -1
- package/python/test_inference_pool_preprocess.py +87 -16
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DvZWLWk1.mjs +0 -26
package/dist/recorder/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const require_dist = require("../dist-
|
|
1
|
+
const require_dist = require("../dist-BqIbYW1A.js");
|
|
2
2
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
3
3
|
const require_restream_intent = require("../restream-intent-Cv9x3jmu.js");
|
|
4
|
-
const require_addon_utils = require("../addon-utils-
|
|
4
|
+
const require_addon_utils = require("../addon-utils-C0ht2KPO.js");
|
|
5
5
|
const require_retire_root_keys = require("../retire-root-keys-KE6D6Xh_.js");
|
|
6
6
|
let node_crypto = require("node:crypto");
|
|
7
7
|
let node_child_process = require("node:child_process");
|
|
@@ -757,6 +757,23 @@ var RecordingIndex = class {
|
|
|
757
757
|
segments(deviceId, profile) {
|
|
758
758
|
return [...this.sortedSegments(deviceId, profile)];
|
|
759
759
|
}
|
|
760
|
+
/**
|
|
761
|
+
* Start-sorted rows whose `startMs` is in `[fromMs, toMs)`.
|
|
762
|
+
* Slices the live view — does not copy the rest of the archive.
|
|
763
|
+
*/
|
|
764
|
+
segmentsStartingIn(deviceId, profile, fromMs, toMs) {
|
|
765
|
+
const segs = this.sortedSegments(deviceId, profile);
|
|
766
|
+
let lo = 0;
|
|
767
|
+
let hi = segs.length;
|
|
768
|
+
while (lo < hi) {
|
|
769
|
+
const mid = lo + hi >> 1;
|
|
770
|
+
if (segs[mid].startMs < fromMs) lo = mid + 1;
|
|
771
|
+
else hi = mid;
|
|
772
|
+
}
|
|
773
|
+
let end = lo;
|
|
774
|
+
while (end < segs.length && segs[end].startMs < toMs) end += 1;
|
|
775
|
+
return lo === 0 && end === segs.length ? segs : segs.slice(lo, end);
|
|
776
|
+
}
|
|
760
777
|
/** True when `relPath` is currently indexed for `deviceId`. O(1). */
|
|
761
778
|
hasSegment(deviceId, relPath) {
|
|
762
779
|
return this.byDevice.get(deviceId)?.has(relPath) === true;
|
|
@@ -2114,6 +2131,29 @@ function utcDayOf(epochMs) {
|
|
|
2114
2131
|
*/
|
|
2115
2132
|
var RANGE_MERGE_GAP_MS$1 = 5e3;
|
|
2116
2133
|
/**
|
|
2134
|
+
* Prefer the RAM index for one hour of the timeline.
|
|
2135
|
+
*
|
|
2136
|
+
* A hydrated hour is closed: the walk (or a targeted `hydrateWindow`) already
|
|
2137
|
+
* looked, so an empty array is a real gap, not "unknown". Write-through
|
|
2138
|
+
* `addSegment` rows ride along once that hour is walked — they do NOT by
|
|
2139
|
+
* themselves make the hour known. A recorder that just respawned at 11:04
|
|
2140
|
+
* has only the post-restart finalizes in RAM; treating those as the whole
|
|
2141
|
+
* hour would hide the 11:00–11:04 footage until the next walk (the 2026-08-22
|
|
2142
|
+
* deploy gap). Unknown hours still fall through to one directory read.
|
|
2143
|
+
*/
|
|
2144
|
+
function hourSegmentsFromIndex(index, deviceId, profile, hourStartMs) {
|
|
2145
|
+
const hour = Math.floor(hourStartMs / HOUR_MS$4) * HOUR_MS$4;
|
|
2146
|
+
const hourEnd = hour + HOUR_MS$4;
|
|
2147
|
+
if (index.hydrationOf(deviceId, hour, hourEnd) !== "hydrated") return null;
|
|
2148
|
+
const rows = [];
|
|
2149
|
+
for (const s of index.segmentsStartingIn(deviceId, profile, hour, hourEnd)) rows.push({
|
|
2150
|
+
startMs: s.startMs,
|
|
2151
|
+
durMs: s.durMs,
|
|
2152
|
+
bytes: s.bytes
|
|
2153
|
+
});
|
|
2154
|
+
return rows;
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2117
2157
|
* How long the CURRENT hour's answer may be reused.
|
|
2118
2158
|
*
|
|
2119
2159
|
* Not caching it at all was the first instinct — it is still being written, so
|
|
@@ -2344,6 +2384,11 @@ var CalendarIndex = class {
|
|
|
2344
2384
|
async segmentsOfChecked(deviceId, profile, hourStartMs, background = false) {
|
|
2345
2385
|
if (!background) this.lastForegroundAt = this.deps.now?.() ?? Date.now();
|
|
2346
2386
|
const hour = Math.floor(hourStartMs / HOUR_MS$4) * HOUR_MS$4;
|
|
2387
|
+
const fromIndex = this.deps.indexHour?.(deviceId, profile, hour);
|
|
2388
|
+
if (fromIndex !== null && fromIndex !== void 0) return {
|
|
2389
|
+
segments: fromIndex,
|
|
2390
|
+
complete: true
|
|
2391
|
+
};
|
|
2347
2392
|
const key = `${deviceId}:${profile}:${hour}`;
|
|
2348
2393
|
const now = this.deps.now?.() ?? Date.now();
|
|
2349
2394
|
const isCurrentHour = hour === Math.floor(now / HOUR_MS$4) * HOUR_MS$4;
|
|
@@ -2413,6 +2458,32 @@ var CalendarIndex = class {
|
|
|
2413
2458
|
return mergeRanges((await mapBounded(Array.from({ length: 24 }, (_, h) => day + h * HOUR_MS$4), READ_CONCURRENCY, (hour) => this.segmentsOf(deviceId, profile, hour))).flat(), RANGE_MERGE_GAP_MS$1);
|
|
2414
2459
|
}
|
|
2415
2460
|
/**
|
|
2461
|
+
* Exact ranges of `[fromMs, toMs)`, scanning ONLY the hours the window
|
|
2462
|
+
* overlaps.
|
|
2463
|
+
*
|
|
2464
|
+
* `dayRanges` always walks 24 hours. The theater asks for a lookback of a
|
|
2465
|
+
* couple of hours, then every 30 s poll (and every scrub that extends
|
|
2466
|
+
* lookback) called that full-day walk PLUS `warmAdjacent` (48 more hours)
|
|
2467
|
+
* on the same FUSE share the feeder was reading. 2026-08-22 15:50Z: locate
|
|
2468
|
+
* was 3 ms from RAM, the .m4s read hit `segment read timeout`.
|
|
2469
|
+
*/
|
|
2470
|
+
async rangesIn(deviceId, profile, fromMs, toMs) {
|
|
2471
|
+
if (toMs <= fromMs) return [];
|
|
2472
|
+
const first = Math.floor(fromMs / HOUR_MS$4) * HOUR_MS$4;
|
|
2473
|
+
const hours = [];
|
|
2474
|
+
for (let hour = first; hour < toMs; hour += HOUR_MS$4) hours.push(hour);
|
|
2475
|
+
const perHour = await mapBounded(hours, READ_CONCURRENCY, (hour) => this.segmentsOf(deviceId, profile, hour));
|
|
2476
|
+
const ranges = [];
|
|
2477
|
+
for (const r of mergeRanges(perHour.flat(), RANGE_MERGE_GAP_MS$1)) {
|
|
2478
|
+
if (r.endMs <= fromMs || r.startMs >= toMs) continue;
|
|
2479
|
+
ranges.push({
|
|
2480
|
+
startMs: Math.max(r.startMs, fromMs),
|
|
2481
|
+
endMs: Math.min(r.endMs, toMs)
|
|
2482
|
+
});
|
|
2483
|
+
}
|
|
2484
|
+
return ranges;
|
|
2485
|
+
}
|
|
2486
|
+
/**
|
|
2416
2487
|
* Warm the days either side of the one being viewed, in the BACKGROUND.
|
|
2417
2488
|
*
|
|
2418
2489
|
* Changing day is one click and the operator expects the timeline to be
|
|
@@ -4405,16 +4476,11 @@ function buildRecordingProvider(deps) {
|
|
|
4405
4476
|
ranges: []
|
|
4406
4477
|
};
|
|
4407
4478
|
const ranges = [];
|
|
4408
|
-
const
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
startMs: r.startMs,
|
|
4414
|
-
endMs: r.endMs
|
|
4415
|
-
});
|
|
4416
|
-
}
|
|
4417
|
-
calendar.warmAdjacent(deviceId, profile, firstDay);
|
|
4479
|
+
for (const r of await calendar.rangesIn(deviceId, profile, fromMs, toMs)) ranges.push({
|
|
4480
|
+
profile,
|
|
4481
|
+
startMs: r.startMs,
|
|
4482
|
+
endMs: r.endMs
|
|
4483
|
+
});
|
|
4418
4484
|
return {
|
|
4419
4485
|
deviceId,
|
|
4420
4486
|
ranges
|
|
@@ -4491,6 +4557,13 @@ function buildRecordingProvider(deps) {
|
|
|
4491
4557
|
},
|
|
4492
4558
|
getDeviceConfig: async ({ deviceId }) => loadDeviceConfig(deps.configStore, deviceId),
|
|
4493
4559
|
locateSegment: async ({ deviceId, profile, epochMs }) => {
|
|
4560
|
+
const covering = deps.index.segmentAt(deviceId, profile, epochMs);
|
|
4561
|
+
if (covering) return {
|
|
4562
|
+
kind: "segment",
|
|
4563
|
+
startMs: covering.startMs,
|
|
4564
|
+
durMs: covering.durMs,
|
|
4565
|
+
bytes: covering.bytes
|
|
4566
|
+
};
|
|
4494
4567
|
const hydrateFromMs = Math.max(0, epochMs - LOCATE_HYDRATE_MARGIN_MS);
|
|
4495
4568
|
const hydrateToMs = epochMs + LOCATE_HYDRATE_MARGIN_MS;
|
|
4496
4569
|
if (deps.index.hydrationOf(deviceId, hydrateFromMs, hydrateToMs) !== "hydrated") await deps.hydrateWindow?.(deviceId, hydrateFromMs, hydrateToMs);
|
|
@@ -7997,34 +8070,87 @@ var RecordingController = class {
|
|
|
7997
8070
|
}
|
|
7998
8071
|
/**
|
|
7999
8072
|
* One profile's ffmpeg writer exhausted its bounded inner restarts and gave
|
|
8000
|
-
* up
|
|
8001
|
-
*
|
|
8002
|
-
*
|
|
8003
|
-
*
|
|
8004
|
-
*
|
|
8005
|
-
*
|
|
8006
|
-
*
|
|
8007
|
-
*
|
|
8073
|
+
* up, or the idle watchdog found it silent.
|
|
8074
|
+
*
|
|
8075
|
+
* Recover THAT profile only when siblings are still producing. Whole-device
|
|
8076
|
+
* detach was the 2026-08-22 gap: high stalled 139 s on 615/590/617/586 and
|
|
8077
|
+
* recovery unpinned low too — the profile the timeline and scrub actually
|
|
8078
|
+
* read — punching 38–172 s holes in continuous cameras. The broker re-dial
|
|
8079
|
+
* argument still holds when NOTHING on the device is healthy; then we detach
|
|
8080
|
+
* the whole set so the next evaluate starts clean.
|
|
8081
|
+
*
|
|
8082
|
+
* Bounded OUTER retry (RECONCILE_TICK_MS / fillMissingProfiles cadence),
|
|
8008
8083
|
* never a tight loop.
|
|
8009
8084
|
*/
|
|
8010
|
-
async recoverWriterGaveUp(deviceId) {
|
|
8085
|
+
async recoverWriterGaveUp(deviceId, profile) {
|
|
8011
8086
|
if (this.stopped) return;
|
|
8012
|
-
this.
|
|
8087
|
+
const current = this.active.get(deviceId);
|
|
8088
|
+
if (!current || current.length === 0) return;
|
|
8089
|
+
const stalled = profile === void 0 ? current : current.filter((r) => r.profile === profile);
|
|
8090
|
+
if (stalled.length === 0) return;
|
|
8091
|
+
const keep = profile === void 0 ? [] : current.filter((r) => r.profile !== profile);
|
|
8092
|
+
if (keep.length === 0) {
|
|
8093
|
+
this.deps.logger.warn("recorder: writer gave up — detaching + re-evaluating device", {
|
|
8094
|
+
tags: { deviceId },
|
|
8095
|
+
...profile !== void 0 ? { meta: { profile } } : {}
|
|
8096
|
+
});
|
|
8097
|
+
try {
|
|
8098
|
+
await this.detachDevice(deviceId);
|
|
8099
|
+
await this.evaluateDevice(deviceId);
|
|
8100
|
+
} catch (err) {
|
|
8101
|
+
this.restore?.add(deviceId, true);
|
|
8102
|
+
this.deps.logger.warn("recorder: writer give-up recovery deferred (retries on next broker ready/reconcile)", {
|
|
8103
|
+
tags: { deviceId },
|
|
8104
|
+
meta: { error: require_dist.errMsg(err) }
|
|
8105
|
+
});
|
|
8106
|
+
}
|
|
8107
|
+
return;
|
|
8108
|
+
}
|
|
8109
|
+
this.deps.logger.warn("recorder: writer gave up — recovering stalled profile, healthy profiles keep recording", {
|
|
8110
|
+
tags: { deviceId },
|
|
8111
|
+
meta: {
|
|
8112
|
+
profile: stalled.map((r) => r.profile),
|
|
8113
|
+
keeping: keep.map((r) => r.profile)
|
|
8114
|
+
}
|
|
8115
|
+
});
|
|
8013
8116
|
try {
|
|
8014
|
-
await this.
|
|
8015
|
-
await this.evaluateDevice(deviceId);
|
|
8117
|
+
await this.recoverStalledProfiles(deviceId, stalled, keep);
|
|
8016
8118
|
} catch (err) {
|
|
8017
8119
|
this.restore?.add(deviceId, true);
|
|
8018
|
-
this.deps.logger.warn("recorder:
|
|
8120
|
+
this.deps.logger.warn("recorder: stalled-profile recovery deferred (retries on next broker ready/reconcile)", {
|
|
8019
8121
|
tags: { deviceId },
|
|
8020
|
-
meta: {
|
|
8122
|
+
meta: {
|
|
8123
|
+
error: require_dist.errMsg(err),
|
|
8124
|
+
profile: stalled.map((r) => r.profile)
|
|
8125
|
+
}
|
|
8021
8126
|
});
|
|
8022
8127
|
}
|
|
8023
8128
|
}
|
|
8024
8129
|
/**
|
|
8130
|
+
* Tear down only the stalled writers, keep the rest pinned, and retry the
|
|
8131
|
+
* missing profiles through {@link fillMissingProfiles}.
|
|
8132
|
+
*/
|
|
8133
|
+
async recoverStalledProfiles(deviceId, stalled, keep) {
|
|
8134
|
+
for (const r of stalled) {
|
|
8135
|
+
this.lastSegmentAt.delete(idleKey(deviceId, r.profile));
|
|
8136
|
+
this.attachRetained.delete(idleKey(deviceId, r.profile));
|
|
8137
|
+
this.firstSegmentSeen.delete(idleKey(deviceId, r.profile));
|
|
8138
|
+
}
|
|
8139
|
+
this.active.set(deviceId, [...keep]);
|
|
8140
|
+
const skipped = this.skippedProfiles.get(deviceId) ?? /* @__PURE__ */ new Set();
|
|
8141
|
+
for (const r of stalled) skipped.add(r.profile);
|
|
8142
|
+
this.skippedProfiles.set(deviceId, skipped);
|
|
8143
|
+
await Promise.all(stalled.map((r) => this.teardownProfile(deviceId, r)));
|
|
8144
|
+
const config = await this.deps.loadConfig(deviceId);
|
|
8145
|
+
if (this.stopped || this.paused) return;
|
|
8146
|
+
await this.fillMissingProfiles(deviceId, config);
|
|
8147
|
+
}
|
|
8148
|
+
/**
|
|
8025
8149
|
* Find writers that hold a broker pin but have finalized nothing for longer
|
|
8026
|
-
* than their idle bound, and recover them through the same
|
|
8027
|
-
*
|
|
8150
|
+
* than their idle bound, and recover them through the same path a give-up
|
|
8151
|
+
* takes. Per-profile when siblings are still producing (high stall must not
|
|
8152
|
+
* unpin the low timeline/scrub writer); whole-device only when every active
|
|
8153
|
+
* profile on that camera is silent.
|
|
8028
8154
|
*
|
|
8029
8155
|
* The counterpart to `onGaveUp`: that one fires when ffmpeg EXITS, this one
|
|
8030
8156
|
* when it does not exit and does not work. Both must exist — a branch that
|
|
@@ -8045,7 +8171,6 @@ var RecordingController = class {
|
|
|
8045
8171
|
boundMs
|
|
8046
8172
|
});
|
|
8047
8173
|
}
|
|
8048
|
-
const devices = new Set(stalled.map((s) => s.deviceId));
|
|
8049
8174
|
for (const s of stalled) this.deps.logger.warn("recorder: writer pinned but no segment finalized — re-pinning", {
|
|
8050
8175
|
tags: { deviceId: s.deviceId },
|
|
8051
8176
|
meta: {
|
|
@@ -8055,7 +8180,20 @@ var RecordingController = class {
|
|
|
8055
8180
|
segmentSeconds: this.active.get(s.deviceId)?.find((r) => r.profile === s.profile)?.segmentSeconds
|
|
8056
8181
|
}
|
|
8057
8182
|
});
|
|
8058
|
-
|
|
8183
|
+
const byDevice = /* @__PURE__ */ new Map();
|
|
8184
|
+
for (const s of stalled) {
|
|
8185
|
+
const list = byDevice.get(s.deviceId);
|
|
8186
|
+
if (list) list.push(s.profile);
|
|
8187
|
+
else byDevice.set(s.deviceId, [s.profile]);
|
|
8188
|
+
}
|
|
8189
|
+
await Promise.all([...byDevice.entries()].map(async ([deviceId, profiles]) => {
|
|
8190
|
+
const activeCount = this.active.get(deviceId)?.length ?? 0;
|
|
8191
|
+
if (profiles.length >= activeCount) {
|
|
8192
|
+
await this.recoverWriterGaveUp(deviceId);
|
|
8193
|
+
return;
|
|
8194
|
+
}
|
|
8195
|
+
for (const profile of profiles) await this.recoverWriterGaveUp(deviceId, profile);
|
|
8196
|
+
}));
|
|
8059
8197
|
}
|
|
8060
8198
|
/** Current trigger state for a device (cold default = no triggers seen). */
|
|
8061
8199
|
triggersFor(deviceId) {
|
|
@@ -8464,7 +8602,7 @@ var RecordingController = class {
|
|
|
8464
8602
|
spawn: this.deps.spawn,
|
|
8465
8603
|
logger: deviceLog,
|
|
8466
8604
|
onGaveUp: () => {
|
|
8467
|
-
this.recoverWriterGaveUp(deviceId);
|
|
8605
|
+
this.recoverWriterGaveUp(deviceId, profile);
|
|
8468
8606
|
},
|
|
8469
8607
|
onResourcePressure: () => this.spawnPressure.report(deviceId)
|
|
8470
8608
|
});
|
|
@@ -10235,10 +10373,10 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
10235
10373
|
/**
|
|
10236
10374
|
* The calendar rungs — which days, which hours, and a day's exact ranges.
|
|
10237
10375
|
*
|
|
10238
|
-
*
|
|
10239
|
-
*
|
|
10240
|
-
*
|
|
10241
|
-
*
|
|
10376
|
+
* Hours the RAM index has already walked (or the live hour it is
|
|
10377
|
+
* write-through on) never touch the recordings volume. Directory reads
|
|
10378
|
+
* remain only for hours still `unknown` — that FUSE readdir is what queued
|
|
10379
|
+
* behind scrub's `locateSegment` and expired its 8 s budget (2026-08-22).
|
|
10242
10380
|
*/
|
|
10243
10381
|
calendarInstance = null;
|
|
10244
10382
|
/** LAZY: a field initialiser runs before `initialize()` assigns `ctx`, and
|
|
@@ -10246,7 +10384,8 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
10246
10384
|
get calendar() {
|
|
10247
10385
|
this.calendarInstance ??= new CalendarIndex({
|
|
10248
10386
|
roots: () => this.resolvedLocations.map((l) => l.root),
|
|
10249
|
-
logger: this.ctx.logger
|
|
10387
|
+
logger: this.ctx.logger,
|
|
10388
|
+
indexHour: (deviceId, profile, hourStartMs) => hourSegmentsFromIndex(this.index, deviceId, profile, hourStartMs)
|
|
10250
10389
|
});
|
|
10251
10390
|
return this.calendarInstance;
|
|
10252
10391
|
}
|
|
@@ -10803,7 +10942,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
10803
10942
|
await this.segmentHours?.load();
|
|
10804
10943
|
this.segmentHours?.hydrateIndex(this.index);
|
|
10805
10944
|
} catch (err) {
|
|
10806
|
-
this.ctx.logger.warn("recorder: segment-hour ledger load failed —
|
|
10945
|
+
this.ctx.logger.warn("recorder: segment-hour ledger load failed — hours stay unknown until a read walks them", { meta: { error: require_dist.errMsg(err) } });
|
|
10807
10946
|
}
|
|
10808
10947
|
let configs;
|
|
10809
10948
|
try {
|
|
@@ -10813,7 +10952,11 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
10813
10952
|
return;
|
|
10814
10953
|
}
|
|
10815
10954
|
this.warmToday([...configs.keys()]);
|
|
10816
|
-
this.
|
|
10955
|
+
this.ctx.logger.info("recorder: full-archive index walk skipped", { meta: {
|
|
10956
|
+
reason: "on-demand-windows",
|
|
10957
|
+
ledgerHours: this.segmentHours?.snapshot().length ?? 0,
|
|
10958
|
+
deviceCount: configs.size
|
|
10959
|
+
} });
|
|
10817
10960
|
this.cleanupLegacyThumbsDirs();
|
|
10818
10961
|
await this.bootstrapExports();
|
|
10819
10962
|
const provider = this.recordingProvider;
|
|
@@ -11239,23 +11382,20 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
11239
11382
|
*/
|
|
11240
11383
|
async warmToday(deviceIds) {
|
|
11241
11384
|
const now = Date.now();
|
|
11242
|
-
const dayMs = 1440 * 60 * 1e3;
|
|
11243
11385
|
const started = now;
|
|
11386
|
+
const liveHour = Math.floor(now / HOUR_MS) * HOUR_MS;
|
|
11244
11387
|
for (const deviceId of deviceIds) try {
|
|
11245
|
-
|
|
11246
|
-
if (profile === null) continue;
|
|
11247
|
-
await this.calendar.warmDayRanges(deviceId, profile, now);
|
|
11248
|
-
await this.calendar.warmDayRanges(deviceId, profile, now - dayMs);
|
|
11388
|
+
await hydrateWindowFromStorage(this.index, deviceId, liveHour, liveHour + HOUR_MS, this.resolvedLocations, this.ctx.logger);
|
|
11249
11389
|
} catch {}
|
|
11250
11390
|
this.ctx.logger.info("recorder: today warmed", { meta: {
|
|
11251
11391
|
devices: deviceIds.length,
|
|
11252
|
-
ms: Date.now() - started
|
|
11392
|
+
ms: Date.now() - started,
|
|
11393
|
+
scope: "live-hour"
|
|
11253
11394
|
} });
|
|
11254
11395
|
}
|
|
11255
11396
|
/**
|
|
11256
|
-
*
|
|
11257
|
-
*
|
|
11258
|
-
* re-armed, and a walk already running is not duplicated.
|
|
11397
|
+
* Opt-in fleet walk — NOT armed at boot (2026-08-22: starved scrub/play).
|
|
11398
|
+
* Kept so an operator path can still request a full reconcile later.
|
|
11259
11399
|
*/
|
|
11260
11400
|
scheduleDeferredFullWalk(deviceIds) {
|
|
11261
11401
|
if (this.fullWalkTimer !== null || this.fullWalkInFlight) return;
|