@camstack/addon-post-analysis 0.1.19 → 0.2.0
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/dist-4mTLJ7BJ.mjs +20750 -0
- package/dist/dist-CS2K80so.js +20933 -0
- package/dist/embedding-encoder/index.js +977 -902
- package/dist/embedding-encoder/index.mjs +967 -860
- package/dist/enrichment-engine/index.js +834 -833
- package/dist/enrichment-engine/index.mjs +828 -832
- package/dist/pipeline-analytics/_stub.js +1680 -1397
- package/dist/pipeline-analytics/_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DOSUJ-U0.mjs +156 -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-DJvmVCso.mjs +26 -0
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-B3Wx5J80.mjs +26 -0
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.js-C0AuF9av.mjs +26 -0
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-Bm-iyjmq.mjs +26 -0
- package/dist/pipeline-analytics/dist-CYZr2fwk.mjs +2726 -0
- package/dist/pipeline-analytics/hostInit-BazRS2O7.mjs +129 -0
- package/dist/pipeline-analytics/index.js +7133 -2558
- package/dist/pipeline-analytics/index.mjs +7124 -2557
- package/dist/pipeline-analytics/remoteEntry.js +134 -2973
- package/dist/pipeline-analytics/remoteEntry.ssr.js +33 -0
- package/dist/pipeline-analytics/virtualExposes-BgYzpJZG.mjs +27 -0
- package/dist/pipeline-analytics/virtual_mf-exposes-ssr___mfe_internal__addon_pipeline_analytics_widgets__remoteEntry_js-D7qgWCKX.mjs +10 -0
- package/dist/resolve-frame-5lMxmeI1.js +57 -0
- package/dist/resolve-frame-CT1T1tWy.mjs +44 -0
- package/package.json +26 -32
- package/dist/embedding-encoder/index.js.map +0 -1
- package/dist/embedding-encoder/index.mjs.map +0 -1
- package/dist/enrichment-engine/index.js.map +0 -1
- package/dist/enrichment-engine/index.mjs.map +0 -1
- package/dist/ffmpeg-config-DRONlBsj.mjs +0 -56
- package/dist/ffmpeg-config-DRONlBsj.mjs.map +0 -1
- package/dist/ffmpeg-config-uANz3sV5.js +0 -73
- package/dist/ffmpeg-config-uANz3sV5.js.map +0 -1
- package/dist/index-BFbwYH1P.js +0 -14343
- package/dist/index-BFbwYH1P.js.map +0 -1
- package/dist/index-BrTlzsrE.mjs +0 -14344
- package/dist/index-BrTlzsrE.mjs.map +0 -1
- package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/AudioHistoryChart.d.ts +0 -4
- package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/AudioMetricsPanel.d.ts +0 -10
- package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/DetectionHistoryChart.d.ts +0 -4
- package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/LiveStatsTab.d.ts +0 -5
- package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/MotionHistoryChart.d.ts +0 -4
- package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/OccupancyHistoryChart.d.ts +0 -4
- package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/OccupancyPanel.d.ts +0 -10
- package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/chart-utils.d.ts +0 -97
- package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/index.d.ts +0 -29
- package/dist/pipeline-analytics/@mf-types/widgets.d.ts +0 -2
- package/dist/pipeline-analytics/@mf-types.d.ts +0 -3
- package/dist/pipeline-analytics/@mf-types.zip +0 -0
- package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_sdk__loadShare__.mjs-h5aXOPSA.mjs +0 -12
- package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-NjF4kxzW.mjs +0 -19
- package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-7HAAnpQu.mjs +0 -18
- package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.mjs-DoWbefqS.mjs +0 -104
- package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_trpc_mf_1_client__loadShare__.mjs-52bfkwC8.mjs +0 -85
- package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.mjs-CVrnrGED.mjs +0 -62
- package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs-BZTB2scQ.mjs +0 -88
- package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-CJO5YKGV.mjs +0 -29
- package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.mjs-BsyrX6NO.mjs +0 -36
- package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_2_dom__loadShare__.mjs-Dp8hqYOB.mjs +0 -45
- package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-B0h0AGOH.mjs +0 -6
- package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_2_dom_mf_1_client__loadShare__.mjs-BZjEt71l.mjs +0 -34
- package/dist/pipeline-analytics/_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-kZBmgzMg.mjs +0 -156
- package/dist/pipeline-analytics/client-BlxIUpgf.mjs +0 -9836
- package/dist/pipeline-analytics/getErrorShape-BPSzUA7W-TlK8ipWe.mjs +0 -211
- package/dist/pipeline-analytics/hostInit-qBB1Thhi.mjs +0 -168
- package/dist/pipeline-analytics/index-BoL0rgZt.mjs +0 -435
- package/dist/pipeline-analytics/index-CR1aiZDH.mjs +0 -185
- package/dist/pipeline-analytics/index-CWkKuNLr.mjs +0 -232
- package/dist/pipeline-analytics/index-DlhiA9R0.mjs +0 -2603
- package/dist/pipeline-analytics/index-DtdgkNgf.mjs +0 -725
- package/dist/pipeline-analytics/index-Dw6Q30NI.mjs +0 -1655
- package/dist/pipeline-analytics/index-Dy2V7VOm.mjs +0 -14379
- package/dist/pipeline-analytics/index-i47purqY.mjs +0 -37880
- package/dist/pipeline-analytics/index-xncRG7-x.mjs +0 -2713
- package/dist/pipeline-analytics/index.js.map +0 -1
- package/dist/pipeline-analytics/index.mjs.map +0 -1
- package/dist/pipeline-analytics/jsx-runtime-Dlbl3gpr.mjs +0 -55
- package/dist/pipeline-analytics/schemas-ClCuS4qa.mjs +0 -3594
- package/dist/pipeline-analytics/virtualExposes-8FzWTdq3.mjs +0 -42
- package/dist/playlist-generator-EhPaB7Hn.js +0 -48
- package/dist/playlist-generator-EhPaB7Hn.js.map +0 -1
- package/dist/playlist-generator-VTkgn53O.mjs +0 -48
- package/dist/playlist-generator-VTkgn53O.mjs.map +0 -1
- package/dist/recording/index.js +0 -257
- package/dist/recording/index.js.map +0 -1
- package/dist/recording/index.mjs +0 -235
- package/dist/recording/index.mjs.map +0 -1
- package/dist/recording-coordinator-BoGr5moz.js +0 -1052
- package/dist/recording-coordinator-BoGr5moz.js.map +0 -1
- package/dist/recording-coordinator-CsYH9LqF.mjs +0 -1012
- package/dist/recording-coordinator-CsYH9LqF.mjs.map +0 -1
- package/dist/recording-db-gOgaoQh0.js +0 -348
- package/dist/recording-db-gOgaoQh0.js.map +0 -1
- package/dist/recording-db-lIkSMTLq.mjs +0 -348
- package/dist/recording-db-lIkSMTLq.mjs.map +0 -1
- package/dist/recording-service-facade-B9lG6OFn.mjs +0 -123
- package/dist/recording-service-facade-B9lG6OFn.mjs.map +0 -1
- package/dist/recording-service-facade-Do1PKlAL.js +0 -123
- package/dist/recording-service-facade-Do1PKlAL.js.map +0 -1
- package/dist/storage-estimator-CRpoQc9j.js +0 -72
- package/dist/storage-estimator-CRpoQc9j.js.map +0 -1
- package/dist/storage-estimator-DzD8gWJH.mjs +0 -72
- package/dist/storage-estimator-DzD8gWJH.mjs.map +0 -1
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
class RecordingDb {
|
|
2
|
-
constructor(db) {
|
|
3
|
-
this.db = db;
|
|
4
|
-
}
|
|
5
|
-
initialize() {
|
|
6
|
-
this.db.exec(`
|
|
7
|
-
CREATE TABLE IF NOT EXISTS recording_segments (
|
|
8
|
-
id TEXT PRIMARY KEY,
|
|
9
|
-
device_id TEXT NOT NULL,
|
|
10
|
-
stream_id TEXT NOT NULL,
|
|
11
|
-
start_time INTEGER NOT NULL,
|
|
12
|
-
end_time INTEGER NOT NULL,
|
|
13
|
-
duration REAL NOT NULL,
|
|
14
|
-
path TEXT NOT NULL,
|
|
15
|
-
storage_name TEXT NOT NULL,
|
|
16
|
-
sub_directory TEXT NOT NULL,
|
|
17
|
-
size_bytes INTEGER NOT NULL,
|
|
18
|
-
codec TEXT NOT NULL,
|
|
19
|
-
has_audio INTEGER NOT NULL DEFAULT 0
|
|
20
|
-
);
|
|
21
|
-
CREATE INDEX IF NOT EXISTS idx_segments_device_time ON recording_segments(device_id, stream_id, start_time);
|
|
22
|
-
CREATE INDEX IF NOT EXISTS idx_segments_start_time ON recording_segments(start_time);
|
|
23
|
-
|
|
24
|
-
CREATE TABLE IF NOT EXISTS recording_thumbnails (
|
|
25
|
-
device_id TEXT NOT NULL,
|
|
26
|
-
timestamp INTEGER NOT NULL,
|
|
27
|
-
path TEXT NOT NULL,
|
|
28
|
-
storage_name TEXT NOT NULL,
|
|
29
|
-
sub_directory TEXT NOT NULL,
|
|
30
|
-
size_bytes INTEGER NOT NULL,
|
|
31
|
-
category TEXT NOT NULL,
|
|
32
|
-
PRIMARY KEY (device_id, timestamp, category)
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
CREATE TABLE IF NOT EXISTS recording_policies (
|
|
36
|
-
device_id TEXT PRIMARY KEY,
|
|
37
|
-
enabled INTEGER NOT NULL DEFAULT 0,
|
|
38
|
-
mode TEXT NOT NULL,
|
|
39
|
-
streams_json TEXT NOT NULL,
|
|
40
|
-
schedule_json TEXT,
|
|
41
|
-
pre_buffer_sec INTEGER NOT NULL DEFAULT 5,
|
|
42
|
-
post_buffer_sec INTEGER NOT NULL DEFAULT 10,
|
|
43
|
-
created_at INTEGER NOT NULL,
|
|
44
|
-
updated_at INTEGER NOT NULL
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
CREATE TABLE IF NOT EXISTS recording_storage_config (
|
|
48
|
-
device_id TEXT NOT NULL,
|
|
49
|
-
data_category TEXT NOT NULL,
|
|
50
|
-
storage_name TEXT NOT NULL,
|
|
51
|
-
sub_directory TEXT NOT NULL,
|
|
52
|
-
retention_days INTEGER,
|
|
53
|
-
retention_gb REAL,
|
|
54
|
-
PRIMARY KEY (device_id, data_category)
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
CREATE TABLE IF NOT EXISTS recording_cleanup_queue (
|
|
58
|
-
device_id TEXT PRIMARY KEY,
|
|
59
|
-
disabled_at INTEGER NOT NULL,
|
|
60
|
-
cleanup_after INTEGER NOT NULL,
|
|
61
|
-
status TEXT NOT NULL,
|
|
62
|
-
started_at INTEGER
|
|
63
|
-
);
|
|
64
|
-
`);
|
|
65
|
-
}
|
|
66
|
-
// --- Segments ---
|
|
67
|
-
insertSegment(seg) {
|
|
68
|
-
this.db.prepare(`
|
|
69
|
-
INSERT INTO recording_segments (id, device_id, stream_id, start_time, end_time, duration, path, storage_name, sub_directory, size_bytes, codec, has_audio)
|
|
70
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
71
|
-
`).run(seg.id, String(seg.deviceId), seg.streamId, seg.startTime, seg.endTime, seg.duration, seg.path, seg.storageName, seg.subDirectory, seg.sizeBytes, seg.codec, seg.hasAudio ? 1 : 0);
|
|
72
|
-
}
|
|
73
|
-
querySegments(deviceId, streamId, startTime, endTime) {
|
|
74
|
-
const rows = this.db.prepare(`
|
|
75
|
-
SELECT * FROM recording_segments
|
|
76
|
-
WHERE device_id = ? AND stream_id = ? AND start_time < ? AND end_time > ?
|
|
77
|
-
ORDER BY start_time ASC
|
|
78
|
-
`).all(String(deviceId), streamId, endTime, startTime);
|
|
79
|
-
return rows.map(rowToSegment);
|
|
80
|
-
}
|
|
81
|
-
deleteSegmentsBefore(deviceId, streamId, beforeTime) {
|
|
82
|
-
const toDelete = this.db.prepare(`
|
|
83
|
-
SELECT * FROM recording_segments WHERE device_id = ? AND stream_id = ? AND start_time < ?
|
|
84
|
-
`).all(String(deviceId), streamId, beforeTime);
|
|
85
|
-
this.db.prepare(`DELETE FROM recording_segments WHERE device_id = ? AND stream_id = ? AND start_time < ?`).run(String(deviceId), streamId, beforeTime);
|
|
86
|
-
return toDelete.map(rowToSegment);
|
|
87
|
-
}
|
|
88
|
-
deleteSegmentsForDevice(deviceId) {
|
|
89
|
-
const toDelete = this.db.prepare(`SELECT * FROM recording_segments WHERE device_id = ?`).all(String(deviceId));
|
|
90
|
-
this.db.prepare(`DELETE FROM recording_segments WHERE device_id = ?`).run(String(deviceId));
|
|
91
|
-
return toDelete.map(rowToSegment);
|
|
92
|
-
}
|
|
93
|
-
getStorageUsage(deviceId, streamId) {
|
|
94
|
-
const row = this.db.prepare(`
|
|
95
|
-
SELECT COALESCE(SUM(size_bytes), 0) as total_bytes, COUNT(*) as segment_count
|
|
96
|
-
FROM recording_segments WHERE device_id = ? AND stream_id = ?
|
|
97
|
-
`).get(String(deviceId), streamId);
|
|
98
|
-
return { totalBytes: row?.total_bytes ?? 0, segmentCount: row?.segment_count ?? 0 };
|
|
99
|
-
}
|
|
100
|
-
/** Global storage usage across all devices and streams. */
|
|
101
|
-
getGlobalStorageUsage() {
|
|
102
|
-
const row = this.db.prepare(`
|
|
103
|
-
SELECT COALESCE(SUM(size_bytes), 0) as total_bytes, COUNT(*) as segment_count
|
|
104
|
-
FROM recording_segments
|
|
105
|
-
`).get();
|
|
106
|
-
return { totalBytes: row?.total_bytes ?? 0, segmentCount: row?.segment_count ?? 0 };
|
|
107
|
-
}
|
|
108
|
-
getOldestSegments(deviceId, streamId, limit) {
|
|
109
|
-
const rows = this.db.prepare(`
|
|
110
|
-
SELECT * FROM recording_segments WHERE device_id = ? AND stream_id = ?
|
|
111
|
-
ORDER BY start_time ASC LIMIT ?
|
|
112
|
-
`).all(String(deviceId), streamId, limit);
|
|
113
|
-
return rows.map(rowToSegment);
|
|
114
|
-
}
|
|
115
|
-
getAvailability(deviceId, startTime, endTime) {
|
|
116
|
-
const rows = this.db.prepare(`
|
|
117
|
-
SELECT start_time, end_time, stream_id FROM recording_segments
|
|
118
|
-
WHERE device_id = ? AND end_time >= ? AND start_time <= ?
|
|
119
|
-
ORDER BY start_time ASC
|
|
120
|
-
`).all(String(deviceId), startTime, endTime);
|
|
121
|
-
if (rows.length === 0) return [];
|
|
122
|
-
const ranges = [];
|
|
123
|
-
let current = { startTime: rows[0].start_time, endTime: rows[0].end_time, streams: /* @__PURE__ */ new Set([rows[0].stream_id]) };
|
|
124
|
-
for (let i = 1; i < rows.length; i++) {
|
|
125
|
-
const row = rows[i];
|
|
126
|
-
if (row.start_time <= current.endTime) {
|
|
127
|
-
current.endTime = Math.max(current.endTime, row.end_time);
|
|
128
|
-
current.streams.add(row.stream_id);
|
|
129
|
-
} else {
|
|
130
|
-
ranges.push(current);
|
|
131
|
-
current = { startTime: row.start_time, endTime: row.end_time, streams: /* @__PURE__ */ new Set([row.stream_id]) };
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
ranges.push(current);
|
|
135
|
-
return ranges.map((r) => ({ startTime: r.startTime, endTime: r.endTime, streams: [...r.streams] }));
|
|
136
|
-
}
|
|
137
|
-
getMotionStats(deviceId, startTime, endTime) {
|
|
138
|
-
const row = this.db.prepare(`
|
|
139
|
-
SELECT COUNT(*) as total_events, COALESCE(AVG(duration), 0) as avg_duration, COALESCE(SUM(duration), 0) as total_duration
|
|
140
|
-
FROM recording_segments
|
|
141
|
-
WHERE device_id = ? AND start_time >= ? AND end_time <= ?
|
|
142
|
-
`).get(String(deviceId), startTime, endTime);
|
|
143
|
-
const timeRangeMs = endTime - startTime;
|
|
144
|
-
const timeRangeDays = Math.max(timeRangeMs / (24 * 60 * 60 * 1e3), 1);
|
|
145
|
-
const timeRangeSec = Math.max(timeRangeMs / 1e3, 1);
|
|
146
|
-
const totalEvents = row?.total_events ?? 0;
|
|
147
|
-
const avgDuration = row?.avg_duration ?? 0;
|
|
148
|
-
const totalDuration = row?.total_duration ?? 0;
|
|
149
|
-
return {
|
|
150
|
-
totalEvents,
|
|
151
|
-
avgDurationSec: Math.round(avgDuration * 100) / 100,
|
|
152
|
-
avgEventsPerDay: Math.round(totalEvents / timeRangeDays * 100) / 100,
|
|
153
|
-
dutyCyclePercent: Math.round(totalDuration / timeRangeSec * 1e4) / 100
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
// --- Thumbnails ---
|
|
157
|
-
insertThumbnail(thumb) {
|
|
158
|
-
this.db.prepare(`
|
|
159
|
-
INSERT OR REPLACE INTO recording_thumbnails (device_id, timestamp, path, storage_name, sub_directory, size_bytes, category)
|
|
160
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
161
|
-
`).run(String(thumb.deviceId), thumb.timestamp, thumb.path, thumb.storageName, thumb.subDirectory, thumb.sizeBytes, thumb.category);
|
|
162
|
-
}
|
|
163
|
-
findNearestThumbnail(deviceId, timestamp, category) {
|
|
164
|
-
const row = this.db.prepare(`
|
|
165
|
-
SELECT * FROM recording_thumbnails
|
|
166
|
-
WHERE device_id = ? AND category = ?
|
|
167
|
-
ORDER BY ABS(timestamp - ?) ASC LIMIT 1
|
|
168
|
-
`).get(String(deviceId), category, timestamp);
|
|
169
|
-
return row ? rowToThumbnail(row) : null;
|
|
170
|
-
}
|
|
171
|
-
deleteThumbnailsBefore(deviceId, beforeTime) {
|
|
172
|
-
const result = this.db.prepare(`DELETE FROM recording_thumbnails WHERE device_id = ? AND timestamp < ?`).run(String(deviceId), beforeTime);
|
|
173
|
-
return result.changes;
|
|
174
|
-
}
|
|
175
|
-
deleteThumbnailsForDevice(deviceId) {
|
|
176
|
-
const result = this.db.prepare(`DELETE FROM recording_thumbnails WHERE device_id = ?`).run(String(deviceId));
|
|
177
|
-
return result.changes;
|
|
178
|
-
}
|
|
179
|
-
// --- Policies ---
|
|
180
|
-
upsertPolicy(policy) {
|
|
181
|
-
const now = Date.now();
|
|
182
|
-
this.db.prepare(`
|
|
183
|
-
INSERT INTO recording_policies (device_id, enabled, mode, streams_json, schedule_json, pre_buffer_sec, post_buffer_sec, created_at, updated_at)
|
|
184
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
185
|
-
ON CONFLICT(device_id) DO UPDATE SET enabled=?, mode=?, streams_json=?, schedule_json=?, pre_buffer_sec=?, post_buffer_sec=?, updated_at=?
|
|
186
|
-
`).run(
|
|
187
|
-
String(policy.deviceId),
|
|
188
|
-
policy.enabled ? 1 : 0,
|
|
189
|
-
policy.mode,
|
|
190
|
-
JSON.stringify(policy.streams),
|
|
191
|
-
policy.scheduleRules ? JSON.stringify(policy.scheduleRules) : null,
|
|
192
|
-
policy.preBufferSec,
|
|
193
|
-
policy.postBufferSec,
|
|
194
|
-
now,
|
|
195
|
-
now,
|
|
196
|
-
policy.enabled ? 1 : 0,
|
|
197
|
-
policy.mode,
|
|
198
|
-
JSON.stringify(policy.streams),
|
|
199
|
-
policy.scheduleRules ? JSON.stringify(policy.scheduleRules) : null,
|
|
200
|
-
policy.preBufferSec,
|
|
201
|
-
policy.postBufferSec,
|
|
202
|
-
now
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
getPolicy(deviceId) {
|
|
206
|
-
const row = this.db.prepare(`SELECT * FROM recording_policies WHERE device_id = ?`).get(String(deviceId));
|
|
207
|
-
return row ? rowToPolicy(row) : null;
|
|
208
|
-
}
|
|
209
|
-
getEnabledPolicies() {
|
|
210
|
-
const rows = this.db.prepare(`SELECT * FROM recording_policies WHERE enabled = 1`).all();
|
|
211
|
-
return rows.map(rowToPolicy);
|
|
212
|
-
}
|
|
213
|
-
deletePolicy(deviceId) {
|
|
214
|
-
this.db.prepare(`DELETE FROM recording_policies WHERE device_id = ?`).run(String(deviceId));
|
|
215
|
-
}
|
|
216
|
-
// --- Storage Config ---
|
|
217
|
-
upsertStorageConfig(config) {
|
|
218
|
-
this.db.prepare(`
|
|
219
|
-
INSERT INTO recording_storage_config (device_id, data_category, storage_name, sub_directory, retention_days, retention_gb)
|
|
220
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
221
|
-
ON CONFLICT(device_id, data_category) DO UPDATE SET storage_name=?, sub_directory=?, retention_days=?, retention_gb=?
|
|
222
|
-
`).run(
|
|
223
|
-
String(config.deviceId),
|
|
224
|
-
config.dataCategory,
|
|
225
|
-
config.storageName,
|
|
226
|
-
config.subDirectory,
|
|
227
|
-
config.retentionDays,
|
|
228
|
-
config.retentionGb,
|
|
229
|
-
config.storageName,
|
|
230
|
-
config.subDirectory,
|
|
231
|
-
config.retentionDays,
|
|
232
|
-
config.retentionGb
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
resolveStorageConfig(deviceId, category) {
|
|
236
|
-
const specific = this.db.prepare(
|
|
237
|
-
`SELECT * FROM recording_storage_config WHERE device_id = ? AND data_category = ?`
|
|
238
|
-
).get(String(deviceId), category);
|
|
239
|
-
if (specific) return rowToStorageConfig(specific);
|
|
240
|
-
const global = this.db.prepare(
|
|
241
|
-
`SELECT * FROM recording_storage_config WHERE device_id = '*' AND data_category = ?`
|
|
242
|
-
).get(category);
|
|
243
|
-
return global ? rowToStorageConfig(global) : null;
|
|
244
|
-
}
|
|
245
|
-
// --- Cleanup Queue ---
|
|
246
|
-
addToCleanupQueue(deviceId, disabledAt) {
|
|
247
|
-
const cleanupAfter = disabledAt + 24 * 60 * 60 * 1e3;
|
|
248
|
-
this.db.prepare(`
|
|
249
|
-
INSERT OR REPLACE INTO recording_cleanup_queue (device_id, disabled_at, cleanup_after, status, started_at)
|
|
250
|
-
VALUES (?, ?, ?, 'pending', NULL)
|
|
251
|
-
`).run(String(deviceId), disabledAt, cleanupAfter);
|
|
252
|
-
}
|
|
253
|
-
cancelCleanup(deviceId) {
|
|
254
|
-
this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'cancelled' WHERE device_id = ? AND status = 'pending'`).run(String(deviceId));
|
|
255
|
-
}
|
|
256
|
-
getCleanupEntry(deviceId) {
|
|
257
|
-
const row = this.db.prepare(`SELECT * FROM recording_cleanup_queue WHERE device_id = ?`).get(String(deviceId));
|
|
258
|
-
return row ? rowToCleanup(row) : null;
|
|
259
|
-
}
|
|
260
|
-
getPendingCleanups() {
|
|
261
|
-
const now = Date.now();
|
|
262
|
-
const rows = this.db.prepare(
|
|
263
|
-
`SELECT * FROM recording_cleanup_queue WHERE status = 'pending' AND cleanup_after <= ?`
|
|
264
|
-
).all(now);
|
|
265
|
-
return rows.map(rowToCleanup);
|
|
266
|
-
}
|
|
267
|
-
resetStaleCleanups(maxAgeMs = 36e5) {
|
|
268
|
-
const cutoff = Date.now() - maxAgeMs;
|
|
269
|
-
this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'pending', started_at = NULL WHERE status = 'in_progress' AND started_at < ?`).run(cutoff);
|
|
270
|
-
}
|
|
271
|
-
markCleanupInProgress(deviceId) {
|
|
272
|
-
this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'in_progress', started_at = ? WHERE device_id = ?`).run(Date.now(), String(deviceId));
|
|
273
|
-
}
|
|
274
|
-
markCleanupCompleted(deviceId) {
|
|
275
|
-
this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'completed' WHERE device_id = ?`).run(String(deviceId));
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
function rowToSegment(row) {
|
|
279
|
-
return {
|
|
280
|
-
id: row.id,
|
|
281
|
-
deviceId: Number(row.device_id),
|
|
282
|
-
streamId: row.stream_id,
|
|
283
|
-
startTime: row.start_time,
|
|
284
|
-
endTime: row.end_time,
|
|
285
|
-
duration: row.duration,
|
|
286
|
-
path: row.path,
|
|
287
|
-
storageName: row.storage_name,
|
|
288
|
-
subDirectory: row.sub_directory,
|
|
289
|
-
sizeBytes: row.size_bytes,
|
|
290
|
-
codec: row.codec,
|
|
291
|
-
hasAudio: row.has_audio === 1
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
function rowToThumbnail(row) {
|
|
295
|
-
return {
|
|
296
|
-
deviceId: Number(row.device_id),
|
|
297
|
-
timestamp: row.timestamp,
|
|
298
|
-
path: row.path,
|
|
299
|
-
storageName: row.storage_name,
|
|
300
|
-
subDirectory: row.sub_directory,
|
|
301
|
-
sizeBytes: row.size_bytes,
|
|
302
|
-
category: row.category
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
function parseStreamPolicies(json) {
|
|
306
|
-
const parsed = JSON.parse(json);
|
|
307
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
308
|
-
}
|
|
309
|
-
function parseScheduleRules(json) {
|
|
310
|
-
if (json === null) return void 0;
|
|
311
|
-
const parsed = JSON.parse(json);
|
|
312
|
-
return Array.isArray(parsed) ? parsed : void 0;
|
|
313
|
-
}
|
|
314
|
-
function rowToPolicy(row) {
|
|
315
|
-
const mode = row.mode;
|
|
316
|
-
return {
|
|
317
|
-
deviceId: Number(row.device_id),
|
|
318
|
-
enabled: row.enabled === 1,
|
|
319
|
-
mode,
|
|
320
|
-
streams: parseStreamPolicies(row.streams_json),
|
|
321
|
-
preBufferSec: row.pre_buffer_sec,
|
|
322
|
-
postBufferSec: row.post_buffer_sec,
|
|
323
|
-
scheduleRules: parseScheduleRules(row.schedule_json)
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
function rowToStorageConfig(row) {
|
|
327
|
-
return {
|
|
328
|
-
deviceId: Number(row.device_id),
|
|
329
|
-
dataCategory: row.data_category,
|
|
330
|
-
storageName: row.storage_name,
|
|
331
|
-
subDirectory: row.sub_directory,
|
|
332
|
-
retentionDays: row.retention_days,
|
|
333
|
-
retentionGb: row.retention_gb
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
function rowToCleanup(row) {
|
|
337
|
-
return {
|
|
338
|
-
deviceId: Number(row.device_id),
|
|
339
|
-
disabledAt: row.disabled_at,
|
|
340
|
-
cleanupAfter: row.cleanup_after,
|
|
341
|
-
status: row.status,
|
|
342
|
-
startedAt: row.started_at
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
export {
|
|
346
|
-
RecordingDb
|
|
347
|
-
};
|
|
348
|
-
//# sourceMappingURL=recording-db-lIkSMTLq.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"recording-db-lIkSMTLq.mjs","sources":["../src/recording/recording/recording-db.ts"],"sourcesContent":["import type Database from 'better-sqlite3'\nimport type {\n RecordingSegment, RecordingThumbnail, RecordingStorageConfig,\n RecordingPolicy, RecordingMode, StreamPolicy, ScheduleRule,\n CleanupQueueEntry, CleanupStatus, DataCategory,\n} from './types.js'\n\nexport interface StorageUsage {\n readonly totalBytes: number\n readonly segmentCount: number\n}\n\nexport interface AvailabilityRange {\n readonly startTime: number\n readonly endTime: number\n readonly streams: readonly string[]\n}\n\n// ---------------------------------------------------------------------------\n// SQL row shapes — typed at the better-sqlite3 prepare<TParams, TRow>() boundary\n// so downstream row mappers receive fully-typed records without casts.\n// ---------------------------------------------------------------------------\n\ninterface SegmentRow {\n readonly id: string\n readonly device_id: string\n readonly stream_id: string\n readonly start_time: number\n readonly end_time: number\n readonly duration: number\n readonly path: string\n readonly storage_name: string\n readonly sub_directory: string\n readonly size_bytes: number\n readonly codec: 'h264' | 'h265'\n readonly has_audio: number\n}\n\ninterface ThumbnailRow {\n readonly device_id: string\n readonly timestamp: number\n readonly path: string\n readonly storage_name: string\n readonly sub_directory: string\n readonly size_bytes: number\n readonly category: 'scrub' | 'event'\n}\n\ninterface PolicyRow {\n readonly device_id: string\n readonly enabled: number\n readonly mode: string\n readonly streams_json: string\n readonly schedule_json: string | null\n readonly pre_buffer_sec: number\n readonly post_buffer_sec: number\n}\n\ninterface StorageConfigRow {\n readonly device_id: string\n readonly data_category: DataCategory\n readonly storage_name: string\n readonly sub_directory: string\n readonly retention_days: number | null\n readonly retention_gb: number | null\n}\n\ninterface CleanupRow {\n readonly device_id: string\n readonly disabled_at: number\n readonly cleanup_after: number\n readonly status: CleanupStatus\n readonly started_at: number | null\n}\n\ninterface StorageUsageRow {\n readonly total_bytes: number\n readonly segment_count: number\n}\n\ninterface MotionStatsRow {\n readonly total_events: number\n readonly avg_duration: number\n readonly total_duration: number\n}\n\ninterface AvailabilityRow {\n readonly start_time: number\n readonly end_time: number\n readonly stream_id: string\n}\n\nexport class RecordingDb {\n constructor(private readonly db: Database.Database) {}\n\n initialize(): void {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS recording_segments (\n id TEXT PRIMARY KEY,\n device_id TEXT NOT NULL,\n stream_id TEXT NOT NULL,\n start_time INTEGER NOT NULL,\n end_time INTEGER NOT NULL,\n duration REAL NOT NULL,\n path TEXT NOT NULL,\n storage_name TEXT NOT NULL,\n sub_directory TEXT NOT NULL,\n size_bytes INTEGER NOT NULL,\n codec TEXT NOT NULL,\n has_audio INTEGER NOT NULL DEFAULT 0\n );\n CREATE INDEX IF NOT EXISTS idx_segments_device_time ON recording_segments(device_id, stream_id, start_time);\n CREATE INDEX IF NOT EXISTS idx_segments_start_time ON recording_segments(start_time);\n\n CREATE TABLE IF NOT EXISTS recording_thumbnails (\n device_id TEXT NOT NULL,\n timestamp INTEGER NOT NULL,\n path TEXT NOT NULL,\n storage_name TEXT NOT NULL,\n sub_directory TEXT NOT NULL,\n size_bytes INTEGER NOT NULL,\n category TEXT NOT NULL,\n PRIMARY KEY (device_id, timestamp, category)\n );\n\n CREATE TABLE IF NOT EXISTS recording_policies (\n device_id TEXT PRIMARY KEY,\n enabled INTEGER NOT NULL DEFAULT 0,\n mode TEXT NOT NULL,\n streams_json TEXT NOT NULL,\n schedule_json TEXT,\n pre_buffer_sec INTEGER NOT NULL DEFAULT 5,\n post_buffer_sec INTEGER NOT NULL DEFAULT 10,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS recording_storage_config (\n device_id TEXT NOT NULL,\n data_category TEXT NOT NULL,\n storage_name TEXT NOT NULL,\n sub_directory TEXT NOT NULL,\n retention_days INTEGER,\n retention_gb REAL,\n PRIMARY KEY (device_id, data_category)\n );\n\n CREATE TABLE IF NOT EXISTS recording_cleanup_queue (\n device_id TEXT PRIMARY KEY,\n disabled_at INTEGER NOT NULL,\n cleanup_after INTEGER NOT NULL,\n status TEXT NOT NULL,\n started_at INTEGER\n );\n `)\n }\n\n // --- Segments ---\n\n insertSegment(seg: RecordingSegment): void {\n this.db.prepare(`\n INSERT INTO recording_segments (id, device_id, stream_id, start_time, end_time, duration, path, storage_name, sub_directory, size_bytes, codec, has_audio)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n `).run(seg.id, String(seg.deviceId), seg.streamId, seg.startTime, seg.endTime, seg.duration, seg.path, seg.storageName, seg.subDirectory, seg.sizeBytes, seg.codec, seg.hasAudio ? 1 : 0)\n }\n\n querySegments(deviceId: number, streamId: string, startTime: number, endTime: number): readonly RecordingSegment[] {\n const rows = this.db.prepare<[string, string, number, number], SegmentRow>(`\n SELECT * FROM recording_segments\n WHERE device_id = ? AND stream_id = ? AND start_time < ? AND end_time > ?\n ORDER BY start_time ASC\n `).all(String(deviceId), streamId, endTime, startTime)\n return rows.map(rowToSegment)\n }\n\n deleteSegmentsBefore(deviceId: number, streamId: string, beforeTime: number): readonly RecordingSegment[] {\n const toDelete = this.db.prepare<[string, string, number], SegmentRow>(`\n SELECT * FROM recording_segments WHERE device_id = ? AND stream_id = ? AND start_time < ?\n `).all(String(deviceId), streamId, beforeTime)\n this.db.prepare(`DELETE FROM recording_segments WHERE device_id = ? AND stream_id = ? AND start_time < ?`).run(String(deviceId), streamId, beforeTime)\n return toDelete.map(rowToSegment)\n }\n\n deleteSegmentsForDevice(deviceId: number): readonly RecordingSegment[] {\n const toDelete = this.db.prepare<[string], SegmentRow>(`SELECT * FROM recording_segments WHERE device_id = ?`).all(String(deviceId))\n this.db.prepare(`DELETE FROM recording_segments WHERE device_id = ?`).run(String(deviceId))\n return toDelete.map(rowToSegment)\n }\n\n getStorageUsage(deviceId: number, streamId: string): StorageUsage {\n const row = this.db.prepare<[string, string], StorageUsageRow>(`\n SELECT COALESCE(SUM(size_bytes), 0) as total_bytes, COUNT(*) as segment_count\n FROM recording_segments WHERE device_id = ? AND stream_id = ?\n `).get(String(deviceId), streamId)\n return { totalBytes: row?.total_bytes ?? 0, segmentCount: row?.segment_count ?? 0 }\n }\n\n /** Global storage usage across all devices and streams. */\n getGlobalStorageUsage(): StorageUsage {\n const row = this.db.prepare<[], StorageUsageRow>(`\n SELECT COALESCE(SUM(size_bytes), 0) as total_bytes, COUNT(*) as segment_count\n FROM recording_segments\n `).get()\n return { totalBytes: row?.total_bytes ?? 0, segmentCount: row?.segment_count ?? 0 }\n }\n\n getOldestSegments(deviceId: number, streamId: string, limit: number): readonly RecordingSegment[] {\n const rows = this.db.prepare<[string, string, number], SegmentRow>(`\n SELECT * FROM recording_segments WHERE device_id = ? AND stream_id = ?\n ORDER BY start_time ASC LIMIT ?\n `).all(String(deviceId), streamId, limit)\n return rows.map(rowToSegment)\n }\n\n getAvailability(deviceId: number, startTime: number, endTime: number): readonly AvailabilityRange[] {\n const rows = this.db.prepare<[string, number, number], AvailabilityRow>(`\n SELECT start_time, end_time, stream_id FROM recording_segments\n WHERE device_id = ? AND end_time >= ? AND start_time <= ?\n ORDER BY start_time ASC\n `).all(String(deviceId), startTime, endTime)\n\n if (rows.length === 0) return []\n\n const ranges: Array<{ startTime: number; endTime: number; streams: Set<string> }> = []\n let current = { startTime: rows[0]!.start_time, endTime: rows[0]!.end_time, streams: new Set([rows[0]!.stream_id]) }\n\n for (let i = 1; i < rows.length; i++) {\n const row = rows[i]!\n if (row.start_time <= current.endTime) {\n current.endTime = Math.max(current.endTime, row.end_time)\n current.streams.add(row.stream_id)\n } else {\n ranges.push(current)\n current = { startTime: row.start_time, endTime: row.end_time, streams: new Set([row.stream_id]) }\n }\n }\n ranges.push(current)\n\n return ranges.map(r => ({ startTime: r.startTime, endTime: r.endTime, streams: [...r.streams] }))\n }\n\n getMotionStats(deviceId: number, startTime: number, endTime: number): {\n readonly totalEvents: number\n readonly avgDurationSec: number\n readonly avgEventsPerDay: number\n readonly dutyCyclePercent: number\n } {\n const row = this.db.prepare<[string, number, number], MotionStatsRow>(`\n SELECT COUNT(*) as total_events, COALESCE(AVG(duration), 0) as avg_duration, COALESCE(SUM(duration), 0) as total_duration\n FROM recording_segments\n WHERE device_id = ? AND start_time >= ? AND end_time <= ?\n `).get(String(deviceId), startTime, endTime)\n\n const timeRangeMs = endTime - startTime\n const timeRangeDays = Math.max(timeRangeMs / (24 * 60 * 60 * 1000), 1)\n const timeRangeSec = Math.max(timeRangeMs / 1000, 1)\n\n const totalEvents = row?.total_events ?? 0\n const avgDuration = row?.avg_duration ?? 0\n const totalDuration = row?.total_duration ?? 0\n\n return {\n totalEvents,\n avgDurationSec: Math.round(avgDuration * 100) / 100,\n avgEventsPerDay: Math.round((totalEvents / timeRangeDays) * 100) / 100,\n dutyCyclePercent: Math.round((totalDuration / timeRangeSec) * 10000) / 100,\n }\n }\n\n // --- Thumbnails ---\n\n insertThumbnail(thumb: RecordingThumbnail): void {\n this.db.prepare(`\n INSERT OR REPLACE INTO recording_thumbnails (device_id, timestamp, path, storage_name, sub_directory, size_bytes, category)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `).run(String(thumb.deviceId), thumb.timestamp, thumb.path, thumb.storageName, thumb.subDirectory, thumb.sizeBytes, thumb.category)\n }\n\n findNearestThumbnail(deviceId: number, timestamp: number, category: string): RecordingThumbnail | null {\n const row = this.db.prepare<[string, string, number], ThumbnailRow>(`\n SELECT * FROM recording_thumbnails\n WHERE device_id = ? AND category = ?\n ORDER BY ABS(timestamp - ?) ASC LIMIT 1\n `).get(String(deviceId), category, timestamp)\n return row ? rowToThumbnail(row) : null\n }\n\n deleteThumbnailsBefore(deviceId: number, beforeTime: number): number {\n const result = this.db.prepare(`DELETE FROM recording_thumbnails WHERE device_id = ? AND timestamp < ?`).run(String(deviceId), beforeTime)\n return result.changes\n }\n\n deleteThumbnailsForDevice(deviceId: number): number {\n const result = this.db.prepare(`DELETE FROM recording_thumbnails WHERE device_id = ?`).run(String(deviceId))\n return result.changes\n }\n\n // --- Policies ---\n\n upsertPolicy(policy: { deviceId: number; enabled: boolean; mode: RecordingMode; streams: readonly StreamPolicy[]; preBufferSec: number; postBufferSec: number; scheduleRules?: readonly ScheduleRule[] }): void {\n const now = Date.now()\n this.db.prepare(`\n INSERT INTO recording_policies (device_id, enabled, mode, streams_json, schedule_json, pre_buffer_sec, post_buffer_sec, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(device_id) DO UPDATE SET enabled=?, mode=?, streams_json=?, schedule_json=?, pre_buffer_sec=?, post_buffer_sec=?, updated_at=?\n `).run(\n String(policy.deviceId), policy.enabled ? 1 : 0, policy.mode, JSON.stringify(policy.streams), policy.scheduleRules ? JSON.stringify(policy.scheduleRules) : null, policy.preBufferSec, policy.postBufferSec, now, now,\n policy.enabled ? 1 : 0, policy.mode, JSON.stringify(policy.streams), policy.scheduleRules ? JSON.stringify(policy.scheduleRules) : null, policy.preBufferSec, policy.postBufferSec, now,\n )\n }\n\n getPolicy(deviceId: number): RecordingPolicy | null {\n const row = this.db.prepare<[string], PolicyRow>(`SELECT * FROM recording_policies WHERE device_id = ?`).get(String(deviceId))\n return row ? rowToPolicy(row) : null\n }\n\n getEnabledPolicies(): readonly RecordingPolicy[] {\n const rows = this.db.prepare<[], PolicyRow>(`SELECT * FROM recording_policies WHERE enabled = 1`).all()\n return rows.map(rowToPolicy)\n }\n\n deletePolicy(deviceId: number): void {\n this.db.prepare(`DELETE FROM recording_policies WHERE device_id = ?`).run(String(deviceId))\n }\n\n // --- Storage Config ---\n\n upsertStorageConfig(config: RecordingStorageConfig): void {\n this.db.prepare(`\n INSERT INTO recording_storage_config (device_id, data_category, storage_name, sub_directory, retention_days, retention_gb)\n VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(device_id, data_category) DO UPDATE SET storage_name=?, sub_directory=?, retention_days=?, retention_gb=?\n `).run(String(config.deviceId), config.dataCategory, config.storageName, config.subDirectory, config.retentionDays, config.retentionGb,\n config.storageName, config.subDirectory, config.retentionDays, config.retentionGb)\n }\n\n resolveStorageConfig(deviceId: number, category: DataCategory): RecordingStorageConfig | null {\n const specific = this.db.prepare<[string, DataCategory], StorageConfigRow>(\n `SELECT * FROM recording_storage_config WHERE device_id = ? AND data_category = ?`,\n ).get(String(deviceId), category)\n if (specific) return rowToStorageConfig(specific)\n const global = this.db.prepare<[DataCategory], StorageConfigRow>(\n `SELECT * FROM recording_storage_config WHERE device_id = '*' AND data_category = ?`,\n ).get(category)\n return global ? rowToStorageConfig(global) : null\n }\n\n // --- Cleanup Queue ---\n\n addToCleanupQueue(deviceId: number, disabledAt: number): void {\n const cleanupAfter = disabledAt + 24 * 60 * 60 * 1000\n this.db.prepare(`\n INSERT OR REPLACE INTO recording_cleanup_queue (device_id, disabled_at, cleanup_after, status, started_at)\n VALUES (?, ?, ?, 'pending', NULL)\n `).run(String(deviceId), disabledAt, cleanupAfter)\n }\n\n cancelCleanup(deviceId: number): void {\n this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'cancelled' WHERE device_id = ? AND status = 'pending'`).run(String(deviceId))\n }\n\n getCleanupEntry(deviceId: number): CleanupQueueEntry | null {\n const row = this.db.prepare<[string], CleanupRow>(`SELECT * FROM recording_cleanup_queue WHERE device_id = ?`).get(String(deviceId))\n return row ? rowToCleanup(row) : null\n }\n\n getPendingCleanups(): readonly CleanupQueueEntry[] {\n const now = Date.now()\n const rows = this.db.prepare<[number], CleanupRow>(\n `SELECT * FROM recording_cleanup_queue WHERE status = 'pending' AND cleanup_after <= ?`,\n ).all(now)\n return rows.map(rowToCleanup)\n }\n\n resetStaleCleanups(maxAgeMs: number = 3600000): void {\n const cutoff = Date.now() - maxAgeMs\n this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'pending', started_at = NULL WHERE status = 'in_progress' AND started_at < ?`).run(cutoff)\n }\n\n markCleanupInProgress(deviceId: number): void {\n this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'in_progress', started_at = ? WHERE device_id = ?`).run(Date.now(), String(deviceId))\n }\n\n markCleanupCompleted(deviceId: number): void {\n this.db.prepare(`UPDATE recording_cleanup_queue SET status = 'completed' WHERE device_id = ?`).run(String(deviceId))\n }\n}\n\n// --- Row mappers ---\n\nfunction rowToSegment(row: SegmentRow): RecordingSegment {\n return {\n id: row.id,\n deviceId: Number(row.device_id),\n streamId: row.stream_id,\n startTime: row.start_time,\n endTime: row.end_time,\n duration: row.duration,\n path: row.path,\n storageName: row.storage_name,\n subDirectory: row.sub_directory,\n sizeBytes: row.size_bytes,\n codec: row.codec,\n hasAudio: row.has_audio === 1,\n }\n}\n\nfunction rowToThumbnail(row: ThumbnailRow): RecordingThumbnail {\n return {\n deviceId: Number(row.device_id),\n timestamp: row.timestamp,\n path: row.path,\n storageName: row.storage_name,\n subDirectory: row.sub_directory,\n sizeBytes: row.size_bytes,\n category: row.category,\n }\n}\n\n/** Parse a policy JSON column with a structural type predicate.\n * JSON columns are serialised at write time via JSON.stringify with the\n * corresponding typed value, so at read time we trust the shape but still\n * narrow via `parseJsonUnknown` to keep the boundary documented. */\nfunction parseStreamPolicies(json: string): StreamPolicy[] {\n const parsed: unknown = JSON.parse(json)\n return Array.isArray(parsed) ? (parsed as StreamPolicy[]) : []\n}\n\nfunction parseScheduleRules(json: string | null): ScheduleRule[] | undefined {\n if (json === null) return undefined\n const parsed: unknown = JSON.parse(json)\n return Array.isArray(parsed) ? (parsed as ScheduleRule[]) : undefined\n}\n\nfunction rowToPolicy(row: PolicyRow): RecordingPolicy {\n // `row.mode` comes from a TEXT column whose values are written with typed\n // RecordingMode — narrowed via single documented bridge at this boundary.\n const mode = row.mode as RecordingMode\n return {\n deviceId: Number(row.device_id),\n enabled: row.enabled === 1,\n mode,\n streams: parseStreamPolicies(row.streams_json),\n preBufferSec: row.pre_buffer_sec,\n postBufferSec: row.post_buffer_sec,\n scheduleRules: parseScheduleRules(row.schedule_json),\n }\n}\n\nfunction rowToStorageConfig(row: StorageConfigRow): RecordingStorageConfig {\n // Note: when device_id == '*' (global config sentinel), Number() yields NaN;\n // callers of resolveStorageConfig only read storageName/subDirectory/retention*,\n // never deviceId, so the sentinel row's deviceId is intentionally not preserved.\n return {\n deviceId: Number(row.device_id),\n dataCategory: row.data_category,\n storageName: row.storage_name,\n subDirectory: row.sub_directory,\n retentionDays: row.retention_days,\n retentionGb: row.retention_gb,\n }\n}\n\nfunction rowToCleanup(row: CleanupRow): CleanupQueueEntry {\n return {\n deviceId: Number(row.device_id),\n disabledAt: row.disabled_at,\n cleanupAfter: row.cleanup_after,\n status: row.status,\n startedAt: row.started_at,\n }\n}\n"],"names":[],"mappings":"AA4FO,MAAM,YAAY;AAAA,EACvB,YAA6B,IAAuB;AAAvB,SAAA,KAAA;AAAA,EAAwB;AAAA,EAErD,aAAmB;AACjB,SAAK,GAAG,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KA0DZ;AAAA,EACH;AAAA;AAAA,EAIA,cAAc,KAA6B;AACzC,SAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,KAGf,EAAE,IAAI,IAAI,IAAI,OAAO,IAAI,QAAQ,GAAG,IAAI,UAAU,IAAI,WAAW,IAAI,SAAS,IAAI,UAAU,IAAI,MAAM,IAAI,aAAa,IAAI,cAAc,IAAI,WAAW,IAAI,OAAO,IAAI,WAAW,IAAI,CAAC;AAAA,EAC1L;AAAA,EAEA,cAAc,UAAkB,UAAkB,WAAmB,SAA8C;AACjH,UAAM,OAAO,KAAK,GAAG,QAAsD;AAAA;AAAA;AAAA;AAAA,KAI1E,EAAE,IAAI,OAAO,QAAQ,GAAG,UAAU,SAAS,SAAS;AACrD,WAAO,KAAK,IAAI,YAAY;AAAA,EAC9B;AAAA,EAEA,qBAAqB,UAAkB,UAAkB,YAAiD;AACxG,UAAM,WAAW,KAAK,GAAG,QAA8C;AAAA;AAAA,KAEtE,EAAE,IAAI,OAAO,QAAQ,GAAG,UAAU,UAAU;AAC7C,SAAK,GAAG,QAAQ,yFAAyF,EAAE,IAAI,OAAO,QAAQ,GAAG,UAAU,UAAU;AACrJ,WAAO,SAAS,IAAI,YAAY;AAAA,EAClC;AAAA,EAEA,wBAAwB,UAA+C;AACrE,UAAM,WAAW,KAAK,GAAG,QAA8B,sDAAsD,EAAE,IAAI,OAAO,QAAQ,CAAC;AACnI,SAAK,GAAG,QAAQ,oDAAoD,EAAE,IAAI,OAAO,QAAQ,CAAC;AAC1F,WAAO,SAAS,IAAI,YAAY;AAAA,EAClC;AAAA,EAEA,gBAAgB,UAAkB,UAAgC;AAChE,UAAM,MAAM,KAAK,GAAG,QAA2C;AAAA;AAAA;AAAA,KAG9D,EAAE,IAAI,OAAO,QAAQ,GAAG,QAAQ;AACjC,WAAO,EAAE,YAAY,KAAK,eAAe,GAAG,cAAc,KAAK,iBAAiB,EAAA;AAAA,EAClF;AAAA;AAAA,EAGA,wBAAsC;AACpC,UAAM,MAAM,KAAK,GAAG,QAA6B;AAAA;AAAA;AAAA,KAGhD,EAAE,IAAA;AACH,WAAO,EAAE,YAAY,KAAK,eAAe,GAAG,cAAc,KAAK,iBAAiB,EAAA;AAAA,EAClF;AAAA,EAEA,kBAAkB,UAAkB,UAAkB,OAA4C;AAChG,UAAM,OAAO,KAAK,GAAG,QAA8C;AAAA;AAAA;AAAA,KAGlE,EAAE,IAAI,OAAO,QAAQ,GAAG,UAAU,KAAK;AACxC,WAAO,KAAK,IAAI,YAAY;AAAA,EAC9B;AAAA,EAEA,gBAAgB,UAAkB,WAAmB,SAA+C;AAClG,UAAM,OAAO,KAAK,GAAG,QAAmD;AAAA;AAAA;AAAA;AAAA,KAIvE,EAAE,IAAI,OAAO,QAAQ,GAAG,WAAW,OAAO;AAE3C,QAAI,KAAK,WAAW,EAAG,QAAO,CAAA;AAE9B,UAAM,SAA8E,CAAA;AACpF,QAAI,UAAU,EAAE,WAAW,KAAK,CAAC,EAAG,YAAY,SAAS,KAAK,CAAC,EAAG,UAAU,6BAAa,IAAI,CAAC,KAAK,CAAC,EAAG,SAAS,CAAC,EAAA;AAEjH,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,YAAM,MAAM,KAAK,CAAC;AAClB,UAAI,IAAI,cAAc,QAAQ,SAAS;AACrC,gBAAQ,UAAU,KAAK,IAAI,QAAQ,SAAS,IAAI,QAAQ;AACxD,gBAAQ,QAAQ,IAAI,IAAI,SAAS;AAAA,MACnC,OAAO;AACL,eAAO,KAAK,OAAO;AACnB,kBAAU,EAAE,WAAW,IAAI,YAAY,SAAS,IAAI,UAAU,SAAS,oBAAI,IAAI,CAAC,IAAI,SAAS,CAAC,EAAA;AAAA,MAChG;AAAA,IACF;AACA,WAAO,KAAK,OAAO;AAEnB,WAAO,OAAO,IAAI,CAAA,OAAM,EAAE,WAAW,EAAE,WAAW,SAAS,EAAE,SAAS,SAAS,CAAC,GAAG,EAAE,OAAO,IAAI;AAAA,EAClG;AAAA,EAEA,eAAe,UAAkB,WAAmB,SAKlD;AACA,UAAM,MAAM,KAAK,GAAG,QAAkD;AAAA;AAAA;AAAA;AAAA,KAIrE,EAAE,IAAI,OAAO,QAAQ,GAAG,WAAW,OAAO;AAE3C,UAAM,cAAc,UAAU;AAC9B,UAAM,gBAAgB,KAAK,IAAI,eAAe,KAAK,KAAK,KAAK,MAAO,CAAC;AACrE,UAAM,eAAe,KAAK,IAAI,cAAc,KAAM,CAAC;AAEnD,UAAM,cAAc,KAAK,gBAAgB;AACzC,UAAM,cAAc,KAAK,gBAAgB;AACzC,UAAM,gBAAgB,KAAK,kBAAkB;AAE7C,WAAO;AAAA,MACL;AAAA,MACA,gBAAgB,KAAK,MAAM,cAAc,GAAG,IAAI;AAAA,MAChD,iBAAiB,KAAK,MAAO,cAAc,gBAAiB,GAAG,IAAI;AAAA,MACnE,kBAAkB,KAAK,MAAO,gBAAgB,eAAgB,GAAK,IAAI;AAAA,IAAA;AAAA,EAE3E;AAAA;AAAA,EAIA,gBAAgB,OAAiC;AAC/C,SAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,KAGf,EAAE,IAAI,OAAO,MAAM,QAAQ,GAAG,MAAM,WAAW,MAAM,MAAM,MAAM,aAAa,MAAM,cAAc,MAAM,WAAW,MAAM,QAAQ;AAAA,EACpI;AAAA,EAEA,qBAAqB,UAAkB,WAAmB,UAA6C;AACrG,UAAM,MAAM,KAAK,GAAG,QAAgD;AAAA;AAAA;AAAA;AAAA,KAInE,EAAE,IAAI,OAAO,QAAQ,GAAG,UAAU,SAAS;AAC5C,WAAO,MAAM,eAAe,GAAG,IAAI;AAAA,EACrC;AAAA,EAEA,uBAAuB,UAAkB,YAA4B;AACnE,UAAM,SAAS,KAAK,GAAG,QAAQ,wEAAwE,EAAE,IAAI,OAAO,QAAQ,GAAG,UAAU;AACzI,WAAO,OAAO;AAAA,EAChB;AAAA,EAEA,0BAA0B,UAA0B;AAClD,UAAM,SAAS,KAAK,GAAG,QAAQ,sDAAsD,EAAE,IAAI,OAAO,QAAQ,CAAC;AAC3G,WAAO,OAAO;AAAA,EAChB;AAAA;AAAA,EAIA,aAAa,QAAmM;AAC9M,UAAM,MAAM,KAAK,IAAA;AACjB,SAAK,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA,KAIf,EAAE;AAAA,MACD,OAAO,OAAO,QAAQ;AAAA,MAAG,OAAO,UAAU,IAAI;AAAA,MAAG,OAAO;AAAA,MAAM,KAAK,UAAU,OAAO,OAAO;AAAA,MAAG,OAAO,gBAAgB,KAAK,UAAU,OAAO,aAAa,IAAI;AAAA,MAAM,OAAO;AAAA,MAAc,OAAO;AAAA,MAAe;AAAA,MAAK;AAAA,MAClN,OAAO,UAAU,IAAI;AAAA,MAAG,OAAO;AAAA,MAAM,KAAK,UAAU,OAAO,OAAO;AAAA,MAAG,OAAO,gBAAgB,KAAK,UAAU,OAAO,aAAa,IAAI;AAAA,MAAM,OAAO;AAAA,MAAc,OAAO;AAAA,MAAe;AAAA,IAAA;AAAA,EAExL;AAAA,EAEA,UAAU,UAA0C;AAClD,UAAM,MAAM,KAAK,GAAG,QAA6B,sDAAsD,EAAE,IAAI,OAAO,QAAQ,CAAC;AAC7H,WAAO,MAAM,YAAY,GAAG,IAAI;AAAA,EAClC;AAAA,EAEA,qBAAiD;AAC/C,UAAM,OAAO,KAAK,GAAG,QAAuB,oDAAoD,EAAE,IAAA;AAClG,WAAO,KAAK,IAAI,WAAW;AAAA,EAC7B;AAAA,EAEA,aAAa,UAAwB;AACnC,SAAK,GAAG,QAAQ,oDAAoD,EAAE,IAAI,OAAO,QAAQ,CAAC;AAAA,EAC5F;AAAA;AAAA,EAIA,oBAAoB,QAAsC;AACxD,SAAK,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA,KAIf,EAAE;AAAA,MAAI,OAAO,OAAO,QAAQ;AAAA,MAAG,OAAO;AAAA,MAAc,OAAO;AAAA,MAAa,OAAO;AAAA,MAAc,OAAO;AAAA,MAAe,OAAO;AAAA,MACzH,OAAO;AAAA,MAAa,OAAO;AAAA,MAAc,OAAO;AAAA,MAAe,OAAO;AAAA,IAAA;AAAA,EAC1E;AAAA,EAEA,qBAAqB,UAAkB,UAAuD;AAC5F,UAAM,WAAW,KAAK,GAAG;AAAA,MACvB;AAAA,IAAA,EACA,IAAI,OAAO,QAAQ,GAAG,QAAQ;AAChC,QAAI,SAAU,QAAO,mBAAmB,QAAQ;AAChD,UAAM,SAAS,KAAK,GAAG;AAAA,MACrB;AAAA,IAAA,EACA,IAAI,QAAQ;AACd,WAAO,SAAS,mBAAmB,MAAM,IAAI;AAAA,EAC/C;AAAA;AAAA,EAIA,kBAAkB,UAAkB,YAA0B;AAC5D,UAAM,eAAe,aAAa,KAAK,KAAK,KAAK;AACjD,SAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,KAGf,EAAE,IAAI,OAAO,QAAQ,GAAG,YAAY,YAAY;AAAA,EACnD;AAAA,EAEA,cAAc,UAAwB;AACpC,SAAK,GAAG,QAAQ,oGAAoG,EAAE,IAAI,OAAO,QAAQ,CAAC;AAAA,EAC5I;AAAA,EAEA,gBAAgB,UAA4C;AAC1D,UAAM,MAAM,KAAK,GAAG,QAA8B,2DAA2D,EAAE,IAAI,OAAO,QAAQ,CAAC;AACnI,WAAO,MAAM,aAAa,GAAG,IAAI;AAAA,EACnC;AAAA,EAEA,qBAAmD;AACjD,UAAM,MAAM,KAAK,IAAA;AACjB,UAAM,OAAO,KAAK,GAAG;AAAA,MACnB;AAAA,IAAA,EACA,IAAI,GAAG;AACT,WAAO,KAAK,IAAI,YAAY;AAAA,EAC9B;AAAA,EAEA,mBAAmB,WAAmB,MAAe;AACnD,UAAM,SAAS,KAAK,IAAA,IAAQ;AAC5B,SAAK,GAAG,QAAQ,0HAA0H,EAAE,IAAI,MAAM;AAAA,EACxJ;AAAA,EAEA,sBAAsB,UAAwB;AAC5C,SAAK,GAAG,QAAQ,+FAA+F,EAAE,IAAI,KAAK,IAAA,GAAO,OAAO,QAAQ,CAAC;AAAA,EACnJ;AAAA,EAEA,qBAAqB,UAAwB;AAC3C,SAAK,GAAG,QAAQ,6EAA6E,EAAE,IAAI,OAAO,QAAQ,CAAC;AAAA,EACrH;AACF;AAIA,SAAS,aAAa,KAAmC;AACvD,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,UAAU,OAAO,IAAI,SAAS;AAAA,IAC9B,UAAU,IAAI;AAAA,IACd,WAAW,IAAI;AAAA,IACf,SAAS,IAAI;AAAA,IACb,UAAU,IAAI;AAAA,IACd,MAAM,IAAI;AAAA,IACV,aAAa,IAAI;AAAA,IACjB,cAAc,IAAI;AAAA,IAClB,WAAW,IAAI;AAAA,IACf,OAAO,IAAI;AAAA,IACX,UAAU,IAAI,cAAc;AAAA,EAAA;AAEhC;AAEA,SAAS,eAAe,KAAuC;AAC7D,SAAO;AAAA,IACL,UAAU,OAAO,IAAI,SAAS;AAAA,IAC9B,WAAW,IAAI;AAAA,IACf,MAAM,IAAI;AAAA,IACV,aAAa,IAAI;AAAA,IACjB,cAAc,IAAI;AAAA,IAClB,WAAW,IAAI;AAAA,IACf,UAAU,IAAI;AAAA,EAAA;AAElB;AAMA,SAAS,oBAAoB,MAA8B;AACzD,QAAM,SAAkB,KAAK,MAAM,IAAI;AACvC,SAAO,MAAM,QAAQ,MAAM,IAAK,SAA4B,CAAA;AAC9D;AAEA,SAAS,mBAAmB,MAAiD;AAC3E,MAAI,SAAS,KAAM,QAAO;AAC1B,QAAM,SAAkB,KAAK,MAAM,IAAI;AACvC,SAAO,MAAM,QAAQ,MAAM,IAAK,SAA4B;AAC9D;AAEA,SAAS,YAAY,KAAiC;AAGpD,QAAM,OAAO,IAAI;AACjB,SAAO;AAAA,IACL,UAAU,OAAO,IAAI,SAAS;AAAA,IAC9B,SAAS,IAAI,YAAY;AAAA,IACzB;AAAA,IACA,SAAS,oBAAoB,IAAI,YAAY;AAAA,IAC7C,cAAc,IAAI;AAAA,IAClB,eAAe,IAAI;AAAA,IACnB,eAAe,mBAAmB,IAAI,aAAa;AAAA,EAAA;AAEvD;AAEA,SAAS,mBAAmB,KAA+C;AAIzE,SAAO;AAAA,IACL,UAAU,OAAO,IAAI,SAAS;AAAA,IAC9B,cAAc,IAAI;AAAA,IAClB,aAAa,IAAI;AAAA,IACjB,cAAc,IAAI;AAAA,IAClB,eAAe,IAAI;AAAA,IACnB,aAAa,IAAI;AAAA,EAAA;AAErB;AAEA,SAAS,aAAa,KAAoC;AACxD,SAAO;AAAA,IACL,UAAU,OAAO,IAAI,SAAS;AAAA,IAC9B,YAAY,IAAI;AAAA,IAChB,cAAc,IAAI;AAAA,IAClB,QAAQ,IAAI;AAAA,IACZ,WAAW,IAAI;AAAA,EAAA;AAEnB;"}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
class RecordingServiceFacade {
|
|
2
|
-
constructor(deps) {
|
|
3
|
-
this.deps = deps;
|
|
4
|
-
}
|
|
5
|
-
// ── Status ────────────────────────────────────────────────────────
|
|
6
|
-
getStatus() {
|
|
7
|
-
const { db, coordinator } = this.deps;
|
|
8
|
-
const activeRecordings = coordinator.getActiveCount();
|
|
9
|
-
const { segmentCount: totalSegments, totalBytes } = db.getGlobalStorageUsage();
|
|
10
|
-
return {
|
|
11
|
-
activeRecordings,
|
|
12
|
-
totalSegments,
|
|
13
|
-
totalSizeMB: Math.round(totalBytes / (1024 * 1024))
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
// ── Lifecycle ─────────────────────────────────────────────────────
|
|
17
|
-
async enable(input) {
|
|
18
|
-
await this.deps.coordinator.enableRecording(input.deviceId, {
|
|
19
|
-
policy: input.policy,
|
|
20
|
-
storageOverrides: input.storageOverrides,
|
|
21
|
-
ffmpegOverrides: input.ffmpegOverrides
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
async disable(input) {
|
|
25
|
-
await this.deps.coordinator.disableRecording(input.deviceId);
|
|
26
|
-
}
|
|
27
|
-
// ── Config ────────────────────────────────────────────────────────
|
|
28
|
-
getConfig(input) {
|
|
29
|
-
return this.deps.db.getPolicy(input.deviceId);
|
|
30
|
-
}
|
|
31
|
-
async updateConfig(input) {
|
|
32
|
-
this.deps.db.upsertPolicy({ ...input.policy, deviceId: input.deviceId });
|
|
33
|
-
if (this.deps.coordinator.isRecording(input.deviceId)) {
|
|
34
|
-
await this.deps.coordinator.disableRecording(input.deviceId);
|
|
35
|
-
await this.deps.coordinator.enableRecording(input.deviceId, {
|
|
36
|
-
policy: input.policy,
|
|
37
|
-
ffmpegOverrides: input.ffmpegOverrides
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
// ── Playback ──────────────────────────────────────────────────────
|
|
42
|
-
getPlaylist(input) {
|
|
43
|
-
return this.deps.playlistGenerator.generate(
|
|
44
|
-
input.deviceId,
|
|
45
|
-
input.streamId,
|
|
46
|
-
input.startTime,
|
|
47
|
-
input.endTime,
|
|
48
|
-
{ live: input.live }
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
getThumbnail(input) {
|
|
52
|
-
return this.deps.db.findNearestThumbnail(
|
|
53
|
-
input.deviceId,
|
|
54
|
-
input.timestamp,
|
|
55
|
-
input.category ?? "scrub"
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
getSegments(input) {
|
|
59
|
-
return this.deps.db.querySegments(
|
|
60
|
-
input.deviceId,
|
|
61
|
-
input.streamId,
|
|
62
|
-
input.startTime,
|
|
63
|
-
input.endTime
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
getAvailability(input) {
|
|
67
|
-
return this.deps.db.getAvailability(
|
|
68
|
-
input.deviceId,
|
|
69
|
-
input.startTime,
|
|
70
|
-
input.endTime
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
// ── Storage ───────────────────────────────────────────────────────
|
|
74
|
-
estimateStorage(input) {
|
|
75
|
-
return this.deps.storageEstimator.estimateForDevice(
|
|
76
|
-
input.deviceId,
|
|
77
|
-
input.motionInput
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
estimateGlobalStorage() {
|
|
81
|
-
return this.deps.storageEstimator.estimateGlobal();
|
|
82
|
-
}
|
|
83
|
-
getStorageUsage(input) {
|
|
84
|
-
return this.deps.db.getStorageUsage(input.deviceId, input.streamId);
|
|
85
|
-
}
|
|
86
|
-
// ── Policy ────────────────────────────────────────────────────────
|
|
87
|
-
setPolicy(input) {
|
|
88
|
-
this.deps.db.upsertPolicy({ ...input.policy, deviceId: input.deviceId });
|
|
89
|
-
}
|
|
90
|
-
getPolicy(input) {
|
|
91
|
-
return this.deps.db.getPolicy(input.deviceId);
|
|
92
|
-
}
|
|
93
|
-
getPolicyStatus(input) {
|
|
94
|
-
const policy = this.deps.db.getPolicy(input.deviceId);
|
|
95
|
-
if (!policy) return null;
|
|
96
|
-
const activeStreams = this.deps.coordinator.isRecording(input.deviceId) ? policy.streams.length : 0;
|
|
97
|
-
return {
|
|
98
|
-
deviceId: input.deviceId,
|
|
99
|
-
enabled: policy.enabled,
|
|
100
|
-
mode: policy.mode,
|
|
101
|
-
activeStreams
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
// ── Retention ─────────────────────────────────────────────────────
|
|
105
|
-
getRetentionConfig(input) {
|
|
106
|
-
return this.deps.db.resolveStorageConfig(input.deviceId, input.dataCategory);
|
|
107
|
-
}
|
|
108
|
-
updateRetentionConfig(input) {
|
|
109
|
-
this.deps.db.upsertStorageConfig(input);
|
|
110
|
-
}
|
|
111
|
-
// ── Motion ────────────────────────────────────────────────────────
|
|
112
|
-
getMotionStats(input) {
|
|
113
|
-
return this.deps.db.getMotionStats(
|
|
114
|
-
input.deviceId,
|
|
115
|
-
input.startTime,
|
|
116
|
-
input.endTime
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
export {
|
|
121
|
-
RecordingServiceFacade
|
|
122
|
-
};
|
|
123
|
-
//# sourceMappingURL=recording-service-facade-B9lG6OFn.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"recording-service-facade-B9lG6OFn.mjs","sources":["../src/recording/recording-service-facade.ts"],"sourcesContent":["/**\n * RecordingServiceFacade — implements the recording-engine capability\n * contract by delegating to the internal service classes.\n *\n * Introduced in session 6 Sprint D.2 to replace the broken hand-written\n * recording.router.ts which invoked non-existent methods via a generic\n * `call()` dispatcher. Each facade method delegates to the correct\n * internal service: RecordingCoordinator, RecordingDb, PlaylistGenerator,\n * or StorageEstimator.\n *\n * The facade is the capability provider registered via\n * `context.registerProvider('recording-engine', facade)` — the auto-\n * generated cap router dispatches typed tRPC procedures directly to\n * these methods with full Zod input/output validation.\n */\n\nimport type { RecordingCoordinator } from './recording/recording-coordinator.js'\nimport type { RecordingDb } from './recording/recording-db.js'\nimport type { PlaylistGenerator } from './recording/playlist-generator.js'\nimport type { StorageEstimator } from './recording/storage-estimator.js'\nimport type {\n RecordingPolicy,\n RecordingSegment,\n RecordingThumbnail,\n RecordingStorageConfig,\n StorageEstimate,\n DataCategory,\n} from './recording/types.js'\n\ninterface FacadeDeps {\n readonly coordinator: RecordingCoordinator\n readonly db: RecordingDb\n readonly playlistGenerator: PlaylistGenerator\n readonly storageEstimator: StorageEstimator\n}\n\nexport class RecordingServiceFacade {\n constructor(private readonly deps: FacadeDeps) {}\n\n // ── Status ────────────────────────────────────────────────────────\n\n getStatus(): { activeRecordings: number; totalSegments: number; totalSizeMB: number } {\n const { db, coordinator } = this.deps\n const activeRecordings = coordinator.getActiveCount()\n const { segmentCount: totalSegments, totalBytes } = db.getGlobalStorageUsage()\n return {\n activeRecordings,\n totalSegments,\n totalSizeMB: Math.round(totalBytes / (1024 * 1024)),\n }\n }\n\n // ── Lifecycle ─────────────────────────────────────────────────────\n\n async enable(input: {\n deviceId: number\n policy: Omit<RecordingPolicy, 'deviceId'>\n storageOverrides?: readonly Omit<RecordingStorageConfig, 'deviceId'>[]\n ffmpegOverrides?: Record<string, unknown>\n }): Promise<void> {\n await this.deps.coordinator.enableRecording(input.deviceId, {\n policy: input.policy,\n storageOverrides: input.storageOverrides,\n ffmpegOverrides: input.ffmpegOverrides,\n })\n }\n\n async disable(input: { deviceId: number }): Promise<void> {\n await this.deps.coordinator.disableRecording(input.deviceId)\n }\n\n // ── Config ────────────────────────────────────────────────────────\n\n getConfig(input: { deviceId: number }): RecordingPolicy | null {\n return this.deps.db.getPolicy(input.deviceId)\n }\n\n async updateConfig(input: {\n deviceId: number\n policy: Omit<RecordingPolicy, 'deviceId'>\n ffmpegOverrides?: Record<string, unknown>\n }): Promise<void> {\n this.deps.db.upsertPolicy({ ...input.policy, deviceId: input.deviceId })\n // If the device is actively recording, restart with the new config\n if (this.deps.coordinator.isRecording(input.deviceId)) {\n await this.deps.coordinator.disableRecording(input.deviceId)\n await this.deps.coordinator.enableRecording(input.deviceId, {\n policy: input.policy,\n ffmpegOverrides: input.ffmpegOverrides,\n })\n }\n }\n\n // ── Playback ──────────────────────────────────────────────────────\n\n getPlaylist(input: {\n deviceId: number\n streamId: string\n startTime: number\n endTime: number\n live?: boolean\n }): string {\n return this.deps.playlistGenerator.generate(\n input.deviceId,\n input.streamId,\n input.startTime,\n input.endTime,\n { live: input.live },\n )\n }\n\n getThumbnail(input: {\n deviceId: number\n timestamp: number\n category?: string\n }): RecordingThumbnail | null {\n return this.deps.db.findNearestThumbnail(\n input.deviceId,\n input.timestamp,\n input.category ?? 'scrub',\n )\n }\n\n getSegments(input: {\n deviceId: number\n streamId: string\n startTime: number\n endTime: number\n }): readonly RecordingSegment[] {\n return this.deps.db.querySegments(\n input.deviceId,\n input.streamId,\n input.startTime,\n input.endTime,\n )\n }\n\n getAvailability(input: {\n deviceId: number\n startTime: number\n endTime: number\n }): readonly { startTime: number; endTime: number; streams: readonly string[] }[] {\n return this.deps.db.getAvailability(\n input.deviceId,\n input.startTime,\n input.endTime,\n )\n }\n\n // ── Storage ───────────────────────────────────────────────────────\n\n estimateStorage(input: {\n deviceId: number\n motionInput?: { avgEventsPerDay: number; avgDurationSec: number }\n }): StorageEstimate {\n return this.deps.storageEstimator.estimateForDevice(\n input.deviceId,\n input.motionInput,\n )\n }\n\n estimateGlobalStorage(): StorageEstimate {\n return this.deps.storageEstimator.estimateGlobal()\n }\n\n getStorageUsage(input: { deviceId: number; streamId: string }): {\n totalBytes: number\n segmentCount: number\n } {\n return this.deps.db.getStorageUsage(input.deviceId, input.streamId)\n }\n\n // ── Policy ────────────────────────────────────────────────────────\n\n setPolicy(input: {\n deviceId: number\n policy: Omit<RecordingPolicy, 'deviceId'>\n }): void {\n this.deps.db.upsertPolicy({ ...input.policy, deviceId: input.deviceId })\n }\n\n getPolicy(input: { deviceId: number }): RecordingPolicy | null {\n return this.deps.db.getPolicy(input.deviceId)\n }\n\n getPolicyStatus(input: { deviceId: number }): {\n deviceId: number\n enabled: boolean\n mode: RecordingPolicy['mode']\n activeStreams: number\n } | null {\n const policy = this.deps.db.getPolicy(input.deviceId)\n if (!policy) return null\n const activeStreams = this.deps.coordinator.isRecording(input.deviceId)\n ? policy.streams.length\n : 0\n return {\n deviceId: input.deviceId,\n enabled: policy.enabled,\n mode: policy.mode,\n activeStreams,\n }\n }\n\n // ── Retention ─────────────────────────────────────────────────────\n\n getRetentionConfig(input: {\n deviceId: number\n dataCategory: DataCategory\n }): RecordingStorageConfig | null {\n return this.deps.db.resolveStorageConfig(input.deviceId, input.dataCategory)\n }\n\n updateRetentionConfig(input: RecordingStorageConfig): void {\n this.deps.db.upsertStorageConfig(input)\n }\n\n // ── Motion ────────────────────────────────────────────────────────\n\n getMotionStats(input: {\n deviceId: number\n startTime: number\n endTime: number\n }): {\n totalEvents: number\n avgDurationSec: number\n avgEventsPerDay: number\n dutyCyclePercent: number\n } {\n return this.deps.db.getMotionStats(\n input.deviceId,\n input.startTime,\n input.endTime,\n )\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
class RecordingServiceFacade {
|
|
4
|
-
constructor(deps) {
|
|
5
|
-
this.deps = deps;
|
|
6
|
-
}
|
|
7
|
-
// ── Status ────────────────────────────────────────────────────────
|
|
8
|
-
getStatus() {
|
|
9
|
-
const { db, coordinator } = this.deps;
|
|
10
|
-
const activeRecordings = coordinator.getActiveCount();
|
|
11
|
-
const { segmentCount: totalSegments, totalBytes } = db.getGlobalStorageUsage();
|
|
12
|
-
return {
|
|
13
|
-
activeRecordings,
|
|
14
|
-
totalSegments,
|
|
15
|
-
totalSizeMB: Math.round(totalBytes / (1024 * 1024))
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
// ── Lifecycle ─────────────────────────────────────────────────────
|
|
19
|
-
async enable(input) {
|
|
20
|
-
await this.deps.coordinator.enableRecording(input.deviceId, {
|
|
21
|
-
policy: input.policy,
|
|
22
|
-
storageOverrides: input.storageOverrides,
|
|
23
|
-
ffmpegOverrides: input.ffmpegOverrides
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
async disable(input) {
|
|
27
|
-
await this.deps.coordinator.disableRecording(input.deviceId);
|
|
28
|
-
}
|
|
29
|
-
// ── Config ────────────────────────────────────────────────────────
|
|
30
|
-
getConfig(input) {
|
|
31
|
-
return this.deps.db.getPolicy(input.deviceId);
|
|
32
|
-
}
|
|
33
|
-
async updateConfig(input) {
|
|
34
|
-
this.deps.db.upsertPolicy({ ...input.policy, deviceId: input.deviceId });
|
|
35
|
-
if (this.deps.coordinator.isRecording(input.deviceId)) {
|
|
36
|
-
await this.deps.coordinator.disableRecording(input.deviceId);
|
|
37
|
-
await this.deps.coordinator.enableRecording(input.deviceId, {
|
|
38
|
-
policy: input.policy,
|
|
39
|
-
ffmpegOverrides: input.ffmpegOverrides
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
// ── Playback ──────────────────────────────────────────────────────
|
|
44
|
-
getPlaylist(input) {
|
|
45
|
-
return this.deps.playlistGenerator.generate(
|
|
46
|
-
input.deviceId,
|
|
47
|
-
input.streamId,
|
|
48
|
-
input.startTime,
|
|
49
|
-
input.endTime,
|
|
50
|
-
{ live: input.live }
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
getThumbnail(input) {
|
|
54
|
-
return this.deps.db.findNearestThumbnail(
|
|
55
|
-
input.deviceId,
|
|
56
|
-
input.timestamp,
|
|
57
|
-
input.category ?? "scrub"
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
getSegments(input) {
|
|
61
|
-
return this.deps.db.querySegments(
|
|
62
|
-
input.deviceId,
|
|
63
|
-
input.streamId,
|
|
64
|
-
input.startTime,
|
|
65
|
-
input.endTime
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
getAvailability(input) {
|
|
69
|
-
return this.deps.db.getAvailability(
|
|
70
|
-
input.deviceId,
|
|
71
|
-
input.startTime,
|
|
72
|
-
input.endTime
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
// ── Storage ───────────────────────────────────────────────────────
|
|
76
|
-
estimateStorage(input) {
|
|
77
|
-
return this.deps.storageEstimator.estimateForDevice(
|
|
78
|
-
input.deviceId,
|
|
79
|
-
input.motionInput
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
estimateGlobalStorage() {
|
|
83
|
-
return this.deps.storageEstimator.estimateGlobal();
|
|
84
|
-
}
|
|
85
|
-
getStorageUsage(input) {
|
|
86
|
-
return this.deps.db.getStorageUsage(input.deviceId, input.streamId);
|
|
87
|
-
}
|
|
88
|
-
// ── Policy ────────────────────────────────────────────────────────
|
|
89
|
-
setPolicy(input) {
|
|
90
|
-
this.deps.db.upsertPolicy({ ...input.policy, deviceId: input.deviceId });
|
|
91
|
-
}
|
|
92
|
-
getPolicy(input) {
|
|
93
|
-
return this.deps.db.getPolicy(input.deviceId);
|
|
94
|
-
}
|
|
95
|
-
getPolicyStatus(input) {
|
|
96
|
-
const policy = this.deps.db.getPolicy(input.deviceId);
|
|
97
|
-
if (!policy) return null;
|
|
98
|
-
const activeStreams = this.deps.coordinator.isRecording(input.deviceId) ? policy.streams.length : 0;
|
|
99
|
-
return {
|
|
100
|
-
deviceId: input.deviceId,
|
|
101
|
-
enabled: policy.enabled,
|
|
102
|
-
mode: policy.mode,
|
|
103
|
-
activeStreams
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
// ── Retention ─────────────────────────────────────────────────────
|
|
107
|
-
getRetentionConfig(input) {
|
|
108
|
-
return this.deps.db.resolveStorageConfig(input.deviceId, input.dataCategory);
|
|
109
|
-
}
|
|
110
|
-
updateRetentionConfig(input) {
|
|
111
|
-
this.deps.db.upsertStorageConfig(input);
|
|
112
|
-
}
|
|
113
|
-
// ── Motion ────────────────────────────────────────────────────────
|
|
114
|
-
getMotionStats(input) {
|
|
115
|
-
return this.deps.db.getMotionStats(
|
|
116
|
-
input.deviceId,
|
|
117
|
-
input.startTime,
|
|
118
|
-
input.endTime
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
exports.RecordingServiceFacade = RecordingServiceFacade;
|
|
123
|
-
//# sourceMappingURL=recording-service-facade-Do1PKlAL.js.map
|