@camstack/addon-post-analysis 1.2.231 → 1.2.233
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-BvU7JaBW.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-Dg-LBlut.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +556 -41
- package/dist/pipeline-analytics/index.mjs +556 -41
- 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(),
|
|
@@ -43140,6 +43287,10 @@ function trackColumnsFor(projection, needsPositions = false) {
|
|
|
43140
43287
|
* whatever order the engine felt like and a cursor that cannot resume.
|
|
43141
43288
|
*/
|
|
43142
43289
|
var RECENT_IDS_COLUMNS = ["lastSeen"];
|
|
43290
|
+
/** How many track ids one filter pass may collect per tier. Bounded for the
|
|
43291
|
+
* same reason every bulk read here is: an unbounded scan of this table is
|
|
43292
|
+
* what D447 measured holding the settings thread for seconds. */
|
|
43293
|
+
var TRACK_MATCH_SCAN_LIMIT = 2e4;
|
|
43143
43294
|
/** Spreadable `columns` for a store query — omitted entirely for `full`. */
|
|
43144
43295
|
function projectionArg(projection, needsPositions = false) {
|
|
43145
43296
|
const columns = trackColumnsFor(projection, needsPositions);
|
|
@@ -44227,6 +44378,42 @@ var TrackStore = class {
|
|
|
44227
44378
|
* current subject out from under it. The orphan audit's grace window would
|
|
44228
44379
|
* hide this for an hour and then stop hiding it.
|
|
44229
44380
|
*/
|
|
44381
|
+
/**
|
|
44382
|
+
* Track ids whose CLASS, sub-class or instance matches any of `terms`.
|
|
44383
|
+
*
|
|
44384
|
+
* The summaries' filter asks its question here rather than on a copy: a
|
|
44385
|
+
* label is a fact about the TRACK, it is decided while the track lives and
|
|
44386
|
+
* can still be re-decided afterwards (class voting, a late face match, an
|
|
44387
|
+
* operator enrolling a face and renaming that person). A copy stamped onto
|
|
44388
|
+
* the summary would answer with whatever was true at one instant and never
|
|
44389
|
+
* learn — which is the whole reason this lookup exists instead.
|
|
44390
|
+
*
|
|
44391
|
+
* Three queries because the filter compiler has no OR across fields, and one
|
|
44392
|
+
* term can be any of the three tiers: `animal` is a class, `dog` is a
|
|
44393
|
+
* sub-class, `Roberta` is an instance. Bounded, like every other bulk read
|
|
44394
|
+
* here.
|
|
44395
|
+
*/
|
|
44396
|
+
async trackIdsMatching(terms) {
|
|
44397
|
+
const wanted = terms.filter((t) => t.length > 0);
|
|
44398
|
+
if (wanted.length === 0) return [];
|
|
44399
|
+
const ids = /* @__PURE__ */ new Set();
|
|
44400
|
+
for (const field of [
|
|
44401
|
+
"className",
|
|
44402
|
+
"label",
|
|
44403
|
+
"subLabel"
|
|
44404
|
+
]) {
|
|
44405
|
+
const rows = await this.store.query.query({
|
|
44406
|
+
collection: TRACKS_COLLECTION,
|
|
44407
|
+
filter: {
|
|
44408
|
+
whereIn: { [field]: [...wanted] },
|
|
44409
|
+
limit: TRACK_MATCH_SCAN_LIMIT
|
|
44410
|
+
},
|
|
44411
|
+
columns: ["id"]
|
|
44412
|
+
});
|
|
44413
|
+
for (const row of rows) ids.add(row.id);
|
|
44414
|
+
}
|
|
44415
|
+
return [...ids];
|
|
44416
|
+
}
|
|
44230
44417
|
async existingTrackIds(trackIds) {
|
|
44231
44418
|
const PAGE = 500;
|
|
44232
44419
|
const found = /* @__PURE__ */ new Set();
|
|
@@ -61331,13 +61518,23 @@ var SUMMARY_MEMBERS_COLUMNS = [
|
|
|
61331
61518
|
type: "TEXT"
|
|
61332
61519
|
})
|
|
61333
61520
|
];
|
|
61334
|
-
var SUMMARY_MEMBERS_INDEXES = [
|
|
61335
|
-
|
|
61336
|
-
|
|
61337
|
-
|
|
61338
|
-
|
|
61339
|
-
|
|
61340
|
-
|
|
61521
|
+
var SUMMARY_MEMBERS_INDEXES = [
|
|
61522
|
+
{
|
|
61523
|
+
name: "summary_members_summary",
|
|
61524
|
+
columns: ["summaryId"]
|
|
61525
|
+
},
|
|
61526
|
+
{
|
|
61527
|
+
name: "summary_members_track",
|
|
61528
|
+
columns: ["trackId"]
|
|
61529
|
+
},
|
|
61530
|
+
(
|
|
61531
|
+
/** The filter's own index: "which groups hold an `animal`" walks members by
|
|
61532
|
+
* class, never summaries by hope. */
|
|
61533
|
+
{
|
|
61534
|
+
name: "summary_members_class",
|
|
61535
|
+
columns: ["className"]
|
|
61536
|
+
})
|
|
61537
|
+
];
|
|
61341
61538
|
/**
|
|
61342
61539
|
* A stored role, or `null`. An unrecognised value reads as `null` rather than
|
|
61343
61540
|
* being passed through: a consumer switching on this must not receive a string
|
|
@@ -61360,6 +61557,22 @@ function readClassName(raw) {
|
|
|
61360
61557
|
* case on a gate camera, not a race. With the track in the key a retry still
|
|
61361
61558
|
* collapses (same birth, same track) and two births stay two groups.
|
|
61362
61559
|
*/
|
|
61560
|
+
/** The widest a JS Date can name — the open end of a half-bounded window. */
|
|
61561
|
+
var TIME_MIN = -864e13;
|
|
61562
|
+
var TIME_MAX = 864e13;
|
|
61563
|
+
/**
|
|
61564
|
+
* How many rows one paged read may materialise before the merge.
|
|
61565
|
+
*
|
|
61566
|
+
* The page is cut AFTER ordering, so the read must be able to serve any cursor
|
|
61567
|
+
* position — but it must also be bounded, because an unbounded read of this
|
|
61568
|
+
* table on a busy hub is the shape D447 measured holding the settings thread
|
|
61569
|
+
* for seconds. Summaries are far fewer than tracks by construction (14 940
|
|
61570
|
+
* against 30 583 on this hub), so this ceiling is generous.
|
|
61571
|
+
*/
|
|
61572
|
+
var RECENT_SUMMARY_SCAN_LIMIT = 5e3;
|
|
61573
|
+
/** How many member rows one filter pass may read per tier. Bounded for the same
|
|
61574
|
+
* reason as the scan above, and generous: members are one row per subject. */
|
|
61575
|
+
var MEMBER_MATCH_SCAN_LIMIT = 2e4;
|
|
61363
61576
|
function summaryId(deviceId, openedAt, foundingTrackId) {
|
|
61364
61577
|
return `sum-${deviceId}-${openedAt}-${foundingTrackId}`;
|
|
61365
61578
|
}
|
|
@@ -61386,10 +61599,12 @@ var SummaryStore = class {
|
|
|
61386
61599
|
store;
|
|
61387
61600
|
logger;
|
|
61388
61601
|
onEvict;
|
|
61602
|
+
trackIdsMatching;
|
|
61389
61603
|
constructor(deps) {
|
|
61390
61604
|
this.store = deps.store;
|
|
61391
61605
|
this.logger = deps.logger;
|
|
61392
61606
|
this.onEvict = deps.onEvict;
|
|
61607
|
+
this.trackIdsMatching = deps.trackIdsMatching;
|
|
61393
61608
|
}
|
|
61394
61609
|
static async declare(store) {
|
|
61395
61610
|
await Promise.all([store.declareCollection.mutate({
|
|
@@ -61757,6 +61972,8 @@ var SummaryStore = class {
|
|
|
61757
61972
|
*/
|
|
61758
61973
|
async listRecent(input) {
|
|
61759
61974
|
if (input.deviceIds.length === 0) return [];
|
|
61975
|
+
const matching = await this.summaryIdsMatching(input.classes);
|
|
61976
|
+
if (matching !== null && matching.size === 0) return [];
|
|
61760
61977
|
const limit = Math.max(1, input.limit);
|
|
61761
61978
|
return (await this.store.query.query({
|
|
61762
61979
|
collection: SUMMARIES_COLLECTION,
|
|
@@ -61769,9 +61986,138 @@ var SummaryStore = class {
|
|
|
61769
61986
|
},
|
|
61770
61987
|
limit
|
|
61771
61988
|
}
|
|
61772
|
-
})).map((r) => toRow$1(r.id, r.data)).sort((a, b) => b.openedAt - a.openedAt).slice(0, limit);
|
|
61989
|
+
})).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
61990
|
}
|
|
61774
61991
|
/** One summary with its members, or `null`. */
|
|
61992
|
+
/**
|
|
61993
|
+
* One PAGE of summaries across a camera set, newest activity first.
|
|
61994
|
+
*
|
|
61995
|
+
* The same contract as `listRecentTracks` and for the same consumers: the
|
|
61996
|
+
* reel on the cameras page and the Events gallery both ride a cursor, so a
|
|
61997
|
+
* feed that could only answer "the last N" would make groups available in the
|
|
61998
|
+
* timeline and nowhere else. `recent-cursor.ts` is reused verbatim — the
|
|
61999
|
+
* ordering is the stable total order (`lastActiveAt` DESC, id DESC), and a
|
|
62000
|
+
* second implementation of that is a second place for two feeds to disagree
|
|
62001
|
+
* about what "newest" means.
|
|
62002
|
+
*
|
|
62003
|
+
* The window is on LAST ACTIVITY, not on the open: a session that started
|
|
62004
|
+
* before the window and ran into it belongs to the window, because it
|
|
62005
|
+
* happened there.
|
|
62006
|
+
*
|
|
62007
|
+
* Members for the WHOLE page come back in one read. One query per row is how
|
|
62008
|
+
* a page of two hundred becomes two hundred round trips — the shape that made
|
|
62009
|
+
* the old digest read two thousand tracks a camera.
|
|
62010
|
+
*/
|
|
62011
|
+
async queryRecent(input) {
|
|
62012
|
+
const deviceIds = [...new Set(input.deviceIds)];
|
|
62013
|
+
if (deviceIds.length === 0) return {
|
|
62014
|
+
summaries: [],
|
|
62015
|
+
nextCursor: null
|
|
62016
|
+
};
|
|
62017
|
+
const cursor = input.cursor === void 0 ? null : decodeRecentCursor(input.cursor);
|
|
62018
|
+
const matching = await this.summaryIdsMatching(input.classes);
|
|
62019
|
+
if (matching !== null && matching.size === 0) return {
|
|
62020
|
+
summaries: [],
|
|
62021
|
+
nextCursor: null
|
|
62022
|
+
};
|
|
62023
|
+
const ordered = (await this.store.query.query({
|
|
62024
|
+
collection: SUMMARIES_COLLECTION,
|
|
62025
|
+
filter: {
|
|
62026
|
+
whereIn: { deviceId: deviceIds },
|
|
62027
|
+
...input.since !== void 0 || input.until !== void 0 ? { whereBetween: { lastActiveAt: [input.since ?? TIME_MIN, input.until ?? TIME_MAX] } } : {},
|
|
62028
|
+
limit: RECENT_SUMMARY_SCAN_LIMIT
|
|
62029
|
+
}
|
|
62030
|
+
})).map((r) => toRow$1(r.id, r.data)).filter((row) => matching === null || matching.has(row.id)).map((row) => ({
|
|
62031
|
+
row,
|
|
62032
|
+
key: {
|
|
62033
|
+
lastSeen: row.lastActiveAt,
|
|
62034
|
+
id: row.id
|
|
62035
|
+
}
|
|
62036
|
+
})).filter((entry) => cursor === null || isAfterCursor(entry.key, cursor)).toSorted((a, b) => compareRecentDesc(a.key, b.key));
|
|
62037
|
+
const page = ordered.slice(0, input.limit);
|
|
62038
|
+
const last = page.at(-1);
|
|
62039
|
+
const nextCursor = last !== void 0 && ordered.length > page.length ? encodeRecentCursor({
|
|
62040
|
+
lastSeen: last.row.lastActiveAt,
|
|
62041
|
+
trackId: last.row.id
|
|
62042
|
+
}) : null;
|
|
62043
|
+
const members = await this.membersOfMany(page.map((e) => e.row.id));
|
|
62044
|
+
return {
|
|
62045
|
+
summaries: page.map((e) => ({
|
|
62046
|
+
summary: e.row,
|
|
62047
|
+
members: members.get(e.row.id) ?? []
|
|
62048
|
+
})),
|
|
62049
|
+
nextCursor
|
|
62050
|
+
};
|
|
62051
|
+
}
|
|
62052
|
+
/**
|
|
62053
|
+
* The summaries whose MEMBERS match any of `classes`, or `null` for no filter.
|
|
62054
|
+
*
|
|
62055
|
+
* TRACKS first, then the members that name them, then the summaries. The
|
|
62056
|
+
* label is a fact about the track and it is asked of the track — a summary
|
|
62057
|
+
* that stored its own copy would answer with whatever was true when it
|
|
62058
|
+
* sealed, and would never learn that a face was enrolled and a person named
|
|
62059
|
+
* an hour later. The operator's rule: *a summary keeps the minimum
|
|
62060
|
+
* information necessary*, and a duplicated label is not the minimum, it is a
|
|
62061
|
+
* second truth with an expiry date.
|
|
62062
|
+
*
|
|
62063
|
+
* Narrows before the page is cut. Filtering the other way round — a page of
|
|
62064
|
+
* summaries, then their members — returns a short or empty page while matches
|
|
62065
|
+
* sit further down the list, and the operator reads that as "there were none".
|
|
62066
|
+
*/
|
|
62067
|
+
async summaryIdsMatching(classes) {
|
|
62068
|
+
const wanted = (classes ?? []).filter((t) => t.length > 0);
|
|
62069
|
+
if (wanted.length === 0) return null;
|
|
62070
|
+
const matchTracks = this.trackIdsMatching;
|
|
62071
|
+
if (matchTracks === void 0) {
|
|
62072
|
+
this.logger.warn("summary filter: no track matcher wired — refusing to answer unfiltered", { meta: { classes: wanted.length } });
|
|
62073
|
+
return /* @__PURE__ */ new Set();
|
|
62074
|
+
}
|
|
62075
|
+
const trackIds = await matchTracks(wanted);
|
|
62076
|
+
if (trackIds.length === 0) return /* @__PURE__ */ new Set();
|
|
62077
|
+
const ids = /* @__PURE__ */ new Set();
|
|
62078
|
+
const PAGE = 500;
|
|
62079
|
+
for (let i = 0; i < trackIds.length; i += PAGE) {
|
|
62080
|
+
const chunk = trackIds.slice(i, i + PAGE);
|
|
62081
|
+
const rows = await this.store.query.query({
|
|
62082
|
+
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
62083
|
+
filter: {
|
|
62084
|
+
whereIn: { trackId: [...chunk] },
|
|
62085
|
+
limit: MEMBER_MATCH_SCAN_LIMIT
|
|
62086
|
+
}
|
|
62087
|
+
});
|
|
62088
|
+
for (const row of rows) ids.add(String(row.data["summaryId"] ?? ""));
|
|
62089
|
+
}
|
|
62090
|
+
ids.delete("");
|
|
62091
|
+
return ids;
|
|
62092
|
+
}
|
|
62093
|
+
/** Members of many summaries, in ONE read, grouped by summary id. */
|
|
62094
|
+
async membersOfMany(summaryIds) {
|
|
62095
|
+
const out = /* @__PURE__ */ new Map();
|
|
62096
|
+
if (summaryIds.length === 0) return out;
|
|
62097
|
+
const rows = await this.store.query.query({
|
|
62098
|
+
collection: SUMMARY_MEMBERS_COLLECTION$1,
|
|
62099
|
+
filter: {
|
|
62100
|
+
whereIn: { summaryId: [...summaryIds] },
|
|
62101
|
+
limit: 1e4
|
|
62102
|
+
}
|
|
62103
|
+
});
|
|
62104
|
+
for (const r of rows) {
|
|
62105
|
+
const summaryIdValue = String(r.data["summaryId"] ?? "");
|
|
62106
|
+
const member = {
|
|
62107
|
+
summaryId: summaryIdValue,
|
|
62108
|
+
trackId: String(r.data["trackId"] ?? ""),
|
|
62109
|
+
deviceId: num$1(r.data["deviceId"]),
|
|
62110
|
+
joinedAt: num$1(r.data["timestamp"]),
|
|
62111
|
+
role: readRole(r.data["role"]),
|
|
62112
|
+
className: readClassName(r.data["className"])
|
|
62113
|
+
};
|
|
62114
|
+
const bucket = out.get(summaryIdValue);
|
|
62115
|
+
if (bucket === void 0) out.set(summaryIdValue, [member]);
|
|
62116
|
+
else bucket.push(member);
|
|
62117
|
+
}
|
|
62118
|
+
for (const bucket of out.values()) bucket.sort((a, b) => a.joinedAt - b.joinedAt);
|
|
62119
|
+
return out;
|
|
62120
|
+
}
|
|
61775
62121
|
async get(id) {
|
|
61776
62122
|
const raw = await this.store.get.query({
|
|
61777
62123
|
collection: SUMMARIES_COLLECTION,
|
|
@@ -72062,6 +72408,33 @@ async function persistSummaryGroupShot(deps, input) {
|
|
|
72062
72408
|
}
|
|
72063
72409
|
}
|
|
72064
72410
|
//#endregion
|
|
72411
|
+
//#region src/pipeline-analytics/summary/summary-composition.ts
|
|
72412
|
+
function summaryComposition(members) {
|
|
72413
|
+
const counts = /* @__PURE__ */ new Map();
|
|
72414
|
+
for (const member of members) counts.set(member.className, (counts.get(member.className) ?? 0) + 1);
|
|
72415
|
+
return { classes: [...counts].map(([className, count]) => ({
|
|
72416
|
+
className,
|
|
72417
|
+
count
|
|
72418
|
+
})).toSorted((a, b) => a.count !== b.count ? b.count - a.count : (a.className ?? "").localeCompare(b.className ?? "")) };
|
|
72419
|
+
}
|
|
72420
|
+
//#endregion
|
|
72421
|
+
//#region src/pipeline-analytics/summary/summary-member-labels.ts
|
|
72422
|
+
function present(value) {
|
|
72423
|
+
return typeof value === "string" && value.length > 0;
|
|
72424
|
+
}
|
|
72425
|
+
function withMemberLabels(members, tracks) {
|
|
72426
|
+
const byId = new Map(tracks.map((t) => [t.trackId, t]));
|
|
72427
|
+
return members.map((member) => {
|
|
72428
|
+
const track = byId.get(member.trackId);
|
|
72429
|
+
return {
|
|
72430
|
+
...member,
|
|
72431
|
+
className: member.className ?? (present(track?.className) ? track.className : null),
|
|
72432
|
+
...present(track?.label) ? { label: track.label } : {},
|
|
72433
|
+
...present(track?.subLabel) ? { subLabel: track.subLabel } : {}
|
|
72434
|
+
};
|
|
72435
|
+
});
|
|
72436
|
+
}
|
|
72437
|
+
//#endregion
|
|
72065
72438
|
//#region src/pipeline-analytics/summary/summary-join.ts
|
|
72066
72439
|
/**
|
|
72067
72440
|
* Which open group a newborn track joins — or none, and it opens its own.
|
|
@@ -72407,7 +72780,21 @@ var SummarySealer = class {
|
|
|
72407
72780
|
return null;
|
|
72408
72781
|
}
|
|
72409
72782
|
}
|
|
72410
|
-
|
|
72783
|
+
/**
|
|
72784
|
+
* Is this group finished?
|
|
72785
|
+
*
|
|
72786
|
+
* Both questions are about THIS group's members, not about the camera. A
|
|
72787
|
+
* camera holds as many groups as it has (D489), so a camera-wide gate means
|
|
72788
|
+
* no group seals until every subject on the camera has gone — and on a busy
|
|
72789
|
+
* camera they never all go at once. Measured: eight summaries open on one
|
|
72790
|
+
* camera over seventeen minutes, none sealed, each new subject opening a
|
|
72791
|
+
* ninth because a group whose members have closed has no positions left for
|
|
72792
|
+
* the proximity rule to judge.
|
|
72793
|
+
*
|
|
72794
|
+
* `null` means "could not find out", and the caller treats that as NOT done —
|
|
72795
|
+
* a read that failed must never seal a live session (D49).
|
|
72796
|
+
*/
|
|
72797
|
+
async isGroupFinished(summary, members) {
|
|
72411
72798
|
const { id: summaryId, deviceId } = summary;
|
|
72412
72799
|
let openTracks;
|
|
72413
72800
|
try {
|
|
@@ -72420,12 +72807,13 @@ var SummarySealer = class {
|
|
|
72420
72807
|
error: errMsg(err)
|
|
72421
72808
|
}
|
|
72422
72809
|
});
|
|
72423
|
-
return;
|
|
72810
|
+
return null;
|
|
72424
72811
|
}
|
|
72425
|
-
|
|
72426
|
-
|
|
72812
|
+
const stillOpen = new Set(openTracks);
|
|
72813
|
+
if (members.some((m) => stillOpen.has(m))) return false;
|
|
72814
|
+
let parked;
|
|
72427
72815
|
try {
|
|
72428
|
-
|
|
72816
|
+
parked = await this.deps.parkedTrackIds(deviceId);
|
|
72429
72817
|
} catch (err) {
|
|
72430
72818
|
this.deps.logger.warn("summary sweep: could not read the stationary registry — seal aborted this sweep", {
|
|
72431
72819
|
tags: { deviceId },
|
|
@@ -72434,9 +72822,26 @@ var SummarySealer = class {
|
|
|
72434
72822
|
error: errMsg(err)
|
|
72435
72823
|
}
|
|
72436
72824
|
});
|
|
72825
|
+
return null;
|
|
72826
|
+
}
|
|
72827
|
+
return !members.some((m) => parked.has(m));
|
|
72828
|
+
}
|
|
72829
|
+
async sealIfDone(summary) {
|
|
72830
|
+
const { id: summaryId, deviceId } = summary;
|
|
72831
|
+
let members;
|
|
72832
|
+
try {
|
|
72833
|
+
members = (await this.deps.summaries.membersOf(summaryId)).map((m) => m.trackId);
|
|
72834
|
+
} catch (err) {
|
|
72835
|
+
this.deps.logger.warn("summary sweep: could not read members — seal aborted this sweep", {
|
|
72836
|
+
tags: { deviceId },
|
|
72837
|
+
meta: {
|
|
72838
|
+
summaryId,
|
|
72839
|
+
error: errMsg(err)
|
|
72840
|
+
}
|
|
72841
|
+
});
|
|
72437
72842
|
return;
|
|
72438
72843
|
}
|
|
72439
|
-
if (
|
|
72844
|
+
if (await this.isGroupFinished(summary, members) !== true) return;
|
|
72440
72845
|
const sealedAt = this.deps.now();
|
|
72441
72846
|
try {
|
|
72442
72847
|
if (await this.deps.summaries.seal(summary, sealedAt)) {
|
|
@@ -74042,6 +74447,21 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
74042
74447
|
await secretState.set(secret);
|
|
74043
74448
|
}
|
|
74044
74449
|
const shelfWindows = await this.pruneWindows();
|
|
74450
|
+
const index = new NcArtifactIndex({
|
|
74451
|
+
store: this.ctx.api.settingsStore,
|
|
74452
|
+
logger: this.ctx.logger.child("nc-artifacts"),
|
|
74453
|
+
addonId: this.ctx.id,
|
|
74454
|
+
retentionMs: shelfWindows.artifactShelfMs
|
|
74455
|
+
});
|
|
74456
|
+
this.ncArtifactIndex = index;
|
|
74457
|
+
const refusal = artifactShelfRefusal(await this.ctx.api.storage.listLocations.query({}), this.ctx.kernel.localNodeId ?? null);
|
|
74458
|
+
if (refusal !== null) {
|
|
74459
|
+
this.ctx.logger.info("nc-artifact shelf not served on this node", { meta: {
|
|
74460
|
+
reason: refusal,
|
|
74461
|
+
indexServed: true
|
|
74462
|
+
} });
|
|
74463
|
+
return;
|
|
74464
|
+
}
|
|
74045
74465
|
const store = new NcArtifactStore({
|
|
74046
74466
|
rootDir: await this.resolveNcArtifactRoot(),
|
|
74047
74467
|
logger: this.ctx.logger.child("nc-artifacts"),
|
|
@@ -74049,13 +74469,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
74049
74469
|
});
|
|
74050
74470
|
await store.start();
|
|
74051
74471
|
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
74472
|
const plane = new NcArtifactPlane({
|
|
74060
74473
|
store,
|
|
74061
74474
|
secret,
|
|
@@ -75405,6 +75818,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
75405
75818
|
logger: logger.child("SensorEventStore")
|
|
75406
75819
|
});
|
|
75407
75820
|
this.summaryStore = new SummaryStore({
|
|
75821
|
+
trackIdsMatching: async (terms) => await this.trackStore?.trackIdsMatching(terms) ?? [],
|
|
75408
75822
|
store: api.settingsStore,
|
|
75409
75823
|
logger: this.ctx.logger.child("summary"),
|
|
75410
75824
|
onEvict: async (ids) => {
|
|
@@ -75415,7 +75829,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
75415
75829
|
summaries: this.summaryStore,
|
|
75416
75830
|
analysedDeviceIds: () => [...this.lastFrameDimsByDevice.keys()],
|
|
75417
75831
|
openTrackIds: async (deviceId) => this.residents.trackIdsForDevice(deviceId),
|
|
75418
|
-
|
|
75832
|
+
parkedTrackIds: async (deviceId) => {
|
|
75833
|
+
const ids = /* @__PURE__ */ new Set();
|
|
75834
|
+
for (const entry of this.stationaryRegistry?.list(deviceId) ?? []) if (entry.sourceTrackId !== void 0) ids.add(entry.sourceTrackId);
|
|
75835
|
+
return ids;
|
|
75836
|
+
},
|
|
75419
75837
|
now: () => Date.now(),
|
|
75420
75838
|
logger: this.ctx.logger.child("summary-sealer"),
|
|
75421
75839
|
frameSize: (deviceId) => {
|
|
@@ -76120,6 +76538,30 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
76120
76538
|
confirmed: t.observations !== void 0 && t.observations >= minHits
|
|
76121
76539
|
}));
|
|
76122
76540
|
},
|
|
76541
|
+
listSummaryGroups: async ({ deviceId, sinceMs, untilMs }) => {
|
|
76542
|
+
const summaryStore = this.summaryStore;
|
|
76543
|
+
if (!summaryStore) return [];
|
|
76544
|
+
const rows = await summaryStore.listRecent({
|
|
76545
|
+
deviceIds: [deviceId],
|
|
76546
|
+
since: sinceMs,
|
|
76547
|
+
until: untilMs,
|
|
76548
|
+
limit: 500
|
|
76549
|
+
});
|
|
76550
|
+
const out = [];
|
|
76551
|
+
for (const row of rows) {
|
|
76552
|
+
const members = await summaryStore.membersOf(row.id);
|
|
76553
|
+
const files = await this.mediaStore?.listInfoByOwner("summary", row.id) ?? [];
|
|
76554
|
+
const shot = files.find((f) => f.kind === "keyFrameSmall") ?? files.find((f) => f.kind === "keyFrame");
|
|
76555
|
+
out.push({
|
|
76556
|
+
summaryId: row.id,
|
|
76557
|
+
deviceId: row.deviceId,
|
|
76558
|
+
memberTrackIds: members.map((m) => m.trackId),
|
|
76559
|
+
primaryTrackId: members.find((m) => m.role === "primary")?.trackId ?? null,
|
|
76560
|
+
groupShotMediaId: shot?.mediaId ?? null
|
|
76561
|
+
});
|
|
76562
|
+
}
|
|
76563
|
+
return out;
|
|
76564
|
+
},
|
|
76123
76565
|
listMediaInfo: async (id) => {
|
|
76124
76566
|
return (await stores.mediaStore.listInfoByOwner("track", id)).map((r) => ({
|
|
76125
76567
|
mediaId: r.mediaId,
|
|
@@ -81268,23 +81710,96 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
81268
81710
|
deviceIds: input.deviceIds,
|
|
81269
81711
|
...input.since !== void 0 ? { since: input.since } : {},
|
|
81270
81712
|
...input.until !== void 0 ? { until: input.until } : {},
|
|
81271
|
-
limit: input.limit ?? 50
|
|
81713
|
+
limit: input.limit ?? 50,
|
|
81714
|
+
...input.classes !== void 0 ? { classes: input.classes } : {}
|
|
81272
81715
|
});
|
|
81273
|
-
return { summaries: await
|
|
81716
|
+
return { summaries: await this.listItemsFor(rows) };
|
|
81274
81717
|
}
|
|
81275
|
-
|
|
81276
|
-
|
|
81277
|
-
|
|
81718
|
+
/**
|
|
81719
|
+
* Rows → what a LIST draws: the envelope plus the composition.
|
|
81720
|
+
*
|
|
81721
|
+
* Two bulk reads for the whole page, never per row: the members in one, their
|
|
81722
|
+
* tracks (for the names) in one. The MEMBERS themselves stay off the list —
|
|
81723
|
+
* a page of two hundred groups would otherwise carry a thousand records so
|
|
81724
|
+
* the client could count them. `getSummary` is where the links live.
|
|
81725
|
+
*/
|
|
81726
|
+
async listItemsFor(rows) {
|
|
81727
|
+
const store = this.summaryStore;
|
|
81728
|
+
if (!store || rows.length === 0) return [];
|
|
81729
|
+
const members = await store.membersOfMany(rows.map((r) => r.id));
|
|
81730
|
+
return await Promise.all(rows.map(async (row) => ({
|
|
81731
|
+
summary: await this.toSummary(row),
|
|
81732
|
+
...summaryComposition(members.get(row.id) ?? [])
|
|
81733
|
+
})));
|
|
81734
|
+
}
|
|
81735
|
+
/** The members of a page of summaries, with the labels a card can show —
|
|
81736
|
+
* in ONE read of the members and ONE of their tracks. */
|
|
81737
|
+
async labelledMembersFor(rows) {
|
|
81738
|
+
const out = /* @__PURE__ */ new Map();
|
|
81739
|
+
const store = this.summaryStore;
|
|
81740
|
+
if (!store || rows.length === 0) return out;
|
|
81741
|
+
const membersBySummary = await store.membersOfMany(rows.map((r) => r.id));
|
|
81742
|
+
const trackIds = [...new Set([...membersBySummary.values()].flatMap((ms) => ms.map((m) => m.trackId)))];
|
|
81743
|
+
let tracks = [];
|
|
81744
|
+
try {
|
|
81745
|
+
tracks = await this.trackStore?.getPersistedByTrackIds(trackIds) ?? [];
|
|
81746
|
+
} catch (err) {
|
|
81747
|
+
this.ctx.logger.warn("summary members: labels unavailable for this page", { meta: {
|
|
81748
|
+
summaries: rows.length,
|
|
81749
|
+
members: trackIds.length,
|
|
81750
|
+
error: require_dist.errMsg(err)
|
|
81751
|
+
} });
|
|
81752
|
+
}
|
|
81753
|
+
for (const row of rows) out.set(row.id, withMemberLabels(membersBySummary.get(row.id) ?? [], tracks));
|
|
81754
|
+
return out;
|
|
81755
|
+
}
|
|
81756
|
+
/**
|
|
81757
|
+
* Rows → the shape every summary surface answers with: the envelope, its
|
|
81758
|
+
* members, and what a thumbnail can render for each of them.
|
|
81759
|
+
*
|
|
81760
|
+
* THREE reads for the whole page, never per row: the members in one, their
|
|
81761
|
+
* tracks in one, and the group shots through `toSummary`. A read per member
|
|
81762
|
+
* is what made a sixty-group page take twenty seconds on the events feed.
|
|
81763
|
+
*/
|
|
81764
|
+
/** ONE summary, with every member and the link each of them carries. */
|
|
81765
|
+
async detailFor(row) {
|
|
81766
|
+
const labelled = (await this.labelledMembersFor([row])).get(row.id) ?? [];
|
|
81278
81767
|
return {
|
|
81279
|
-
summary: await this.toSummary(
|
|
81280
|
-
members:
|
|
81768
|
+
summary: await this.toSummary(row),
|
|
81769
|
+
members: labelled.map((m) => ({
|
|
81281
81770
|
trackId: m.trackId,
|
|
81282
81771
|
className: m.className,
|
|
81283
81772
|
role: m.role,
|
|
81284
|
-
joinedAt: m.joinedAt
|
|
81773
|
+
joinedAt: m.joinedAt,
|
|
81774
|
+
...m.label !== void 0 ? { label: m.label } : {},
|
|
81775
|
+
...m.subLabel !== void 0 ? { subLabel: m.subLabel } : {}
|
|
81285
81776
|
}))
|
|
81286
81777
|
};
|
|
81287
81778
|
}
|
|
81779
|
+
async listRecentSummaries(input) {
|
|
81780
|
+
const store = this.summaryStore;
|
|
81781
|
+
if (!store) return {
|
|
81782
|
+
summaries: [],
|
|
81783
|
+
nextCursor: null
|
|
81784
|
+
};
|
|
81785
|
+
const page = await store.queryRecent({
|
|
81786
|
+
deviceIds: input.deviceIds,
|
|
81787
|
+
...input.since !== void 0 ? { since: input.since } : {},
|
|
81788
|
+
...input.until !== void 0 ? { until: input.until } : {},
|
|
81789
|
+
limit: input.limit,
|
|
81790
|
+
...input.cursor !== void 0 ? { cursor: input.cursor } : {},
|
|
81791
|
+
...input.classes !== void 0 ? { classes: input.classes } : {}
|
|
81792
|
+
});
|
|
81793
|
+
return {
|
|
81794
|
+
summaries: await this.listItemsFor(page.summaries.map((d) => d.summary)),
|
|
81795
|
+
nextCursor: page.nextCursor
|
|
81796
|
+
};
|
|
81797
|
+
}
|
|
81798
|
+
async getSummary(input) {
|
|
81799
|
+
const detail = await this.summaryStore?.get(input.id);
|
|
81800
|
+
if (!detail) return null;
|
|
81801
|
+
return await this.detailFor(detail.summary);
|
|
81802
|
+
}
|
|
81288
81803
|
/** The row plus its group shot: the web companion when it landed, else the native key frame. */
|
|
81289
81804
|
async toSummary(row) {
|
|
81290
81805
|
const files = await this.mediaStore?.listInfoByOwner("summary", row.id) ?? [];
|