@camstack/addon-post-analysis 1.2.91 → 1.2.93
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-CquggoRC.mjs → dist-3QRyOemL.mjs} +353 -20
- package/dist/{dist-CTIUuGRB.js → dist-DYoWeRRt.js} +370 -19
- package/dist/embedding-encoder/index.js +171 -4
- package/dist/embedding-encoder/index.mjs +171 -4
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Q3EjHtct.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-C4-ljPpt.mjs} +3 -3
- package/dist/pipeline-analytics/{hostInit-D2EZ3nOf.mjs → hostInit-BoUejl7E.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +389 -160
- package/dist/pipeline-analytics/index.mjs +352 -123
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +3 -2
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-DYoWeRRt.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -8289,6 +8289,8 @@ var en_default = {
|
|
|
8289
8289
|
"system.node-offline.body.worker": "Worker {{nodeId}} is offline",
|
|
8290
8290
|
"system.node-inference-unavailable.title": "Node has no usable inference device",
|
|
8291
8291
|
"system.node-inference-unavailable.body": "Node {{nodeId}} can no longer run inference — every enabled device is unavailable ({{devices}}). It will not receive cameras until one returns.",
|
|
8292
|
+
"system.addon-crash-loop.title": "Addon stopped after repeated crashes",
|
|
8293
|
+
"system.addon-crash-loop.body": "The {{addons}} addon on node {{nodeId}} crashed {{count}} times in a row and will NOT be restarted. Everything it provides is offline until it is repaired.",
|
|
8292
8294
|
"system.detection-blind.title": "Camera detecting nothing",
|
|
8293
8295
|
"system.detection-blind.body": {
|
|
8294
8296
|
"one": "Camera {{deviceName}} has been analysed for {{minutes}} minute with no detection at all",
|
|
@@ -8501,6 +8503,8 @@ var it_default = {
|
|
|
8501
8503
|
"system.node-offline.body.worker": "Il worker {{nodeId}} è offline",
|
|
8502
8504
|
"system.node-inference-unavailable.title": "Nodo senza dispositivi di inferenza utilizzabili",
|
|
8503
8505
|
"system.node-inference-unavailable.body": "Il nodo {{nodeId}} non può più eseguire inferenza — tutti i dispositivi abilitati non sono disponibili ({{devices}}). Non riceverà camere finché uno non torna disponibile.",
|
|
8506
|
+
"system.addon-crash-loop.title": "Addon fermato dopo ripetuti crash",
|
|
8507
|
+
"system.addon-crash-loop.body": "L'addon {{addons}} sul nodo {{nodeId}} è andato in crash {{count}} volte di seguito e NON verrà riavviato. Tutto ciò che fornisce resta offline finché non viene riparato.",
|
|
8504
8508
|
"system.detection-blind.title": "Camera che non rileva nulla",
|
|
8505
8509
|
"system.detection-blind.body": {
|
|
8506
8510
|
"one": "La camera {{deviceName}} è analizzata da {{minutes}} minuto senza alcun rilevamento",
|
|
@@ -10273,6 +10277,27 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
10273
10277
|
nodeId
|
|
10274
10278
|
});
|
|
10275
10279
|
}
|
|
10280
|
+
case "addon.runner-failed": {
|
|
10281
|
+
const nodeId = stringField(data, "nodeId");
|
|
10282
|
+
const runnerId = stringField(data, "runnerId");
|
|
10283
|
+
const crashes = numberField(data, "crashesInWindow");
|
|
10284
|
+
if (nodeId === void 0 || runnerId === void 0 || crashes === void 0) return null;
|
|
10285
|
+
const ids = data["addonIds"];
|
|
10286
|
+
const addons = Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
|
|
10287
|
+
return systemSubject({
|
|
10288
|
+
facts,
|
|
10289
|
+
kind: "addon-crash-loop",
|
|
10290
|
+
subject: `addon-crash:${nodeId}:${runnerId}`,
|
|
10291
|
+
titleKey: "system.addon-crash-loop.title",
|
|
10292
|
+
bodyKey: "system.addon-crash-loop.body",
|
|
10293
|
+
textParams: {
|
|
10294
|
+
nodeId,
|
|
10295
|
+
addons: addons.length > 0 ? addons.join(", ") : runnerId,
|
|
10296
|
+
count: String(crashes)
|
|
10297
|
+
},
|
|
10298
|
+
nodeId
|
|
10299
|
+
});
|
|
10300
|
+
}
|
|
10276
10301
|
case "pipeline.detection-blind": {
|
|
10277
10302
|
const deviceId = numberField(data, "deviceId");
|
|
10278
10303
|
const blindForMs = numberField(data, "blindForMs");
|
|
@@ -21940,6 +21965,9 @@ function systemEventKeys(kind) {
|
|
|
21940
21965
|
}
|
|
21941
21966
|
/** The arm mode a synthetic alarm event stands in for — see {@link systemEventKeys}. */
|
|
21942
21967
|
var TEST_ALARM_MODE = "away";
|
|
21968
|
+
/** The runner a synthetic `addon-crash-loop` stands in for. A test fire has no
|
|
21969
|
+
* real crashed runner, and a blank one would render "The addon on node hub". */
|
|
21970
|
+
var TEST_CRASHED_RUNNER = "recorder";
|
|
21943
21971
|
/** The dedup subject the live intake stamps for this kind — same families. */
|
|
21944
21972
|
function systemEventSubject(kind, scope) {
|
|
21945
21973
|
if (kind === "device-disabled" || kind === "device-enabled") return `device-switch:${scope.deviceId ?? 0}`;
|
|
@@ -21948,6 +21976,7 @@ function systemEventSubject(kind, scope) {
|
|
|
21948
21976
|
if (kind === "node-online" || kind === "node-offline") return `node:${scope.nodeId ?? "hub"}`;
|
|
21949
21977
|
if (kind === "node-inference-unavailable") return `node-inference:${scope.nodeId ?? "hub"}`;
|
|
21950
21978
|
if (kind === "detection-blind") return `detection-blind:${scope.deviceId ?? 0}`;
|
|
21979
|
+
if (kind === "addon-crash-loop") return `addon-crash:${scope.nodeId ?? "hub"}:${TEST_CRASHED_RUNNER}`;
|
|
21951
21980
|
if (kind === "addon-update-available" || kind === "server-update-available") return `package:${scope.packageName ?? "@camstack/server"}`;
|
|
21952
21981
|
if (kind === "addon-updated") return `packages:addon-updated:${scope.packageName ?? "pkg"}`;
|
|
21953
21982
|
if (kind === "server-updated") return `server-updated:${scope.nodeId ?? "hub"}`;
|
|
@@ -22064,6 +22093,7 @@ function planRuleTest(input) {
|
|
|
22064
22093
|
seconds: "30",
|
|
22065
22094
|
devices: "cuda:0",
|
|
22066
22095
|
minutes: "15",
|
|
22096
|
+
addons: TEST_CRASHED_RUNNER,
|
|
22067
22097
|
list: "@camstack/addon-pipeline 1.0.0 (current 0.0.0)",
|
|
22068
22098
|
nodes: "hub",
|
|
22069
22099
|
fromVersion: "0.0.0",
|
|
@@ -28856,88 +28886,6 @@ var PlateGalleryProvider = class {
|
|
|
28856
28886
|
}
|
|
28857
28887
|
};
|
|
28858
28888
|
//#endregion
|
|
28859
|
-
//#region src/pipeline-analytics/retention/orphan-audit-actions.ts
|
|
28860
|
-
/**
|
|
28861
|
-
* The operator button for the orphan audit.
|
|
28862
|
-
*
|
|
28863
|
-
* The audit engine, its five bounds and its resumable cursor already existed
|
|
28864
|
-
* and had run in `report` mode for weeks; what it had no way to be was
|
|
28865
|
-
* *asked*. Its only trigger was a 6-hourly timer reading a durable setting, so
|
|
28866
|
-
* draining a standing backlog meant flipping a persistent mode, waiting up to
|
|
28867
|
-
* six hours, and remembering to flip it back — with nothing returned to look
|
|
28868
|
-
* at. On 2026-08-14 the backlog was 74,700 media rows / 18.9 GB plus 23,296
|
|
28869
|
-
* object events and 4,543 CLIP vectors (D161), and that is not a thing to
|
|
28870
|
-
* drain by leaving a switch on.
|
|
28871
|
-
*
|
|
28872
|
-
* Three properties this action does NOT leave to the caller's care:
|
|
28873
|
-
*
|
|
28874
|
-
* 1. **Dry run is the default.** `mode` omitted means `report`: it counts,
|
|
28875
|
-
* logs and returns, and deletes nothing — regardless of what the durable
|
|
28876
|
-
* `orphanAuditMode` setting says. `reclaim` is a word that has to be
|
|
28877
|
-
* typed. The engine's own default is the same for the same reason.
|
|
28878
|
-
* 2. **It is paced.** `pageSize` bounds one read, `maxRowsPerScope` bounds
|
|
28879
|
-
* one pass, `maxReclaimPerScope` bounds the damage, and the engine yields
|
|
28880
|
-
* the thread between pages — `better-sqlite3` is synchronous and shares
|
|
28881
|
-
* hub-main with every live frame.
|
|
28882
|
-
* 3. **It answers.** The report comes back per collection with rows AND
|
|
28883
|
-
* bytes, so "did it do anything" is not a question anyone has to infer
|
|
28884
|
-
* from a log line.
|
|
28885
|
-
*/
|
|
28886
|
-
/**
|
|
28887
|
-
* `report` counts; `reclaim` deletes.
|
|
28888
|
-
*
|
|
28889
|
-
* `.default('report')` is load-bearing, not decoration: the settings form
|
|
28890
|
-
* sends `{}` for a button with no fields, and the safe interpretation of an
|
|
28891
|
-
* unstated intent here is the one that destroys nothing.
|
|
28892
|
-
*/
|
|
28893
|
-
var OrphanAuditActionModeSchema = require_dist._enum(["report", "reclaim"]).default("report");
|
|
28894
|
-
var OrphanAuditActionInputSchema = require_dist.object({
|
|
28895
|
-
mode: OrphanAuditActionModeSchema,
|
|
28896
|
-
/**
|
|
28897
|
-
* Start every scope from the top instead of resuming its persisted cursor.
|
|
28898
|
-
*
|
|
28899
|
-
* What draining a backlog wants, and never the default — the cursor exists
|
|
28900
|
-
* so a 6-hourly pass makes progress instead of re-walking the same head.
|
|
28901
|
-
*/
|
|
28902
|
-
restart: require_dist.boolean().optional(),
|
|
28903
|
-
/** Rows read per statement. Smaller = more yields = a gentler pass. */
|
|
28904
|
-
pageSize: require_dist.number().int().min(50).max(5e3).optional(),
|
|
28905
|
-
/** Rows EXAMINED per collection in this pass — the pacing bound. */
|
|
28906
|
-
maxRowsPerScope: require_dist.number().int().min(1).max(2e5).optional(),
|
|
28907
|
-
/** Rows DELETED per collection in this pass — the blast-radius bound. */
|
|
28908
|
-
maxReclaimPerScope: require_dist.number().int().min(1).max(1e5).optional(),
|
|
28909
|
-
/**
|
|
28910
|
-
* Minutes of immunity for a recent row. Protective ONLY — raising it can
|
|
28911
|
-
* exclusively save rows, never select them, which is what keeps this off
|
|
28912
|
-
* the one-age-keyed-sweep guard's books.
|
|
28913
|
-
*/
|
|
28914
|
-
graceMinutes: require_dist.number().int().min(1).max(10080).optional()
|
|
28915
|
-
});
|
|
28916
|
-
var OrphanAuditScopeReportSchema = require_dist.object({
|
|
28917
|
-
collection: require_dist.string(),
|
|
28918
|
-
examined: require_dist.number(),
|
|
28919
|
-
orphans: require_dist.number(),
|
|
28920
|
-
reclaimed: require_dist.number(),
|
|
28921
|
-
bytesReclaimed: require_dist.number(),
|
|
28922
|
-
exempt: require_dist.number(),
|
|
28923
|
-
withinGrace: require_dist.number(),
|
|
28924
|
-
unresolvable: require_dist.number(),
|
|
28925
|
-
stoppedBy: require_dist.string(),
|
|
28926
|
-
/** Where the NEXT pass resumes. `0` = the scope was walked to the end. */
|
|
28927
|
-
nextCursor: require_dist.number()
|
|
28928
|
-
});
|
|
28929
|
-
var OrphanAuditActionResultSchema = require_dist.object({
|
|
28930
|
-
/** `false` when a pass was already in flight — nothing was done. */
|
|
28931
|
-
ran: require_dist.boolean(),
|
|
28932
|
-
mode: require_dist._enum(["report", "reclaim"]),
|
|
28933
|
-
scopes: require_dist.array(OrphanAuditScopeReportSchema),
|
|
28934
|
-
totalOrphans: require_dist.number(),
|
|
28935
|
-
totalReclaimed: require_dist.number(),
|
|
28936
|
-
totalBytesReclaimed: require_dist.number(),
|
|
28937
|
-
durationMs: require_dist.number()
|
|
28938
|
-
});
|
|
28939
|
-
var orphanAuditActions = require_dist.defineCustomActions({ "retention.orphanAudit": require_dist.customAction(OrphanAuditActionInputSchema, OrphanAuditActionResultSchema, { auth: "admin" }) });
|
|
28940
|
-
//#endregion
|
|
28941
28889
|
//#region src/pipeline-analytics/store/storage-pressure.ts
|
|
28942
28890
|
/**
|
|
28943
28891
|
* storage-pressure — make a full disk something the system REACTS to, not just
|
|
@@ -29104,6 +29052,11 @@ var FACE_MEDIA_OWNER_PREFIX = "face-";
|
|
|
29104
29052
|
* `plate-<trackId>`. Used by `deleteByTracks` to derive the plate crop owners
|
|
29105
29053
|
* of a set of tracks without a `trackId` column on media rows. */
|
|
29106
29054
|
var PLATE_MEDIA_OWNER_PREFIX = "plate-";
|
|
29055
|
+
/** Index rows per page inside one presence chunk. Must stay under
|
|
29056
|
+
* `SETTINGS_QUERY_DEFAULT_ROW_CAP` (2 000) so the engine's backstop can never
|
|
29057
|
+
* silently truncate a page — an owner reported absent because a cap bit would
|
|
29058
|
+
* DELETE a live thumbnail from the surface. */
|
|
29059
|
+
var MEDIA_PRESENCE_PAGE_SIZE = 1e3;
|
|
29107
29060
|
/**
|
|
29108
29061
|
* Kinds that hold exactly ONE row per `(ownerKind, ownerId)` — the current
|
|
29109
29062
|
* "best"/rolling/first artefact, never a filmstrip. Their row id + blob path are
|
|
@@ -29531,6 +29484,59 @@ var MediaStore = class {
|
|
|
29531
29484
|
});
|
|
29532
29485
|
}
|
|
29533
29486
|
/**
|
|
29487
|
+
* Which of `ownerIds` hold at least ONE media row — index only, no blob is
|
|
29488
|
+
* opened, no base64 is allocated.
|
|
29489
|
+
*
|
|
29490
|
+
* This is the licence `videoclips` needs to publish a `thumbnail`. The
|
|
29491
|
+
* event-media URL is stamped from a template for every event in the system,
|
|
29492
|
+
* so "the plane is up" was being served as "there are bytes": 64 of one
|
|
29493
|
+
* camera's 179 clips pointed at a 404 on 2026-08-17.
|
|
29494
|
+
*
|
|
29495
|
+
* Batched by `whereIn` and paged by offset. NOT `listInfoByOwner` in a loop:
|
|
29496
|
+
* that is one query per owner, which is the N+1 this exists to prevent (and
|
|
29497
|
+
* `queryObjectByTrackIds` already records what believing `whereIn` was
|
|
29498
|
+
* unavailable cost the search path).
|
|
29499
|
+
*
|
|
29500
|
+
* `deviceId` is the AUTHORIZATION filter, exactly as in `listByOwner`.
|
|
29501
|
+
*
|
|
29502
|
+
* THROWS on a query failure rather than answering partially — a probe that
|
|
29503
|
+
* quietly under-reports reads as "these owners have nothing", and the caller
|
|
29504
|
+
* drops thumbnails that exist.
|
|
29505
|
+
*/
|
|
29506
|
+
async ownersWithMedia(ownerKind, ownerIds, deviceId) {
|
|
29507
|
+
const found = /* @__PURE__ */ new Set();
|
|
29508
|
+
const unique = [...new Set(ownerIds)];
|
|
29509
|
+
for (let i = 0; i < unique.length; i += 200) {
|
|
29510
|
+
const chunk = unique.slice(i, i + 200);
|
|
29511
|
+
const answered = /* @__PURE__ */ new Set();
|
|
29512
|
+
for (let offset = 0;; offset += MEDIA_PRESENCE_PAGE_SIZE) {
|
|
29513
|
+
const rows = await this.store.query.query({
|
|
29514
|
+
collection: MEDIA_COLLECTION,
|
|
29515
|
+
filter: {
|
|
29516
|
+
where: {
|
|
29517
|
+
ownerKind,
|
|
29518
|
+
deviceId
|
|
29519
|
+
},
|
|
29520
|
+
whereIn: { ownerId: chunk },
|
|
29521
|
+
orderBy: {
|
|
29522
|
+
field: "ownerId",
|
|
29523
|
+
direction: "asc"
|
|
29524
|
+
},
|
|
29525
|
+
limit: MEDIA_PRESENCE_PAGE_SIZE,
|
|
29526
|
+
offset
|
|
29527
|
+
}
|
|
29528
|
+
});
|
|
29529
|
+
for (const row of rows) {
|
|
29530
|
+
const ownerId = row.data["ownerId"];
|
|
29531
|
+
if (typeof ownerId === "string") answered.add(ownerId);
|
|
29532
|
+
}
|
|
29533
|
+
if (rows.length < 1e3 || answered.size >= chunk.length) break;
|
|
29534
|
+
}
|
|
29535
|
+
for (const ownerId of answered) found.add(ownerId);
|
|
29536
|
+
}
|
|
29537
|
+
return found;
|
|
29538
|
+
}
|
|
29539
|
+
/**
|
|
29534
29540
|
* Delete all media (blobs + index rows) for the given owners of one kind.
|
|
29535
29541
|
* Best-effort per row; returns the number of index rows removed.
|
|
29536
29542
|
*/
|
|
@@ -29878,6 +29884,88 @@ async function reconcileDeviceFromDisk(deps, deviceId) {
|
|
|
29878
29884
|
};
|
|
29879
29885
|
}
|
|
29880
29886
|
//#endregion
|
|
29887
|
+
//#region src/pipeline-analytics/retention/orphan-audit-actions.ts
|
|
29888
|
+
/**
|
|
29889
|
+
* The operator button for the orphan audit.
|
|
29890
|
+
*
|
|
29891
|
+
* The audit engine, its five bounds and its resumable cursor already existed
|
|
29892
|
+
* and had run in `report` mode for weeks; what it had no way to be was
|
|
29893
|
+
* *asked*. Its only trigger was a 6-hourly timer reading a durable setting, so
|
|
29894
|
+
* draining a standing backlog meant flipping a persistent mode, waiting up to
|
|
29895
|
+
* six hours, and remembering to flip it back — with nothing returned to look
|
|
29896
|
+
* at. On 2026-08-14 the backlog was 74,700 media rows / 18.9 GB plus 23,296
|
|
29897
|
+
* object events and 4,543 CLIP vectors (D161), and that is not a thing to
|
|
29898
|
+
* drain by leaving a switch on.
|
|
29899
|
+
*
|
|
29900
|
+
* Three properties this action does NOT leave to the caller's care:
|
|
29901
|
+
*
|
|
29902
|
+
* 1. **Dry run is the default.** `mode` omitted means `report`: it counts,
|
|
29903
|
+
* logs and returns, and deletes nothing — regardless of what the durable
|
|
29904
|
+
* `orphanAuditMode` setting says. `reclaim` is a word that has to be
|
|
29905
|
+
* typed. The engine's own default is the same for the same reason.
|
|
29906
|
+
* 2. **It is paced.** `pageSize` bounds one read, `maxRowsPerScope` bounds
|
|
29907
|
+
* one pass, `maxReclaimPerScope` bounds the damage, and the engine yields
|
|
29908
|
+
* the thread between pages — `better-sqlite3` is synchronous and shares
|
|
29909
|
+
* hub-main with every live frame.
|
|
29910
|
+
* 3. **It answers.** The report comes back per collection with rows AND
|
|
29911
|
+
* bytes, so "did it do anything" is not a question anyone has to infer
|
|
29912
|
+
* from a log line.
|
|
29913
|
+
*/
|
|
29914
|
+
/**
|
|
29915
|
+
* `report` counts; `reclaim` deletes.
|
|
29916
|
+
*
|
|
29917
|
+
* `.default('report')` is load-bearing, not decoration: the settings form
|
|
29918
|
+
* sends `{}` for a button with no fields, and the safe interpretation of an
|
|
29919
|
+
* unstated intent here is the one that destroys nothing.
|
|
29920
|
+
*/
|
|
29921
|
+
var OrphanAuditActionModeSchema = require_dist._enum(["report", "reclaim"]).default("report");
|
|
29922
|
+
var OrphanAuditActionInputSchema = require_dist.object({
|
|
29923
|
+
mode: OrphanAuditActionModeSchema,
|
|
29924
|
+
/**
|
|
29925
|
+
* Start every scope from the top instead of resuming its persisted cursor.
|
|
29926
|
+
*
|
|
29927
|
+
* What draining a backlog wants, and never the default — the cursor exists
|
|
29928
|
+
* so a 6-hourly pass makes progress instead of re-walking the same head.
|
|
29929
|
+
*/
|
|
29930
|
+
restart: require_dist.boolean().optional(),
|
|
29931
|
+
/** Rows read per statement. Smaller = more yields = a gentler pass. */
|
|
29932
|
+
pageSize: require_dist.number().int().min(50).max(5e3).optional(),
|
|
29933
|
+
/** Rows EXAMINED per collection in this pass — the pacing bound. */
|
|
29934
|
+
maxRowsPerScope: require_dist.number().int().min(1).max(2e5).optional(),
|
|
29935
|
+
/** Rows DELETED per collection in this pass — the blast-radius bound. */
|
|
29936
|
+
maxReclaimPerScope: require_dist.number().int().min(1).max(1e5).optional(),
|
|
29937
|
+
/**
|
|
29938
|
+
* Minutes of immunity for a recent row. Protective ONLY — raising it can
|
|
29939
|
+
* exclusively save rows, never select them, which is what keeps this off
|
|
29940
|
+
* the one-age-keyed-sweep guard's books.
|
|
29941
|
+
*/
|
|
29942
|
+
graceMinutes: require_dist.number().int().min(1).max(10080).optional()
|
|
29943
|
+
});
|
|
29944
|
+
var OrphanAuditScopeReportSchema = require_dist.object({
|
|
29945
|
+
collection: require_dist.string(),
|
|
29946
|
+
examined: require_dist.number(),
|
|
29947
|
+
orphans: require_dist.number(),
|
|
29948
|
+
reclaimed: require_dist.number(),
|
|
29949
|
+
bytesReclaimed: require_dist.number(),
|
|
29950
|
+
exempt: require_dist.number(),
|
|
29951
|
+
withinGrace: require_dist.number(),
|
|
29952
|
+
unresolvable: require_dist.number(),
|
|
29953
|
+
stoppedBy: require_dist.string(),
|
|
29954
|
+
/** Where the NEXT pass resumes. `0` = the scope was walked to the end. */
|
|
29955
|
+
nextCursor: require_dist.number()
|
|
29956
|
+
});
|
|
29957
|
+
var OrphanAuditActionResultSchema = require_dist.object({
|
|
29958
|
+
/** `false` when a pass was already in flight — nothing was done. */
|
|
29959
|
+
ran: require_dist.boolean(),
|
|
29960
|
+
mode: require_dist._enum(["report", "reclaim"]),
|
|
29961
|
+
scopes: require_dist.array(OrphanAuditScopeReportSchema),
|
|
29962
|
+
totalOrphans: require_dist.number(),
|
|
29963
|
+
totalReclaimed: require_dist.number(),
|
|
29964
|
+
totalBytesReclaimed: require_dist.number(),
|
|
29965
|
+
durationMs: require_dist.number()
|
|
29966
|
+
});
|
|
29967
|
+
var orphanAuditActions = require_dist.defineCustomActions({ "retention.orphanAudit": require_dist.customAction(OrphanAuditActionInputSchema, OrphanAuditActionResultSchema, { auth: "admin" }) });
|
|
29968
|
+
//#endregion
|
|
29881
29969
|
//#region src/pipeline-analytics/videoclip-visits.ts
|
|
29882
29970
|
/**
|
|
29883
29971
|
* Videoclip visits — the grain a clip should have on an events-mode camera.
|
|
@@ -29944,7 +30032,22 @@ function dominantKind(attached) {
|
|
|
29944
30032
|
for (const kind of KIND_ORDER) if (attached.some((a) => a.kind === kind)) return kind;
|
|
29945
30033
|
return "motion";
|
|
29946
30034
|
}
|
|
29947
|
-
|
|
30035
|
+
/**
|
|
30036
|
+
* The instant a recorded still can actually be decoded inside a visit: the
|
|
30037
|
+
* midpoint of its LONGEST contiguous range.
|
|
30038
|
+
*
|
|
30039
|
+
* NOT the midpoint of the visit. A visit spans its holes by construction (it is
|
|
30040
|
+
* ranges glued across gaps up to `VISIT_MERGE_GAP_MS`), so the naive midpoint
|
|
30041
|
+
* lands in dead air roughly as often as not and the recorder's still route
|
|
30042
|
+
* answers 204 for a visit with plenty of footage.
|
|
30043
|
+
*/
|
|
30044
|
+
function stillInstantMs(ranges) {
|
|
30045
|
+
let best;
|
|
30046
|
+
for (const range of ranges) if (best === void 0 || range.endMs - range.startMs > best.endMs - best.startMs) best = range;
|
|
30047
|
+
if (best === void 0) return void 0;
|
|
30048
|
+
return Math.floor((best.startMs + best.endMs) / 2);
|
|
30049
|
+
}
|
|
30050
|
+
function visitsToClips(visits, events, pad, vouched) {
|
|
29948
30051
|
const tagged = [
|
|
29949
30052
|
...events.object.map((event) => ({
|
|
29950
30053
|
kind: "object",
|
|
@@ -29962,7 +30065,8 @@ function visitsToClips(visits, events, pad) {
|
|
|
29962
30065
|
return visits.map((visit) => {
|
|
29963
30066
|
const attached = tagged.filter((t) => eventOverlapsVisit(t.event, visit, pad));
|
|
29964
30067
|
const kind = dominantKind(attached);
|
|
29965
|
-
const withThumb = attached.find((a) => a.kind === kind && a.event.mediaUrl) ?? attached.find((a) => a.event.mediaUrl);
|
|
30068
|
+
const withThumb = attached.find((a) => a.kind === kind && a.event.mediaUrl && vouched.has(a.event.id)) ?? attached.find((a) => a.event.mediaUrl && vouched.has(a.event.id));
|
|
30069
|
+
const stillAtMs = stillInstantMs(visit.ranges);
|
|
29966
30070
|
return {
|
|
29967
30071
|
id: `analytics:${visit.startMs}:${visit.endMs}:${VISIT_EVENT_ID}`,
|
|
29968
30072
|
source: "analytics",
|
|
@@ -29971,8 +30075,10 @@ function visitsToClips(visits, events, pad) {
|
|
|
29971
30075
|
startMs: visit.startMs,
|
|
29972
30076
|
endMs: visit.endMs
|
|
29973
30077
|
},
|
|
29974
|
-
thumbnail: withThumb
|
|
29975
|
-
|
|
30078
|
+
...withThumb?.event.mediaUrl !== void 0 ? { thumbnail: withThumb.event.mediaUrl } : {},
|
|
30079
|
+
...stillAtMs !== void 0 ? { stillAtMs } : {},
|
|
30080
|
+
eventIds: attached.slice(0, 24).map((a) => a.event.id),
|
|
30081
|
+
eventCount: attached.length,
|
|
29976
30082
|
holes: [...visit.holes]
|
|
29977
30083
|
};
|
|
29978
30084
|
}).toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
|
|
@@ -29996,6 +30102,17 @@ function visitsToClips(visits, events, pad) {
|
|
|
29996
30102
|
* injected fetch/resolve deps so the compose logic is testable without I/O.
|
|
29997
30103
|
*/
|
|
29998
30104
|
var SOURCE = "analytics";
|
|
30105
|
+
/**
|
|
30106
|
+
* Backstop on the ANNOTATION read — never on the clip list.
|
|
30107
|
+
*
|
|
30108
|
+
* A visit is annotated by every event inside it, so the clip `limit` must not
|
|
30109
|
+
* reach this fetch (one 615 visit legitimately spans 2 345 motion rows). This
|
|
30110
|
+
* bound exists only so a pathological camera-day cannot pull an unbounded page:
|
|
30111
|
+
* the store orders `timestamp DESC`, so a truncation drops the OLDEST rows and
|
|
30112
|
+
* the EARLIEST visits silently lose their annotations — which is why hitting it
|
|
30113
|
+
* is logged, per camera, rather than absorbed.
|
|
30114
|
+
*/
|
|
30115
|
+
var VISIT_EVENT_FETCH_LIMIT = 5e3;
|
|
29999
30116
|
function clipIdFor(eventId, startMs, endMs) {
|
|
30000
30117
|
return `${SOURCE}:${startMs}:${endMs}:${eventId}`;
|
|
30001
30118
|
}
|
|
@@ -30016,7 +30133,7 @@ function parseClipId(clipId) {
|
|
|
30016
30133
|
};
|
|
30017
30134
|
}
|
|
30018
30135
|
/** Map per-kind events into time-ranged clips, newest-first. Pure. */
|
|
30019
|
-
function eventsToClips(events, pad) {
|
|
30136
|
+
function eventsToClips(events, pad, vouched) {
|
|
30020
30137
|
const make = (e, kind) => {
|
|
30021
30138
|
const startMs = e.timestamp - pad.preMs;
|
|
30022
30139
|
const endMs = e.timestamp + pad.postMs;
|
|
@@ -30028,7 +30145,7 @@ function eventsToClips(events, pad) {
|
|
|
30028
30145
|
startMs,
|
|
30029
30146
|
endMs
|
|
30030
30147
|
},
|
|
30031
|
-
thumbnail: e.mediaUrl
|
|
30148
|
+
...e.mediaUrl !== void 0 && vouched.has(e.id) ? { thumbnail: e.mediaUrl } : {}
|
|
30032
30149
|
};
|
|
30033
30150
|
};
|
|
30034
30151
|
return [
|
|
@@ -30037,29 +30154,106 @@ function eventsToClips(events, pad) {
|
|
|
30037
30154
|
...events.audio.map((e) => make(e, "audio"))
|
|
30038
30155
|
].toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
|
|
30039
30156
|
}
|
|
30157
|
+
/** Every id in the three buckets, in the order the composers tag them. */
|
|
30158
|
+
function allEventIds(events) {
|
|
30159
|
+
return [
|
|
30160
|
+
...events.motion,
|
|
30161
|
+
...events.object,
|
|
30162
|
+
...events.audio
|
|
30163
|
+
].map((e) => e.id);
|
|
30164
|
+
}
|
|
30165
|
+
/**
|
|
30166
|
+
* Resolve the event ids whose `mediaUrl` is backed by real bytes, mirroring
|
|
30167
|
+
* `resolveDefaultEventMedia`: the event's OWN media when it has any, else its
|
|
30168
|
+
* owning TRACK's. Two batched index reads at most, and the second only over the
|
|
30169
|
+
* object events the first did not already answer for — motion and audio are
|
|
30170
|
+
* trackless roots, so they never need it.
|
|
30171
|
+
*
|
|
30172
|
+
* THROWS on a failed read. The caller fails closed on the CLAIM and open on the
|
|
30173
|
+
* LIST; swallowing here would collapse those two opposite directions into one.
|
|
30174
|
+
*/
|
|
30175
|
+
async function resolveVouchedEvents(deviceId, events, fetchMediaOwners) {
|
|
30176
|
+
const eventIds = allEventIds(events);
|
|
30177
|
+
if (eventIds.length === 0) return /* @__PURE__ */ new Set();
|
|
30178
|
+
const ownMedia = await fetchMediaOwners({
|
|
30179
|
+
deviceId,
|
|
30180
|
+
ownerKind: "event",
|
|
30181
|
+
ownerIds: eventIds
|
|
30182
|
+
});
|
|
30183
|
+
const residualTracks = [];
|
|
30184
|
+
const seenTracks = /* @__PURE__ */ new Set();
|
|
30185
|
+
for (const event of events.object) {
|
|
30186
|
+
const trackId = event.trackId;
|
|
30187
|
+
if (trackId === void 0 || ownMedia.has(event.id) || seenTracks.has(trackId)) continue;
|
|
30188
|
+
seenTracks.add(trackId);
|
|
30189
|
+
residualTracks.push(trackId);
|
|
30190
|
+
}
|
|
30191
|
+
const trackMedia = residualTracks.length === 0 ? /* @__PURE__ */ new Set() : await fetchMediaOwners({
|
|
30192
|
+
deviceId,
|
|
30193
|
+
ownerKind: "track",
|
|
30194
|
+
ownerIds: residualTracks
|
|
30195
|
+
});
|
|
30196
|
+
const vouched = new Set(ownMedia);
|
|
30197
|
+
for (const event of events.object) {
|
|
30198
|
+
const trackId = event.trackId;
|
|
30199
|
+
if (trackId !== void 0 && trackMedia.has(trackId)) vouched.add(event.id);
|
|
30200
|
+
}
|
|
30201
|
+
return vouched;
|
|
30202
|
+
}
|
|
30203
|
+
/** Keep only clips that actually touch the caller's window. The reads are
|
|
30204
|
+
* widened by {@link leftOverhangMs}; the ANSWER is not. */
|
|
30205
|
+
function intersectsWindow(clip, since, until) {
|
|
30206
|
+
return clip.timeRange.startMs < until && clip.timeRange.endMs > since;
|
|
30207
|
+
}
|
|
30208
|
+
/**
|
|
30209
|
+
* How far BEFORE `since` the event + availability reads must reach.
|
|
30210
|
+
*
|
|
30211
|
+
* A visit straddling `since` keeps its heartbeat rows and loses the transition
|
|
30212
|
+
* row — the only one that ever carries a snapshot — so the same visit shows a
|
|
30213
|
+
* picture or doesn't depending on where the operator scrolled. One wider range
|
|
30214
|
+
* scan on an indexed column buys a list that stops changing under the cursor.
|
|
30215
|
+
*/
|
|
30216
|
+
function leftOverhangMs(pad) {
|
|
30217
|
+
return require_dist.VISIT_MERGE_GAP_MS + pad.preMs;
|
|
30218
|
+
}
|
|
30040
30219
|
function createVideoclipsProvider(deps) {
|
|
30041
30220
|
return {
|
|
30042
30221
|
listClips: async ({ deviceId, since, until, limit }) => {
|
|
30222
|
+
const readSince = since - leftOverhangMs(deps.defaultPad);
|
|
30043
30223
|
const events = await deps.fetchEvents({
|
|
30044
30224
|
deviceId,
|
|
30045
|
-
since,
|
|
30225
|
+
since: readSince,
|
|
30046
30226
|
until,
|
|
30047
|
-
|
|
30227
|
+
limit: deps.fetchAvailability ? VISIT_EVENT_FETCH_LIMIT : limit
|
|
30048
30228
|
});
|
|
30049
|
-
|
|
30229
|
+
warnOnTruncatedFetch(deps, deviceId, events);
|
|
30230
|
+
let vouched = /* @__PURE__ */ new Set();
|
|
30231
|
+
try {
|
|
30232
|
+
vouched = await resolveVouchedEvents(deviceId, events, deps.fetchMediaOwners);
|
|
30233
|
+
} catch (err) {
|
|
30234
|
+
deps.logWarn?.("videoclips: media presence read failed — listing without thumbnails", {
|
|
30235
|
+
deviceId,
|
|
30236
|
+
events: allEventIds(events).length,
|
|
30237
|
+
error: err instanceof Error ? err.message : String(err)
|
|
30238
|
+
});
|
|
30239
|
+
}
|
|
30240
|
+
let clips = eventsToClips(events, deps.defaultPad, vouched);
|
|
30050
30241
|
if (deps.fetchAvailability) try {
|
|
30051
30242
|
clips = visitsToClips(clusterFootageVisits(await deps.fetchAvailability({
|
|
30052
30243
|
deviceId,
|
|
30053
|
-
since,
|
|
30244
|
+
since: readSince,
|
|
30054
30245
|
until
|
|
30055
|
-
}), require_dist.VISIT_MERGE_GAP_MS), events, deps.defaultPad);
|
|
30246
|
+
}), require_dist.VISIT_MERGE_GAP_MS), events, deps.defaultPad, vouched);
|
|
30056
30247
|
} catch (err) {
|
|
30057
30248
|
deps.logWarn?.("videoclips: availability read failed — listing unfiltered", {
|
|
30058
30249
|
deviceId,
|
|
30059
30250
|
error: err instanceof Error ? err.message : String(err)
|
|
30060
30251
|
});
|
|
30061
30252
|
}
|
|
30062
|
-
|
|
30253
|
+
const inWindow = clips.filter((c) => intersectsWindow(c, since, until));
|
|
30254
|
+
const answer = limit != null ? inWindow.slice(0, limit) : inWindow;
|
|
30255
|
+
reportUnvouchedClips(deps, deviceId, answer);
|
|
30256
|
+
return answer;
|
|
30063
30257
|
},
|
|
30064
30258
|
getClipPlayback: async ({ deviceId, clipId }) => {
|
|
30065
30259
|
const parsed = parseClipId(clipId);
|
|
@@ -30072,6 +30266,33 @@ function createVideoclipsProvider(deps) {
|
|
|
30072
30266
|
}
|
|
30073
30267
|
};
|
|
30074
30268
|
}
|
|
30269
|
+
/** The annotation backstop biting is a SILENT loss of the earliest visits'
|
|
30270
|
+
* events — say so, per camera, instead of shipping a thinner list. */
|
|
30271
|
+
function warnOnTruncatedFetch(deps, deviceId, events) {
|
|
30272
|
+
if (!deps.fetchAvailability) return;
|
|
30273
|
+
const truncated = [
|
|
30274
|
+
"motion",
|
|
30275
|
+
"object",
|
|
30276
|
+
"audio"
|
|
30277
|
+
].filter((kind) => events[kind].length >= VISIT_EVENT_FETCH_LIMIT);
|
|
30278
|
+
if (truncated.length === 0) return;
|
|
30279
|
+
deps.logWarn?.("videoclips: annotation fetch truncated — earliest visits lose their events", {
|
|
30280
|
+
deviceId,
|
|
30281
|
+
kinds: truncated,
|
|
30282
|
+
limit: VISIT_EVENT_FETCH_LIMIT
|
|
30283
|
+
});
|
|
30284
|
+
}
|
|
30285
|
+
/** The operator's acceptance criterion as a number: a rising count on one
|
|
30286
|
+
* camera is its motion-snapshot capturer failing, not a quiet day. */
|
|
30287
|
+
function reportUnvouchedClips(deps, deviceId, clips) {
|
|
30288
|
+
const clipsWithoutStill = clips.filter((c) => c.thumbnail === void 0).length;
|
|
30289
|
+
if (clipsWithoutStill === 0) return;
|
|
30290
|
+
deps.logInfo?.("videoclips: visit has no vouched still", {
|
|
30291
|
+
deviceId,
|
|
30292
|
+
clipsWithoutStill,
|
|
30293
|
+
clips: clips.length
|
|
30294
|
+
});
|
|
30295
|
+
}
|
|
30075
30296
|
//#endregion
|
|
30076
30297
|
//#region src/pipeline-analytics/viewer-settings-actions.ts
|
|
30077
30298
|
/**
|
|
@@ -45918,6 +46139,7 @@ var NC_SYSTEM_EVENT_CATEGORIES = [
|
|
|
45918
46139
|
require_dist.EventCategory.WorkerOffline,
|
|
45919
46140
|
require_dist.EventCategory.PipelineNodeInferenceUnavailable,
|
|
45920
46141
|
require_dist.EventCategory.PipelineDetectionBlind,
|
|
46142
|
+
require_dist.EventCategory.AddonRunnerFailed,
|
|
45921
46143
|
require_dist.EventCategory.UpdateAvailable,
|
|
45922
46144
|
require_dist.EventCategory.AddonUpdated,
|
|
45923
46145
|
require_dist.EventCategory.SystemRestartCompleted,
|
|
@@ -47262,6 +47484,66 @@ var EventMediaDispatcher = class {
|
|
|
47262
47484
|
}
|
|
47263
47485
|
}
|
|
47264
47486
|
};
|
|
47487
|
+
var SnapshotTimeoutError$1 = class extends Error {
|
|
47488
|
+
constructor(ms) {
|
|
47489
|
+
super(`snapshot cap timed out after ${ms}ms`);
|
|
47490
|
+
this.name = "SnapshotTimeoutError";
|
|
47491
|
+
}
|
|
47492
|
+
};
|
|
47493
|
+
function withTimeout$1(promise, ms) {
|
|
47494
|
+
return new Promise((resolve, reject) => {
|
|
47495
|
+
const timer = setTimeout(() => reject(new SnapshotTimeoutError$1(ms)), ms);
|
|
47496
|
+
promise.then((value) => {
|
|
47497
|
+
clearTimeout(timer);
|
|
47498
|
+
resolve(value);
|
|
47499
|
+
}, (err) => {
|
|
47500
|
+
clearTimeout(timer);
|
|
47501
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
47502
|
+
});
|
|
47503
|
+
});
|
|
47504
|
+
}
|
|
47505
|
+
var MotionEventSnapshotCapturer = class {
|
|
47506
|
+
deps;
|
|
47507
|
+
snapshotTimeoutMs;
|
|
47508
|
+
constructor(deps) {
|
|
47509
|
+
this.deps = deps;
|
|
47510
|
+
this.snapshotTimeoutMs = deps.snapshotTimeoutMs ?? 2500;
|
|
47511
|
+
}
|
|
47512
|
+
/**
|
|
47513
|
+
* Fetch a still and persist it as the motion event's snapshot. Never
|
|
47514
|
+
* throws — the durable record is the motion row, already inserted by
|
|
47515
|
+
* the caller.
|
|
47516
|
+
*/
|
|
47517
|
+
async capture(input) {
|
|
47518
|
+
let snap;
|
|
47519
|
+
try {
|
|
47520
|
+
snap = await withTimeout$1(this.deps.snapshot.getSnapshot({ deviceId: input.deviceId }), this.snapshotTimeoutMs);
|
|
47521
|
+
} catch (err) {
|
|
47522
|
+
this.deps.onError?.(err instanceof SnapshotTimeoutError$1 ? "snapshotTimeout" : "snapshotFetch", err, input.deviceId);
|
|
47523
|
+
return;
|
|
47524
|
+
}
|
|
47525
|
+
if (snap === null) return;
|
|
47526
|
+
const raw = Buffer.from(snap.base64, "base64");
|
|
47527
|
+
let data = raw;
|
|
47528
|
+
try {
|
|
47529
|
+
data = await downscaleFullFrameJpeg(raw, 960, 540);
|
|
47530
|
+
} catch (err) {
|
|
47531
|
+
this.deps.onError?.("downscaleSnapshot", err, input.deviceId);
|
|
47532
|
+
}
|
|
47533
|
+
try {
|
|
47534
|
+
await this.deps.media.put({
|
|
47535
|
+
deviceId: input.deviceId,
|
|
47536
|
+
ownerKind: "event",
|
|
47537
|
+
ownerId: input.eventId,
|
|
47538
|
+
kind: "snapshot",
|
|
47539
|
+
timestamp: input.timestamp,
|
|
47540
|
+
data
|
|
47541
|
+
});
|
|
47542
|
+
} catch (err) {
|
|
47543
|
+
this.deps.onError?.("snapshotMedia", err, input.deviceId);
|
|
47544
|
+
}
|
|
47545
|
+
}
|
|
47546
|
+
};
|
|
47265
47547
|
/**
|
|
47266
47548
|
* Project one `DeviceStateChanged` of a mapped sensor/control cap into a
|
|
47267
47549
|
* synthetic track per camera the event belongs to. Returns the number of
|
|
@@ -47358,16 +47640,16 @@ var DEFAULT_DEBOUNCE_MS = 1e4;
|
|
|
47358
47640
|
/** A hanging snapshot cap must not stall the sensor state-change path. */
|
|
47359
47641
|
var DEFAULT_SNAPSHOT_TIMEOUT_MS = 1e4;
|
|
47360
47642
|
/** Distinguishes a snapshot-cap timeout from a fetch failure for reporting. */
|
|
47361
|
-
var SnapshotTimeoutError
|
|
47643
|
+
var SnapshotTimeoutError = class extends Error {
|
|
47362
47644
|
constructor(ms) {
|
|
47363
47645
|
super(`snapshot cap timed out after ${ms}ms`);
|
|
47364
47646
|
this.name = "SnapshotTimeoutError";
|
|
47365
47647
|
}
|
|
47366
47648
|
};
|
|
47367
47649
|
/** Reject with `SnapshotTimeoutError` if `promise` doesn't settle within `ms`. */
|
|
47368
|
-
function withTimeout
|
|
47650
|
+
function withTimeout(promise, ms) {
|
|
47369
47651
|
return new Promise((resolve, reject) => {
|
|
47370
|
-
const timer = setTimeout(() => reject(new SnapshotTimeoutError
|
|
47652
|
+
const timer = setTimeout(() => reject(new SnapshotTimeoutError(ms)), ms);
|
|
47371
47653
|
promise.then((value) => {
|
|
47372
47654
|
clearTimeout(timer);
|
|
47373
47655
|
resolve(value);
|
|
@@ -47422,11 +47704,11 @@ var SyntheticTrackMaterializer = class {
|
|
|
47422
47704
|
*/
|
|
47423
47705
|
async fetchSnapshot(cameraId, timeoutMs) {
|
|
47424
47706
|
try {
|
|
47425
|
-
const snap = await withTimeout
|
|
47707
|
+
const snap = await withTimeout(this.deps.snapshot.getSnapshot({ deviceId: cameraId }), timeoutMs);
|
|
47426
47708
|
if (snap === null) this.deps.onError?.("getSnapshot", /* @__PURE__ */ new Error("snapshot returned null"), cameraId);
|
|
47427
47709
|
return snap;
|
|
47428
47710
|
} catch (err) {
|
|
47429
|
-
this.deps.onError?.(err instanceof SnapshotTimeoutError
|
|
47711
|
+
this.deps.onError?.(err instanceof SnapshotTimeoutError ? "snapshotTimeout" : "snapshotMedia", err, cameraId);
|
|
47430
47712
|
return null;
|
|
47431
47713
|
}
|
|
47432
47714
|
}
|
|
@@ -47492,66 +47774,6 @@ var SyntheticTrackMaterializer = class {
|
|
|
47492
47774
|
return track;
|
|
47493
47775
|
}
|
|
47494
47776
|
};
|
|
47495
|
-
var SnapshotTimeoutError = class extends Error {
|
|
47496
|
-
constructor(ms) {
|
|
47497
|
-
super(`snapshot cap timed out after ${ms}ms`);
|
|
47498
|
-
this.name = "SnapshotTimeoutError";
|
|
47499
|
-
}
|
|
47500
|
-
};
|
|
47501
|
-
function withTimeout(promise, ms) {
|
|
47502
|
-
return new Promise((resolve, reject) => {
|
|
47503
|
-
const timer = setTimeout(() => reject(new SnapshotTimeoutError(ms)), ms);
|
|
47504
|
-
promise.then((value) => {
|
|
47505
|
-
clearTimeout(timer);
|
|
47506
|
-
resolve(value);
|
|
47507
|
-
}, (err) => {
|
|
47508
|
-
clearTimeout(timer);
|
|
47509
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
47510
|
-
});
|
|
47511
|
-
});
|
|
47512
|
-
}
|
|
47513
|
-
var MotionEventSnapshotCapturer = class {
|
|
47514
|
-
deps;
|
|
47515
|
-
snapshotTimeoutMs;
|
|
47516
|
-
constructor(deps) {
|
|
47517
|
-
this.deps = deps;
|
|
47518
|
-
this.snapshotTimeoutMs = deps.snapshotTimeoutMs ?? 2500;
|
|
47519
|
-
}
|
|
47520
|
-
/**
|
|
47521
|
-
* Fetch a still and persist it as the motion event's snapshot. Never
|
|
47522
|
-
* throws — the durable record is the motion row, already inserted by
|
|
47523
|
-
* the caller.
|
|
47524
|
-
*/
|
|
47525
|
-
async capture(input) {
|
|
47526
|
-
let snap;
|
|
47527
|
-
try {
|
|
47528
|
-
snap = await withTimeout(this.deps.snapshot.getSnapshot({ deviceId: input.deviceId }), this.snapshotTimeoutMs);
|
|
47529
|
-
} catch (err) {
|
|
47530
|
-
this.deps.onError?.(err instanceof SnapshotTimeoutError ? "snapshotTimeout" : "snapshotFetch", err, input.deviceId);
|
|
47531
|
-
return;
|
|
47532
|
-
}
|
|
47533
|
-
if (snap === null) return;
|
|
47534
|
-
const raw = Buffer.from(snap.base64, "base64");
|
|
47535
|
-
let data = raw;
|
|
47536
|
-
try {
|
|
47537
|
-
data = await downscaleFullFrameJpeg(raw, 960, 540);
|
|
47538
|
-
} catch (err) {
|
|
47539
|
-
this.deps.onError?.("downscaleSnapshot", err, input.deviceId);
|
|
47540
|
-
}
|
|
47541
|
-
try {
|
|
47542
|
-
await this.deps.media.put({
|
|
47543
|
-
deviceId: input.deviceId,
|
|
47544
|
-
ownerKind: "event",
|
|
47545
|
-
ownerId: input.eventId,
|
|
47546
|
-
kind: "snapshot",
|
|
47547
|
-
timestamp: input.timestamp,
|
|
47548
|
-
data
|
|
47549
|
-
});
|
|
47550
|
-
} catch (err) {
|
|
47551
|
-
this.deps.onError?.("snapshotMedia", err, input.deviceId);
|
|
47552
|
-
}
|
|
47553
|
-
}
|
|
47554
|
-
};
|
|
47555
47777
|
var TrackingSettingsSchema = require_dist.object({
|
|
47556
47778
|
/**
|
|
47557
47779
|
* How much of a detection's box must lie inside a zone (0-1 fraction of the
|
|
@@ -53091,6 +53313,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53091
53313
|
audio
|
|
53092
53314
|
};
|
|
53093
53315
|
},
|
|
53316
|
+
fetchMediaOwners: async ({ deviceId, ownerKind, ownerIds }) => {
|
|
53317
|
+
return stores.mediaStore.ownersWithMedia(ownerKind, ownerIds, deviceId);
|
|
53318
|
+
},
|
|
53094
53319
|
fetchAvailability: async ({ deviceId, since, until }) => {
|
|
53095
53320
|
return (await api.recording.getAvailability.query({
|
|
53096
53321
|
deviceId,
|
|
@@ -53102,6 +53327,10 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53102
53327
|
tags: { deviceId: Number(meta["deviceId"]) },
|
|
53103
53328
|
meta
|
|
53104
53329
|
}),
|
|
53330
|
+
logInfo: (message, meta) => this.ctx.logger.info(message, {
|
|
53331
|
+
tags: { deviceId: Number(meta["deviceId"]) },
|
|
53332
|
+
meta
|
|
53333
|
+
}),
|
|
53105
53334
|
resolvePlayback: async ({ deviceId, startMs, endMs }) => {
|
|
53106
53335
|
const manifest = await api.recording.getPlaybackManifest.query({
|
|
53107
53336
|
deviceId,
|