@camstack/addon-post-analysis 1.2.152 → 1.2.153
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-DSg_jDMt.mjs → dist-DDIu0Q1W.mjs} +86 -1
- package/dist/{dist-Bw6cidiJ.js → dist-DeMgxNFC.js} +91 -0
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DTO1HvN7.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Cil5ur25.mjs} +3 -3
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DxVeMmnD.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CG8y8a6j.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-r4RyRXk7.mjs → hostInit-I37PqSoI.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +370 -54
- package/dist/pipeline-analytics/index.mjs +370 -54
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as audioMetricsCapability, A as NcSnoozeInputSchema, At as errMsg, B as SCENE_DEFAULT_ANCHOR_THRESHOLD, Bt as boolean, C as NC_TAXONOMY, D as NcRuleSchema, Dt as vectorDimFromBase64, E as NcRulePatchSchema, Et as systemEventFilterApplies, F as OpsLogEntrySchema, Ft as isDeviceScopedCap, G as TimelapseRuleInputSchema, Gt as partialRecord, H as SCENE_DIVERGED, Ht as literal, It as nodePin, J as TrackSourceSchema, Jt as unknown, K as TimelapseRulePatchSchema, Kt as record, L as RECORDING_EXPORT_MAX_READ_BYTES, Lt as sleep, M as NcSnoozeSuppressedSchema, Mt as DeviceType, N as NcSystemEventKindSchema, Nt as createEvent, O as NcRuleTargetSchema, Ot as videoclipsCapability, P as NcTaxonomySchema, Pt as hydrateSchema, Q as assertTimelapseCadences, R as RetrainStatusSchema, Rt as _enum, St as readTimelapseGeneratedAt, T as NcRuleInputSchema, Tt as subKindsOf, U as SceneMonitorSchema, Ut as number, V as SCENE_DEFAULT_UNCOVERED_POLICY, Vt as discriminatedUnion, W as TIMELAPSE_DENSE_FLOOR_SEC, Wt as object, X as addonWidgetsSourceCapability, Y as VISIT_MERGE_GAP_MS, Yt as EventCategory, Z as alarmPanelCapability, _ as MACRO_LABELS, a as COCO_TO_MACRO, at as deriveRecordingMode, b as NC_CONDITION_CATALOG, bt as plateGalleryCapability, ct as evaluateSensorEdge, d as DeclaredDevices, et as audioModeOf, f as EVENT_KIND_BY_CAP, ft as isDetectionMacroClass, g as LabelAttributionSchema, gt as notificationRulesCapability, h as FailureCounters, ht as kebabToCamel, i as CLUSTER_MODEL_SCOPED_STEPS, it as defineCustomActions, j as NcSnoozeSchema, jt as BaseAddon, k as NcScheduleSchema, kt as zoneAnalyticsCapability, l as DETECTION_MACRO_CLASSES, lt as faceGalleryCapability, m as FULL_IMAGE_BBOX, mt as isSourceCap, n as AnalyticsGroupRecordSchema, nt as cosineSimilarity$1, o as DEFAULT_EVENT_COLOR, p as EVENT_PAD_MS, pt as isScheduleActive, q as TimelapseRuleSchema, qt as string, r as BaseDevice, rt as customAction, st as encodeVectorBase64, t as AnalyticsGroupDetailSchema, tt as buildEventKindDescriptor, ut as failureContributionCapability, v as MediaFileKindEnum, vt as pickClusterStepModels, w as NcConditionDescriptorSchema, wt as sceneMonitorCapability, x as NC_DEFAULT_SNOOZE_MINUTES, xt as readDeviceStateFrom, y as NC_ALARM_SYSTEM_EVENT_KINDS, yt as pipelineAnalyticsCapability, zt as array } from "../dist-DDIu0Q1W.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { promises } from "node:fs";
|
|
@@ -25672,6 +25672,67 @@ var MEDIA_INDEXES = [
|
|
|
25672
25672
|
{
|
|
25673
25673
|
name: "idx_media_location",
|
|
25674
25674
|
columns: ["locationId"]
|
|
25675
|
+
}),
|
|
25676
|
+
(
|
|
25677
|
+
/**
|
|
25678
|
+
* The events-management footprint — `GROUP BY deviceId` and
|
|
25679
|
+
* `SUM(sizeBytes) WHERE deviceId = ?`. See {@link MediaStore.footprintByDevice}.
|
|
25680
|
+
*
|
|
25681
|
+
* **Why not `idx_media_device_ts`.** That index already leads with
|
|
25682
|
+
* `deviceId`, so it can find the range — but it does not carry `sizeBytes`,
|
|
25683
|
+
* so the sum has to fetch the table row behind every index entry it visits.
|
|
25684
|
+
* Over the whole collection that is one scattered read of the entire ~1.25 M
|
|
25685
|
+
* row table (`path` alone is ~80 characters per row) on the volume the
|
|
25686
|
+
* recorder is writing to. With `sizeBytes` in the index the planner never
|
|
25687
|
+
* touches the table: `SCAN … USING COVERING INDEX idx_media_device_size`.
|
|
25688
|
+
*
|
|
25689
|
+
* It has to be a NEW index rather than a third column on the existing one.
|
|
25690
|
+
* `declareCollection` emits `CREATE INDEX IF NOT EXISTS` keyed on the index
|
|
25691
|
+
* NAME, so widening `idx_media_device_ts` in place would be a silent no-op on
|
|
25692
|
+
* every hub that already has it — the declaration would read as done and the
|
|
25693
|
+
* query would still hit the table.
|
|
25694
|
+
*
|
|
25695
|
+
* **What it costs.** ~19 bytes of index per row (two integers plus the
|
|
25696
|
+
* rowid), so ~24 MB at 1.25 M rows, plus one more b-tree maintained by every
|
|
25697
|
+
* insert into a table taking ~92 k rows/day. At ~1 insert/s that maintenance
|
|
25698
|
+
* is noise beside the JPEG write the same call performs. The cost that is not
|
|
25699
|
+
* noise is the one-off: the first `declareCollection` after this ships builds
|
|
25700
|
+
* the index over every existing row. Do not deploy it beside a running drain
|
|
25701
|
+
* or seal — the same caveat `idx_media_location` carries, for the same
|
|
25702
|
+
* reason.
|
|
25703
|
+
*/
|
|
25704
|
+
{
|
|
25705
|
+
name: "idx_media_device_size",
|
|
25706
|
+
columns: ["deviceId", "sizeBytes"]
|
|
25707
|
+
}),
|
|
25708
|
+
(
|
|
25709
|
+
/**
|
|
25710
|
+
* The per-KIND footprint — `COUNT(*)`/`SUM(sizeBytes) WHERE kind = ?`, and
|
|
25711
|
+
* the same scoped to one camera. See {@link MediaStore.footprintByKind}.
|
|
25712
|
+
*
|
|
25713
|
+
* `kind` leads no existing index, so without this each of the thirteen
|
|
25714
|
+
* per-kind questions is a full table scan and the breakdown costs thirteen of
|
|
25715
|
+
* them. With it, each kind is a contiguous covering range and the thirteen
|
|
25716
|
+
* together scan the index once — the cost of the whole breakdown is one pass
|
|
25717
|
+
* over ~36 MB of index instead of thirteen passes over the table.
|
|
25718
|
+
*
|
|
25719
|
+
* `deviceId` sits between `kind` and `sizeBytes` so the per-camera variant
|
|
25720
|
+
* (`WHERE kind = ? AND deviceId = ?`) stays covering too, for ~3 bytes a row
|
|
25721
|
+
* more than `(kind, sizeBytes)` would cost. Without it that query finds its
|
|
25722
|
+
* range from the index and then reads the table for every row in it.
|
|
25723
|
+
*
|
|
25724
|
+
* ~29 bytes of index per row, so ~36 MB at 1.25 M rows, and one more b-tree
|
|
25725
|
+
* on every insert. Same one-off caveat as the two indexes above: the first
|
|
25726
|
+
* declare builds it over every existing row, so do not ship it beside a
|
|
25727
|
+
* running drain or seal.
|
|
25728
|
+
*/
|
|
25729
|
+
{
|
|
25730
|
+
name: "idx_media_kind_device_size",
|
|
25731
|
+
columns: [
|
|
25732
|
+
"kind",
|
|
25733
|
+
"deviceId",
|
|
25734
|
+
"sizeBytes"
|
|
25735
|
+
]
|
|
25675
25736
|
})
|
|
25676
25737
|
];
|
|
25677
25738
|
/** The storage location of a media row/record: its stamped `locationId`, or
|
|
@@ -26326,47 +26387,136 @@ var MediaStore = class {
|
|
|
26326
26387
|
};
|
|
26327
26388
|
}
|
|
26328
26389
|
/**
|
|
26329
|
-
* On-demand footprint aggregation for the events-management UI:
|
|
26330
|
-
* `sizeBytes`
|
|
26331
|
-
*
|
|
26332
|
-
*
|
|
26333
|
-
*
|
|
26390
|
+
* On-demand footprint aggregation for the events-management UI: rows and
|
|
26391
|
+
* summed `sizeBytes` per camera, across the whole media collection.
|
|
26392
|
+
*
|
|
26393
|
+
* ## Why this is a histogram plus one aggregate per camera
|
|
26394
|
+
*
|
|
26395
|
+
* It used to page the ENTIRE collection — `ORDER BY id LIMIT 1000 OFFSET n`,
|
|
26396
|
+
* no predicate — and add `sizeBytes` up in JS. SQLite honours an OFFSET by
|
|
26397
|
+
* stepping over `n` entries, so the walk is quadratic in the table: at the
|
|
26398
|
+
* live hub's 1.25 M rows that is ~1 250 round trips and ~780 M index steps,
|
|
26399
|
+
* with every row serialised across an RPC boundary and rebuilt in V8 to be
|
|
26400
|
+
* added to a number. It timed out at 60 s on every call it ever made, and it
|
|
26401
|
+
* is the only thing that can size the surplus-media deletion it gates.
|
|
26402
|
+
*
|
|
26403
|
+
* The answer is at most a few dozen `(deviceId, rows, bytes)` triples.
|
|
26404
|
+
*
|
|
26405
|
+
* - The DEVICE SET comes from a histogram on `deviceId` with `bucketSize: 1`
|
|
26406
|
+
* — `GROUP BY deviceId`, served from `idx_media_device_size` as a covering
|
|
26407
|
+
* scan, the same trick `TrackStore.listDeviceIds` already uses (D56).
|
|
26408
|
+
* `bucket` IS the deviceId: the column is INTEGER and the bucket
|
|
26409
|
+
* expression is `CAST((deviceId - 0) / 1 AS INTEGER)`.
|
|
26410
|
+
* - The NUMBERS come from one `aggregate` per camera — `COUNT(*)` and
|
|
26411
|
+
* `SUM(sizeBytes)` in a single statement over an indexed `deviceId` range.
|
|
26412
|
+
*
|
|
26413
|
+
* Both figures for a camera come from the SAME statement deliberately. Taking
|
|
26414
|
+
* `rows` from the histogram and `bytes` from the aggregate would let a write
|
|
26415
|
+
* landing between the two produce a footprint whose row count and byte count
|
|
26416
|
+
* describe different populations — a disagreement an operator sizing a
|
|
26417
|
+
* deletion has no way to detect.
|
|
26418
|
+
*
|
|
26419
|
+
* A read that fails THROWS. It must never degrade to a zero: "this camera
|
|
26420
|
+
* holds nothing" is precisely the claim a failed measurement must not be
|
|
26421
|
+
* allowed to make to something about to delete (D295, D312).
|
|
26334
26422
|
*/
|
|
26335
26423
|
async footprintByDevice() {
|
|
26336
|
-
const
|
|
26424
|
+
const buckets = await this.store.histogram.query({
|
|
26425
|
+
collection: MEDIA_COLLECTION,
|
|
26426
|
+
field: "deviceId",
|
|
26427
|
+
bucketSize: 1,
|
|
26428
|
+
origin: 0
|
|
26429
|
+
});
|
|
26337
26430
|
const out = /* @__PURE__ */ new Map();
|
|
26338
|
-
|
|
26339
|
-
|
|
26340
|
-
const
|
|
26431
|
+
for (const bucket of buckets) {
|
|
26432
|
+
if (bucket.count === 0 || !Number.isFinite(bucket.bucket)) continue;
|
|
26433
|
+
const deviceId = bucket.bucket;
|
|
26434
|
+
const agg = await this.store.aggregate.query({
|
|
26341
26435
|
collection: MEDIA_COLLECTION,
|
|
26342
|
-
|
|
26343
|
-
|
|
26344
|
-
|
|
26345
|
-
|
|
26346
|
-
|
|
26347
|
-
|
|
26348
|
-
|
|
26349
|
-
|
|
26436
|
+
fields: [{
|
|
26437
|
+
as: "bytes",
|
|
26438
|
+
field: "sizeBytes",
|
|
26439
|
+
op: "sum"
|
|
26440
|
+
}],
|
|
26441
|
+
filter: { where: { deviceId } }
|
|
26442
|
+
});
|
|
26443
|
+
if (agg.count === 0) continue;
|
|
26444
|
+
const bytes = agg.values["bytes"];
|
|
26445
|
+
out.set(deviceId, {
|
|
26446
|
+
bytes: bytes ?? 0,
|
|
26447
|
+
rows: agg.count
|
|
26350
26448
|
});
|
|
26351
|
-
if (rows.length === 0) break;
|
|
26352
|
-
for (const row of rows) {
|
|
26353
|
-
const data = row.data;
|
|
26354
|
-
const deviceId = Number(data["deviceId"]);
|
|
26355
|
-
const sizeBytes = Number(data["sizeBytes"]);
|
|
26356
|
-
if (!Number.isFinite(deviceId)) continue;
|
|
26357
|
-
const acc = out.get(deviceId) ?? {
|
|
26358
|
-
bytes: 0,
|
|
26359
|
-
rows: 0
|
|
26360
|
-
};
|
|
26361
|
-
acc.bytes += Number.isFinite(sizeBytes) ? sizeBytes : 0;
|
|
26362
|
-
acc.rows += 1;
|
|
26363
|
-
out.set(deviceId, acc);
|
|
26364
|
-
}
|
|
26365
|
-
if (rows.length < PAGE) break;
|
|
26366
|
-
offset += PAGE;
|
|
26367
26449
|
}
|
|
26368
26450
|
return out;
|
|
26369
26451
|
}
|
|
26452
|
+
/**
|
|
26453
|
+
* The media footprint broken down by KIND — fleet-wide, or for one camera.
|
|
26454
|
+
*
|
|
26455
|
+
* `footprintByDevice` answers "how much is where"; this answers "how much is
|
|
26456
|
+
* WHAT", which is the axis a deletion decision turns on. The deletable set
|
|
26457
|
+
* (the periodic `snapshot` filmstrip, the surplus per-edge motion stills) and
|
|
26458
|
+
* the keep set (`firstFrame`, rolling `lastFrame`, `thumbnail` /
|
|
26459
|
+
* `thumbnailSmall`, `keyFrame` / `keyFrameSmall`, the face and plate buffers,
|
|
26460
|
+
* gallery media, the CLIP `crop`) are told apart by `kind` and by nothing
|
|
26461
|
+
* else. A byte total cannot size a deletion; this can.
|
|
26462
|
+
*
|
|
26463
|
+
* One `aggregate` per kind, each an indexed covering range on
|
|
26464
|
+
* `idx_media_kind_device_size`, plus ONE unfiltered aggregate for the totals.
|
|
26465
|
+
*
|
|
26466
|
+
* That last statement is the point of the design and not a spare round trip.
|
|
26467
|
+
* `MediaFileKindEnum.options` is the closed set the writers use, but the
|
|
26468
|
+
* TABLE is not closed: a row written by a retired path, or by a version that
|
|
26469
|
+
* knew a kind this one does not, matches no per-kind query. Adding the kinds
|
|
26470
|
+
* up to produce the total would make such a row invisible and the denominator
|
|
26471
|
+
* quietly too small — the failure mode that matters most here, because the
|
|
26472
|
+
* caller is about to delete against it. So the total is measured
|
|
26473
|
+
* independently and the difference is REPORTED as `unaccounted*` rather than
|
|
26474
|
+
* being absorbed.
|
|
26475
|
+
*/
|
|
26476
|
+
async footprintByKind(deviceId) {
|
|
26477
|
+
const scope = deviceId === void 0 ? void 0 : { where: { deviceId } };
|
|
26478
|
+
const fields = [{
|
|
26479
|
+
as: "bytes",
|
|
26480
|
+
field: "sizeBytes",
|
|
26481
|
+
op: "sum"
|
|
26482
|
+
}];
|
|
26483
|
+
const totals = await this.store.aggregate.query({
|
|
26484
|
+
collection: MEDIA_COLLECTION,
|
|
26485
|
+
fields: [...fields],
|
|
26486
|
+
...scope === void 0 ? {} : { filter: scope }
|
|
26487
|
+
});
|
|
26488
|
+
const kinds = [];
|
|
26489
|
+
let countedRows = 0;
|
|
26490
|
+
let countedBytes = 0;
|
|
26491
|
+
for (const kind of MediaFileKindEnum.options) {
|
|
26492
|
+
const agg = await this.store.aggregate.query({
|
|
26493
|
+
collection: MEDIA_COLLECTION,
|
|
26494
|
+
fields: [...fields],
|
|
26495
|
+
filter: { where: {
|
|
26496
|
+
kind,
|
|
26497
|
+
...deviceId === void 0 ? {} : { deviceId }
|
|
26498
|
+
} }
|
|
26499
|
+
});
|
|
26500
|
+
if (agg.count === 0) continue;
|
|
26501
|
+
const bytes = agg.values["bytes"] ?? 0;
|
|
26502
|
+
countedRows += agg.count;
|
|
26503
|
+
countedBytes += bytes;
|
|
26504
|
+
kinds.push({
|
|
26505
|
+
kind,
|
|
26506
|
+
rows: agg.count,
|
|
26507
|
+
bytes
|
|
26508
|
+
});
|
|
26509
|
+
}
|
|
26510
|
+
kinds.sort((a, b) => b.bytes - a.bytes);
|
|
26511
|
+
const totalBytes = totals.values["bytes"] ?? 0;
|
|
26512
|
+
return {
|
|
26513
|
+
totalRows: totals.count,
|
|
26514
|
+
totalBytes,
|
|
26515
|
+
kinds,
|
|
26516
|
+
unaccountedRows: totals.count - countedRows,
|
|
26517
|
+
unaccountedBytes: totalBytes - countedBytes
|
|
26518
|
+
};
|
|
26519
|
+
}
|
|
26370
26520
|
/** Move a single media entry from one owner to another.
|
|
26371
26521
|
* The new copy is written before the old one is removed.
|
|
26372
26522
|
* The old-entry deletion is best-effort: on failure a warning is logged
|
|
@@ -36978,10 +37128,34 @@ var AnalyticsQueryFacade = class {
|
|
|
36978
37128
|
}
|
|
36979
37129
|
/**
|
|
36980
37130
|
* Durable event-store footprint for the events-management UI: event ROWS
|
|
36981
|
-
* (motion + object + audio)
|
|
36982
|
-
*
|
|
36983
|
-
*
|
|
36984
|
-
*
|
|
37131
|
+
* (motion + object + audio) per camera + total, and event-owned media BYTES
|
|
37132
|
+
* on disk per camera + total. Cameras with neither rows nor bytes are
|
|
37133
|
+
* omitted; the rest come back ordered by bytes, descending, which is the
|
|
37134
|
+
* order the operator's table renders.
|
|
37135
|
+
*
|
|
37136
|
+
* ## The device set is a union of THREE sources
|
|
37137
|
+
*
|
|
37138
|
+
* It used to be `media ∪ tracks`, and a camera holding events but no media
|
|
37139
|
+
* row and no persisted track was therefore invisible — not listed, and its
|
|
37140
|
+
* events missing from `totalRows`. That combination is not exotic: media
|
|
37141
|
+
* retention, a media deletion and `pruneTracksBefore` all run on clocks of
|
|
37142
|
+
* their own, and every one of them can outlive the event rows. The number
|
|
37143
|
+
* this method exists to produce is the denominator for a deletion, so an
|
|
37144
|
+
* undercount is the one failure mode that matters, and it was silent.
|
|
37145
|
+
*
|
|
37146
|
+
* Events now contribute their own device set, so a camera is present if ANY
|
|
37147
|
+
* of the three sources has ever heard of it.
|
|
37148
|
+
*
|
|
37149
|
+
* ## The cost is the answer, not the table
|
|
37150
|
+
*
|
|
37151
|
+
* Three grouped reads for the event rows (one per event collection, never
|
|
37152
|
+
* three per camera), one grouped read plus one aggregate per camera for the
|
|
37153
|
+
* media bytes, one for the track device set. The retired shape paid ~1 250
|
|
37154
|
+
* round trips for the media half alone and timed out at 60 s on every call.
|
|
37155
|
+
* See {@link MediaStore.footprintByDevice} and {@link EventStore.countByDevice}.
|
|
37156
|
+
*
|
|
37157
|
+
* A store that cannot answer THROWS rather than returning zeroes — see the
|
|
37158
|
+
* same note on both.
|
|
36985
37159
|
*/
|
|
36986
37160
|
async getEventStoreFootprint() {
|
|
36987
37161
|
const eventStore = this.deps.eventStore();
|
|
@@ -36991,15 +37165,16 @@ var AnalyticsQueryFacade = class {
|
|
|
36991
37165
|
totalBytes: 0,
|
|
36992
37166
|
devices: []
|
|
36993
37167
|
};
|
|
36994
|
-
const mediaByDevice = await mediaStore.footprintByDevice();
|
|
37168
|
+
const [mediaByDevice, eventRowsByDevice] = await Promise.all([mediaStore.footprintByDevice(), eventStore.countByDevice()]);
|
|
36995
37169
|
const deviceIds = new Set(mediaByDevice.keys());
|
|
37170
|
+
for (const id of eventRowsByDevice.keys()) deviceIds.add(id);
|
|
36996
37171
|
const trackStore = this.deps.trackStore();
|
|
36997
37172
|
if (trackStore) for (const id of await trackStore.listDeviceIds()) deviceIds.add(id);
|
|
36998
37173
|
const devices = [];
|
|
36999
37174
|
let totalRows = 0;
|
|
37000
37175
|
let totalBytes = 0;
|
|
37001
37176
|
for (const deviceId of deviceIds) {
|
|
37002
|
-
const rows =
|
|
37177
|
+
const rows = eventRowsByDevice.get(deviceId) ?? 0;
|
|
37003
37178
|
const bytes = mediaByDevice.get(deviceId)?.bytes ?? 0;
|
|
37004
37179
|
if (rows === 0 && bytes === 0) continue;
|
|
37005
37180
|
totalRows += rows;
|
|
@@ -37018,6 +37193,26 @@ var AnalyticsQueryFacade = class {
|
|
|
37018
37193
|
};
|
|
37019
37194
|
}
|
|
37020
37195
|
/**
|
|
37196
|
+
* The media footprint by KIND — the axis that says what is safe to delete.
|
|
37197
|
+
* See {@link MediaStore.footprintByKind}.
|
|
37198
|
+
*
|
|
37199
|
+
* An absent media store is an empty breakdown, not a refusal: it is the same
|
|
37200
|
+
* "the addon has not finished booting" branch every other method here takes,
|
|
37201
|
+
* and the zeroes are accompanied by an empty `kinds` list, which no reader
|
|
37202
|
+
* can mistake for a measured "there is nothing".
|
|
37203
|
+
*/
|
|
37204
|
+
async getEventMediaFootprintByKind(input) {
|
|
37205
|
+
const mediaStore = this.deps.mediaStore();
|
|
37206
|
+
if (!mediaStore) return {
|
|
37207
|
+
totalRows: 0,
|
|
37208
|
+
totalBytes: 0,
|
|
37209
|
+
kinds: [],
|
|
37210
|
+
unaccountedRows: 0,
|
|
37211
|
+
unaccountedBytes: 0
|
|
37212
|
+
};
|
|
37213
|
+
return mediaStore.footprintByKind(input.deviceId);
|
|
37214
|
+
}
|
|
37215
|
+
/**
|
|
37021
37216
|
* Cluster-wide prune of events with `timestamp < olderThanMs` across every
|
|
37022
37217
|
* camera (via the device-agnostic `evictBefore`), deleting each pruned event's
|
|
37023
37218
|
* media in lockstep. Logs one global (`deviceId:null`) events ops-log row.
|
|
@@ -44685,6 +44880,15 @@ function snapshot(j) {
|
|
|
44685
44880
|
*/
|
|
44686
44881
|
var LOCATION_ID_FIELD = "locationId";
|
|
44687
44882
|
/**
|
|
44883
|
+
* Unstamped rows claimed per bulk seal statement.
|
|
44884
|
+
*
|
|
44885
|
+
* Bounds two things at once: the number of bound parameters in the `IN` list,
|
|
44886
|
+
* and how long one `UPDATE` holds the write lock on the database every process
|
|
44887
|
+
* in the cluster reads configuration through. Deliberately well under SQLite's
|
|
44888
|
+
* variable ceiling, and small enough that a cancel lands within one statement.
|
|
44889
|
+
*/
|
|
44890
|
+
var SEAL_BATCH_SIZE = 500;
|
|
44891
|
+
/**
|
|
44688
44892
|
* True when the row names NO location — the population `seal` exists for.
|
|
44689
44893
|
*
|
|
44690
44894
|
* Deliberately NOT `mediaRowLocation(data) === DEFAULT_MEDIA_LOCATION`: that
|
|
@@ -44916,6 +45120,17 @@ var MediaRelocateEngine = class {
|
|
|
44916
45120
|
}
|
|
44917
45121
|
async countSelected(mode, toLocationId, stop) {
|
|
44918
45122
|
try {
|
|
45123
|
+
if (mode === "seal") {
|
|
45124
|
+
let sealable = 0;
|
|
45125
|
+
for (const collection of collectionsFor(mode)) {
|
|
45126
|
+
if (stop()) return null;
|
|
45127
|
+
sealable += await this.deps.store.count.query({
|
|
45128
|
+
collection: collection.name,
|
|
45129
|
+
filter: { where: { [LOCATION_ID_FIELD]: null } }
|
|
45130
|
+
});
|
|
45131
|
+
}
|
|
45132
|
+
return { rows: sealable };
|
|
45133
|
+
}
|
|
44919
45134
|
let rows = 0;
|
|
44920
45135
|
for (const collection of collectionsFor(mode)) {
|
|
44921
45136
|
const isRetrain = collection.name === RETRAIN_FRAMES_COLLECTION;
|
|
@@ -44947,6 +45162,14 @@ var MediaRelocateEngine = class {
|
|
|
44947
45162
|
* gets believed. */
|
|
44948
45163
|
let pathlessRows = 0;
|
|
44949
45164
|
job.filesTotal = (await this.countSelected(job.mode, job.toLocationId, () => job.cancelRequested))?.rows ?? null;
|
|
45165
|
+
if (job.mode === "seal") {
|
|
45166
|
+
for (const collection of collectionsFor(job.mode)) {
|
|
45167
|
+
if (job.cancelRequested) break;
|
|
45168
|
+
await this.sealCollection(collection.name, job);
|
|
45169
|
+
}
|
|
45170
|
+
job.state = job.cancelRequested ? "cancelled" : "done";
|
|
45171
|
+
return;
|
|
45172
|
+
}
|
|
44950
45173
|
for (const collection of collectionsFor(job.mode)) {
|
|
44951
45174
|
if (job.cancelRequested) break;
|
|
44952
45175
|
const isRetrain = collection.name === RETRAIN_FRAMES_COLLECTION;
|
|
@@ -44954,20 +45177,6 @@ var MediaRelocateEngine = class {
|
|
|
44954
45177
|
if (job.cancelRequested) break;
|
|
44955
45178
|
const data = row.data;
|
|
44956
45179
|
if (!selectsRow(job.mode, job.toLocationId, data, isRetrain)) continue;
|
|
44957
|
-
if (job.mode === "seal") {
|
|
44958
|
-
try {
|
|
44959
|
-
await this.stamp(collection.name, row.id, data, job.toLocationId);
|
|
44960
|
-
job.filesMoved++;
|
|
44961
|
-
} catch (err) {
|
|
44962
|
-
failedRows++;
|
|
44963
|
-
this.deps.logger.debug("event-media seal row failed", { meta: {
|
|
44964
|
-
collection: collection.name,
|
|
44965
|
-
key: row.id,
|
|
44966
|
-
error: String(err)
|
|
44967
|
-
} });
|
|
44968
|
-
}
|
|
44969
|
-
continue;
|
|
44970
|
-
}
|
|
44971
45180
|
const from = mediaRowLocation(data);
|
|
44972
45181
|
const relativePath = String(data["path"] ?? "");
|
|
44973
45182
|
if (relativePath.length === 0) {
|
|
@@ -45030,6 +45239,73 @@ var MediaRelocateEngine = class {
|
|
|
45030
45239
|
this.deps.onFinished?.(snapshot(job));
|
|
45031
45240
|
}
|
|
45032
45241
|
}
|
|
45242
|
+
/**
|
|
45243
|
+
* Stamp every unstamped row of one collection with the job's target, in
|
|
45244
|
+
* bounded bulk statements.
|
|
45245
|
+
*
|
|
45246
|
+
* ## What this replaced
|
|
45247
|
+
*
|
|
45248
|
+
* A per-row loop: page the whole collection through {@link pageRows}, then
|
|
45249
|
+
* one `store.set.mutate` per selected row — a full-row UPSERT rewriting every
|
|
45250
|
+
* column to change one field, one RPC each, plus a second full walk before it
|
|
45251
|
+
* to produce the denominator. Measured on the live hub against the 464 902
|
|
45252
|
+
* unstamped rows: **2.3 rows/s, falling to 0.7 rows/s under a competing
|
|
45253
|
+
* drain**, i.e. over two and a half days for a metadata stamp that reads no
|
|
45254
|
+
* file and moves no byte. It was cancelled. This is the write-side twin of
|
|
45255
|
+
* the read-side walk {@link countUnstamped} retired, and the fourth instance
|
|
45256
|
+
* of the same pattern found in one day — work looped over the door instead of
|
|
45257
|
+
* pushed into the statement.
|
|
45258
|
+
*
|
|
45259
|
+
* ## The shape
|
|
45260
|
+
*
|
|
45261
|
+
* Probe the indexed `IS NULL` range for a batch of ids, then ONE
|
|
45262
|
+
* `updateWhere` over those ids. Two round trips per {@link SEAL_BATCH_SIZE}
|
|
45263
|
+
* rows instead of one write per row.
|
|
45264
|
+
*
|
|
45265
|
+
* The probe carries no cursor and needs none: the predicate CONSUMES itself.
|
|
45266
|
+
* A stamped row leaves the range, so the next probe returns the next batch,
|
|
45267
|
+
* and a run that is interrupted anywhere simply resumes on what is left. That
|
|
45268
|
+
* is also why the seal is idempotent (a second run probes an empty range and
|
|
45269
|
+
* stamps nothing) and why it cannot repoint a row that already names a
|
|
45270
|
+
* different location — `IS NULL` never selects one.
|
|
45271
|
+
*
|
|
45272
|
+
* A single unbounded `UPDATE … WHERE locationId IS NULL` would be one
|
|
45273
|
+
* statement instead of ~1 860, and is not used: half a million row rewrites,
|
|
45274
|
+
* each maintaining four indexes, is an unbounded statement holding the write
|
|
45275
|
+
* lock on a database the whole cluster reads configuration through, under an
|
|
45276
|
+
* RPC deadline that must not be raised. Batching bounds the statement, keeps
|
|
45277
|
+
* `cancel` responsive between batches, and keeps `filesMoved` honest.
|
|
45278
|
+
*
|
|
45279
|
+
* ## The termination guard
|
|
45280
|
+
*
|
|
45281
|
+
* If the probe finds rows and the update reports **0**, this THROWS. That is
|
|
45282
|
+
* not defensive noise: it is precisely what a backend compiling
|
|
45283
|
+
* `locationId = NULL` does — matches nothing, reports success (D312) — and
|
|
45284
|
+
* against it a self-consuming loop never terminates and a non-looping one
|
|
45285
|
+
* reports a completed seal having stamped nothing. Zero unstamped rows is the
|
|
45286
|
+
* single value the migration gate reads as "safe to cut over".
|
|
45287
|
+
*/
|
|
45288
|
+
async sealCollection(collection, job) {
|
|
45289
|
+
for (;;) {
|
|
45290
|
+
if (job.cancelRequested) return;
|
|
45291
|
+
const batch = await this.deps.store.query.query({
|
|
45292
|
+
collection,
|
|
45293
|
+
filter: {
|
|
45294
|
+
where: { [LOCATION_ID_FIELD]: null },
|
|
45295
|
+
limit: SEAL_BATCH_SIZE
|
|
45296
|
+
},
|
|
45297
|
+
columns: [LOCATION_ID_FIELD]
|
|
45298
|
+
});
|
|
45299
|
+
if (batch.length === 0) return;
|
|
45300
|
+
const { updated } = await this.deps.store.updateWhere.mutate({
|
|
45301
|
+
collection,
|
|
45302
|
+
filter: { whereIn: { id: batch.map((row) => row.id) } },
|
|
45303
|
+
data: { [LOCATION_ID_FIELD]: job.toLocationId }
|
|
45304
|
+
});
|
|
45305
|
+
if (updated === 0) throw new Error(`event-media seal stamped 0 of ${batch.length} unstamped row(s) in "${collection}" — the bulk predicate matched nothing, so the population cannot be sealed`);
|
|
45306
|
+
job.filesMoved += updated;
|
|
45307
|
+
}
|
|
45308
|
+
}
|
|
45033
45309
|
async stamp(collection, key, data, locationId) {
|
|
45034
45310
|
await this.deps.store.set.mutate({
|
|
45035
45311
|
collection,
|
|
@@ -51761,6 +52037,43 @@ var EventStore = class {
|
|
|
51761
52037
|
return motion + object + audio;
|
|
51762
52038
|
}
|
|
51763
52039
|
/**
|
|
52040
|
+
* Persisted event rows (motion + object + audio) for EVERY device that has
|
|
52041
|
+
* any — three round trips for the whole fleet.
|
|
52042
|
+
*
|
|
52043
|
+
* {@link countForDevice} answers one camera in three `COUNT(*)`s, which is
|
|
52044
|
+
* right for one camera and wrong in a loop: the events-management footprint
|
|
52045
|
+
* called it once per device, so the cost of "how many events does each camera
|
|
52046
|
+
* hold" grew with the camera count for a result that is a few dozen integers.
|
|
52047
|
+
*
|
|
52048
|
+
* A histogram on `deviceId` with `bucketSize: 1` is `GROUP BY deviceId`, and
|
|
52049
|
+
* every event collection already carries `idx_<kind>_device_ts`, which leads
|
|
52050
|
+
* with `deviceId` — so the planner serves each of these from a covering index
|
|
52051
|
+
* with no table access at all. `bucket` IS the deviceId (the column is
|
|
52052
|
+
* INTEGER, the bucket expression is `CAST((deviceId - 0) / 1 AS INTEGER)`).
|
|
52053
|
+
*
|
|
52054
|
+
* Not best-effort: a failure propagates. The one caller is sizing a deletion,
|
|
52055
|
+
* and a camera silently reported as holding zero events is the answer that
|
|
52056
|
+
* must never come from a read that did not happen.
|
|
52057
|
+
*/
|
|
52058
|
+
async countByDevice() {
|
|
52059
|
+
const perCollection = await Promise.all([
|
|
52060
|
+
MOTION_EVENTS_COLLECTION,
|
|
52061
|
+
OBJECT_EVENTS_COLLECTION,
|
|
52062
|
+
AUDIO_EVENTS_COLLECTION
|
|
52063
|
+
].map((collection) => this.store.histogram.query({
|
|
52064
|
+
collection,
|
|
52065
|
+
field: "deviceId",
|
|
52066
|
+
bucketSize: 1,
|
|
52067
|
+
origin: 0
|
|
52068
|
+
})));
|
|
52069
|
+
const out = /* @__PURE__ */ new Map();
|
|
52070
|
+
for (const buckets of perCollection) for (const bucket of buckets) {
|
|
52071
|
+
if (bucket.count === 0 || !Number.isFinite(bucket.bucket)) continue;
|
|
52072
|
+
out.set(bucket.bucket, (out.get(bucket.bucket) ?? 0) + bucket.count);
|
|
52073
|
+
}
|
|
52074
|
+
return out;
|
|
52075
|
+
}
|
|
52076
|
+
/**
|
|
51764
52077
|
* Return per-kind event counts in equal-width time buckets.
|
|
51765
52078
|
*
|
|
51766
52079
|
* Each bucket maps to a `bucketStart = since + i * bucketMs`.
|
|
@@ -71484,6 +71797,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
71484
71797
|
async getEventStoreFootprint() {
|
|
71485
71798
|
return this.queryFacade.getEventStoreFootprint();
|
|
71486
71799
|
}
|
|
71800
|
+
async getEventMediaFootprintByKind(input) {
|
|
71801
|
+
return this.queryFacade.getEventMediaFootprintByKind(input);
|
|
71802
|
+
}
|
|
71487
71803
|
async pruneEvents(input) {
|
|
71488
71804
|
return this.queryFacade.pruneEvents(input);
|
|
71489
71805
|
}
|
|
@@ -30,7 +30,7 @@ async function d(e) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
async function f() {
|
|
33
|
-
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-
|
|
33
|
+
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Cil5ur25.mjs")).catch((e) => {
|
|
34
34
|
throw l = void 0, e;
|
|
35
35
|
}), l;
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-post-analysis",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.153",
|
|
4
4
|
"description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|