@camstack/addon-post-analysis 1.2.231 → 1.2.232
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-DAXBbWmc.js → dist-Dbf6ceXh.js} +106 -5
- package/dist/{dist-QjlwKSkb.mjs → dist-yyQ1H5yE.mjs} +106 -5
- 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-C5_YZ4mw.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-w7xIpvHc.mjs} +2 -2
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-yat2fBJg.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-C5YuStaN.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-BJyNnbrv.mjs → hostInit-2-KE2jBo.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +605 -42
- package/dist/pipeline-analytics/index.mjs +605 -42
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -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-Dbf6ceXh.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");
|
|
@@ -1972,8 +1972,8 @@ var NC_ARTIFACTS_INDEXES = [
|
|
|
1972
1972
|
}
|
|
1973
1973
|
];
|
|
1974
1974
|
/** Upper bound for a half-open `whereBetween` — the store has no `>=`. */
|
|
1975
|
-
var TIME_MAX = 864e13;
|
|
1976
|
-
var TIME_MIN = -864e13;
|
|
1975
|
+
var TIME_MAX$1 = 864e13;
|
|
1976
|
+
var TIME_MIN$1 = -864e13;
|
|
1977
1977
|
/** Prune scan ceiling — the index is bounded by the shelf it mirrors (2000
|
|
1978
1978
|
* entries by default), so this only has to be comfortably above it. */
|
|
1979
1979
|
var NC_ARTIFACT_PRUNE_SCAN_LIMIT = 2e4;
|
|
@@ -2213,7 +2213,7 @@ var NcArtifactIndex = class {
|
|
|
2213
2213
|
try {
|
|
2214
2214
|
const expired = await this.store.deleteWhere.mutate({
|
|
2215
2215
|
collection: NC_ARTIFACTS_COLLECTION,
|
|
2216
|
-
filter: { whereBetween: { expiresAt: [TIME_MIN, this.now() - 1] } }
|
|
2216
|
+
filter: { whereBetween: { expiresAt: [TIME_MIN$1, this.now() - 1] } }
|
|
2217
2217
|
});
|
|
2218
2218
|
removed += expired.deleted;
|
|
2219
2219
|
if (keptIds === null) {
|
|
@@ -2255,8 +2255,8 @@ var NcArtifactIndex = class {
|
|
|
2255
2255
|
whereIn["kind"] = query.kinds !== void 0 && query.kinds.length > 0 ? [...query.kinds] : [...NC_GALLERY_ARTIFACT_KINDS];
|
|
2256
2256
|
if (query.deviceIds !== void 0 && query.deviceIds.length > 0) whereIn["deviceId"] = [...query.deviceIds];
|
|
2257
2257
|
const whereBetween = {
|
|
2258
|
-
capturedAt: [query.fromMs ?? TIME_MIN, query.toMs ?? TIME_MAX],
|
|
2259
|
-
expiresAt: [this.now(), TIME_MAX]
|
|
2258
|
+
capturedAt: [query.fromMs ?? TIME_MIN$1, query.toMs ?? TIME_MAX$1],
|
|
2259
|
+
expiresAt: [this.now(), TIME_MAX$1]
|
|
2260
2260
|
};
|
|
2261
2261
|
return {
|
|
2262
2262
|
...query.ruleId !== void 0 ? { where: { ruleId: query.ruleId } } : {},
|
|
@@ -2281,6 +2281,23 @@ var NcArtifactIndex = class {
|
|
|
2281
2281
|
return out;
|
|
2282
2282
|
}
|
|
2283
2283
|
};
|
|
2284
|
+
/**
|
|
2285
|
+
* A sentence explaining why this node must not keep the shelf, or `null` when
|
|
2286
|
+
* it may. The sentence is the log line: an operator reading it should learn
|
|
2287
|
+
* WHICH node owns the shelf without opening the storage page.
|
|
2288
|
+
*/
|
|
2289
|
+
function artifactShelfRefusal(locations, localNodeId) {
|
|
2290
|
+
if (localNodeId === null || localNodeId.length === 0) return null;
|
|
2291
|
+
const here = localNodeId.includes("/") ? localNodeId.split("/")[0] ?? "" : localNodeId;
|
|
2292
|
+
if (here.length === 0) return null;
|
|
2293
|
+
for (const location of locations) {
|
|
2294
|
+
if (!location.id.startsWith("artifacts")) continue;
|
|
2295
|
+
const owner = location.nodeId;
|
|
2296
|
+
if (owner === void 0 || owner.length === 0 || owner === here) continue;
|
|
2297
|
+
return `the artefact shelf lives on node "${owner}" (location "${location.id}"), not on "${here}" — the path it resolves to is that node's disk, and creating it here would make a directory of the same name that nothing reads. Artefacts are produced, stored and served by "${owner}".`;
|
|
2298
|
+
}
|
|
2299
|
+
return null;
|
|
2300
|
+
}
|
|
2284
2301
|
//#endregion
|
|
2285
2302
|
//#region ../types/dist/node.mjs
|
|
2286
2303
|
var node_exports = /* @__PURE__ */ require_dist.__exportAll({
|
|
@@ -18693,6 +18710,52 @@ function buildSummaryOutboxInputs(input) {
|
|
|
18693
18710
|
});
|
|
18694
18711
|
}
|
|
18695
18712
|
//#endregion
|
|
18713
|
+
//#region src/notification-center/summary/summary-group-collapse.ts
|
|
18714
|
+
function collapseToGroups(candidates, groups) {
|
|
18715
|
+
const claim = /* @__PURE__ */ new Map();
|
|
18716
|
+
for (const group of groups) for (const trackId of group.memberTrackIds) if (!claim.has(trackId)) claim.set(trackId, group);
|
|
18717
|
+
const survivorsByGroup = /* @__PURE__ */ new Map();
|
|
18718
|
+
for (const candidate of candidates) {
|
|
18719
|
+
const group = claim.get(candidate.trackId);
|
|
18720
|
+
if (group === void 0) continue;
|
|
18721
|
+
const bucket = survivorsByGroup.get(group.summaryId);
|
|
18722
|
+
if (bucket === void 0) survivorsByGroup.set(group.summaryId, [candidate]);
|
|
18723
|
+
else bucket.push(candidate);
|
|
18724
|
+
}
|
|
18725
|
+
const representative = /* @__PURE__ */ new Map();
|
|
18726
|
+
for (const [summaryId, survivors] of survivorsByGroup) {
|
|
18727
|
+
const group = groups.find((g) => g.summaryId === summaryId);
|
|
18728
|
+
if (group === void 0) continue;
|
|
18729
|
+
const chosen = survivors.find((c) => c.trackId === group.primaryTrackId) ?? survivors[0];
|
|
18730
|
+
if (chosen === void 0) continue;
|
|
18731
|
+
representative.set(summaryId, {
|
|
18732
|
+
...chosen,
|
|
18733
|
+
groupSize: survivors.length,
|
|
18734
|
+
...group.groupShotMediaId !== null ? { groupShotMediaId: group.groupShotMediaId } : {}
|
|
18735
|
+
});
|
|
18736
|
+
}
|
|
18737
|
+
const kept = [];
|
|
18738
|
+
let collapsed = 0;
|
|
18739
|
+
for (const candidate of candidates) {
|
|
18740
|
+
const group = claim.get(candidate.trackId);
|
|
18741
|
+
if (group === void 0) {
|
|
18742
|
+
kept.push(candidate);
|
|
18743
|
+
continue;
|
|
18744
|
+
}
|
|
18745
|
+
const chosen = representative.get(group.summaryId);
|
|
18746
|
+
if (chosen === void 0) {
|
|
18747
|
+
kept.push(candidate);
|
|
18748
|
+
continue;
|
|
18749
|
+
}
|
|
18750
|
+
if (chosen.trackId === candidate.trackId) kept.push(chosen);
|
|
18751
|
+
else collapsed += 1;
|
|
18752
|
+
}
|
|
18753
|
+
return {
|
|
18754
|
+
kept,
|
|
18755
|
+
collapsed
|
|
18756
|
+
};
|
|
18757
|
+
}
|
|
18758
|
+
//#endregion
|
|
18696
18759
|
//#region src/notification-center/summary/summary-selection.ts
|
|
18697
18760
|
/**
|
|
18698
18761
|
* Which tracks become tiles — filter, rank, and the per-camera quota.
|
|
@@ -19265,7 +19328,8 @@ var NcSummaryProducer = class {
|
|
|
19265
19328
|
try {
|
|
19266
19329
|
const candidates = await this.collectCandidates(rule, window);
|
|
19267
19330
|
const filtered = filterSummaryCandidates(candidates, rule.filters, this.deps.zoneOwner);
|
|
19268
|
-
const
|
|
19331
|
+
const grouped = await this.collapseIfGrouping(rule, window, filtered.kept);
|
|
19332
|
+
const selection = selectSummaryTiles(grouped.kept, {
|
|
19269
19333
|
maxTiles: rule.maxTiles,
|
|
19270
19334
|
rank: rule.rank
|
|
19271
19335
|
});
|
|
@@ -19294,7 +19358,7 @@ var NcSummaryProducer = class {
|
|
|
19294
19358
|
const tileLoad = await this.loadTiles({
|
|
19295
19359
|
rule,
|
|
19296
19360
|
preferred: selection.tiles,
|
|
19297
|
-
refill: remainingSummaryCandidates(
|
|
19361
|
+
refill: remainingSummaryCandidates(grouped.kept, selection.tiles, rule.rank),
|
|
19298
19362
|
maxTiles: rule.maxTiles
|
|
19299
19363
|
});
|
|
19300
19364
|
const loaded = tileLoad.tiles;
|
|
@@ -19335,7 +19399,7 @@ var NcSummaryProducer = class {
|
|
|
19335
19399
|
priority: rule.priority,
|
|
19336
19400
|
tileCount: loaded.length,
|
|
19337
19401
|
cameraCount: new Set(loaded.map((t) => t.candidate.deviceId)).size,
|
|
19338
|
-
matchedCount:
|
|
19402
|
+
matchedCount: grouped.kept.length,
|
|
19339
19403
|
detections,
|
|
19340
19404
|
...mosaic.ref !== null ? { mosaic: mosaic.ref } : {},
|
|
19341
19405
|
generatedAt: this.deps.now(),
|
|
@@ -19347,7 +19411,7 @@ var NcSummaryProducer = class {
|
|
|
19347
19411
|
this.deps.logger.info("summary produced", { meta: {
|
|
19348
19412
|
...meta,
|
|
19349
19413
|
scanned: candidates.length,
|
|
19350
|
-
matched:
|
|
19414
|
+
matched: grouped.kept.length,
|
|
19351
19415
|
droppedStationary: filtered.droppedStationary,
|
|
19352
19416
|
droppedUnconfirmed: filtered.droppedUnconfirmed,
|
|
19353
19417
|
unknownConfirmation: filtered.unknownConfirmation,
|
|
@@ -19370,7 +19434,7 @@ var NcSummaryProducer = class {
|
|
|
19370
19434
|
window,
|
|
19371
19435
|
tiles: loaded.length,
|
|
19372
19436
|
cameras: new Set(loaded.map((t) => t.candidate.deviceId)).size,
|
|
19373
|
-
matched:
|
|
19437
|
+
matched: grouped.kept.length,
|
|
19374
19438
|
droppedStationary: filtered.droppedStationary,
|
|
19375
19439
|
enqueued,
|
|
19376
19440
|
...mosaic.bytes > 0 ? { mosaicBytes: mosaic.bytes } : {},
|
|
@@ -19391,6 +19455,54 @@ var NcSummaryProducer = class {
|
|
|
19391
19455
|
};
|
|
19392
19456
|
}
|
|
19393
19457
|
}
|
|
19458
|
+
/**
|
|
19459
|
+
* Fold each session summary's surviving members into one entry — or do
|
|
19460
|
+
* nothing at all, which is what `tracks` (the default) means.
|
|
19461
|
+
*
|
|
19462
|
+
* A failure to read the groups does NOT fail the digest: it falls back to
|
|
19463
|
+
* subjects and says so. A digest of four lines instead of one is a worse
|
|
19464
|
+
* reading, not a lost one, and losing the night's record over a store read
|
|
19465
|
+
* is the wrong trade.
|
|
19466
|
+
*/
|
|
19467
|
+
async collapseIfGrouping(rule, window, kept) {
|
|
19468
|
+
if (rule.subject !== "groups") return {
|
|
19469
|
+
kept,
|
|
19470
|
+
collapsed: 0
|
|
19471
|
+
};
|
|
19472
|
+
const listGroups = this.deps.listSummaryGroups;
|
|
19473
|
+
if (listGroups === void 0) {
|
|
19474
|
+
this.deps.logger.warn("summary rule asks for groups but this node cannot read them", { meta: { ruleId: rule.id } });
|
|
19475
|
+
return {
|
|
19476
|
+
kept,
|
|
19477
|
+
collapsed: 0
|
|
19478
|
+
};
|
|
19479
|
+
}
|
|
19480
|
+
const groups = [];
|
|
19481
|
+
for (const deviceId of rule.deviceIds) try {
|
|
19482
|
+
groups.push(...await listGroups({
|
|
19483
|
+
deviceId,
|
|
19484
|
+
sinceMs: window.startMs,
|
|
19485
|
+
untilMs: window.endMs
|
|
19486
|
+
}));
|
|
19487
|
+
} catch (err) {
|
|
19488
|
+
this.deps.logger.warn("summary could not read a camera’s groups — read as subjects", {
|
|
19489
|
+
tags: { deviceId },
|
|
19490
|
+
meta: {
|
|
19491
|
+
ruleId: rule.id,
|
|
19492
|
+
error: String(err)
|
|
19493
|
+
}
|
|
19494
|
+
});
|
|
19495
|
+
}
|
|
19496
|
+
const outcome = collapseToGroups(kept, groups);
|
|
19497
|
+
this.deps.logger.info("summary read as groups", { meta: {
|
|
19498
|
+
ruleId: rule.id,
|
|
19499
|
+
groups: groups.length,
|
|
19500
|
+
subjects: kept.length,
|
|
19501
|
+
lines: outcome.kept.length,
|
|
19502
|
+
collapsed: outcome.collapsed
|
|
19503
|
+
} });
|
|
19504
|
+
return outcome;
|
|
19505
|
+
}
|
|
19394
19506
|
/** Every camera's tracks in the window, capped per camera and counted. */
|
|
19395
19507
|
async collectCandidates(rule, window) {
|
|
19396
19508
|
const all = [];
|
|
@@ -19440,6 +19552,25 @@ var NcSummaryProducer = class {
|
|
|
19440
19552
|
seenIds.add(candidate.trackId);
|
|
19441
19553
|
const tags = { deviceId: candidate.deviceId };
|
|
19442
19554
|
try {
|
|
19555
|
+
const groupShot = candidate.groupShotMediaId;
|
|
19556
|
+
if (groupShot !== void 0) {
|
|
19557
|
+
const jpeg = await this.deps.readMedia(groupShot);
|
|
19558
|
+
if (jpeg !== null && jpeg.byteLength > 0) {
|
|
19559
|
+
tiles.push({
|
|
19560
|
+
candidate,
|
|
19561
|
+
jpeg
|
|
19562
|
+
});
|
|
19563
|
+
continue;
|
|
19564
|
+
}
|
|
19565
|
+
this.deps.logger.info("summary tile: group shot unreadable — using a member", {
|
|
19566
|
+
tags,
|
|
19567
|
+
meta: {
|
|
19568
|
+
ruleId: input.rule.id,
|
|
19569
|
+
trackId: candidate.trackId,
|
|
19570
|
+
mediaId: groupShot
|
|
19571
|
+
}
|
|
19572
|
+
});
|
|
19573
|
+
}
|
|
19443
19574
|
const rows = await this.deps.listMediaInfo(candidate.trackId);
|
|
19444
19575
|
const picked = pickSummaryMedia(rows, (candidate.firstSeen + candidate.lastSeen) / 2);
|
|
19445
19576
|
if (picked === null) {
|
|
@@ -19902,6 +20033,21 @@ var NcSummaryRuleInputSchema = require_dist.object({
|
|
|
19902
20033
|
deviceIds: DeviceIdsField,
|
|
19903
20034
|
window: NcSummaryWindowSchema,
|
|
19904
20035
|
filters: NcSummaryFiltersSchema.optional(),
|
|
20036
|
+
/**
|
|
20037
|
+
* What ONE line of this digest is: a subject, or a group of subjects that
|
|
20038
|
+
* arrived together.
|
|
20039
|
+
*
|
|
20040
|
+
* `tracks` is what the digest has always done and stays the default — the
|
|
20041
|
+
* operator's instruction when groups were added was that the first behaviour
|
|
20042
|
+
* remains. `groups` collapses the members of each session summary to one
|
|
20043
|
+
* entry, so a family walking in is one line and not four
|
|
20044
|
+
* (`summary-group-collapse.ts`).
|
|
20045
|
+
*
|
|
20046
|
+
* `.default('tracks')` is safe here where `NcSummaryFiltersSchema` fields are
|
|
20047
|
+
* not: this is a field of the RULE, not of the partial `filters` object that
|
|
20048
|
+
* a patch materialises wholesale.
|
|
20049
|
+
*/
|
|
20050
|
+
subject: require_dist._enum(["tracks", "groups"]).default("tracks"),
|
|
19905
20051
|
maxTiles: MaxTilesField.default(12),
|
|
19906
20052
|
maxAiImages: MaxAiImagesField.default(6),
|
|
19907
20053
|
rank: NcSummaryRankSchema.default("importance"),
|
|
@@ -19936,6 +20082,7 @@ var NcSummaryRulePatchSchema = require_dist.object({
|
|
|
19936
20082
|
maxTiles: MaxTilesField.optional(),
|
|
19937
20083
|
maxAiImages: MaxAiImagesField.optional(),
|
|
19938
20084
|
rank: NcSummaryRankSchema.optional(),
|
|
20085
|
+
subject: require_dist._enum(["tracks", "groups"]).optional(),
|
|
19939
20086
|
barrierSeconds: BarrierSecondsField.optional(),
|
|
19940
20087
|
targets: TargetsField.optional(),
|
|
19941
20088
|
template: NcSummaryTemplateSchema.nullable().optional(),
|
|
@@ -61329,15 +61476,48 @@ var SUMMARY_MEMBERS_COLUMNS = [
|
|
|
61329
61476
|
{
|
|
61330
61477
|
name: "className",
|
|
61331
61478
|
type: "TEXT"
|
|
61479
|
+
}),
|
|
61480
|
+
(
|
|
61481
|
+
/**
|
|
61482
|
+
* The member's tier-1 label (`dog`, `van`) and tier-2 instance (a name, a
|
|
61483
|
+
* plate), STAMPED AT SEAL.
|
|
61484
|
+
*
|
|
61485
|
+
* Not at the join, and the timing is the whole point: recognition lands
|
|
61486
|
+
* during a track's life, almost always after it joined, so a label written at
|
|
61487
|
+
* the join would be empty on exactly the members worth naming. At the seal
|
|
61488
|
+
* every member's track has closed, so its label is final — there is nothing
|
|
61489
|
+
* left to change it.
|
|
61490
|
+
*
|
|
61491
|
+
* They are COLUMNS because a filter has to be able to ask for them: "show me
|
|
61492
|
+
* the arrivals with a dog" is a query over members, and answering it from the
|
|
61493
|
+
* tracks would mean reading every track of every summary in the window.
|
|
61494
|
+
*/
|
|
61495
|
+
{
|
|
61496
|
+
name: "label",
|
|
61497
|
+
type: "TEXT"
|
|
61498
|
+
}),
|
|
61499
|
+
{
|
|
61500
|
+
name: "subLabel",
|
|
61501
|
+
type: "TEXT"
|
|
61502
|
+
}
|
|
61503
|
+
];
|
|
61504
|
+
var SUMMARY_MEMBERS_INDEXES = [
|
|
61505
|
+
{
|
|
61506
|
+
name: "summary_members_summary",
|
|
61507
|
+
columns: ["summaryId"]
|
|
61508
|
+
},
|
|
61509
|
+
{
|
|
61510
|
+
name: "summary_members_track",
|
|
61511
|
+
columns: ["trackId"]
|
|
61512
|
+
},
|
|
61513
|
+
(
|
|
61514
|
+
/** The filter's own index: "which groups hold an `animal`" walks members by
|
|
61515
|
+
* class, never summaries by hope. */
|
|
61516
|
+
{
|
|
61517
|
+
name: "summary_members_class",
|
|
61518
|
+
columns: ["className"]
|
|
61332
61519
|
})
|
|
61333
61520
|
];
|
|
61334
|
-
var SUMMARY_MEMBERS_INDEXES = [{
|
|
61335
|
-
name: "summary_members_summary",
|
|
61336
|
-
columns: ["summaryId"]
|
|
61337
|
-
}, {
|
|
61338
|
-
name: "summary_members_track",
|
|
61339
|
-
columns: ["trackId"]
|
|
61340
|
-
}];
|
|
61341
61521
|
/**
|
|
61342
61522
|
* A stored role, or `null`. An unrecognised value reads as `null` rather than
|
|
61343
61523
|
* being passed through: a consumer switching on this must not receive a string
|
|
@@ -61347,6 +61527,16 @@ var SUMMARY_MEMBERS_INDEXES = [{
|
|
|
61347
61527
|
function readRole(raw) {
|
|
61348
61528
|
return raw === "primary" || raw === "sibling" || raw === "child" ? raw : null;
|
|
61349
61529
|
}
|
|
61530
|
+
/** The two stamped labels, present only when they hold something. An empty
|
|
61531
|
+
* string is not a label and must never reach a filter as one. */
|
|
61532
|
+
function labelFields(data) {
|
|
61533
|
+
const label = data["label"];
|
|
61534
|
+
const subLabel = data["subLabel"];
|
|
61535
|
+
return {
|
|
61536
|
+
...typeof label === "string" && label.length > 0 ? { label } : {},
|
|
61537
|
+
...typeof subLabel === "string" && subLabel.length > 0 ? { subLabel } : {}
|
|
61538
|
+
};
|
|
61539
|
+
}
|
|
61350
61540
|
function readClassName(raw) {
|
|
61351
61541
|
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
61352
61542
|
}
|
|
@@ -61360,6 +61550,22 @@ function readClassName(raw) {
|
|
|
61360
61550
|
* case on a gate camera, not a race. With the track in the key a retry still
|
|
61361
61551
|
* collapses (same birth, same track) and two births stay two groups.
|
|
61362
61552
|
*/
|
|
61553
|
+
/** The widest a JS Date can name — the open end of a half-bounded window. */
|
|
61554
|
+
var TIME_MIN = -864e13;
|
|
61555
|
+
var TIME_MAX = 864e13;
|
|
61556
|
+
/**
|
|
61557
|
+
* How many rows one paged read may materialise before the merge.
|
|
61558
|
+
*
|
|
61559
|
+
* The page is cut AFTER ordering, so the read must be able to serve any cursor
|
|
61560
|
+
* position — but it must also be bounded, because an unbounded read of this
|
|
61561
|
+
* table on a busy hub is the shape D447 measured holding the settings thread
|
|
61562
|
+
* for seconds. Summaries are far fewer than tracks by construction (14 940
|
|
61563
|
+
* against 30 583 on this hub), so this ceiling is generous.
|
|
61564
|
+
*/
|
|
61565
|
+
var RECENT_SUMMARY_SCAN_LIMIT = 5e3;
|
|
61566
|
+
/** How many member rows one filter pass may read per tier. Bounded for the same
|
|
61567
|
+
* reason as the scan above, and generous: members are one row per subject. */
|
|
61568
|
+
var MEMBER_MATCH_SCAN_LIMIT = 2e4;
|
|
61363
61569
|
function summaryId(deviceId, openedAt, foundingTrackId) {
|
|
61364
61570
|
return `sum-${deviceId}-${openedAt}-${foundingTrackId}`;
|
|
61365
61571
|
}
|
|
@@ -61444,7 +61650,8 @@ var SummaryStore = class {
|
|
|
61444
61650
|
deviceId: num$1(r.data["deviceId"]),
|
|
61445
61651
|
joinedAt: num$1(r.data["timestamp"]),
|
|
61446
61652
|
role: readRole(r.data["role"]),
|
|
61447
|
-
className: readClassName(r.data["className"])
|
|
61653
|
+
className: readClassName(r.data["className"]),
|
|
61654
|
+
...labelFields(r.data)
|
|
61448
61655
|
}));
|
|
61449
61656
|
}
|
|
61450
61657
|
/**
|
|
@@ -61632,6 +61839,23 @@ var SummaryStore = class {
|
|
|
61632
61839
|
memberCount: leaving.length
|
|
61633
61840
|
};
|
|
61634
61841
|
}
|
|
61842
|
+
/** Stamp a member's final labels. Best-effort and loud: a missing stamp
|
|
61843
|
+
* costs a filter a match, never a membership. */
|
|
61844
|
+
async stampMemberLabels(summaryIdValue, trackId, labels) {
|
|
61845
|
+
try {
|
|
61846
|
+
await this.store.update.mutate({
|
|
61847
|
+
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
61848
|
+
id: memberRowId(summaryIdValue, trackId),
|
|
61849
|
+
data: { ...labels }
|
|
61850
|
+
});
|
|
61851
|
+
} catch (err) {
|
|
61852
|
+
this.logger.warn("summary: could not stamp a member label", { meta: {
|
|
61853
|
+
summaryId: summaryIdValue,
|
|
61854
|
+
trackId,
|
|
61855
|
+
error: String(err)
|
|
61856
|
+
} });
|
|
61857
|
+
}
|
|
61858
|
+
}
|
|
61635
61859
|
/**
|
|
61636
61860
|
* Put every OTHER member back to `sibling` after a new primary took the role.
|
|
61637
61861
|
*
|
|
@@ -61757,6 +61981,8 @@ var SummaryStore = class {
|
|
|
61757
61981
|
*/
|
|
61758
61982
|
async listRecent(input) {
|
|
61759
61983
|
if (input.deviceIds.length === 0) return [];
|
|
61984
|
+
const matching = await this.summaryIdsMatching(input.classes);
|
|
61985
|
+
if (matching !== null && matching.size === 0) return [];
|
|
61760
61986
|
const limit = Math.max(1, input.limit);
|
|
61761
61987
|
return (await this.store.query.query({
|
|
61762
61988
|
collection: SUMMARIES_COLLECTION,
|
|
@@ -61769,9 +61995,132 @@ var SummaryStore = class {
|
|
|
61769
61995
|
},
|
|
61770
61996
|
limit
|
|
61771
61997
|
}
|
|
61772
|
-
})).map((r) => toRow$1(r.id, r.data)).sort((a, b) => b.openedAt - a.openedAt).slice(0, limit);
|
|
61998
|
+
})).map((r) => toRow$1(r.id, r.data)).filter((row) => matching === null || matching.has(row.id)).sort((a, b) => b.openedAt - a.openedAt).slice(0, limit);
|
|
61773
61999
|
}
|
|
61774
62000
|
/** One summary with its members, or `null`. */
|
|
62001
|
+
/**
|
|
62002
|
+
* One PAGE of summaries across a camera set, newest activity first.
|
|
62003
|
+
*
|
|
62004
|
+
* The same contract as `listRecentTracks` and for the same consumers: the
|
|
62005
|
+
* reel on the cameras page and the Events gallery both ride a cursor, so a
|
|
62006
|
+
* feed that could only answer "the last N" would make groups available in the
|
|
62007
|
+
* timeline and nowhere else. `recent-cursor.ts` is reused verbatim — the
|
|
62008
|
+
* ordering is the stable total order (`lastActiveAt` DESC, id DESC), and a
|
|
62009
|
+
* second implementation of that is a second place for two feeds to disagree
|
|
62010
|
+
* about what "newest" means.
|
|
62011
|
+
*
|
|
62012
|
+
* The window is on LAST ACTIVITY, not on the open: a session that started
|
|
62013
|
+
* before the window and ran into it belongs to the window, because it
|
|
62014
|
+
* happened there.
|
|
62015
|
+
*
|
|
62016
|
+
* Members for the WHOLE page come back in one read. One query per row is how
|
|
62017
|
+
* a page of two hundred becomes two hundred round trips — the shape that made
|
|
62018
|
+
* the old digest read two thousand tracks a camera.
|
|
62019
|
+
*/
|
|
62020
|
+
async queryRecent(input) {
|
|
62021
|
+
const deviceIds = [...new Set(input.deviceIds)];
|
|
62022
|
+
if (deviceIds.length === 0) return {
|
|
62023
|
+
summaries: [],
|
|
62024
|
+
nextCursor: null
|
|
62025
|
+
};
|
|
62026
|
+
const cursor = input.cursor === void 0 ? null : decodeRecentCursor(input.cursor);
|
|
62027
|
+
const matching = await this.summaryIdsMatching(input.classes);
|
|
62028
|
+
if (matching !== null && matching.size === 0) return {
|
|
62029
|
+
summaries: [],
|
|
62030
|
+
nextCursor: null
|
|
62031
|
+
};
|
|
62032
|
+
const ordered = (await this.store.query.query({
|
|
62033
|
+
collection: SUMMARIES_COLLECTION,
|
|
62034
|
+
filter: {
|
|
62035
|
+
whereIn: { deviceId: deviceIds },
|
|
62036
|
+
...input.since !== void 0 || input.until !== void 0 ? { whereBetween: { lastActiveAt: [input.since ?? TIME_MIN, input.until ?? TIME_MAX] } } : {},
|
|
62037
|
+
limit: RECENT_SUMMARY_SCAN_LIMIT
|
|
62038
|
+
}
|
|
62039
|
+
})).map((r) => toRow$1(r.id, r.data)).filter((row) => matching === null || matching.has(row.id)).map((row) => ({
|
|
62040
|
+
row,
|
|
62041
|
+
key: {
|
|
62042
|
+
lastSeen: row.lastActiveAt,
|
|
62043
|
+
id: row.id
|
|
62044
|
+
}
|
|
62045
|
+
})).filter((entry) => cursor === null || isAfterCursor(entry.key, cursor)).toSorted((a, b) => compareRecentDesc(a.key, b.key));
|
|
62046
|
+
const page = ordered.slice(0, input.limit);
|
|
62047
|
+
const last = page.at(-1);
|
|
62048
|
+
const nextCursor = last !== void 0 && ordered.length > page.length ? encodeRecentCursor({
|
|
62049
|
+
lastSeen: last.row.lastActiveAt,
|
|
62050
|
+
trackId: last.row.id
|
|
62051
|
+
}) : null;
|
|
62052
|
+
const members = await this.membersOfMany(page.map((e) => e.row.id));
|
|
62053
|
+
return {
|
|
62054
|
+
summaries: page.map((e) => ({
|
|
62055
|
+
summary: e.row,
|
|
62056
|
+
members: members.get(e.row.id) ?? []
|
|
62057
|
+
})),
|
|
62058
|
+
nextCursor
|
|
62059
|
+
};
|
|
62060
|
+
}
|
|
62061
|
+
/**
|
|
62062
|
+
* The summaries whose MEMBERS match any of `classes`, or `null` for no filter.
|
|
62063
|
+
*
|
|
62064
|
+
* Narrows first, on the members table, and only then reads summary rows.
|
|
62065
|
+
* Filtering the other way round — a page of summaries, then their members —
|
|
62066
|
+
* returns a short or empty page while matches sit further down the list, and
|
|
62067
|
+
* the operator reads that as "there were none".
|
|
62068
|
+
*
|
|
62069
|
+
* Three queries because the filter compiler has no OR across fields, and one
|
|
62070
|
+
* term can be any of the three tiers: `animal` is a class, `dog` is a
|
|
62071
|
+
* sub-class, `Roberta` is an instance. The caller does not have to know
|
|
62072
|
+
* which — that is the whole point of the operator's example.
|
|
62073
|
+
*/
|
|
62074
|
+
async summaryIdsMatching(classes) {
|
|
62075
|
+
const wanted = (classes ?? []).filter((t) => t.length > 0);
|
|
62076
|
+
if (wanted.length === 0) return null;
|
|
62077
|
+
const ids = /* @__PURE__ */ new Set();
|
|
62078
|
+
for (const field of [
|
|
62079
|
+
"className",
|
|
62080
|
+
"label",
|
|
62081
|
+
"subLabel"
|
|
62082
|
+
]) {
|
|
62083
|
+
const rows = await this.store.query.query({
|
|
62084
|
+
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
62085
|
+
filter: {
|
|
62086
|
+
whereIn: { [field]: [...wanted] },
|
|
62087
|
+
limit: MEMBER_MATCH_SCAN_LIMIT
|
|
62088
|
+
}
|
|
62089
|
+
});
|
|
62090
|
+
for (const row of rows) ids.add(String(row.data["summaryId"] ?? ""));
|
|
62091
|
+
}
|
|
62092
|
+
ids.delete("");
|
|
62093
|
+
return ids;
|
|
62094
|
+
}
|
|
62095
|
+
/** Members of many summaries, in ONE read, grouped by summary id. */
|
|
62096
|
+
async membersOfMany(summaryIds) {
|
|
62097
|
+
const out = /* @__PURE__ */ new Map();
|
|
62098
|
+
if (summaryIds.length === 0) return out;
|
|
62099
|
+
const rows = await this.store.query.query({
|
|
62100
|
+
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
62101
|
+
filter: {
|
|
62102
|
+
whereIn: { summaryId: [...summaryIds] },
|
|
62103
|
+
limit: 1e4
|
|
62104
|
+
}
|
|
62105
|
+
});
|
|
62106
|
+
for (const r of rows) {
|
|
62107
|
+
const summaryIdValue = String(r.data["summaryId"] ?? "");
|
|
62108
|
+
const member = {
|
|
62109
|
+
summaryId: summaryIdValue,
|
|
62110
|
+
trackId: String(r.data["trackId"] ?? ""),
|
|
62111
|
+
deviceId: num$1(r.data["deviceId"]),
|
|
62112
|
+
joinedAt: num$1(r.data["timestamp"]),
|
|
62113
|
+
role: readRole(r.data["role"]),
|
|
62114
|
+
className: readClassName(r.data["className"]),
|
|
62115
|
+
...labelFields(r.data)
|
|
62116
|
+
};
|
|
62117
|
+
const bucket = out.get(summaryIdValue);
|
|
62118
|
+
if (bucket === void 0) out.set(summaryIdValue, [member]);
|
|
62119
|
+
else bucket.push(member);
|
|
62120
|
+
}
|
|
62121
|
+
for (const bucket of out.values()) bucket.sort((a, b) => a.joinedAt - b.joinedAt);
|
|
62122
|
+
return out;
|
|
62123
|
+
}
|
|
61775
62124
|
async get(id) {
|
|
61776
62125
|
const raw = await this.store.get.query({
|
|
61777
62126
|
collection: SUMMARIES_COLLECTION,
|
|
@@ -72062,6 +72411,33 @@ async function persistSummaryGroupShot(deps, input) {
|
|
|
72062
72411
|
}
|
|
72063
72412
|
}
|
|
72064
72413
|
//#endregion
|
|
72414
|
+
//#region src/pipeline-analytics/summary/summary-composition.ts
|
|
72415
|
+
function summaryComposition(members) {
|
|
72416
|
+
const counts = /* @__PURE__ */ new Map();
|
|
72417
|
+
for (const member of members) counts.set(member.className, (counts.get(member.className) ?? 0) + 1);
|
|
72418
|
+
return { classes: [...counts].map(([className, count]) => ({
|
|
72419
|
+
className,
|
|
72420
|
+
count
|
|
72421
|
+
})).toSorted((a, b) => a.count !== b.count ? b.count - a.count : (a.className ?? "").localeCompare(b.className ?? "")) };
|
|
72422
|
+
}
|
|
72423
|
+
//#endregion
|
|
72424
|
+
//#region src/pipeline-analytics/summary/summary-member-labels.ts
|
|
72425
|
+
function present(value) {
|
|
72426
|
+
return typeof value === "string" && value.length > 0;
|
|
72427
|
+
}
|
|
72428
|
+
function withMemberLabels(members, tracks) {
|
|
72429
|
+
const byId = new Map(tracks.map((t) => [t.trackId, t]));
|
|
72430
|
+
return members.map((member) => {
|
|
72431
|
+
const track = byId.get(member.trackId);
|
|
72432
|
+
return {
|
|
72433
|
+
...member,
|
|
72434
|
+
className: member.className ?? (present(track?.className) ? track.className : null),
|
|
72435
|
+
...present(track?.label) ? { label: track.label } : {},
|
|
72436
|
+
...present(track?.subLabel) ? { subLabel: track.subLabel } : {}
|
|
72437
|
+
};
|
|
72438
|
+
});
|
|
72439
|
+
}
|
|
72440
|
+
//#endregion
|
|
72065
72441
|
//#region src/pipeline-analytics/summary/summary-join.ts
|
|
72066
72442
|
/**
|
|
72067
72443
|
* Which open group a newborn track joins — or none, and it opens its own.
|
|
@@ -72407,7 +72783,21 @@ var SummarySealer = class {
|
|
|
72407
72783
|
return null;
|
|
72408
72784
|
}
|
|
72409
72785
|
}
|
|
72410
|
-
|
|
72786
|
+
/**
|
|
72787
|
+
* Is this group finished?
|
|
72788
|
+
*
|
|
72789
|
+
* Both questions are about THIS group's members, not about the camera. A
|
|
72790
|
+
* camera holds as many groups as it has (D489), so a camera-wide gate means
|
|
72791
|
+
* no group seals until every subject on the camera has gone — and on a busy
|
|
72792
|
+
* camera they never all go at once. Measured: eight summaries open on one
|
|
72793
|
+
* camera over seventeen minutes, none sealed, each new subject opening a
|
|
72794
|
+
* ninth because a group whose members have closed has no positions left for
|
|
72795
|
+
* the proximity rule to judge.
|
|
72796
|
+
*
|
|
72797
|
+
* `null` means "could not find out", and the caller treats that as NOT done —
|
|
72798
|
+
* a read that failed must never seal a live session (D49).
|
|
72799
|
+
*/
|
|
72800
|
+
async isGroupFinished(summary, members) {
|
|
72411
72801
|
const { id: summaryId, deviceId } = summary;
|
|
72412
72802
|
let openTracks;
|
|
72413
72803
|
try {
|
|
@@ -72420,12 +72810,13 @@ var SummarySealer = class {
|
|
|
72420
72810
|
error: errMsg(err)
|
|
72421
72811
|
}
|
|
72422
72812
|
});
|
|
72423
|
-
return;
|
|
72813
|
+
return null;
|
|
72424
72814
|
}
|
|
72425
|
-
|
|
72426
|
-
|
|
72815
|
+
const stillOpen = new Set(openTracks);
|
|
72816
|
+
if (members.some((m) => stillOpen.has(m))) return false;
|
|
72817
|
+
let parked;
|
|
72427
72818
|
try {
|
|
72428
|
-
|
|
72819
|
+
parked = await this.deps.parkedTrackIds(deviceId);
|
|
72429
72820
|
} catch (err) {
|
|
72430
72821
|
this.deps.logger.warn("summary sweep: could not read the stationary registry — seal aborted this sweep", {
|
|
72431
72822
|
tags: { deviceId },
|
|
@@ -72434,10 +72825,28 @@ var SummarySealer = class {
|
|
|
72434
72825
|
error: errMsg(err)
|
|
72435
72826
|
}
|
|
72436
72827
|
});
|
|
72828
|
+
return null;
|
|
72829
|
+
}
|
|
72830
|
+
return !members.some((m) => parked.has(m));
|
|
72831
|
+
}
|
|
72832
|
+
async sealIfDone(summary) {
|
|
72833
|
+
const { id: summaryId, deviceId } = summary;
|
|
72834
|
+
let members;
|
|
72835
|
+
try {
|
|
72836
|
+
members = (await this.deps.summaries.membersOf(summaryId)).map((m) => m.trackId);
|
|
72837
|
+
} catch (err) {
|
|
72838
|
+
this.deps.logger.warn("summary sweep: could not read members — seal aborted this sweep", {
|
|
72839
|
+
tags: { deviceId },
|
|
72840
|
+
meta: {
|
|
72841
|
+
summaryId,
|
|
72842
|
+
error: errMsg(err)
|
|
72843
|
+
}
|
|
72844
|
+
});
|
|
72437
72845
|
return;
|
|
72438
72846
|
}
|
|
72439
|
-
if (
|
|
72847
|
+
if (await this.isGroupFinished(summary, members) !== true) return;
|
|
72440
72848
|
const sealedAt = this.deps.now();
|
|
72849
|
+
await this.stampLabels(summary, members);
|
|
72441
72850
|
try {
|
|
72442
72851
|
if (await this.deps.summaries.seal(summary, sealedAt)) {
|
|
72443
72852
|
const members = this.forgetGroup(deviceId, summaryId);
|
|
@@ -72470,6 +72879,37 @@ var SummarySealer = class {
|
|
|
72470
72879
|
if (members === void 0) groups.set(summaryIdValue, new Set([trackId]));
|
|
72471
72880
|
else members.add(trackId);
|
|
72472
72881
|
}
|
|
72882
|
+
/**
|
|
72883
|
+
* Write each member's final label onto its row.
|
|
72884
|
+
*
|
|
72885
|
+
* At the seal every member's track has closed, so what the pipeline decided
|
|
72886
|
+
* about it is final — this is the first and last moment the stamp can be
|
|
72887
|
+
* both complete and correct.
|
|
72888
|
+
*/
|
|
72889
|
+
async stampLabels(summary, members) {
|
|
72890
|
+
const read = this.deps.labelsFor;
|
|
72891
|
+
if (read === void 0 || members.length === 0) return;
|
|
72892
|
+
try {
|
|
72893
|
+
const labels = await read(members);
|
|
72894
|
+
for (const [trackId, value] of labels) {
|
|
72895
|
+
const label = value.label;
|
|
72896
|
+
const subLabel = value.subLabel;
|
|
72897
|
+
if ((label === void 0 || label.length === 0) && (subLabel === void 0 || subLabel.length === 0)) continue;
|
|
72898
|
+
await this.deps.summaries.stampMemberLabels(summary.id, trackId, {
|
|
72899
|
+
...label !== void 0 && label.length > 0 ? { label } : {},
|
|
72900
|
+
...subLabel !== void 0 && subLabel.length > 0 ? { subLabel } : {}
|
|
72901
|
+
});
|
|
72902
|
+
}
|
|
72903
|
+
} catch (err) {
|
|
72904
|
+
this.deps.logger.warn("summary: could not stamp member labels — the seal stands", {
|
|
72905
|
+
tags: { deviceId: summary.deviceId },
|
|
72906
|
+
meta: {
|
|
72907
|
+
summaryId: summary.id,
|
|
72908
|
+
error: errMsg(err)
|
|
72909
|
+
}
|
|
72910
|
+
});
|
|
72911
|
+
}
|
|
72912
|
+
}
|
|
72473
72913
|
/** Follows a split in the mirror, so the frame path scores each half on its
|
|
72474
72914
|
* own members from the very next frame. */
|
|
72475
72915
|
moveMirrorMembers(deviceId, fromId, toId, trackIds) {
|
|
@@ -74042,6 +74482,21 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
74042
74482
|
await secretState.set(secret);
|
|
74043
74483
|
}
|
|
74044
74484
|
const shelfWindows = await this.pruneWindows();
|
|
74485
|
+
const index = new NcArtifactIndex({
|
|
74486
|
+
store: this.ctx.api.settingsStore,
|
|
74487
|
+
logger: this.ctx.logger.child("nc-artifacts"),
|
|
74488
|
+
addonId: this.ctx.id,
|
|
74489
|
+
retentionMs: shelfWindows.artifactShelfMs
|
|
74490
|
+
});
|
|
74491
|
+
this.ncArtifactIndex = index;
|
|
74492
|
+
const refusal = artifactShelfRefusal(await this.ctx.api.storage.listLocations.query({}), this.ctx.kernel.localNodeId ?? null);
|
|
74493
|
+
if (refusal !== null) {
|
|
74494
|
+
this.ctx.logger.info("nc-artifact shelf not served on this node", { meta: {
|
|
74495
|
+
reason: refusal,
|
|
74496
|
+
indexServed: true
|
|
74497
|
+
} });
|
|
74498
|
+
return;
|
|
74499
|
+
}
|
|
74045
74500
|
const store = new NcArtifactStore({
|
|
74046
74501
|
rootDir: await this.resolveNcArtifactRoot(),
|
|
74047
74502
|
logger: this.ctx.logger.child("nc-artifacts"),
|
|
@@ -74049,13 +74504,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
74049
74504
|
});
|
|
74050
74505
|
await store.start();
|
|
74051
74506
|
this.ncArtifactStore = store;
|
|
74052
|
-
const index = new NcArtifactIndex({
|
|
74053
|
-
store: this.ctx.api.settingsStore,
|
|
74054
|
-
logger: this.ctx.logger.child("nc-artifacts"),
|
|
74055
|
-
addonId: this.ctx.id,
|
|
74056
|
-
retentionMs: store.retentionMs
|
|
74057
|
-
});
|
|
74058
|
-
this.ncArtifactIndex = index;
|
|
74059
74507
|
const plane = new NcArtifactPlane({
|
|
74060
74508
|
store,
|
|
74061
74509
|
secret,
|
|
@@ -75415,7 +75863,25 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
75415
75863
|
summaries: this.summaryStore,
|
|
75416
75864
|
analysedDeviceIds: () => [...this.lastFrameDimsByDevice.keys()],
|
|
75417
75865
|
openTrackIds: async (deviceId) => this.residents.trackIdsForDevice(deviceId),
|
|
75418
|
-
|
|
75866
|
+
parkedTrackIds: async (deviceId) => {
|
|
75867
|
+
const ids = /* @__PURE__ */ new Set();
|
|
75868
|
+
for (const entry of this.stationaryRegistry?.list(deviceId) ?? []) if (entry.sourceTrackId !== void 0) ids.add(entry.sourceTrackId);
|
|
75869
|
+
return ids;
|
|
75870
|
+
},
|
|
75871
|
+
labelsFor: async (trackIds) => {
|
|
75872
|
+
const out = /* @__PURE__ */ new Map();
|
|
75873
|
+
const tracks = await this.trackStore?.getPersistedByTrackIds(trackIds) ?? [];
|
|
75874
|
+
for (const track of tracks) {
|
|
75875
|
+
const label = track.label;
|
|
75876
|
+
const subLabel = track.subLabel;
|
|
75877
|
+
if (label === void 0 && subLabel === void 0) continue;
|
|
75878
|
+
out.set(track.trackId, {
|
|
75879
|
+
...label !== void 0 ? { label } : {},
|
|
75880
|
+
...subLabel !== void 0 ? { subLabel } : {}
|
|
75881
|
+
});
|
|
75882
|
+
}
|
|
75883
|
+
return out;
|
|
75884
|
+
},
|
|
75419
75885
|
now: () => Date.now(),
|
|
75420
75886
|
logger: this.ctx.logger.child("summary-sealer"),
|
|
75421
75887
|
frameSize: (deviceId) => {
|
|
@@ -76120,6 +76586,30 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
76120
76586
|
confirmed: t.observations !== void 0 && t.observations >= minHits
|
|
76121
76587
|
}));
|
|
76122
76588
|
},
|
|
76589
|
+
listSummaryGroups: async ({ deviceId, sinceMs, untilMs }) => {
|
|
76590
|
+
const summaryStore = this.summaryStore;
|
|
76591
|
+
if (!summaryStore) return [];
|
|
76592
|
+
const rows = await summaryStore.listRecent({
|
|
76593
|
+
deviceIds: [deviceId],
|
|
76594
|
+
since: sinceMs,
|
|
76595
|
+
until: untilMs,
|
|
76596
|
+
limit: 500
|
|
76597
|
+
});
|
|
76598
|
+
const out = [];
|
|
76599
|
+
for (const row of rows) {
|
|
76600
|
+
const members = await summaryStore.membersOf(row.id);
|
|
76601
|
+
const files = await this.mediaStore?.listInfoByOwner("summary", row.id) ?? [];
|
|
76602
|
+
const shot = files.find((f) => f.kind === "keyFrameSmall") ?? files.find((f) => f.kind === "keyFrame");
|
|
76603
|
+
out.push({
|
|
76604
|
+
summaryId: row.id,
|
|
76605
|
+
deviceId: row.deviceId,
|
|
76606
|
+
memberTrackIds: members.map((m) => m.trackId),
|
|
76607
|
+
primaryTrackId: members.find((m) => m.role === "primary")?.trackId ?? null,
|
|
76608
|
+
groupShotMediaId: shot?.mediaId ?? null
|
|
76609
|
+
});
|
|
76610
|
+
}
|
|
76611
|
+
return out;
|
|
76612
|
+
},
|
|
76123
76613
|
listMediaInfo: async (id) => {
|
|
76124
76614
|
return (await stores.mediaStore.listInfoByOwner("track", id)).map((r) => ({
|
|
76125
76615
|
mediaId: r.mediaId,
|
|
@@ -81268,23 +81758,96 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
81268
81758
|
deviceIds: input.deviceIds,
|
|
81269
81759
|
...input.since !== void 0 ? { since: input.since } : {},
|
|
81270
81760
|
...input.until !== void 0 ? { until: input.until } : {},
|
|
81271
|
-
limit: input.limit ?? 50
|
|
81761
|
+
limit: input.limit ?? 50,
|
|
81762
|
+
...input.classes !== void 0 ? { classes: input.classes } : {}
|
|
81272
81763
|
});
|
|
81273
|
-
return { summaries: await
|
|
81764
|
+
return { summaries: await this.listItemsFor(rows) };
|
|
81274
81765
|
}
|
|
81275
|
-
|
|
81276
|
-
|
|
81277
|
-
|
|
81766
|
+
/**
|
|
81767
|
+
* Rows → what a LIST draws: the envelope plus the composition.
|
|
81768
|
+
*
|
|
81769
|
+
* Two bulk reads for the whole page, never per row: the members in one, their
|
|
81770
|
+
* tracks (for the names) in one. The MEMBERS themselves stay off the list —
|
|
81771
|
+
* a page of two hundred groups would otherwise carry a thousand records so
|
|
81772
|
+
* the client could count them. `getSummary` is where the links live.
|
|
81773
|
+
*/
|
|
81774
|
+
async listItemsFor(rows) {
|
|
81775
|
+
const store = this.summaryStore;
|
|
81776
|
+
if (!store || rows.length === 0) return [];
|
|
81777
|
+
const members = await store.membersOfMany(rows.map((r) => r.id));
|
|
81778
|
+
return await Promise.all(rows.map(async (row) => ({
|
|
81779
|
+
summary: await this.toSummary(row),
|
|
81780
|
+
...summaryComposition(members.get(row.id) ?? [])
|
|
81781
|
+
})));
|
|
81782
|
+
}
|
|
81783
|
+
/** The members of a page of summaries, with the labels a card can show —
|
|
81784
|
+
* in ONE read of the members and ONE of their tracks. */
|
|
81785
|
+
async labelledMembersFor(rows) {
|
|
81786
|
+
const out = /* @__PURE__ */ new Map();
|
|
81787
|
+
const store = this.summaryStore;
|
|
81788
|
+
if (!store || rows.length === 0) return out;
|
|
81789
|
+
const membersBySummary = await store.membersOfMany(rows.map((r) => r.id));
|
|
81790
|
+
const trackIds = [...new Set([...membersBySummary.values()].flatMap((ms) => ms.map((m) => m.trackId)))];
|
|
81791
|
+
let tracks = [];
|
|
81792
|
+
try {
|
|
81793
|
+
tracks = await this.trackStore?.getPersistedByTrackIds(trackIds) ?? [];
|
|
81794
|
+
} catch (err) {
|
|
81795
|
+
this.ctx.logger.warn("summary members: labels unavailable for this page", { meta: {
|
|
81796
|
+
summaries: rows.length,
|
|
81797
|
+
members: trackIds.length,
|
|
81798
|
+
error: require_dist.errMsg(err)
|
|
81799
|
+
} });
|
|
81800
|
+
}
|
|
81801
|
+
for (const row of rows) out.set(row.id, withMemberLabels(membersBySummary.get(row.id) ?? [], tracks));
|
|
81802
|
+
return out;
|
|
81803
|
+
}
|
|
81804
|
+
/**
|
|
81805
|
+
* Rows → the shape every summary surface answers with: the envelope, its
|
|
81806
|
+
* members, and what a thumbnail can render for each of them.
|
|
81807
|
+
*
|
|
81808
|
+
* THREE reads for the whole page, never per row: the members in one, their
|
|
81809
|
+
* tracks in one, and the group shots through `toSummary`. A read per member
|
|
81810
|
+
* is what made a sixty-group page take twenty seconds on the events feed.
|
|
81811
|
+
*/
|
|
81812
|
+
/** ONE summary, with every member and the link each of them carries. */
|
|
81813
|
+
async detailFor(row) {
|
|
81814
|
+
const labelled = (await this.labelledMembersFor([row])).get(row.id) ?? [];
|
|
81278
81815
|
return {
|
|
81279
|
-
summary: await this.toSummary(
|
|
81280
|
-
members:
|
|
81816
|
+
summary: await this.toSummary(row),
|
|
81817
|
+
members: labelled.map((m) => ({
|
|
81281
81818
|
trackId: m.trackId,
|
|
81282
81819
|
className: m.className,
|
|
81283
81820
|
role: m.role,
|
|
81284
|
-
joinedAt: m.joinedAt
|
|
81821
|
+
joinedAt: m.joinedAt,
|
|
81822
|
+
...m.label !== void 0 ? { label: m.label } : {},
|
|
81823
|
+
...m.subLabel !== void 0 ? { subLabel: m.subLabel } : {}
|
|
81285
81824
|
}))
|
|
81286
81825
|
};
|
|
81287
81826
|
}
|
|
81827
|
+
async listRecentSummaries(input) {
|
|
81828
|
+
const store = this.summaryStore;
|
|
81829
|
+
if (!store) return {
|
|
81830
|
+
summaries: [],
|
|
81831
|
+
nextCursor: null
|
|
81832
|
+
};
|
|
81833
|
+
const page = await store.queryRecent({
|
|
81834
|
+
deviceIds: input.deviceIds,
|
|
81835
|
+
...input.since !== void 0 ? { since: input.since } : {},
|
|
81836
|
+
...input.until !== void 0 ? { until: input.until } : {},
|
|
81837
|
+
limit: input.limit,
|
|
81838
|
+
...input.cursor !== void 0 ? { cursor: input.cursor } : {},
|
|
81839
|
+
...input.classes !== void 0 ? { classes: input.classes } : {}
|
|
81840
|
+
});
|
|
81841
|
+
return {
|
|
81842
|
+
summaries: await this.listItemsFor(page.summaries.map((d) => d.summary)),
|
|
81843
|
+
nextCursor: page.nextCursor
|
|
81844
|
+
};
|
|
81845
|
+
}
|
|
81846
|
+
async getSummary(input) {
|
|
81847
|
+
const detail = await this.summaryStore?.get(input.id);
|
|
81848
|
+
if (!detail) return null;
|
|
81849
|
+
return await this.detailFor(detail.summary);
|
|
81850
|
+
}
|
|
81288
81851
|
/** The row plus its group shot: the web companion when it landed, else the native key frame. */
|
|
81289
81852
|
async toSummary(row) {
|
|
81290
81853
|
const files = await this.mediaStore?.listInfoByOwner("summary", row.id) ?? [];
|