@camstack/addon-post-analysis 1.2.65 → 1.2.66
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-DTClXZLm.js → dist-DKsqvmSl.js} +25 -9
- package/dist/{dist-4dhFDogy.mjs → dist-DPzTTt9R.mjs} +25 -9
- 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-Ck9Z2cZR.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BdNH8u64.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-DvCGVFX8.mjs → hostInit-D8a3hjwW.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +95 -3
- package/dist/pipeline-analytics/index.mjs +95 -3
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -8001,6 +8001,21 @@ var StorageLocationSchema = object({
|
|
|
8001
8001
|
nodeId: string().optional(),
|
|
8002
8002
|
isDefault: boolean().default(false),
|
|
8003
8003
|
isSystem: boolean().default(false),
|
|
8004
|
+
/**
|
|
8005
|
+
* Operator opt-in: whether consumers that BALANCE across several locations
|
|
8006
|
+
* of a type may write here. Recordings reads it today; event media and
|
|
8007
|
+
* backups are the next consumers, which is why the flag lives on the
|
|
8008
|
+
* location rather than in any one addon's store — nothing has to be
|
|
8009
|
+
* extended to add the next consumer.
|
|
8010
|
+
*
|
|
8011
|
+
* OPTIONAL, and ABSENT MEANS ACTIVE. Every location persisted before the
|
|
8012
|
+
* flag existed reads back with no flag and keeps working exactly as before;
|
|
8013
|
+
* that is the whole compat story, and it is why no migration ships with it.
|
|
8014
|
+
* A newly CREATED sibling is stamped `false` by the orchestrator (creating a
|
|
8015
|
+
* disk must not silently start writing to it); the default of a type is
|
|
8016
|
+
* always stamped `true`.
|
|
8017
|
+
*/
|
|
8018
|
+
enabled: boolean().optional(),
|
|
8004
8019
|
/** COMPUTED at read time by the orchestrator (statfs of the backing volume
|
|
8005
8020
|
* for node-local locations it can reach) — never persisted, absent when the
|
|
8006
8021
|
* volume is remote/unreachable. The single capacity truth every UI reads. */
|
|
@@ -12540,7 +12555,8 @@ var embeddingEncoderCapability = {
|
|
|
12540
12555
|
};
|
|
12541
12556
|
/**
|
|
12542
12557
|
* filesystem-browse — per-node capability for browsing the node's local
|
|
12543
|
-
* filesystem
|
|
12558
|
+
* filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
|
|
12559
|
+
* are sandboxed to operator-configured allowed roots (D115). Used by the
|
|
12544
12560
|
* admin "Add filesystem location" flow to pick a node + path. `mode:'per-node'`
|
|
12545
12561
|
* (one provider per node); the hub calls it with `{nodeId}` so the codegen
|
|
12546
12562
|
* routes to that exact node (default `nodeIdMode:'routing'`).
|
|
@@ -25999,7 +26015,7 @@ method(object({
|
|
|
25999
26015
|
auth: "admin"
|
|
26000
26016
|
});
|
|
26001
26017
|
/**
|
|
26002
|
-
* `
|
|
26018
|
+
* `recording-export` cap — render a footage time range into a single downloadable
|
|
26003
26019
|
* MP4 (regular / accelerated / decelerated / timelapse, ± audio), kept for a
|
|
26004
26020
|
* bounded lifetime with a durable history, auto-expiry, and optional
|
|
26005
26021
|
* delete-after-download.
|
|
@@ -33023,43 +33039,43 @@ Object.freeze({
|
|
|
33023
33039
|
access: "create"
|
|
33024
33040
|
},
|
|
33025
33041
|
"recordingExport.cancelExport": {
|
|
33026
|
-
capName: "
|
|
33042
|
+
capName: "recording-export",
|
|
33027
33043
|
capScope: "system",
|
|
33028
33044
|
addonId: null,
|
|
33029
33045
|
access: "create"
|
|
33030
33046
|
},
|
|
33031
33047
|
"recordingExport.createExport": {
|
|
33032
|
-
capName: "
|
|
33048
|
+
capName: "recording-export",
|
|
33033
33049
|
capScope: "system",
|
|
33034
33050
|
addonId: null,
|
|
33035
33051
|
access: "create"
|
|
33036
33052
|
},
|
|
33037
33053
|
"recordingExport.deleteExport": {
|
|
33038
|
-
capName: "
|
|
33054
|
+
capName: "recording-export",
|
|
33039
33055
|
capScope: "system",
|
|
33040
33056
|
addonId: null,
|
|
33041
33057
|
access: "delete"
|
|
33042
33058
|
},
|
|
33043
33059
|
"recordingExport.getDownloadUrl": {
|
|
33044
|
-
capName: "
|
|
33060
|
+
capName: "recording-export",
|
|
33045
33061
|
capScope: "system",
|
|
33046
33062
|
addonId: null,
|
|
33047
33063
|
access: "view"
|
|
33048
33064
|
},
|
|
33049
33065
|
"recordingExport.getExport": {
|
|
33050
|
-
capName: "
|
|
33066
|
+
capName: "recording-export",
|
|
33051
33067
|
capScope: "system",
|
|
33052
33068
|
addonId: null,
|
|
33053
33069
|
access: "view"
|
|
33054
33070
|
},
|
|
33055
33071
|
"recordingExport.listExports": {
|
|
33056
|
-
capName: "
|
|
33072
|
+
capName: "recording-export",
|
|
33057
33073
|
capScope: "system",
|
|
33058
33074
|
addonId: null,
|
|
33059
33075
|
access: "view"
|
|
33060
33076
|
},
|
|
33061
33077
|
"recordingExport.readExportBytes": {
|
|
33062
|
-
capName: "
|
|
33078
|
+
capName: "recording-export",
|
|
33063
33079
|
capScope: "system",
|
|
33064
33080
|
addonId: null,
|
|
33065
33081
|
access: "view"
|
|
@@ -7970,6 +7970,21 @@ var StorageLocationSchema = object({
|
|
|
7970
7970
|
nodeId: string().optional(),
|
|
7971
7971
|
isDefault: boolean().default(false),
|
|
7972
7972
|
isSystem: boolean().default(false),
|
|
7973
|
+
/**
|
|
7974
|
+
* Operator opt-in: whether consumers that BALANCE across several locations
|
|
7975
|
+
* of a type may write here. Recordings reads it today; event media and
|
|
7976
|
+
* backups are the next consumers, which is why the flag lives on the
|
|
7977
|
+
* location rather than in any one addon's store — nothing has to be
|
|
7978
|
+
* extended to add the next consumer.
|
|
7979
|
+
*
|
|
7980
|
+
* OPTIONAL, and ABSENT MEANS ACTIVE. Every location persisted before the
|
|
7981
|
+
* flag existed reads back with no flag and keeps working exactly as before;
|
|
7982
|
+
* that is the whole compat story, and it is why no migration ships with it.
|
|
7983
|
+
* A newly CREATED sibling is stamped `false` by the orchestrator (creating a
|
|
7984
|
+
* disk must not silently start writing to it); the default of a type is
|
|
7985
|
+
* always stamped `true`.
|
|
7986
|
+
*/
|
|
7987
|
+
enabled: boolean().optional(),
|
|
7973
7988
|
/** COMPUTED at read time by the orchestrator (statfs of the backing volume
|
|
7974
7989
|
* for node-local locations it can reach) — never persisted, absent when the
|
|
7975
7990
|
* volume is remote/unreachable. The single capacity truth every UI reads. */
|
|
@@ -12509,7 +12524,8 @@ var embeddingEncoderCapability = {
|
|
|
12509
12524
|
};
|
|
12510
12525
|
/**
|
|
12511
12526
|
* filesystem-browse — per-node capability for browsing the node's local
|
|
12512
|
-
* filesystem
|
|
12527
|
+
* filesystem. Reads are unconfined (whole filesystem, from `/` down); WRITES
|
|
12528
|
+
* are sandboxed to operator-configured allowed roots (D115). Used by the
|
|
12513
12529
|
* admin "Add filesystem location" flow to pick a node + path. `mode:'per-node'`
|
|
12514
12530
|
* (one provider per node); the hub calls it with `{nodeId}` so the codegen
|
|
12515
12531
|
* routes to that exact node (default `nodeIdMode:'routing'`).
|
|
@@ -25968,7 +25984,7 @@ method(object({
|
|
|
25968
25984
|
auth: "admin"
|
|
25969
25985
|
});
|
|
25970
25986
|
/**
|
|
25971
|
-
* `
|
|
25987
|
+
* `recording-export` cap — render a footage time range into a single downloadable
|
|
25972
25988
|
* MP4 (regular / accelerated / decelerated / timelapse, ± audio), kept for a
|
|
25973
25989
|
* bounded lifetime with a durable history, auto-expiry, and optional
|
|
25974
25990
|
* delete-after-download.
|
|
@@ -32992,43 +33008,43 @@ Object.freeze({
|
|
|
32992
33008
|
access: "create"
|
|
32993
33009
|
},
|
|
32994
33010
|
"recordingExport.cancelExport": {
|
|
32995
|
-
capName: "
|
|
33011
|
+
capName: "recording-export",
|
|
32996
33012
|
capScope: "system",
|
|
32997
33013
|
addonId: null,
|
|
32998
33014
|
access: "create"
|
|
32999
33015
|
},
|
|
33000
33016
|
"recordingExport.createExport": {
|
|
33001
|
-
capName: "
|
|
33017
|
+
capName: "recording-export",
|
|
33002
33018
|
capScope: "system",
|
|
33003
33019
|
addonId: null,
|
|
33004
33020
|
access: "create"
|
|
33005
33021
|
},
|
|
33006
33022
|
"recordingExport.deleteExport": {
|
|
33007
|
-
capName: "
|
|
33023
|
+
capName: "recording-export",
|
|
33008
33024
|
capScope: "system",
|
|
33009
33025
|
addonId: null,
|
|
33010
33026
|
access: "delete"
|
|
33011
33027
|
},
|
|
33012
33028
|
"recordingExport.getDownloadUrl": {
|
|
33013
|
-
capName: "
|
|
33029
|
+
capName: "recording-export",
|
|
33014
33030
|
capScope: "system",
|
|
33015
33031
|
addonId: null,
|
|
33016
33032
|
access: "view"
|
|
33017
33033
|
},
|
|
33018
33034
|
"recordingExport.getExport": {
|
|
33019
|
-
capName: "
|
|
33035
|
+
capName: "recording-export",
|
|
33020
33036
|
capScope: "system",
|
|
33021
33037
|
addonId: null,
|
|
33022
33038
|
access: "view"
|
|
33023
33039
|
},
|
|
33024
33040
|
"recordingExport.listExports": {
|
|
33025
|
-
capName: "
|
|
33041
|
+
capName: "recording-export",
|
|
33026
33042
|
capScope: "system",
|
|
33027
33043
|
addonId: null,
|
|
33028
33044
|
access: "view"
|
|
33029
33045
|
},
|
|
33030
33046
|
"recordingExport.readExportBytes": {
|
|
33031
|
-
capName: "
|
|
33047
|
+
capName: "recording-export",
|
|
33032
33048
|
capScope: "system",
|
|
33033
33049
|
addonId: null,
|
|
33034
33050
|
access: "view"
|
|
@@ -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-DKsqvmSl.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
let node_fs$1 = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
node_fs = require_dist.__toESM(node_fs);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { I as hfModelUrl, N as embeddingEncoderCapability, Y as BaseAddon } from "../dist-
|
|
1
|
+
import { I as hfModelUrl, N as embeddingEncoderCapability, Y as BaseAddon } from "../dist-DPzTTt9R.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import "node:fs";
|
|
@@ -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-DKsqvmSl.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");
|
|
@@ -5346,8 +5346,20 @@ function applyTemplatePatch(merged, template) {
|
|
|
5346
5346
|
const { template: _cleared, ...withoutTemplate } = merged;
|
|
5347
5347
|
return withoutTemplate;
|
|
5348
5348
|
}
|
|
5349
|
+
/** Narrowing guard — the persisted `rule` column is `unknown` until parsed. */
|
|
5350
|
+
function isRecord$1(value) {
|
|
5351
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5352
|
+
}
|
|
5349
5353
|
var TimelapseStore = class {
|
|
5350
5354
|
byId = /* @__PURE__ */ new Map();
|
|
5355
|
+
/**
|
|
5356
|
+
* Birth stamps synthesised for LEGACY rows (see {@link withCreatedAt}), kept
|
|
5357
|
+
* so a repeated `load()` re-uses the FIRST one. Without it the synthesised
|
|
5358
|
+
* `createdAt` would advance to "now" on every refresh, and the scheduler's
|
|
5359
|
+
* eligibility floor — which is `max(stamp, createdAt)` — would creep past
|
|
5360
|
+
* every window such a rule was ever offered, silently producing nothing.
|
|
5361
|
+
*/
|
|
5362
|
+
synthesisedCreatedAt = /* @__PURE__ */ new Map();
|
|
5351
5363
|
store;
|
|
5352
5364
|
logger;
|
|
5353
5365
|
now;
|
|
@@ -5379,8 +5391,11 @@ var TimelapseStore = class {
|
|
|
5379
5391
|
});
|
|
5380
5392
|
this.byId.clear();
|
|
5381
5393
|
let skipped = 0;
|
|
5394
|
+
let synthesised = 0;
|
|
5382
5395
|
for (const row of rows) {
|
|
5383
|
-
const
|
|
5396
|
+
const raw = this.withCreatedAt(row.id, row.data["rule"]);
|
|
5397
|
+
if (raw !== row.data["rule"]) synthesised += 1;
|
|
5398
|
+
const parsed = require_dist.TimelapseRuleSchema.safeParse(raw);
|
|
5384
5399
|
if (!parsed.success) {
|
|
5385
5400
|
skipped += 1;
|
|
5386
5401
|
continue;
|
|
@@ -5389,12 +5404,40 @@ var TimelapseStore = class {
|
|
|
5389
5404
|
}
|
|
5390
5405
|
this.logger.debug("timelapse rules loaded", { meta: {
|
|
5391
5406
|
rules: this.byId.size,
|
|
5392
|
-
...skipped > 0 ? { skippedInvalid: skipped } : {}
|
|
5407
|
+
...skipped > 0 ? { skippedInvalid: skipped } : {},
|
|
5408
|
+
...synthesised > 0 ? { synthesisedCreatedAt: synthesised } : {}
|
|
5393
5409
|
} });
|
|
5394
5410
|
} catch (err) {
|
|
5395
5411
|
this.logger.warn("timelapse rules load failed", { meta: { error: String(err) } });
|
|
5396
5412
|
}
|
|
5397
5413
|
}
|
|
5414
|
+
/**
|
|
5415
|
+
* Give a persisted blob a `createdAt` if it has none, WITHOUT persisting
|
|
5416
|
+
* anything: the rule reads as born at this load.
|
|
5417
|
+
*
|
|
5418
|
+
* `createdAt` has always been required by `TimelapseRuleSchema`, so a row
|
|
5419
|
+
* without one is a pre-schema artefact — and before this it was dropped
|
|
5420
|
+
* outright at load (a silently vanished rule). Reading it as "born now" is
|
|
5421
|
+
* the conservative choice in BOTH directions that matter: the rule survives,
|
|
5422
|
+
* and the scheduler's eligibility floor stops it back-filling a window that
|
|
5423
|
+
* closed years before anyone looked at it. The cost is one skipped window if
|
|
5424
|
+
* a legacy rule is loaded between a close and its settle — a bounded miss,
|
|
5425
|
+
* against an unbounded retry of a window no footage can satisfy.
|
|
5426
|
+
*
|
|
5427
|
+
* The synthesised value is remembered per rule id so a later refresh does not
|
|
5428
|
+
* move the floor, and the first `update`/`markGenerated` writes it through —
|
|
5429
|
+
* after which the row is no longer legacy.
|
|
5430
|
+
*/
|
|
5431
|
+
withCreatedAt(rowId, raw) {
|
|
5432
|
+
if (!isRecord$1(raw)) return raw;
|
|
5433
|
+
if (typeof raw["createdAt"] === "number") return raw;
|
|
5434
|
+
const stamped = this.synthesisedCreatedAt.get(rowId) ?? this.now();
|
|
5435
|
+
this.synthesisedCreatedAt.set(rowId, stamped);
|
|
5436
|
+
return {
|
|
5437
|
+
...raw,
|
|
5438
|
+
createdAt: stamped
|
|
5439
|
+
};
|
|
5440
|
+
}
|
|
5398
5441
|
/** Every rule, newest-first (admin path). */
|
|
5399
5442
|
list() {
|
|
5400
5443
|
return [...this.byId.values()].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
@@ -6027,6 +6070,35 @@ function findLastClosedWindow(input) {
|
|
|
6027
6070
|
};
|
|
6028
6071
|
return null;
|
|
6029
6072
|
}
|
|
6073
|
+
/**
|
|
6074
|
+
* The earliest window CLOSE a (rule, device) pair may ever be offered.
|
|
6075
|
+
*
|
|
6076
|
+
* `max(this device's last generation, the rule's birth)`:
|
|
6077
|
+
* - the per-device stamp is the re-generation guard — a window already
|
|
6078
|
+
* produced for THIS camera is done (and a sibling camera's success says
|
|
6079
|
+
* nothing about it, hence `readTimelapseGeneratedAt` and not the rule-wide
|
|
6080
|
+
* field);
|
|
6081
|
+
* - `createdAt` is what stops a brand-new rule mining the past. Without it an
|
|
6082
|
+
* unstamped pair is indistinguishable from one the hub was down for, and
|
|
6083
|
+
* the back-fill happily reaches back a full lookback for footage no rule was
|
|
6084
|
+
* ever recording against.
|
|
6085
|
+
*
|
|
6086
|
+
* The comparison is against the window's CLOSE, matching the stamp guard: a
|
|
6087
|
+
* rule created mid-window still gets that window (its footage exists), a rule
|
|
6088
|
+
* created after the close never does.
|
|
6089
|
+
*
|
|
6090
|
+
* RULE-LEVEL, deliberately. `TimelapseRule.deviceIds` is a plain array that a
|
|
6091
|
+
* patch replaces wholesale, so there is no per-device add time to floor
|
|
6092
|
+
* against; a camera added to an old rule may therefore be offered the one
|
|
6093
|
+
* window that closed before it joined. That over-produces at most a single
|
|
6094
|
+
* video, whereas the defect this function exists to stop is unbounded — an
|
|
6095
|
+
* every-minute retry of a window nothing can satisfy. If a per-device add time
|
|
6096
|
+
* is ever persisted, raise the floor to `max(…, addedAt)` here and nowhere
|
|
6097
|
+
* else.
|
|
6098
|
+
*/
|
|
6099
|
+
function timelapseEligibilityFloor(rule, deviceId) {
|
|
6100
|
+
return Math.max(require_dist.readTimelapseGeneratedAt(rule, deviceId), rule.createdAt);
|
|
6101
|
+
}
|
|
6030
6102
|
var TimelapseScheduler = class {
|
|
6031
6103
|
deps;
|
|
6032
6104
|
timer = null;
|
|
@@ -6064,6 +6136,7 @@ var TimelapseScheduler = class {
|
|
|
6064
6136
|
produced: 0,
|
|
6065
6137
|
waiting: 0,
|
|
6066
6138
|
alreadyDone: 0,
|
|
6139
|
+
beforeFloor: 0,
|
|
6067
6140
|
failed: 0,
|
|
6068
6141
|
idle: 0
|
|
6069
6142
|
};
|
|
@@ -6072,6 +6145,7 @@ var TimelapseScheduler = class {
|
|
|
6072
6145
|
produced: 0,
|
|
6073
6146
|
waiting: 0,
|
|
6074
6147
|
alreadyDone: 0,
|
|
6148
|
+
beforeFloor: 0,
|
|
6075
6149
|
failed: 0,
|
|
6076
6150
|
idle: 0
|
|
6077
6151
|
};
|
|
@@ -6096,6 +6170,19 @@ var TimelapseScheduler = class {
|
|
|
6096
6170
|
report.alreadyDone += 1;
|
|
6097
6171
|
continue;
|
|
6098
6172
|
}
|
|
6173
|
+
if (timelapseEligibilityFloor(rule, deviceId) >= window.endMs) {
|
|
6174
|
+
report.beforeFloor += 1;
|
|
6175
|
+
this.deps.logger.debug("timelapse window predates the rule — not attempted", {
|
|
6176
|
+
tags: { deviceId },
|
|
6177
|
+
meta: {
|
|
6178
|
+
ruleId: rule.id,
|
|
6179
|
+
windowStart: window.startMs,
|
|
6180
|
+
windowEnd: window.endMs,
|
|
6181
|
+
createdAt: rule.createdAt
|
|
6182
|
+
}
|
|
6183
|
+
});
|
|
6184
|
+
continue;
|
|
6185
|
+
}
|
|
6099
6186
|
if ((await this.produce(rule, deviceId, window, { stamp: true })).produced > 0) report.produced += 1;
|
|
6100
6187
|
else report.failed += 1;
|
|
6101
6188
|
}
|
|
@@ -6113,6 +6200,11 @@ var TimelapseScheduler = class {
|
|
|
6113
6200
|
* test that stops short of the notifier does not test the thing that breaks.
|
|
6114
6201
|
* It deliberately does NOT stamp: pressing Test at noon must not cancel
|
|
6115
6202
|
* tonight's scheduled run.
|
|
6203
|
+
*
|
|
6204
|
+
* It also ignores the eligibility floor. The floor exists to stop an
|
|
6205
|
+
* UNATTENDED loop mining the past; an operator who presses Test on a rule
|
|
6206
|
+
* created an hour ago is asking exactly that question, and gets the honest
|
|
6207
|
+
* answer (`no footage …`) once, not every minute.
|
|
6116
6208
|
*/
|
|
6117
6209
|
async runNow(input) {
|
|
6118
6210
|
const rule = this.deps.store.get(input.ruleId);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as isDeviceScopedCap, A as customAction, B as pipelineAnalyticsCapability, C as TimelapseRuleSchema, D as audioMetricsCapability, E as alarmPanelCapability, F as faceGalleryCapability, G as vectorDimFromBase64, H as readDeviceStateFrom, J as errMsg, K as videoclipsCapability, L as isScheduleActive, M as deriveRecordingMode, O as buildEventKindDescriptor, P as encodeVectorBase64, Q as hydrateSchema, R as kebabToCamel, S as TimelapseRulePatchSchema, T as addonWidgetsSourceCapability, U as readTimelapseGeneratedAt, V as plateGalleryCapability, W as subKindsOf, X as DeviceType, Y as BaseAddon, Z as createEvent, _ as NcTaxonomySchema, a as EVENT_PAD_MS, at as literal, b as RetrainStatusSchema, c as NC_CONDITION_CATALOG, ct as record, d as NcRuleInputSchema, dt as EventCategory, et as nodePin, f as NcRulePatchSchema, g as NcSnoozeSuppressedSchema, h as NcSnoozeSchema, i as EVENT_KIND_BY_CAP, it as boolean, j as defineCustomActions, k as cosineSimilarity, l as NC_TAXONOMY, lt as string, m as NcSnoozeInputSchema, n as DEFAULT_EVENT_COLOR, nt as _enum, o as LabelAttributionSchema, ot as number, p as NcRuleSchema, q as zoneAnalyticsCapability, r as DeclaredDevices, rt as array, s as MACRO_LABELS, st as object, t as BaseDevice, tt as sleep, u as NcConditionDescriptorSchema, ut as unknown, v as OpsLogEntrySchema, w as TrackSourceSchema, x as TimelapseRuleInputSchema, y as RECORDING_EXPORT_MAX_READ_BYTES, z as notificationRulesCapability } from "../dist-
|
|
1
|
+
import { $ as isDeviceScopedCap, A as customAction, B as pipelineAnalyticsCapability, C as TimelapseRuleSchema, D as audioMetricsCapability, E as alarmPanelCapability, F as faceGalleryCapability, G as vectorDimFromBase64, H as readDeviceStateFrom, J as errMsg, K as videoclipsCapability, L as isScheduleActive, M as deriveRecordingMode, O as buildEventKindDescriptor, P as encodeVectorBase64, Q as hydrateSchema, R as kebabToCamel, S as TimelapseRulePatchSchema, T as addonWidgetsSourceCapability, U as readTimelapseGeneratedAt, V as plateGalleryCapability, W as subKindsOf, X as DeviceType, Y as BaseAddon, Z as createEvent, _ as NcTaxonomySchema, a as EVENT_PAD_MS, at as literal, b as RetrainStatusSchema, c as NC_CONDITION_CATALOG, ct as record, d as NcRuleInputSchema, dt as EventCategory, et as nodePin, f as NcRulePatchSchema, g as NcSnoozeSuppressedSchema, h as NcSnoozeSchema, i as EVENT_KIND_BY_CAP, it as boolean, j as defineCustomActions, k as cosineSimilarity, l as NC_TAXONOMY, lt as string, m as NcSnoozeInputSchema, n as DEFAULT_EVENT_COLOR, nt as _enum, o as LabelAttributionSchema, ot as number, p as NcRuleSchema, q as zoneAnalyticsCapability, r as DeclaredDevices, rt as array, s as MACRO_LABELS, st as object, t as BaseDevice, tt as sleep, u as NcConditionDescriptorSchema, ut as unknown, v as OpsLogEntrySchema, w as TrackSourceSchema, x as TimelapseRuleInputSchema, y as RECORDING_EXPORT_MAX_READ_BYTES, z as notificationRulesCapability } from "../dist-DPzTTt9R.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";
|
|
@@ -5333,8 +5333,20 @@ function applyTemplatePatch(merged, template) {
|
|
|
5333
5333
|
const { template: _cleared, ...withoutTemplate } = merged;
|
|
5334
5334
|
return withoutTemplate;
|
|
5335
5335
|
}
|
|
5336
|
+
/** Narrowing guard — the persisted `rule` column is `unknown` until parsed. */
|
|
5337
|
+
function isRecord$1(value) {
|
|
5338
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5339
|
+
}
|
|
5336
5340
|
var TimelapseStore = class {
|
|
5337
5341
|
byId = /* @__PURE__ */ new Map();
|
|
5342
|
+
/**
|
|
5343
|
+
* Birth stamps synthesised for LEGACY rows (see {@link withCreatedAt}), kept
|
|
5344
|
+
* so a repeated `load()` re-uses the FIRST one. Without it the synthesised
|
|
5345
|
+
* `createdAt` would advance to "now" on every refresh, and the scheduler's
|
|
5346
|
+
* eligibility floor — which is `max(stamp, createdAt)` — would creep past
|
|
5347
|
+
* every window such a rule was ever offered, silently producing nothing.
|
|
5348
|
+
*/
|
|
5349
|
+
synthesisedCreatedAt = /* @__PURE__ */ new Map();
|
|
5338
5350
|
store;
|
|
5339
5351
|
logger;
|
|
5340
5352
|
now;
|
|
@@ -5366,8 +5378,11 @@ var TimelapseStore = class {
|
|
|
5366
5378
|
});
|
|
5367
5379
|
this.byId.clear();
|
|
5368
5380
|
let skipped = 0;
|
|
5381
|
+
let synthesised = 0;
|
|
5369
5382
|
for (const row of rows) {
|
|
5370
|
-
const
|
|
5383
|
+
const raw = this.withCreatedAt(row.id, row.data["rule"]);
|
|
5384
|
+
if (raw !== row.data["rule"]) synthesised += 1;
|
|
5385
|
+
const parsed = TimelapseRuleSchema.safeParse(raw);
|
|
5371
5386
|
if (!parsed.success) {
|
|
5372
5387
|
skipped += 1;
|
|
5373
5388
|
continue;
|
|
@@ -5376,12 +5391,40 @@ var TimelapseStore = class {
|
|
|
5376
5391
|
}
|
|
5377
5392
|
this.logger.debug("timelapse rules loaded", { meta: {
|
|
5378
5393
|
rules: this.byId.size,
|
|
5379
|
-
...skipped > 0 ? { skippedInvalid: skipped } : {}
|
|
5394
|
+
...skipped > 0 ? { skippedInvalid: skipped } : {},
|
|
5395
|
+
...synthesised > 0 ? { synthesisedCreatedAt: synthesised } : {}
|
|
5380
5396
|
} });
|
|
5381
5397
|
} catch (err) {
|
|
5382
5398
|
this.logger.warn("timelapse rules load failed", { meta: { error: String(err) } });
|
|
5383
5399
|
}
|
|
5384
5400
|
}
|
|
5401
|
+
/**
|
|
5402
|
+
* Give a persisted blob a `createdAt` if it has none, WITHOUT persisting
|
|
5403
|
+
* anything: the rule reads as born at this load.
|
|
5404
|
+
*
|
|
5405
|
+
* `createdAt` has always been required by `TimelapseRuleSchema`, so a row
|
|
5406
|
+
* without one is a pre-schema artefact — and before this it was dropped
|
|
5407
|
+
* outright at load (a silently vanished rule). Reading it as "born now" is
|
|
5408
|
+
* the conservative choice in BOTH directions that matter: the rule survives,
|
|
5409
|
+
* and the scheduler's eligibility floor stops it back-filling a window that
|
|
5410
|
+
* closed years before anyone looked at it. The cost is one skipped window if
|
|
5411
|
+
* a legacy rule is loaded between a close and its settle — a bounded miss,
|
|
5412
|
+
* against an unbounded retry of a window no footage can satisfy.
|
|
5413
|
+
*
|
|
5414
|
+
* The synthesised value is remembered per rule id so a later refresh does not
|
|
5415
|
+
* move the floor, and the first `update`/`markGenerated` writes it through —
|
|
5416
|
+
* after which the row is no longer legacy.
|
|
5417
|
+
*/
|
|
5418
|
+
withCreatedAt(rowId, raw) {
|
|
5419
|
+
if (!isRecord$1(raw)) return raw;
|
|
5420
|
+
if (typeof raw["createdAt"] === "number") return raw;
|
|
5421
|
+
const stamped = this.synthesisedCreatedAt.get(rowId) ?? this.now();
|
|
5422
|
+
this.synthesisedCreatedAt.set(rowId, stamped);
|
|
5423
|
+
return {
|
|
5424
|
+
...raw,
|
|
5425
|
+
createdAt: stamped
|
|
5426
|
+
};
|
|
5427
|
+
}
|
|
5385
5428
|
/** Every rule, newest-first (admin path). */
|
|
5386
5429
|
list() {
|
|
5387
5430
|
return [...this.byId.values()].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
@@ -6014,6 +6057,35 @@ function findLastClosedWindow(input) {
|
|
|
6014
6057
|
};
|
|
6015
6058
|
return null;
|
|
6016
6059
|
}
|
|
6060
|
+
/**
|
|
6061
|
+
* The earliest window CLOSE a (rule, device) pair may ever be offered.
|
|
6062
|
+
*
|
|
6063
|
+
* `max(this device's last generation, the rule's birth)`:
|
|
6064
|
+
* - the per-device stamp is the re-generation guard — a window already
|
|
6065
|
+
* produced for THIS camera is done (and a sibling camera's success says
|
|
6066
|
+
* nothing about it, hence `readTimelapseGeneratedAt` and not the rule-wide
|
|
6067
|
+
* field);
|
|
6068
|
+
* - `createdAt` is what stops a brand-new rule mining the past. Without it an
|
|
6069
|
+
* unstamped pair is indistinguishable from one the hub was down for, and
|
|
6070
|
+
* the back-fill happily reaches back a full lookback for footage no rule was
|
|
6071
|
+
* ever recording against.
|
|
6072
|
+
*
|
|
6073
|
+
* The comparison is against the window's CLOSE, matching the stamp guard: a
|
|
6074
|
+
* rule created mid-window still gets that window (its footage exists), a rule
|
|
6075
|
+
* created after the close never does.
|
|
6076
|
+
*
|
|
6077
|
+
* RULE-LEVEL, deliberately. `TimelapseRule.deviceIds` is a plain array that a
|
|
6078
|
+
* patch replaces wholesale, so there is no per-device add time to floor
|
|
6079
|
+
* against; a camera added to an old rule may therefore be offered the one
|
|
6080
|
+
* window that closed before it joined. That over-produces at most a single
|
|
6081
|
+
* video, whereas the defect this function exists to stop is unbounded — an
|
|
6082
|
+
* every-minute retry of a window nothing can satisfy. If a per-device add time
|
|
6083
|
+
* is ever persisted, raise the floor to `max(…, addedAt)` here and nowhere
|
|
6084
|
+
* else.
|
|
6085
|
+
*/
|
|
6086
|
+
function timelapseEligibilityFloor(rule, deviceId) {
|
|
6087
|
+
return Math.max(readTimelapseGeneratedAt(rule, deviceId), rule.createdAt);
|
|
6088
|
+
}
|
|
6017
6089
|
var TimelapseScheduler = class {
|
|
6018
6090
|
deps;
|
|
6019
6091
|
timer = null;
|
|
@@ -6051,6 +6123,7 @@ var TimelapseScheduler = class {
|
|
|
6051
6123
|
produced: 0,
|
|
6052
6124
|
waiting: 0,
|
|
6053
6125
|
alreadyDone: 0,
|
|
6126
|
+
beforeFloor: 0,
|
|
6054
6127
|
failed: 0,
|
|
6055
6128
|
idle: 0
|
|
6056
6129
|
};
|
|
@@ -6059,6 +6132,7 @@ var TimelapseScheduler = class {
|
|
|
6059
6132
|
produced: 0,
|
|
6060
6133
|
waiting: 0,
|
|
6061
6134
|
alreadyDone: 0,
|
|
6135
|
+
beforeFloor: 0,
|
|
6062
6136
|
failed: 0,
|
|
6063
6137
|
idle: 0
|
|
6064
6138
|
};
|
|
@@ -6083,6 +6157,19 @@ var TimelapseScheduler = class {
|
|
|
6083
6157
|
report.alreadyDone += 1;
|
|
6084
6158
|
continue;
|
|
6085
6159
|
}
|
|
6160
|
+
if (timelapseEligibilityFloor(rule, deviceId) >= window.endMs) {
|
|
6161
|
+
report.beforeFloor += 1;
|
|
6162
|
+
this.deps.logger.debug("timelapse window predates the rule — not attempted", {
|
|
6163
|
+
tags: { deviceId },
|
|
6164
|
+
meta: {
|
|
6165
|
+
ruleId: rule.id,
|
|
6166
|
+
windowStart: window.startMs,
|
|
6167
|
+
windowEnd: window.endMs,
|
|
6168
|
+
createdAt: rule.createdAt
|
|
6169
|
+
}
|
|
6170
|
+
});
|
|
6171
|
+
continue;
|
|
6172
|
+
}
|
|
6086
6173
|
if ((await this.produce(rule, deviceId, window, { stamp: true })).produced > 0) report.produced += 1;
|
|
6087
6174
|
else report.failed += 1;
|
|
6088
6175
|
}
|
|
@@ -6100,6 +6187,11 @@ var TimelapseScheduler = class {
|
|
|
6100
6187
|
* test that stops short of the notifier does not test the thing that breaks.
|
|
6101
6188
|
* It deliberately does NOT stamp: pressing Test at noon must not cancel
|
|
6102
6189
|
* tonight's scheduled run.
|
|
6190
|
+
*
|
|
6191
|
+
* It also ignores the eligibility floor. The floor exists to stop an
|
|
6192
|
+
* UNATTENDED loop mining the past; an operator who presses Test on a rule
|
|
6193
|
+
* created an hour ago is asking exactly that question, and gets the honest
|
|
6194
|
+
* answer (`no footage …`) once, not every minute.
|
|
6103
6195
|
*/
|
|
6104
6196
|
async runNow(input) {
|
|
6105
6197
|
const rule = this.deps.store.get(input.ruleId);
|
|
@@ -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-BdNH8u64.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.66",
|
|
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",
|