@camstack/addon-post-analysis 1.2.150 → 1.2.151
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-C8cA7PX_.mjs → dist-B5K63Zmt.mjs} +65 -7
- package/dist/{dist-BTviHGUC.js → dist-DpQno5NK.js} +65 -7
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DZGvAMuh.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-bK-ELdFD.mjs} +3 -3
- package/dist/pipeline-analytics/{hostInit-DgkTmoHq.mjs → hostInit-CgUMPlDe.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +279 -39
- package/dist/pipeline-analytics/index.mjs +279 -39
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -8242,6 +8242,21 @@ var RelocateJobSchema = object({
|
|
|
8242
8242
|
bytesMoved: number().int(),
|
|
8243
8243
|
/** Total files discovered up front; null while (or when) unknown. */
|
|
8244
8244
|
filesTotal: number().int().nullable(),
|
|
8245
|
+
/**
|
|
8246
|
+
* Rows this run CORRECTED while moving them — a durable mutation the move
|
|
8247
|
+
* made that nobody asked for, so it is reported where the operator reads the
|
|
8248
|
+
* job rather than only in a log line.
|
|
8249
|
+
*
|
|
8250
|
+
* A footage segment records its byte count in its own NAME, and the durable
|
|
8251
|
+
* hour row derives its aggregates from those names. A file that does not
|
|
8252
|
+
* match its name therefore makes the ledger's sums — and with them quota and
|
|
8253
|
+
* pressure eviction — wrong by the difference, and only a rename can fix it.
|
|
8254
|
+
* On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
|
|
8255
|
+
*
|
|
8256
|
+
* Absent on lanes where the question has no meaning: a media blob's size is
|
|
8257
|
+
* in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
|
|
8258
|
+
*/
|
|
8259
|
+
rowsReconciled: number().int().nonnegative().optional(),
|
|
8245
8260
|
startedAt: number(),
|
|
8246
8261
|
finishedAt: number().nullable(),
|
|
8247
8262
|
error: string().nullable()
|
|
@@ -8310,14 +8325,42 @@ var RelocateMediaInputSchema = object({
|
|
|
8310
8325
|
/** Omitted = `move`, the pre-existing behaviour. */
|
|
8311
8326
|
mode: MediaRelocateModeSchema.optional()
|
|
8312
8327
|
});
|
|
8313
|
-
/**
|
|
8314
|
-
*
|
|
8315
|
-
*
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8328
|
+
/**
|
|
8329
|
+
* The unstamped population of ONE collection — split, because the gate and the
|
|
8330
|
+
* operator ask two different questions and only one of them has to be cheap.
|
|
8331
|
+
*
|
|
8332
|
+
* `present` is the GATE: "is there at least one row that would be orphaned by a
|
|
8333
|
+
* repoint". It is a single indexed seek to the first matching row, so it stays
|
|
8334
|
+
* answerable on a saturated disk and answers in O(log n) precisely in the state
|
|
8335
|
+
* that matters — after a seal, when the population is empty.
|
|
8336
|
+
*
|
|
8337
|
+
* `rows` is the NUMBER, for the refusal message and the operator's sense of
|
|
8338
|
+
* scale. It is a second, indexed `COUNT(*)`, and `null` means **not
|
|
8339
|
+
* measurable** — never zero. `{ present: true, rows: null }` is a legitimate
|
|
8340
|
+
* and useful answer: "there are some, and this read could not say how many"
|
|
8341
|
+
* still refuses the cutover, which is the whole job.
|
|
8342
|
+
*/
|
|
8343
|
+
var UnstampedRowsSchema = object({
|
|
8344
|
+
present: boolean(),
|
|
8345
|
+
rows: number().int().nonnegative().nullable()
|
|
8320
8346
|
});
|
|
8347
|
+
/**
|
|
8348
|
+
* How many rows still carry NO `locationId` — the population a repoint would
|
|
8349
|
+
* silently re-aim at a disk that does not hold their bytes.
|
|
8350
|
+
*
|
|
8351
|
+
* **`null` = the count could not be taken**, and it is NOT permission to cut
|
|
8352
|
+
* over. The gate opens on a measured absence and on nothing else; an unread
|
|
8353
|
+
* collection and an empty one are different facts, and this repo has already
|
|
8354
|
+
* paid for conflating them (`RelocateResidueSchema`, D295).
|
|
8355
|
+
*/
|
|
8356
|
+
var UnstampedEventMediaCountSchema = object({
|
|
8357
|
+
media: UnstampedRowsSchema,
|
|
8358
|
+
retrainFrames: UnstampedRowsSchema,
|
|
8359
|
+
/** True when EITHER collection holds one. The refusal reads this. */
|
|
8360
|
+
anyPresent: boolean(),
|
|
8361
|
+
/** Sum across both, or `null` when either lane could not be counted. */
|
|
8362
|
+
total: number().int().nonnegative().nullable()
|
|
8363
|
+
}).nullable();
|
|
8321
8364
|
var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
|
|
8322
8365
|
/** The independently selectable logical storage classes — every class
|
|
8323
8366
|
* `storage.listLocationDeclarations` reports, so an operator never meets a
|
|
@@ -8428,6 +8471,10 @@ var StorageMigrationMoveProgressSchema = object({
|
|
|
8428
8471
|
/** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
|
|
8429
8472
|
filesTotal: number().int().nonnegative().nullable(),
|
|
8430
8473
|
bytesMoved: number().int().nonnegative(),
|
|
8474
|
+
/** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
|
|
8475
|
+
* a lane that cannot reconcile. A migration that silently rewrote durable
|
|
8476
|
+
* rows would be the same failure as one that silently skipped them. */
|
|
8477
|
+
rowsReconciled: number().int().nonnegative().optional(),
|
|
8431
8478
|
/** The MOVER's start, not the migration's: a drain restarted after an addon
|
|
8432
8479
|
* crash gets a new mover, and a rate computed from the migration's start
|
|
8433
8480
|
* would silently average in the time nothing was running. */
|
|
@@ -20455,6 +20502,17 @@ var pipelineAnalyticsCapability = {
|
|
|
20455
20502
|
* happens to stamp it. This count is what the migration planner's
|
|
20456
20503
|
* non-blocking gate reads; `relocateMedia({ mode: 'seal' })` is what drives
|
|
20457
20504
|
* it to zero.
|
|
20505
|
+
*
|
|
20506
|
+
* TWO indexed statements per collection, not a walk. It used to page the
|
|
20507
|
+
* whole collection at 200 rows per RPC ordered by an unindexed column, so
|
|
20508
|
+
* on the live hub — 1 254 576 rows — it hit the 60 s RPC deadline every
|
|
20509
|
+
* time it was called, and the migration it gates could never start. The
|
|
20510
|
+
* cheap question (`present`: is there at least one) is asked first and
|
|
20511
|
+
* separately from the expensive one (`rows`), because only the first has
|
|
20512
|
+
* to be answerable for the gate to do its job.
|
|
20513
|
+
*
|
|
20514
|
+
* **`null` is "not measurable", never zero** — at either level. An
|
|
20515
|
+
* unreadable collection must not read as a sealed one.
|
|
20458
20516
|
*/
|
|
20459
20517
|
countUnstampedEventMedia: method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }),
|
|
20460
20518
|
/**
|
|
@@ -8273,6 +8273,21 @@ var RelocateJobSchema = object({
|
|
|
8273
8273
|
bytesMoved: number().int(),
|
|
8274
8274
|
/** Total files discovered up front; null while (or when) unknown. */
|
|
8275
8275
|
filesTotal: number().int().nullable(),
|
|
8276
|
+
/**
|
|
8277
|
+
* Rows this run CORRECTED while moving them — a durable mutation the move
|
|
8278
|
+
* made that nobody asked for, so it is reported where the operator reads the
|
|
8279
|
+
* job rather than only in a log line.
|
|
8280
|
+
*
|
|
8281
|
+
* A footage segment records its byte count in its own NAME, and the durable
|
|
8282
|
+
* hour row derives its aggregates from those names. A file that does not
|
|
8283
|
+
* match its name therefore makes the ledger's sums — and with them quota and
|
|
8284
|
+
* pressure eviction — wrong by the difference, and only a rename can fix it.
|
|
8285
|
+
* On 2026-08-30 one such row also stalled a 110 749-file drain permanently.
|
|
8286
|
+
*
|
|
8287
|
+
* Absent on lanes where the question has no meaning: a media blob's size is
|
|
8288
|
+
* in its row, not in its name, so `MediaRelocateEngine` never reconciles one.
|
|
8289
|
+
*/
|
|
8290
|
+
rowsReconciled: number().int().nonnegative().optional(),
|
|
8276
8291
|
startedAt: number(),
|
|
8277
8292
|
finishedAt: number().nullable(),
|
|
8278
8293
|
error: string().nullable()
|
|
@@ -8341,14 +8356,42 @@ var RelocateMediaInputSchema = object({
|
|
|
8341
8356
|
/** Omitted = `move`, the pre-existing behaviour. */
|
|
8342
8357
|
mode: MediaRelocateModeSchema.optional()
|
|
8343
8358
|
});
|
|
8344
|
-
/**
|
|
8345
|
-
*
|
|
8346
|
-
*
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8359
|
+
/**
|
|
8360
|
+
* The unstamped population of ONE collection — split, because the gate and the
|
|
8361
|
+
* operator ask two different questions and only one of them has to be cheap.
|
|
8362
|
+
*
|
|
8363
|
+
* `present` is the GATE: "is there at least one row that would be orphaned by a
|
|
8364
|
+
* repoint". It is a single indexed seek to the first matching row, so it stays
|
|
8365
|
+
* answerable on a saturated disk and answers in O(log n) precisely in the state
|
|
8366
|
+
* that matters — after a seal, when the population is empty.
|
|
8367
|
+
*
|
|
8368
|
+
* `rows` is the NUMBER, for the refusal message and the operator's sense of
|
|
8369
|
+
* scale. It is a second, indexed `COUNT(*)`, and `null` means **not
|
|
8370
|
+
* measurable** — never zero. `{ present: true, rows: null }` is a legitimate
|
|
8371
|
+
* and useful answer: "there are some, and this read could not say how many"
|
|
8372
|
+
* still refuses the cutover, which is the whole job.
|
|
8373
|
+
*/
|
|
8374
|
+
var UnstampedRowsSchema = object({
|
|
8375
|
+
present: boolean(),
|
|
8376
|
+
rows: number().int().nonnegative().nullable()
|
|
8351
8377
|
});
|
|
8378
|
+
/**
|
|
8379
|
+
* How many rows still carry NO `locationId` — the population a repoint would
|
|
8380
|
+
* silently re-aim at a disk that does not hold their bytes.
|
|
8381
|
+
*
|
|
8382
|
+
* **`null` = the count could not be taken**, and it is NOT permission to cut
|
|
8383
|
+
* over. The gate opens on a measured absence and on nothing else; an unread
|
|
8384
|
+
* collection and an empty one are different facts, and this repo has already
|
|
8385
|
+
* paid for conflating them (`RelocateResidueSchema`, D295).
|
|
8386
|
+
*/
|
|
8387
|
+
var UnstampedEventMediaCountSchema = object({
|
|
8388
|
+
media: UnstampedRowsSchema,
|
|
8389
|
+
retrainFrames: UnstampedRowsSchema,
|
|
8390
|
+
/** True when EITHER collection holds one. The refusal reads this. */
|
|
8391
|
+
anyPresent: boolean(),
|
|
8392
|
+
/** Sum across both, or `null` when either lane could not be counted. */
|
|
8393
|
+
total: number().int().nonnegative().nullable()
|
|
8394
|
+
}).nullable();
|
|
8352
8395
|
var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
|
|
8353
8396
|
/** The independently selectable logical storage classes — every class
|
|
8354
8397
|
* `storage.listLocationDeclarations` reports, so an operator never meets a
|
|
@@ -8459,6 +8502,10 @@ var StorageMigrationMoveProgressSchema = object({
|
|
|
8459
8502
|
/** The archive census — the **M** of "N of M" (D295). `null` = unknowable. */
|
|
8460
8503
|
filesTotal: number().int().nonnegative().nullable(),
|
|
8461
8504
|
bytesMoved: number().int().nonnegative(),
|
|
8505
|
+
/** Rows the mover corrected while moving them — see `RelocateJob`. Absent on
|
|
8506
|
+
* a lane that cannot reconcile. A migration that silently rewrote durable
|
|
8507
|
+
* rows would be the same failure as one that silently skipped them. */
|
|
8508
|
+
rowsReconciled: number().int().nonnegative().optional(),
|
|
8462
8509
|
/** The MOVER's start, not the migration's: a drain restarted after an addon
|
|
8463
8510
|
* crash gets a new mover, and a rate computed from the migration's start
|
|
8464
8511
|
* would silently average in the time nothing was running. */
|
|
@@ -20486,6 +20533,17 @@ var pipelineAnalyticsCapability = {
|
|
|
20486
20533
|
* happens to stamp it. This count is what the migration planner's
|
|
20487
20534
|
* non-blocking gate reads; `relocateMedia({ mode: 'seal' })` is what drives
|
|
20488
20535
|
* it to zero.
|
|
20536
|
+
*
|
|
20537
|
+
* TWO indexed statements per collection, not a walk. It used to page the
|
|
20538
|
+
* whole collection at 200 rows per RPC ordered by an unindexed column, so
|
|
20539
|
+
* on the live hub — 1 254 576 rows — it hit the 60 s RPC deadline every
|
|
20540
|
+
* time it was called, and the migration it gates could never start. The
|
|
20541
|
+
* cheap question (`present`: is there at least one) is asked first and
|
|
20542
|
+
* separately from the expensive one (`rows`), because only the first has
|
|
20543
|
+
* to be answerable for the gate to do its job.
|
|
20544
|
+
*
|
|
20545
|
+
* **`null` is "not measurable", never zero** — at either level. An
|
|
20546
|
+
* unreadable collection must not read as a sealed one.
|
|
20489
20547
|
*/
|
|
20490
20548
|
countUnstampedEventMedia: method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }),
|
|
20491
20549
|
/**
|
|
@@ -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-DpQno5NK.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_dist.__toESM(node_fs);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { At as BaseAddon, F as PoolMemoryWatchdog, St as resolvePoolMemoryPolicy, at as embeddingEncoderCapability, gt as parseProcStatus, ut as hfModelUrl } from "../dist-
|
|
1
|
+
import { At as BaseAddon, F as PoolMemoryWatchdog, St as resolvePoolMemoryPolicy, at as embeddingEncoderCapability, gt as parseProcStatus, ut as hfModelUrl } from "../dist-B5K63Zmt.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
|
|
|
3
3
|
var e = {
|
|
4
4
|
"@camstack/sdk": {
|
|
5
5
|
name: "@camstack/sdk",
|
|
6
|
-
version: "1.2.
|
|
6
|
+
version: "1.2.49",
|
|
7
7
|
scope: ["default"],
|
|
8
8
|
loaded: !1,
|
|
9
9
|
from: "addon_pipeline_analytics_widgets",
|
|
@@ -18,7 +18,7 @@ var e = {
|
|
|
18
18
|
},
|
|
19
19
|
"@camstack/types": {
|
|
20
20
|
name: "@camstack/types",
|
|
21
|
-
version: "1.2.
|
|
21
|
+
version: "1.2.127",
|
|
22
22
|
scope: ["default"],
|
|
23
23
|
loaded: !1,
|
|
24
24
|
from: "addon_pipeline_analytics_widgets",
|
|
@@ -33,7 +33,7 @@ var e = {
|
|
|
33
33
|
},
|
|
34
34
|
"@camstack/ui-library": {
|
|
35
35
|
name: "@camstack/ui-library",
|
|
36
|
-
version: "1.2.
|
|
36
|
+
version: "1.2.90",
|
|
37
37
|
scope: ["default"],
|
|
38
38
|
loaded: !1,
|
|
39
39
|
from: "addon_pipeline_analytics_widgets",
|
|
@@ -36,7 +36,7 @@ async function r() {
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"@camstack/types": {
|
|
39
|
-
version: "1.2.
|
|
39
|
+
version: "1.2.127",
|
|
40
40
|
scope: "default",
|
|
41
41
|
shareConfig: {
|
|
42
42
|
singleton: !0,
|
|
@@ -45,7 +45,7 @@ async function r() {
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"@camstack/sdk": {
|
|
48
|
-
version: "1.2.
|
|
48
|
+
version: "1.2.49",
|
|
49
49
|
scope: "default",
|
|
50
50
|
shareConfig: {
|
|
51
51
|
singleton: !0,
|
|
@@ -81,7 +81,7 @@ async function r() {
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"@camstack/ui-library": {
|
|
84
|
-
version: "1.2.
|
|
84
|
+
version: "1.2.90",
|
|
85
85
|
scope: "default",
|
|
86
86
|
shareConfig: {
|
|
87
87
|
singleton: !0,
|
|
@@ -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-DpQno5NK.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");
|
|
@@ -6270,12 +6270,86 @@ var NcDeviceStateCache = class {
|
|
|
6270
6270
|
}
|
|
6271
6271
|
};
|
|
6272
6272
|
//#endregion
|
|
6273
|
+
//#region src/notification-center/system-event-tap-url.ts
|
|
6274
|
+
/** The admin UI's own routes, as `packages/addon-admin-ui/src/App.tsx` declares them. */
|
|
6275
|
+
var ADMIN_ADDONS_PATH = "/system/addons";
|
|
6276
|
+
var ADMIN_CLUSTER_PATH = "/system/cluster";
|
|
6277
|
+
var ADMIN_DEVICES_PATH = "/devices";
|
|
6278
|
+
/**
|
|
6279
|
+
* The query parameter naming a node.
|
|
6280
|
+
*
|
|
6281
|
+
* One spelling for both admin pages, mirrored by `NODE_PARAM` in
|
|
6282
|
+
* `packages/addon-admin-ui/src/lib/node-url-selection.ts` and pinned across
|
|
6283
|
+
* the two packages by `scripts/check-notification-tap-targets.ts`.
|
|
6284
|
+
*/
|
|
6285
|
+
var NODE_PARAM = "node";
|
|
6286
|
+
/** The device page's top-level tab parameter, read by `DeviceDetail`. */
|
|
6287
|
+
var TAB_PARAM = "tab";
|
|
6288
|
+
/** The Stream Broker tab id — a `WELL_KNOWN_TABS` member, hoisted `top-tab`. */
|
|
6289
|
+
var STREAM_BROKER_TAB = "stream-broker";
|
|
6290
|
+
/** `DeviceType.Camera`. Spelled here rather than imported so this stays a pure
|
|
6291
|
+
* string map with no device-model dependency; the guard pins it. */
|
|
6292
|
+
var CAMERA_DEVICE_TYPE = "camera";
|
|
6293
|
+
/** Kinds whose action is "go and apply an ADDON update". */
|
|
6294
|
+
var ADDON_UPDATE_KINDS = new Set(["addon-update-available"]);
|
|
6295
|
+
/** Kinds whose action is "go and apply a NODE's own (`@camstack/server`) update". */
|
|
6296
|
+
var NODE_UPDATE_KINDS = new Set(["server-update-available"]);
|
|
6297
|
+
/**
|
|
6298
|
+
* Kinds whose action is "go and look at why this camera stopped producing".
|
|
6299
|
+
*
|
|
6300
|
+
* The legacy `camera-offline` is here for the same reason it is still in the
|
|
6301
|
+
* enum: outbox and history rows enqueued before the rename carry it, and an
|
|
6302
|
+
* old row is exactly the one whose action has to keep working.
|
|
6303
|
+
*/
|
|
6304
|
+
var DEVICE_DOWN_KINDS = new Set([
|
|
6305
|
+
"device-offline",
|
|
6306
|
+
"stream-offline",
|
|
6307
|
+
"camera-offline"
|
|
6308
|
+
]);
|
|
6309
|
+
/**
|
|
6310
|
+
* Kinds whose subject is a camera BY CONSTRUCTION, whatever the row says.
|
|
6311
|
+
*
|
|
6312
|
+
* A stream belongs to a camera and nothing else. The legacy `camera-offline`
|
|
6313
|
+
* is one too — that name is precisely the claim it made, and it is why the
|
|
6314
|
+
* kind was split into `device-offline` + a `deviceTypes` condition.
|
|
6315
|
+
*/
|
|
6316
|
+
var CAMERA_BY_CONSTRUCTION_KINDS = new Set(["stream-offline", "camera-offline"]);
|
|
6317
|
+
function withParam(path, key, value) {
|
|
6318
|
+
if (value === void 0 || value.length === 0) return path;
|
|
6319
|
+
return `${path}?${key}=${encodeURIComponent(value)}`;
|
|
6320
|
+
}
|
|
6321
|
+
/**
|
|
6322
|
+
* The admin path (with its query) a system event taps through to, or `null`
|
|
6323
|
+
* when the kind has no action.
|
|
6324
|
+
*
|
|
6325
|
+
* Exported for the cross-package guard and for tests that want the path
|
|
6326
|
+
* without a base; the dispatcher goes through {@link systemEventTapUrl}.
|
|
6327
|
+
*/
|
|
6328
|
+
function systemEventTapPath(subject) {
|
|
6329
|
+
if (ADDON_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_ADDONS_PATH, NODE_PARAM, subject.nodeId);
|
|
6330
|
+
if (NODE_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_CLUSTER_PATH, NODE_PARAM, subject.nodeId);
|
|
6331
|
+
if (DEVICE_DOWN_KINDS.has(subject.kind)) {
|
|
6332
|
+
const deviceId = subject.deviceId;
|
|
6333
|
+
if (deviceId === void 0 || !Number.isInteger(deviceId) || deviceId <= 0) return null;
|
|
6334
|
+
const devicePath = `${ADMIN_DEVICES_PATH}/${String(deviceId)}`;
|
|
6335
|
+
return CAMERA_BY_CONSTRUCTION_KINDS.has(subject.kind) || subject.deviceType === CAMERA_DEVICE_TYPE ? withParam(devicePath, TAB_PARAM, STREAM_BROKER_TAB) : devicePath;
|
|
6336
|
+
}
|
|
6337
|
+
return null;
|
|
6338
|
+
}
|
|
6339
|
+
/** Full URL, or `null` when the kind has no action or no base is reachable. */
|
|
6340
|
+
function systemEventTapUrl(subject, baseUrl) {
|
|
6341
|
+
if (baseUrl === null || baseUrl.length === 0) return null;
|
|
6342
|
+
const path = systemEventTapPath(subject);
|
|
6343
|
+
if (path === null) return null;
|
|
6344
|
+
return `${baseUrl.replace(/\/+$/, "")}${path}`;
|
|
6345
|
+
}
|
|
6346
|
+
//#endregion
|
|
6273
6347
|
//#region src/notification-center/default-tap-url.ts
|
|
6274
6348
|
/** The viewer's hosted base path under the hub. */
|
|
6275
6349
|
var VIEWER_BASE_PATH = "/viewer/camstack";
|
|
6276
6350
|
function defaultTapUrl(input) {
|
|
6277
|
-
if (input.systemEvent === true) return null;
|
|
6278
6351
|
if (input.baseUrl === null || input.baseUrl.length === 0) return null;
|
|
6352
|
+
if (input.systemEvent !== void 0) return systemEventTapUrl(input.systemEvent, input.baseUrl);
|
|
6279
6353
|
if (!Number.isInteger(input.deviceId) || input.deviceId <= 0) return null;
|
|
6280
6354
|
return `${input.baseUrl.replace(/\/+$/, "")}${VIEWER_BASE_PATH}/camera/${String(input.deviceId)}?at=${String(input.timestampMs)}`;
|
|
6281
6355
|
}
|
|
@@ -8868,13 +8942,14 @@ function trailAt(trail, timestamp) {
|
|
|
8868
8942
|
* cannot be asked (no trail, no polygon) or when nobody qualifies.
|
|
8869
8943
|
*/
|
|
8870
8944
|
function selectTriggerZoneCandidates(input) {
|
|
8871
|
-
const { candidates, trail, polygons } = input;
|
|
8945
|
+
const { candidates, trail, polygons, strict } = input;
|
|
8872
8946
|
if (candidates.length === 0 || trail.length === 0 || polygons.length === 0) return candidates;
|
|
8873
8947
|
const qualifying = candidates.filter((candidate) => {
|
|
8874
8948
|
const point = trailAt(trail, candidate.timestamp);
|
|
8875
8949
|
return point !== null && zoneOverlap(point, polygons) > 0;
|
|
8876
8950
|
});
|
|
8877
|
-
|
|
8951
|
+
if (qualifying.length > 0) return qualifying;
|
|
8952
|
+
return strict === true ? [] : candidates;
|
|
8878
8953
|
}
|
|
8879
8954
|
//#endregion
|
|
8880
8955
|
//#region src/notification-center/dispatcher.ts
|
|
@@ -8912,6 +8987,24 @@ var NC_STILL_WAIT_MS = 3e3;
|
|
|
8912
8987
|
* is the instant; the ladder then runs among the frames that are about it.
|
|
8913
8988
|
*/
|
|
8914
8989
|
var NC_TRIGGER_STILL_MAX_AGE_MS = TRAIL_MATCH_MAX_MS;
|
|
8990
|
+
/**
|
|
8991
|
+
* Whether this row's subject can still be photographed AGAIN after its trigger.
|
|
8992
|
+
*
|
|
8993
|
+
* The one thing that separates a row worth waiting for from a row whose only
|
|
8994
|
+
* frame already exists. A live visual track keeps writing media for as long as
|
|
8995
|
+
* it is open — its first frame, its periodic snapshots, its last frame all land
|
|
8996
|
+
* after the instant a zone condition fired. Every other record is photographed
|
|
8997
|
+
* once: an audio window, a device event and a package event are served by the
|
|
8998
|
+
* still shelf, which D178 makes take ONE picture for a whole burst, and a
|
|
8999
|
+
* `track-end` row is by definition past the last of them.
|
|
9000
|
+
*
|
|
9001
|
+
* Waiting on the wrong side of this line has cost both ways in production — 3 s
|
|
9002
|
+
* of latency on a scream when nothing more was coming, and an empty doorstep on
|
|
9003
|
+
* 'Persona su Uscio' when something was.
|
|
9004
|
+
*/
|
|
9005
|
+
function canStillGainMedia(entry) {
|
|
9006
|
+
return entry.recordKind === "object-event";
|
|
9007
|
+
}
|
|
8915
9008
|
var NcDispatcher = class {
|
|
8916
9009
|
deps;
|
|
8917
9010
|
targetCache = null;
|
|
@@ -9391,7 +9484,12 @@ var NcDispatcher = class {
|
|
|
9391
9484
|
deviceId: systemEvent !== void 0 ? 0 : subject.deviceId,
|
|
9392
9485
|
timestampMs: subject.timestamp,
|
|
9393
9486
|
baseUrl: await this.deps.viewerUrlBase().catch(() => null),
|
|
9394
|
-
systemEvent
|
|
9487
|
+
...systemEvent !== void 0 ? { systemEvent: {
|
|
9488
|
+
kind: systemEvent.kind,
|
|
9489
|
+
...systemEvent.deviceId !== void 0 ? { deviceId: systemEvent.deviceId } : {},
|
|
9490
|
+
...systemEvent.deviceType !== void 0 ? { deviceType: systemEvent.deviceType } : {},
|
|
9491
|
+
...systemEvent.nodeId !== void 0 ? { nodeId: systemEvent.nodeId } : {}
|
|
9492
|
+
} } : {}
|
|
9395
9493
|
}) : null;
|
|
9396
9494
|
const actions = await this.mintActions(entry, target);
|
|
9397
9495
|
return {
|
|
@@ -9526,7 +9624,7 @@ var NcDispatcher = class {
|
|
|
9526
9624
|
const out = [];
|
|
9527
9625
|
const zoneIdsWanted = entry.payload.mediaZoneIds;
|
|
9528
9626
|
const stillOverride = zoneIdsWanted !== void 0 && zoneIdsWanted.length > 0 ? "keyFrame" : void 0;
|
|
9529
|
-
const firstLook = await this.resolveAttachment(entry, stillOverride, "any");
|
|
9627
|
+
const firstLook = await this.resolveAttachment(entry, stillOverride, canStillGainMedia(entry) ? "trigger-only" : "any");
|
|
9530
9628
|
const footagePending = this.renderFootage(entry);
|
|
9531
9629
|
const still = firstLook !== null || entry.payload.media === "none" ? firstLook : await this.waitForStill(entry, stillOverride);
|
|
9532
9630
|
const footage = await footagePending;
|
|
@@ -9770,20 +9868,34 @@ var NcDispatcher = class {
|
|
|
9770
9868
|
* throw. And when nothing qualifies, the full list comes back: a real picture
|
|
9771
9869
|
* beats no picture (the same rule D212 settled for the best shot).
|
|
9772
9870
|
*/
|
|
9773
|
-
async narrowToTriggerZone(entry, ordered) {
|
|
9871
|
+
async narrowToTriggerZone(entry, ordered, freshness) {
|
|
9774
9872
|
const zoneIds = entry.payload.triggerZoneIds;
|
|
9775
9873
|
const trackId = entry.payload.subject.trackId;
|
|
9776
9874
|
const getTrail = this.deps.getSubjectTrail;
|
|
9777
9875
|
const deviceId = entry.payload.subject.deviceId;
|
|
9778
|
-
|
|
9876
|
+
const strict = freshness === "trigger-only";
|
|
9877
|
+
if (zoneIds === void 0 || zoneIds.length === 0 || trackId === void 0 || getTrail === void 0 || ordered.length < 2 && !strict) return ordered;
|
|
9779
9878
|
try {
|
|
9780
9879
|
const [polygons, trail] = await Promise.all([this.deps.getZonePolygons?.(deviceId, zoneIds) ?? Promise.resolve([]), getTrail(deviceId, trackId)]);
|
|
9781
9880
|
const kept = selectTriggerZoneCandidates({
|
|
9782
9881
|
candidates: ordered,
|
|
9783
9882
|
trail,
|
|
9784
9883
|
polygons,
|
|
9785
|
-
triggerAt: entry.payload.subject.timestamp
|
|
9884
|
+
triggerAt: entry.payload.subject.timestamp,
|
|
9885
|
+
strict
|
|
9786
9886
|
});
|
|
9887
|
+
if (kept.length === 0) {
|
|
9888
|
+
this.deps.logger.debug("no still from the zone that fired the rule — still waiting", {
|
|
9889
|
+
tags: { deviceId },
|
|
9890
|
+
meta: {
|
|
9891
|
+
ruleId: entry.ruleId,
|
|
9892
|
+
trackId,
|
|
9893
|
+
zoneIds: zoneIds.join(","),
|
|
9894
|
+
rejected: ordered.length
|
|
9895
|
+
}
|
|
9896
|
+
});
|
|
9897
|
+
return kept;
|
|
9898
|
+
}
|
|
9787
9899
|
if (kept.length === ordered.length) return kept;
|
|
9788
9900
|
this.deps.logger.info("still narrowed to the zone that fired the rule", {
|
|
9789
9901
|
tags: { deviceId },
|
|
@@ -9863,7 +9975,8 @@ var NcDispatcher = class {
|
|
|
9863
9975
|
for (const kind of preference) for (const file of files) if (file.kind === kind) ordered.push(file);
|
|
9864
9976
|
const inBand = this.narrowToTriggerInstant(entry, ordered, freshness);
|
|
9865
9977
|
if (inBand.length === 0) continue;
|
|
9866
|
-
const shortlist = await this.narrowToTriggerZone(entry, inBand);
|
|
9978
|
+
const shortlist = await this.narrowToTriggerZone(entry, inBand, freshness);
|
|
9979
|
+
if (shortlist.length === 0) continue;
|
|
9867
9980
|
for (const file of shortlist) {
|
|
9868
9981
|
const raw = Buffer.from(file.base64, "base64");
|
|
9869
9982
|
if (raw.byteLength === 0) continue;
|
|
@@ -25509,13 +25622,46 @@ var MEDIA_COLUMNS = [
|
|
|
25509
25622
|
type: "TEXT"
|
|
25510
25623
|
})
|
|
25511
25624
|
];
|
|
25512
|
-
var MEDIA_INDEXES = [
|
|
25513
|
-
|
|
25514
|
-
|
|
25515
|
-
|
|
25516
|
-
|
|
25517
|
-
|
|
25518
|
-
|
|
25625
|
+
var MEDIA_INDEXES = [
|
|
25626
|
+
{
|
|
25627
|
+
name: "idx_media_owner",
|
|
25628
|
+
columns: ["ownerKind", "ownerId"]
|
|
25629
|
+
},
|
|
25630
|
+
{
|
|
25631
|
+
name: "idx_media_device_ts",
|
|
25632
|
+
columns: ["deviceId", "timestamp"]
|
|
25633
|
+
},
|
|
25634
|
+
(
|
|
25635
|
+
/**
|
|
25636
|
+
* The `locationId IS NULL` seal gate — see `media-relocate-engine.ts`.
|
|
25637
|
+
*
|
|
25638
|
+
* NULLs sort FIRST in a SQLite index, so `WHERE locationId IS NULL` is a
|
|
25639
|
+
* contiguous range from the head of the b-tree: its cost is proportional to
|
|
25640
|
+
* the ANSWER, not to the table. That is the property the gate needs. The
|
|
25641
|
+
* question it must answer fastest is "are there none left", and with this
|
|
25642
|
+
* index that is one seek that immediately falls off the end of an empty
|
|
25643
|
+
* range — regardless of how many million stamped rows sit behind it.
|
|
25644
|
+
*
|
|
25645
|
+
* **What it costs.** One extra b-tree maintained by every insert into a
|
|
25646
|
+
* table taking ~92 k rows/day, and roughly 25 bytes of index per row (~30 MB
|
|
25647
|
+
* at 1.25 M rows). At ~1 insert/s sustained that maintenance is noise
|
|
25648
|
+
* against the JPEG write the same call performs. The one-off cost is real
|
|
25649
|
+
* and worth naming: the FIRST declare after this ships builds the index over
|
|
25650
|
+
* every existing row, on a recordings volume that is currently saturated —
|
|
25651
|
+
* expect the analytics addon's boot to stall for tens of seconds to a couple
|
|
25652
|
+
* of minutes, once.
|
|
25653
|
+
*
|
|
25654
|
+
* A PARTIAL index (`WHERE locationId IS NULL`) would be smaller still and
|
|
25655
|
+
* would cost nothing at all once sealed. It is not used because
|
|
25656
|
+
* `CollectionIndexSchema` has no predicate and the schema fingerprint does
|
|
25657
|
+
* not hash one — adding both to serve one query is a bigger change than the
|
|
25658
|
+
* query deserves, and the plain index already makes the gate O(log n).
|
|
25659
|
+
*/
|
|
25660
|
+
{
|
|
25661
|
+
name: "idx_media_location",
|
|
25662
|
+
columns: ["locationId"]
|
|
25663
|
+
})
|
|
25664
|
+
];
|
|
25519
25665
|
/** The storage location of a media row/record: its stamped `locationId`, or
|
|
25520
25666
|
* the default `eventMedia` location for NULL (pre-multi-location) rows. */
|
|
25521
25667
|
var DEFAULT_MEDIA_LOCATION = "eventMedia";
|
|
@@ -41130,13 +41276,29 @@ var RETRAIN_FRAME_COLUMNS = [
|
|
|
41130
41276
|
type: "TEXT"
|
|
41131
41277
|
})
|
|
41132
41278
|
];
|
|
41133
|
-
var RETRAIN_FRAME_INDEXES = [
|
|
41134
|
-
|
|
41135
|
-
|
|
41136
|
-
|
|
41137
|
-
|
|
41138
|
-
|
|
41139
|
-
|
|
41279
|
+
var RETRAIN_FRAME_INDEXES = [
|
|
41280
|
+
{
|
|
41281
|
+
name: "idx_retrain_frames_track",
|
|
41282
|
+
columns: ["trackId"]
|
|
41283
|
+
},
|
|
41284
|
+
{
|
|
41285
|
+
name: "idx_retrain_frames_device",
|
|
41286
|
+
columns: ["deviceId", "copiedAt"]
|
|
41287
|
+
},
|
|
41288
|
+
(
|
|
41289
|
+
/**
|
|
41290
|
+
* The seal gate's other half. This collection is orders of magnitude smaller
|
|
41291
|
+
* than `pipeline-analytics:media` — every row is an operator-staged frame —
|
|
41292
|
+
* so the index buys much less here. It exists anyway because the gate reads
|
|
41293
|
+
* the two lanes with ONE query shape, and a lane that answers by scanning is
|
|
41294
|
+
* a lane that starts timing out the day the dataset grows. See
|
|
41295
|
+
* `MEDIA_INDEXES` for the cost this is a small multiple of.
|
|
41296
|
+
*/
|
|
41297
|
+
{
|
|
41298
|
+
name: "idx_retrain_frames_location",
|
|
41299
|
+
columns: ["locationId"]
|
|
41300
|
+
})
|
|
41301
|
+
];
|
|
41140
41302
|
/**
|
|
41141
41303
|
* The copy's id, derived from `(trackId, sourceMediaKey)`.
|
|
41142
41304
|
*
|
|
@@ -44402,14 +44564,27 @@ function snapshot(j) {
|
|
|
44402
44564
|
};
|
|
44403
44565
|
}
|
|
44404
44566
|
/**
|
|
44567
|
+
* The column both walked collections use for the storage location.
|
|
44568
|
+
*
|
|
44569
|
+
* Named once because it is now a QUERY PREDICATE (`where: { [it]: null }`) as
|
|
44570
|
+
* well as a row field. A predicate naming a column the collection cannot
|
|
44571
|
+
* express is refused by `count` rather than dropped, so a rename that misses
|
|
44572
|
+
* one site fails loudly instead of counting the whole table.
|
|
44573
|
+
*/
|
|
44574
|
+
var LOCATION_ID_FIELD = "locationId";
|
|
44575
|
+
/**
|
|
44405
44576
|
* True when the row names NO location — the population `seal` exists for.
|
|
44406
44577
|
*
|
|
44407
44578
|
* Deliberately NOT `mediaRowLocation(data) === DEFAULT_MEDIA_LOCATION`: that
|
|
44408
44579
|
* helper collapses "unstamped" and "stamped with the bare type ref" into one
|
|
44409
44580
|
* answer, and only the first is a row whose meaning changes under a repoint.
|
|
44581
|
+
*
|
|
44582
|
+
* The in-JS twin of the `IS NULL` predicate {@link MediaRelocateEngine.countUnstamped}
|
|
44583
|
+
* sends. It stays because the PASS still walks rows and must decide per row;
|
|
44584
|
+
* the two must agree, and `unstamped-count-matches-the-pass` asserts they do.
|
|
44410
44585
|
*/
|
|
44411
44586
|
function rowIsUnstamped(data) {
|
|
44412
|
-
const id = data[
|
|
44587
|
+
const id = data[LOCATION_ID_FIELD];
|
|
44413
44588
|
return !(typeof id === "string" && id.length > 0);
|
|
44414
44589
|
}
|
|
44415
44590
|
/** True for the enrolled-gallery rows `move` skips and `gallery` selects. */
|
|
@@ -44467,26 +44642,91 @@ var MediaRelocateEngine = class {
|
|
|
44467
44642
|
return true;
|
|
44468
44643
|
}
|
|
44469
44644
|
/**
|
|
44470
|
-
*
|
|
44645
|
+
* Rows that still carry no `locationId`, per collection.
|
|
44471
44646
|
*
|
|
44472
|
-
* Read-only, and the ONLY
|
|
44473
|
-
* is allowed to trust: it is
|
|
44647
|
+
* Read-only, and the ONLY answer the migration planner's non-blocking gate
|
|
44648
|
+
* is allowed to trust: it is measured here, against the rows themselves,
|
|
44474
44649
|
* rather than inferred from "a seal job reported done" — a seal that failed
|
|
44475
44650
|
* some rows still reports a terminal state.
|
|
44651
|
+
*
|
|
44652
|
+
* ── Why this is not the walk the rest of this file uses ──────────────────
|
|
44653
|
+
*
|
|
44654
|
+
* It used to be. It paged both collections through {@link pageRows} — 200
|
|
44655
|
+
* rows per RPC, ordered by `timestamp`, a column that leads no index — and
|
|
44656
|
+
* counted `rowIsUnstamped` in JS. On the live hub that is ~6 300 round trips
|
|
44657
|
+
* over 1 254 576 rows, each paying its own sort, and it hit the 60 s RPC
|
|
44658
|
+
* deadline **every time it was called since it shipped**. The gate it exists
|
|
44659
|
+
* to serve could therefore never open, and 152 GB of event media could not
|
|
44660
|
+
* be migrated.
|
|
44661
|
+
*
|
|
44662
|
+
* The walk is right for the PASS, which must visit every row to act on it.
|
|
44663
|
+
* It was never right for a question the store can answer with a predicate,
|
|
44664
|
+
* and the predicate is now expressible: `where: { locationId: null }`
|
|
44665
|
+
* compiles to `IS NULL` (it used to compile to `= NULL`, which matches
|
|
44666
|
+
* nothing and reports it as an answer — the number the gate treats as "safe
|
|
44667
|
+
* to cut over"). With `idx_media_location` in place both statements read a
|
|
44668
|
+
* contiguous range from the head of a b-tree.
|
|
44669
|
+
*
|
|
44670
|
+
* The cheap question is asked FIRST and on its own. `present` is a single
|
|
44671
|
+
* seek and decides the gate; `rows` is a second statement and only decorates
|
|
44672
|
+
* the refusal. So a count that cannot be taken still leaves a usable answer
|
|
44673
|
+
* — `{ present: true, rows: null }` refuses just as hard — and `null` never
|
|
44674
|
+
* degrades into `0`.
|
|
44476
44675
|
*/
|
|
44477
44676
|
async countUnstamped() {
|
|
44478
|
-
|
|
44479
|
-
|
|
44480
|
-
|
|
44481
|
-
|
|
44482
|
-
|
|
44483
|
-
|
|
44484
|
-
|
|
44677
|
+
try {
|
|
44678
|
+
const media = await this.countUnstampedIn(MEDIA_COLLECTION);
|
|
44679
|
+
const retrainFrames = await this.countUnstampedIn(RETRAIN_FRAMES_COLLECTION);
|
|
44680
|
+
return {
|
|
44681
|
+
media,
|
|
44682
|
+
retrainFrames,
|
|
44683
|
+
anyPresent: media.present || retrainFrames.present,
|
|
44684
|
+
total: media.rows === null || retrainFrames.rows === null ? null : media.rows + retrainFrames.rows
|
|
44685
|
+
};
|
|
44686
|
+
} catch (err) {
|
|
44687
|
+
this.deps.logger.warn("event-media seal gate: could not determine whether any row is unstamped — a migration must treat this as unknown, never as sealed", { meta: { error: String(err) } });
|
|
44688
|
+
return null;
|
|
44689
|
+
}
|
|
44485
44690
|
}
|
|
44486
|
-
|
|
44487
|
-
|
|
44488
|
-
|
|
44489
|
-
|
|
44691
|
+
/**
|
|
44692
|
+
* One collection's unstamped population: existence, then size.
|
|
44693
|
+
*
|
|
44694
|
+
* The `limit: 1` probe is deliberately a `query` and not a `count`: a count
|
|
44695
|
+
* of a million matching rows still walks a million index entries, while the
|
|
44696
|
+
* probe stops at the first one. Absence is proved by the same seek finding
|
|
44697
|
+
* nothing, which is where the index earns its keep — after a seal, the range
|
|
44698
|
+
* is empty and the answer is one b-tree descent.
|
|
44699
|
+
*/
|
|
44700
|
+
async countUnstampedIn(collection) {
|
|
44701
|
+
if ((await this.deps.store.query.query({
|
|
44702
|
+
collection,
|
|
44703
|
+
filter: {
|
|
44704
|
+
where: { [LOCATION_ID_FIELD]: null },
|
|
44705
|
+
limit: 1
|
|
44706
|
+
},
|
|
44707
|
+
columns: [LOCATION_ID_FIELD]
|
|
44708
|
+
})).length === 0) return {
|
|
44709
|
+
present: false,
|
|
44710
|
+
rows: 0
|
|
44711
|
+
};
|
|
44712
|
+
try {
|
|
44713
|
+
return {
|
|
44714
|
+
present: true,
|
|
44715
|
+
rows: await this.deps.store.count.query({
|
|
44716
|
+
collection,
|
|
44717
|
+
filter: { where: { [LOCATION_ID_FIELD]: null } }
|
|
44718
|
+
})
|
|
44719
|
+
};
|
|
44720
|
+
} catch (err) {
|
|
44721
|
+
this.deps.logger.warn("event-media seal gate: unstamped rows exist but could not be counted — reporting the population as unknown, not as empty", { meta: {
|
|
44722
|
+
collection,
|
|
44723
|
+
error: String(err)
|
|
44724
|
+
} });
|
|
44725
|
+
return {
|
|
44726
|
+
present: true,
|
|
44727
|
+
rows: null
|
|
44728
|
+
};
|
|
44729
|
+
}
|
|
44490
44730
|
}
|
|
44491
44731
|
start(input) {
|
|
44492
44732
|
for (const j of this.jobs.values()) if (j.state === "running") throw new Error(`a media relocation is already running (${j.jobId})`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as audioModeOf, A as NcSnoozeSchema, At as BaseAddon, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as discriminatedUnion, C as NcConditionDescriptorSchema, Ct as sceneMonitorCapability, D as NcRuleTargetSchema, Dt as videoclipsCapability, E as NcRuleSchema, Et as vectorDimFromBase64, Ft as nodePin, G as TimelapseRulePatchSchema, Gt as record, H as SceneMonitorSchema, Ht as number, I as RECORDING_EXPORT_MAX_READ_BYTES, It as sleep, J as VISIT_MERGE_GAP_MS, Jt as EventCategory, K as TimelapseRuleSchema, Kt as string, L as RetrainStatusSchema, Lt as _enum, M as NcSystemEventKindSchema, Mt as createEvent, N as NcTaxonomySchema, Nt as hydrateSchema, O as NcScheduleSchema, Ot as zoneAnalyticsCapability, P as OpsLogEntrySchema, Pt as isDeviceScopedCap, Q as audioMetricsCapability, Rt as array, S as NC_TAXONOMY, T as NcRulePatchSchema, Tt as systemEventFilterApplies, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as object, V as SCENE_DIVERGED, Vt as literal, W as TimelapseRuleInputSchema, Wt as partialRecord, X as alarmPanelCapability, Y as addonWidgetsSourceCapability, Z as assertTimelapseCadences, _ as MACRO_LABELS, _t as pickClusterStepModels, a as COCO_TO_MACRO, b as NC_DEFAULT_SNOOZE_MINUTES, bt as readDeviceStateFrom, ct as faceGalleryCapability, d as DeclaredDevices, dt as isDetectionMacroClass, et as buildEventKindDescriptor, f as EVENT_KIND_BY_CAP, ft as isScheduleActive, g as LabelAttributionSchema, h as FailureCounters, ht as notificationRulesCapability, i as CLUSTER_MODEL_SCOPED_STEPS, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as DeviceType, k as NcSnoozeInputSchema, kt as errMsg, l as DETECTION_MACRO_CLASSES, lt as failureContributionCapability, m as FULL_IMAGE_BBOX, mt as kebabToCamel, n as AnalyticsGroupRecordSchema, nt as customAction, o as DEFAULT_EVENT_COLOR, ot as encodeVectorBase64, p as EVENT_PAD_MS, pt as isSourceCap, q as TrackSourceSchema, qt as unknown, r as BaseDevice, rt as defineCustomActions, st as evaluateSensorEdge, t as AnalyticsGroupDetailSchema, tt as cosineSimilarity$1, v as NC_ALARM_SYSTEM_EVENT_KINDS, vt as pipelineAnalyticsCapability, w as NcRuleInputSchema, wt as subKindsOf, xt as readTimelapseGeneratedAt, y as NC_CONDITION_CATALOG, yt as plateGalleryCapability, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as boolean } from "../dist-
|
|
1
|
+
import { $ as audioModeOf, A as NcSnoozeSchema, At as BaseAddon, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as discriminatedUnion, C as NcConditionDescriptorSchema, Ct as sceneMonitorCapability, D as NcRuleTargetSchema, Dt as videoclipsCapability, E as NcRuleSchema, Et as vectorDimFromBase64, Ft as nodePin, G as TimelapseRulePatchSchema, Gt as record, H as SceneMonitorSchema, Ht as number, I as RECORDING_EXPORT_MAX_READ_BYTES, It as sleep, J as VISIT_MERGE_GAP_MS, Jt as EventCategory, K as TimelapseRuleSchema, Kt as string, L as RetrainStatusSchema, Lt as _enum, M as NcSystemEventKindSchema, Mt as createEvent, N as NcTaxonomySchema, Nt as hydrateSchema, O as NcScheduleSchema, Ot as zoneAnalyticsCapability, P as OpsLogEntrySchema, Pt as isDeviceScopedCap, Q as audioMetricsCapability, Rt as array, S as NC_TAXONOMY, T as NcRulePatchSchema, Tt as systemEventFilterApplies, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as object, V as SCENE_DIVERGED, Vt as literal, W as TimelapseRuleInputSchema, Wt as partialRecord, X as alarmPanelCapability, Y as addonWidgetsSourceCapability, Z as assertTimelapseCadences, _ as MACRO_LABELS, _t as pickClusterStepModels, a as COCO_TO_MACRO, b as NC_DEFAULT_SNOOZE_MINUTES, bt as readDeviceStateFrom, ct as faceGalleryCapability, d as DeclaredDevices, dt as isDetectionMacroClass, et as buildEventKindDescriptor, f as EVENT_KIND_BY_CAP, ft as isScheduleActive, g as LabelAttributionSchema, h as FailureCounters, ht as notificationRulesCapability, i as CLUSTER_MODEL_SCOPED_STEPS, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as DeviceType, k as NcSnoozeInputSchema, kt as errMsg, l as DETECTION_MACRO_CLASSES, lt as failureContributionCapability, m as FULL_IMAGE_BBOX, mt as kebabToCamel, n as AnalyticsGroupRecordSchema, nt as customAction, o as DEFAULT_EVENT_COLOR, ot as encodeVectorBase64, p as EVENT_PAD_MS, pt as isSourceCap, q as TrackSourceSchema, qt as unknown, r as BaseDevice, rt as defineCustomActions, st as evaluateSensorEdge, t as AnalyticsGroupDetailSchema, tt as cosineSimilarity$1, v as NC_ALARM_SYSTEM_EVENT_KINDS, vt as pipelineAnalyticsCapability, w as NcRuleInputSchema, wt as subKindsOf, xt as readTimelapseGeneratedAt, y as NC_CONDITION_CATALOG, yt as plateGalleryCapability, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as boolean } from "../dist-B5K63Zmt.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";
|
|
@@ -6263,12 +6263,86 @@ var NcDeviceStateCache = class {
|
|
|
6263
6263
|
}
|
|
6264
6264
|
};
|
|
6265
6265
|
//#endregion
|
|
6266
|
+
//#region src/notification-center/system-event-tap-url.ts
|
|
6267
|
+
/** The admin UI's own routes, as `packages/addon-admin-ui/src/App.tsx` declares them. */
|
|
6268
|
+
var ADMIN_ADDONS_PATH = "/system/addons";
|
|
6269
|
+
var ADMIN_CLUSTER_PATH = "/system/cluster";
|
|
6270
|
+
var ADMIN_DEVICES_PATH = "/devices";
|
|
6271
|
+
/**
|
|
6272
|
+
* The query parameter naming a node.
|
|
6273
|
+
*
|
|
6274
|
+
* One spelling for both admin pages, mirrored by `NODE_PARAM` in
|
|
6275
|
+
* `packages/addon-admin-ui/src/lib/node-url-selection.ts` and pinned across
|
|
6276
|
+
* the two packages by `scripts/check-notification-tap-targets.ts`.
|
|
6277
|
+
*/
|
|
6278
|
+
var NODE_PARAM = "node";
|
|
6279
|
+
/** The device page's top-level tab parameter, read by `DeviceDetail`. */
|
|
6280
|
+
var TAB_PARAM = "tab";
|
|
6281
|
+
/** The Stream Broker tab id — a `WELL_KNOWN_TABS` member, hoisted `top-tab`. */
|
|
6282
|
+
var STREAM_BROKER_TAB = "stream-broker";
|
|
6283
|
+
/** `DeviceType.Camera`. Spelled here rather than imported so this stays a pure
|
|
6284
|
+
* string map with no device-model dependency; the guard pins it. */
|
|
6285
|
+
var CAMERA_DEVICE_TYPE = "camera";
|
|
6286
|
+
/** Kinds whose action is "go and apply an ADDON update". */
|
|
6287
|
+
var ADDON_UPDATE_KINDS = new Set(["addon-update-available"]);
|
|
6288
|
+
/** Kinds whose action is "go and apply a NODE's own (`@camstack/server`) update". */
|
|
6289
|
+
var NODE_UPDATE_KINDS = new Set(["server-update-available"]);
|
|
6290
|
+
/**
|
|
6291
|
+
* Kinds whose action is "go and look at why this camera stopped producing".
|
|
6292
|
+
*
|
|
6293
|
+
* The legacy `camera-offline` is here for the same reason it is still in the
|
|
6294
|
+
* enum: outbox and history rows enqueued before the rename carry it, and an
|
|
6295
|
+
* old row is exactly the one whose action has to keep working.
|
|
6296
|
+
*/
|
|
6297
|
+
var DEVICE_DOWN_KINDS = new Set([
|
|
6298
|
+
"device-offline",
|
|
6299
|
+
"stream-offline",
|
|
6300
|
+
"camera-offline"
|
|
6301
|
+
]);
|
|
6302
|
+
/**
|
|
6303
|
+
* Kinds whose subject is a camera BY CONSTRUCTION, whatever the row says.
|
|
6304
|
+
*
|
|
6305
|
+
* A stream belongs to a camera and nothing else. The legacy `camera-offline`
|
|
6306
|
+
* is one too — that name is precisely the claim it made, and it is why the
|
|
6307
|
+
* kind was split into `device-offline` + a `deviceTypes` condition.
|
|
6308
|
+
*/
|
|
6309
|
+
var CAMERA_BY_CONSTRUCTION_KINDS = new Set(["stream-offline", "camera-offline"]);
|
|
6310
|
+
function withParam(path, key, value) {
|
|
6311
|
+
if (value === void 0 || value.length === 0) return path;
|
|
6312
|
+
return `${path}?${key}=${encodeURIComponent(value)}`;
|
|
6313
|
+
}
|
|
6314
|
+
/**
|
|
6315
|
+
* The admin path (with its query) a system event taps through to, or `null`
|
|
6316
|
+
* when the kind has no action.
|
|
6317
|
+
*
|
|
6318
|
+
* Exported for the cross-package guard and for tests that want the path
|
|
6319
|
+
* without a base; the dispatcher goes through {@link systemEventTapUrl}.
|
|
6320
|
+
*/
|
|
6321
|
+
function systemEventTapPath(subject) {
|
|
6322
|
+
if (ADDON_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_ADDONS_PATH, NODE_PARAM, subject.nodeId);
|
|
6323
|
+
if (NODE_UPDATE_KINDS.has(subject.kind)) return withParam(ADMIN_CLUSTER_PATH, NODE_PARAM, subject.nodeId);
|
|
6324
|
+
if (DEVICE_DOWN_KINDS.has(subject.kind)) {
|
|
6325
|
+
const deviceId = subject.deviceId;
|
|
6326
|
+
if (deviceId === void 0 || !Number.isInteger(deviceId) || deviceId <= 0) return null;
|
|
6327
|
+
const devicePath = `${ADMIN_DEVICES_PATH}/${String(deviceId)}`;
|
|
6328
|
+
return CAMERA_BY_CONSTRUCTION_KINDS.has(subject.kind) || subject.deviceType === CAMERA_DEVICE_TYPE ? withParam(devicePath, TAB_PARAM, STREAM_BROKER_TAB) : devicePath;
|
|
6329
|
+
}
|
|
6330
|
+
return null;
|
|
6331
|
+
}
|
|
6332
|
+
/** Full URL, or `null` when the kind has no action or no base is reachable. */
|
|
6333
|
+
function systemEventTapUrl(subject, baseUrl) {
|
|
6334
|
+
if (baseUrl === null || baseUrl.length === 0) return null;
|
|
6335
|
+
const path = systemEventTapPath(subject);
|
|
6336
|
+
if (path === null) return null;
|
|
6337
|
+
return `${baseUrl.replace(/\/+$/, "")}${path}`;
|
|
6338
|
+
}
|
|
6339
|
+
//#endregion
|
|
6266
6340
|
//#region src/notification-center/default-tap-url.ts
|
|
6267
6341
|
/** The viewer's hosted base path under the hub. */
|
|
6268
6342
|
var VIEWER_BASE_PATH = "/viewer/camstack";
|
|
6269
6343
|
function defaultTapUrl(input) {
|
|
6270
|
-
if (input.systemEvent === true) return null;
|
|
6271
6344
|
if (input.baseUrl === null || input.baseUrl.length === 0) return null;
|
|
6345
|
+
if (input.systemEvent !== void 0) return systemEventTapUrl(input.systemEvent, input.baseUrl);
|
|
6272
6346
|
if (!Number.isInteger(input.deviceId) || input.deviceId <= 0) return null;
|
|
6273
6347
|
return `${input.baseUrl.replace(/\/+$/, "")}${VIEWER_BASE_PATH}/camera/${String(input.deviceId)}?at=${String(input.timestampMs)}`;
|
|
6274
6348
|
}
|
|
@@ -8861,13 +8935,14 @@ function trailAt(trail, timestamp) {
|
|
|
8861
8935
|
* cannot be asked (no trail, no polygon) or when nobody qualifies.
|
|
8862
8936
|
*/
|
|
8863
8937
|
function selectTriggerZoneCandidates(input) {
|
|
8864
|
-
const { candidates, trail, polygons } = input;
|
|
8938
|
+
const { candidates, trail, polygons, strict } = input;
|
|
8865
8939
|
if (candidates.length === 0 || trail.length === 0 || polygons.length === 0) return candidates;
|
|
8866
8940
|
const qualifying = candidates.filter((candidate) => {
|
|
8867
8941
|
const point = trailAt(trail, candidate.timestamp);
|
|
8868
8942
|
return point !== null && zoneOverlap(point, polygons) > 0;
|
|
8869
8943
|
});
|
|
8870
|
-
|
|
8944
|
+
if (qualifying.length > 0) return qualifying;
|
|
8945
|
+
return strict === true ? [] : candidates;
|
|
8871
8946
|
}
|
|
8872
8947
|
//#endregion
|
|
8873
8948
|
//#region src/notification-center/dispatcher.ts
|
|
@@ -8905,6 +8980,24 @@ var NC_STILL_WAIT_MS = 3e3;
|
|
|
8905
8980
|
* is the instant; the ladder then runs among the frames that are about it.
|
|
8906
8981
|
*/
|
|
8907
8982
|
var NC_TRIGGER_STILL_MAX_AGE_MS = TRAIL_MATCH_MAX_MS;
|
|
8983
|
+
/**
|
|
8984
|
+
* Whether this row's subject can still be photographed AGAIN after its trigger.
|
|
8985
|
+
*
|
|
8986
|
+
* The one thing that separates a row worth waiting for from a row whose only
|
|
8987
|
+
* frame already exists. A live visual track keeps writing media for as long as
|
|
8988
|
+
* it is open — its first frame, its periodic snapshots, its last frame all land
|
|
8989
|
+
* after the instant a zone condition fired. Every other record is photographed
|
|
8990
|
+
* once: an audio window, a device event and a package event are served by the
|
|
8991
|
+
* still shelf, which D178 makes take ONE picture for a whole burst, and a
|
|
8992
|
+
* `track-end` row is by definition past the last of them.
|
|
8993
|
+
*
|
|
8994
|
+
* Waiting on the wrong side of this line has cost both ways in production — 3 s
|
|
8995
|
+
* of latency on a scream when nothing more was coming, and an empty doorstep on
|
|
8996
|
+
* 'Persona su Uscio' when something was.
|
|
8997
|
+
*/
|
|
8998
|
+
function canStillGainMedia(entry) {
|
|
8999
|
+
return entry.recordKind === "object-event";
|
|
9000
|
+
}
|
|
8908
9001
|
var NcDispatcher = class {
|
|
8909
9002
|
deps;
|
|
8910
9003
|
targetCache = null;
|
|
@@ -9384,7 +9477,12 @@ var NcDispatcher = class {
|
|
|
9384
9477
|
deviceId: systemEvent !== void 0 ? 0 : subject.deviceId,
|
|
9385
9478
|
timestampMs: subject.timestamp,
|
|
9386
9479
|
baseUrl: await this.deps.viewerUrlBase().catch(() => null),
|
|
9387
|
-
systemEvent
|
|
9480
|
+
...systemEvent !== void 0 ? { systemEvent: {
|
|
9481
|
+
kind: systemEvent.kind,
|
|
9482
|
+
...systemEvent.deviceId !== void 0 ? { deviceId: systemEvent.deviceId } : {},
|
|
9483
|
+
...systemEvent.deviceType !== void 0 ? { deviceType: systemEvent.deviceType } : {},
|
|
9484
|
+
...systemEvent.nodeId !== void 0 ? { nodeId: systemEvent.nodeId } : {}
|
|
9485
|
+
} } : {}
|
|
9388
9486
|
}) : null;
|
|
9389
9487
|
const actions = await this.mintActions(entry, target);
|
|
9390
9488
|
return {
|
|
@@ -9519,7 +9617,7 @@ var NcDispatcher = class {
|
|
|
9519
9617
|
const out = [];
|
|
9520
9618
|
const zoneIdsWanted = entry.payload.mediaZoneIds;
|
|
9521
9619
|
const stillOverride = zoneIdsWanted !== void 0 && zoneIdsWanted.length > 0 ? "keyFrame" : void 0;
|
|
9522
|
-
const firstLook = await this.resolveAttachment(entry, stillOverride, "any");
|
|
9620
|
+
const firstLook = await this.resolveAttachment(entry, stillOverride, canStillGainMedia(entry) ? "trigger-only" : "any");
|
|
9523
9621
|
const footagePending = this.renderFootage(entry);
|
|
9524
9622
|
const still = firstLook !== null || entry.payload.media === "none" ? firstLook : await this.waitForStill(entry, stillOverride);
|
|
9525
9623
|
const footage = await footagePending;
|
|
@@ -9763,20 +9861,34 @@ var NcDispatcher = class {
|
|
|
9763
9861
|
* throw. And when nothing qualifies, the full list comes back: a real picture
|
|
9764
9862
|
* beats no picture (the same rule D212 settled for the best shot).
|
|
9765
9863
|
*/
|
|
9766
|
-
async narrowToTriggerZone(entry, ordered) {
|
|
9864
|
+
async narrowToTriggerZone(entry, ordered, freshness) {
|
|
9767
9865
|
const zoneIds = entry.payload.triggerZoneIds;
|
|
9768
9866
|
const trackId = entry.payload.subject.trackId;
|
|
9769
9867
|
const getTrail = this.deps.getSubjectTrail;
|
|
9770
9868
|
const deviceId = entry.payload.subject.deviceId;
|
|
9771
|
-
|
|
9869
|
+
const strict = freshness === "trigger-only";
|
|
9870
|
+
if (zoneIds === void 0 || zoneIds.length === 0 || trackId === void 0 || getTrail === void 0 || ordered.length < 2 && !strict) return ordered;
|
|
9772
9871
|
try {
|
|
9773
9872
|
const [polygons, trail] = await Promise.all([this.deps.getZonePolygons?.(deviceId, zoneIds) ?? Promise.resolve([]), getTrail(deviceId, trackId)]);
|
|
9774
9873
|
const kept = selectTriggerZoneCandidates({
|
|
9775
9874
|
candidates: ordered,
|
|
9776
9875
|
trail,
|
|
9777
9876
|
polygons,
|
|
9778
|
-
triggerAt: entry.payload.subject.timestamp
|
|
9877
|
+
triggerAt: entry.payload.subject.timestamp,
|
|
9878
|
+
strict
|
|
9779
9879
|
});
|
|
9880
|
+
if (kept.length === 0) {
|
|
9881
|
+
this.deps.logger.debug("no still from the zone that fired the rule — still waiting", {
|
|
9882
|
+
tags: { deviceId },
|
|
9883
|
+
meta: {
|
|
9884
|
+
ruleId: entry.ruleId,
|
|
9885
|
+
trackId,
|
|
9886
|
+
zoneIds: zoneIds.join(","),
|
|
9887
|
+
rejected: ordered.length
|
|
9888
|
+
}
|
|
9889
|
+
});
|
|
9890
|
+
return kept;
|
|
9891
|
+
}
|
|
9780
9892
|
if (kept.length === ordered.length) return kept;
|
|
9781
9893
|
this.deps.logger.info("still narrowed to the zone that fired the rule", {
|
|
9782
9894
|
tags: { deviceId },
|
|
@@ -9856,7 +9968,8 @@ var NcDispatcher = class {
|
|
|
9856
9968
|
for (const kind of preference) for (const file of files) if (file.kind === kind) ordered.push(file);
|
|
9857
9969
|
const inBand = this.narrowToTriggerInstant(entry, ordered, freshness);
|
|
9858
9970
|
if (inBand.length === 0) continue;
|
|
9859
|
-
const shortlist = await this.narrowToTriggerZone(entry, inBand);
|
|
9971
|
+
const shortlist = await this.narrowToTriggerZone(entry, inBand, freshness);
|
|
9972
|
+
if (shortlist.length === 0) continue;
|
|
9860
9973
|
for (const file of shortlist) {
|
|
9861
9974
|
const raw = Buffer.from(file.base64, "base64");
|
|
9862
9975
|
if (raw.byteLength === 0) continue;
|
|
@@ -25487,13 +25600,46 @@ var MEDIA_COLUMNS = [
|
|
|
25487
25600
|
type: "TEXT"
|
|
25488
25601
|
})
|
|
25489
25602
|
];
|
|
25490
|
-
var MEDIA_INDEXES = [
|
|
25491
|
-
|
|
25492
|
-
|
|
25493
|
-
|
|
25494
|
-
|
|
25495
|
-
|
|
25496
|
-
|
|
25603
|
+
var MEDIA_INDEXES = [
|
|
25604
|
+
{
|
|
25605
|
+
name: "idx_media_owner",
|
|
25606
|
+
columns: ["ownerKind", "ownerId"]
|
|
25607
|
+
},
|
|
25608
|
+
{
|
|
25609
|
+
name: "idx_media_device_ts",
|
|
25610
|
+
columns: ["deviceId", "timestamp"]
|
|
25611
|
+
},
|
|
25612
|
+
(
|
|
25613
|
+
/**
|
|
25614
|
+
* The `locationId IS NULL` seal gate — see `media-relocate-engine.ts`.
|
|
25615
|
+
*
|
|
25616
|
+
* NULLs sort FIRST in a SQLite index, so `WHERE locationId IS NULL` is a
|
|
25617
|
+
* contiguous range from the head of the b-tree: its cost is proportional to
|
|
25618
|
+
* the ANSWER, not to the table. That is the property the gate needs. The
|
|
25619
|
+
* question it must answer fastest is "are there none left", and with this
|
|
25620
|
+
* index that is one seek that immediately falls off the end of an empty
|
|
25621
|
+
* range — regardless of how many million stamped rows sit behind it.
|
|
25622
|
+
*
|
|
25623
|
+
* **What it costs.** One extra b-tree maintained by every insert into a
|
|
25624
|
+
* table taking ~92 k rows/day, and roughly 25 bytes of index per row (~30 MB
|
|
25625
|
+
* at 1.25 M rows). At ~1 insert/s sustained that maintenance is noise
|
|
25626
|
+
* against the JPEG write the same call performs. The one-off cost is real
|
|
25627
|
+
* and worth naming: the FIRST declare after this ships builds the index over
|
|
25628
|
+
* every existing row, on a recordings volume that is currently saturated —
|
|
25629
|
+
* expect the analytics addon's boot to stall for tens of seconds to a couple
|
|
25630
|
+
* of minutes, once.
|
|
25631
|
+
*
|
|
25632
|
+
* A PARTIAL index (`WHERE locationId IS NULL`) would be smaller still and
|
|
25633
|
+
* would cost nothing at all once sealed. It is not used because
|
|
25634
|
+
* `CollectionIndexSchema` has no predicate and the schema fingerprint does
|
|
25635
|
+
* not hash one — adding both to serve one query is a bigger change than the
|
|
25636
|
+
* query deserves, and the plain index already makes the gate O(log n).
|
|
25637
|
+
*/
|
|
25638
|
+
{
|
|
25639
|
+
name: "idx_media_location",
|
|
25640
|
+
columns: ["locationId"]
|
|
25641
|
+
})
|
|
25642
|
+
];
|
|
25497
25643
|
/** The storage location of a media row/record: its stamped `locationId`, or
|
|
25498
25644
|
* the default `eventMedia` location for NULL (pre-multi-location) rows. */
|
|
25499
25645
|
var DEFAULT_MEDIA_LOCATION = "eventMedia";
|
|
@@ -41066,13 +41212,29 @@ var RETRAIN_FRAME_COLUMNS = [
|
|
|
41066
41212
|
type: "TEXT"
|
|
41067
41213
|
})
|
|
41068
41214
|
];
|
|
41069
|
-
var RETRAIN_FRAME_INDEXES = [
|
|
41070
|
-
|
|
41071
|
-
|
|
41072
|
-
|
|
41073
|
-
|
|
41074
|
-
|
|
41075
|
-
|
|
41215
|
+
var RETRAIN_FRAME_INDEXES = [
|
|
41216
|
+
{
|
|
41217
|
+
name: "idx_retrain_frames_track",
|
|
41218
|
+
columns: ["trackId"]
|
|
41219
|
+
},
|
|
41220
|
+
{
|
|
41221
|
+
name: "idx_retrain_frames_device",
|
|
41222
|
+
columns: ["deviceId", "copiedAt"]
|
|
41223
|
+
},
|
|
41224
|
+
(
|
|
41225
|
+
/**
|
|
41226
|
+
* The seal gate's other half. This collection is orders of magnitude smaller
|
|
41227
|
+
* than `pipeline-analytics:media` — every row is an operator-staged frame —
|
|
41228
|
+
* so the index buys much less here. It exists anyway because the gate reads
|
|
41229
|
+
* the two lanes with ONE query shape, and a lane that answers by scanning is
|
|
41230
|
+
* a lane that starts timing out the day the dataset grows. See
|
|
41231
|
+
* `MEDIA_INDEXES` for the cost this is a small multiple of.
|
|
41232
|
+
*/
|
|
41233
|
+
{
|
|
41234
|
+
name: "idx_retrain_frames_location",
|
|
41235
|
+
columns: ["locationId"]
|
|
41236
|
+
})
|
|
41237
|
+
];
|
|
41076
41238
|
/**
|
|
41077
41239
|
* The copy's id, derived from `(trackId, sourceMediaKey)`.
|
|
41078
41240
|
*
|
|
@@ -44337,14 +44499,27 @@ function snapshot(j) {
|
|
|
44337
44499
|
};
|
|
44338
44500
|
}
|
|
44339
44501
|
/**
|
|
44502
|
+
* The column both walked collections use for the storage location.
|
|
44503
|
+
*
|
|
44504
|
+
* Named once because it is now a QUERY PREDICATE (`where: { [it]: null }`) as
|
|
44505
|
+
* well as a row field. A predicate naming a column the collection cannot
|
|
44506
|
+
* express is refused by `count` rather than dropped, so a rename that misses
|
|
44507
|
+
* one site fails loudly instead of counting the whole table.
|
|
44508
|
+
*/
|
|
44509
|
+
var LOCATION_ID_FIELD = "locationId";
|
|
44510
|
+
/**
|
|
44340
44511
|
* True when the row names NO location — the population `seal` exists for.
|
|
44341
44512
|
*
|
|
44342
44513
|
* Deliberately NOT `mediaRowLocation(data) === DEFAULT_MEDIA_LOCATION`: that
|
|
44343
44514
|
* helper collapses "unstamped" and "stamped with the bare type ref" into one
|
|
44344
44515
|
* answer, and only the first is a row whose meaning changes under a repoint.
|
|
44516
|
+
*
|
|
44517
|
+
* The in-JS twin of the `IS NULL` predicate {@link MediaRelocateEngine.countUnstamped}
|
|
44518
|
+
* sends. It stays because the PASS still walks rows and must decide per row;
|
|
44519
|
+
* the two must agree, and `unstamped-count-matches-the-pass` asserts they do.
|
|
44345
44520
|
*/
|
|
44346
44521
|
function rowIsUnstamped(data) {
|
|
44347
|
-
const id = data[
|
|
44522
|
+
const id = data[LOCATION_ID_FIELD];
|
|
44348
44523
|
return !(typeof id === "string" && id.length > 0);
|
|
44349
44524
|
}
|
|
44350
44525
|
/** True for the enrolled-gallery rows `move` skips and `gallery` selects. */
|
|
@@ -44402,26 +44577,91 @@ var MediaRelocateEngine = class {
|
|
|
44402
44577
|
return true;
|
|
44403
44578
|
}
|
|
44404
44579
|
/**
|
|
44405
|
-
*
|
|
44580
|
+
* Rows that still carry no `locationId`, per collection.
|
|
44406
44581
|
*
|
|
44407
|
-
* Read-only, and the ONLY
|
|
44408
|
-
* is allowed to trust: it is
|
|
44582
|
+
* Read-only, and the ONLY answer the migration planner's non-blocking gate
|
|
44583
|
+
* is allowed to trust: it is measured here, against the rows themselves,
|
|
44409
44584
|
* rather than inferred from "a seal job reported done" — a seal that failed
|
|
44410
44585
|
* some rows still reports a terminal state.
|
|
44586
|
+
*
|
|
44587
|
+
* ── Why this is not the walk the rest of this file uses ──────────────────
|
|
44588
|
+
*
|
|
44589
|
+
* It used to be. It paged both collections through {@link pageRows} — 200
|
|
44590
|
+
* rows per RPC, ordered by `timestamp`, a column that leads no index — and
|
|
44591
|
+
* counted `rowIsUnstamped` in JS. On the live hub that is ~6 300 round trips
|
|
44592
|
+
* over 1 254 576 rows, each paying its own sort, and it hit the 60 s RPC
|
|
44593
|
+
* deadline **every time it was called since it shipped**. The gate it exists
|
|
44594
|
+
* to serve could therefore never open, and 152 GB of event media could not
|
|
44595
|
+
* be migrated.
|
|
44596
|
+
*
|
|
44597
|
+
* The walk is right for the PASS, which must visit every row to act on it.
|
|
44598
|
+
* It was never right for a question the store can answer with a predicate,
|
|
44599
|
+
* and the predicate is now expressible: `where: { locationId: null }`
|
|
44600
|
+
* compiles to `IS NULL` (it used to compile to `= NULL`, which matches
|
|
44601
|
+
* nothing and reports it as an answer — the number the gate treats as "safe
|
|
44602
|
+
* to cut over"). With `idx_media_location` in place both statements read a
|
|
44603
|
+
* contiguous range from the head of a b-tree.
|
|
44604
|
+
*
|
|
44605
|
+
* The cheap question is asked FIRST and on its own. `present` is a single
|
|
44606
|
+
* seek and decides the gate; `rows` is a second statement and only decorates
|
|
44607
|
+
* the refusal. So a count that cannot be taken still leaves a usable answer
|
|
44608
|
+
* — `{ present: true, rows: null }` refuses just as hard — and `null` never
|
|
44609
|
+
* degrades into `0`.
|
|
44411
44610
|
*/
|
|
44412
44611
|
async countUnstamped() {
|
|
44413
|
-
|
|
44414
|
-
|
|
44415
|
-
|
|
44416
|
-
|
|
44417
|
-
|
|
44418
|
-
|
|
44419
|
-
|
|
44612
|
+
try {
|
|
44613
|
+
const media = await this.countUnstampedIn(MEDIA_COLLECTION);
|
|
44614
|
+
const retrainFrames = await this.countUnstampedIn(RETRAIN_FRAMES_COLLECTION);
|
|
44615
|
+
return {
|
|
44616
|
+
media,
|
|
44617
|
+
retrainFrames,
|
|
44618
|
+
anyPresent: media.present || retrainFrames.present,
|
|
44619
|
+
total: media.rows === null || retrainFrames.rows === null ? null : media.rows + retrainFrames.rows
|
|
44620
|
+
};
|
|
44621
|
+
} catch (err) {
|
|
44622
|
+
this.deps.logger.warn("event-media seal gate: could not determine whether any row is unstamped — a migration must treat this as unknown, never as sealed", { meta: { error: String(err) } });
|
|
44623
|
+
return null;
|
|
44624
|
+
}
|
|
44420
44625
|
}
|
|
44421
|
-
|
|
44422
|
-
|
|
44423
|
-
|
|
44424
|
-
|
|
44626
|
+
/**
|
|
44627
|
+
* One collection's unstamped population: existence, then size.
|
|
44628
|
+
*
|
|
44629
|
+
* The `limit: 1` probe is deliberately a `query` and not a `count`: a count
|
|
44630
|
+
* of a million matching rows still walks a million index entries, while the
|
|
44631
|
+
* probe stops at the first one. Absence is proved by the same seek finding
|
|
44632
|
+
* nothing, which is where the index earns its keep — after a seal, the range
|
|
44633
|
+
* is empty and the answer is one b-tree descent.
|
|
44634
|
+
*/
|
|
44635
|
+
async countUnstampedIn(collection) {
|
|
44636
|
+
if ((await this.deps.store.query.query({
|
|
44637
|
+
collection,
|
|
44638
|
+
filter: {
|
|
44639
|
+
where: { [LOCATION_ID_FIELD]: null },
|
|
44640
|
+
limit: 1
|
|
44641
|
+
},
|
|
44642
|
+
columns: [LOCATION_ID_FIELD]
|
|
44643
|
+
})).length === 0) return {
|
|
44644
|
+
present: false,
|
|
44645
|
+
rows: 0
|
|
44646
|
+
};
|
|
44647
|
+
try {
|
|
44648
|
+
return {
|
|
44649
|
+
present: true,
|
|
44650
|
+
rows: await this.deps.store.count.query({
|
|
44651
|
+
collection,
|
|
44652
|
+
filter: { where: { [LOCATION_ID_FIELD]: null } }
|
|
44653
|
+
})
|
|
44654
|
+
};
|
|
44655
|
+
} catch (err) {
|
|
44656
|
+
this.deps.logger.warn("event-media seal gate: unstamped rows exist but could not be counted — reporting the population as unknown, not as empty", { meta: {
|
|
44657
|
+
collection,
|
|
44658
|
+
error: String(err)
|
|
44659
|
+
} });
|
|
44660
|
+
return {
|
|
44661
|
+
present: true,
|
|
44662
|
+
rows: null
|
|
44663
|
+
};
|
|
44664
|
+
}
|
|
44425
44665
|
}
|
|
44426
44666
|
start(input) {
|
|
44427
44667
|
for (const j of this.jobs.values()) if (j.state === "running") throw new Error(`a media relocation is already running (${j.jobId})`);
|
|
@@ -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-bK-ELdFD.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.151",
|
|
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",
|