@camstack/addon-post-analysis 1.2.640 → 1.2.642
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/{clip-model-registry-B1HxbGuP.mjs → clip-model-registry-CoH7ORtC.mjs} +218 -30
- package/dist/{clip-model-registry-C8xdbChV.js → clip-model-registry-Dj3lrvL2.js} +218 -30
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +2 -2
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CO5qrMxU.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Dhan9FH4.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-_TUthUZh.mjs +26 -0
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DIbQ2FJ4.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CuNXxDlT.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-BN2pgAbz.mjs → hostInit-j1gxJvS7.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +470 -27
- package/dist/pipeline-analytics/index.mjs +470 -27
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CENVyAAQ.mjs +0 -26
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_clip_model_registry = require("../clip-model-registry-
|
|
5
|
+
const require_clip_model_registry = require("../clip-model-registry-Dj3lrvL2.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_clip_model_registry.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -757,6 +757,8 @@ var DEFAULT_ACTION_TTL_MS = 15 * 6e4;
|
|
|
757
757
|
* notification that is not fresh.
|
|
758
758
|
*/
|
|
759
759
|
var SNOOZE_ACTION_TTL_MS = 60 * 6e4;
|
|
760
|
+
/** How long a Correct / Wrong button stays live: one day. */
|
|
761
|
+
var VERDICT_ACTION_TTL_MS = 1440 * 6e4;
|
|
760
762
|
/**
|
|
761
763
|
* How long a DISARM button stays live: five minutes.
|
|
762
764
|
*
|
|
@@ -784,6 +786,7 @@ function actionTtlMs(subject) {
|
|
|
784
786
|
case "sequence": return DEFAULT_ACTION_TTL_MS;
|
|
785
787
|
case "addon-update": return DEFAULT_ACTION_TTL_MS;
|
|
786
788
|
case "server-update": return DEFAULT_ACTION_TTL_MS;
|
|
789
|
+
case "detection-verdict": return VERDICT_ACTION_TTL_MS;
|
|
787
790
|
}
|
|
788
791
|
}
|
|
789
792
|
var NcActionTokenRegistry = class {
|
|
@@ -827,6 +830,30 @@ var NcActionTokenRegistry = class {
|
|
|
827
830
|
grant: found.grant
|
|
828
831
|
};
|
|
829
832
|
}
|
|
833
|
+
/**
|
|
834
|
+
* Read the grant without spending it. ONLY the confirm page of a link-only
|
|
835
|
+
* channel may call this: it exists so a link preview that opens the URL
|
|
836
|
+
* cannot spend the token, and it never runs anything.
|
|
837
|
+
*/
|
|
838
|
+
peek(id) {
|
|
839
|
+
const found = this.grants.get(id);
|
|
840
|
+
if (found === void 0) return {
|
|
841
|
+
ok: false,
|
|
842
|
+
reason: "unknown"
|
|
843
|
+
};
|
|
844
|
+
if (found.used) return {
|
|
845
|
+
ok: false,
|
|
846
|
+
reason: "already-used"
|
|
847
|
+
};
|
|
848
|
+
if (found.grant.expiresAt <= this.now()) return {
|
|
849
|
+
ok: false,
|
|
850
|
+
reason: "expired"
|
|
851
|
+
};
|
|
852
|
+
return {
|
|
853
|
+
ok: true,
|
|
854
|
+
grant: found.grant
|
|
855
|
+
};
|
|
856
|
+
}
|
|
830
857
|
/** Drop records that can no longer be claimed. Memory hygiene only — an
|
|
831
858
|
* expired record is already refused by {@link claim}, so a sweep that never
|
|
832
859
|
* ran would cost bytes, never safety. */
|
|
@@ -868,6 +895,7 @@ function describeGrant(grant) {
|
|
|
868
895
|
return grant.action === "disarm" ? "Alarm disarmed" : `Alarm armed${grant.mode !== void 0 ? ` (${grant.mode})` : ""}`;
|
|
869
896
|
case "addon-update": return `${grant.packages.length} addon update(s) on ${grant.nodeId}`;
|
|
870
897
|
case "server-update": return `Server update on ${grant.nodeId}`;
|
|
898
|
+
case "detection-verdict": return grant.verdict === "correct" ? "Marked correct — kept for retraining" : grant.suggestedClass === void 0 ? "Marked wrong — kept for retraining" : `Marked wrong, it was a ${grant.suggestedClass} — kept for retraining`;
|
|
871
899
|
}
|
|
872
900
|
}
|
|
873
901
|
/** The audit line's `meta`, per family. A snooze that logged a `sequence` it
|
|
@@ -921,6 +949,15 @@ function grantMeta(grant) {
|
|
|
921
949
|
...common,
|
|
922
950
|
nodeId: grant.nodeId
|
|
923
951
|
};
|
|
952
|
+
case "detection-verdict": return {
|
|
953
|
+
...common,
|
|
954
|
+
ruleId: grant.ruleId,
|
|
955
|
+
rule: grant.ruleName,
|
|
956
|
+
userId: grant.userId,
|
|
957
|
+
trackId: grant.trackId,
|
|
958
|
+
verdict: grant.verdict,
|
|
959
|
+
suggestedClass: grant.suggestedClass ?? null
|
|
960
|
+
};
|
|
924
961
|
}
|
|
925
962
|
}
|
|
926
963
|
/** What the human standing in front of the page is told. */
|
|
@@ -929,6 +966,11 @@ var FAILURE_COPY = {
|
|
|
929
966
|
expired: "This button has expired. Open CamStack to do it there.",
|
|
930
967
|
"already-used": "Already done — this button had been used."
|
|
931
968
|
};
|
|
969
|
+
/** The question a link-only channel lands on, answered by a POST form. */
|
|
970
|
+
function confirmPage(url, grant) {
|
|
971
|
+
const action = `${url.pathname}?exp=${encodeURIComponent(url.searchParams.get("exp") ?? "")}&sig=${encodeURIComponent(url.searchParams.get("sig") ?? "")}&confirm=1`;
|
|
972
|
+
return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Confirm</title><style>body{font:16px/1.5 system-ui,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#111;color:#eee}main{max-width:24rem;padding:2rem;text-align:center}h1{font-size:1.25rem;margin:0 0 1rem}button{font:inherit;padding:.6rem 1.4rem;border-radius:.5rem;border:0;background:#3b82f6;color:#fff}</style><main><h1>Confirm</h1><p>${grant.verdict === "correct" ? "Mark this detection as correct?" : grant.suggestedClass === void 0 ? "Mark this detection as wrong?" : `Mark this detection as wrong, it was a ${grant.suggestedClass}?`}</p><form method="post" action="${action}"><button type="submit">Confirm</button></form></main>`;
|
|
973
|
+
}
|
|
932
974
|
function page(title, detail) {
|
|
933
975
|
return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${title}</title><style>body{font:16px/1.5 system-ui,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#111;color:#eee}main{max-width:24rem;padding:2rem;text-align:center}h1{font-size:1.25rem;margin:0 0 .5rem}p{margin:0;color:#aaa}</style><main><h1>${title}</h1><p>${detail}</p></main>`;
|
|
934
976
|
}
|
|
@@ -942,7 +984,8 @@ var NcActionPlane = class {
|
|
|
942
984
|
handler = async (req, res) => {
|
|
943
985
|
const url = new URL(req.url ?? "/", "http://placeholder");
|
|
944
986
|
const id = decodeURIComponent(url.pathname.split("/").filter(Boolean).pop() ?? "");
|
|
945
|
-
const
|
|
987
|
+
const posted = (req.method ?? "GET").toUpperCase() === "POST";
|
|
988
|
+
const background = posted && url.searchParams.get("confirm") !== "1";
|
|
946
989
|
if (!verifyActionSignature({
|
|
947
990
|
secret: this.deps.secret,
|
|
948
991
|
id,
|
|
@@ -954,6 +997,13 @@ var NcActionPlane = class {
|
|
|
954
997
|
this.reply(res, background, 404, "Not found", "This link is not valid.");
|
|
955
998
|
return;
|
|
956
999
|
}
|
|
1000
|
+
if (!posted) {
|
|
1001
|
+
const peeked = this.deps.registry.peek(id);
|
|
1002
|
+
if (peeked.ok && peeked.grant.kind === "detection-verdict" && peeked.grant.viaLink === true) {
|
|
1003
|
+
this.sendBody(res, 200, "text/html; charset=utf-8", confirmPage(url, peeked.grant));
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
957
1007
|
const claim = this.deps.registry.claim(id);
|
|
958
1008
|
if (!claim.ok) {
|
|
959
1009
|
this.deps.logger.info("nc action callback not claimable", { meta: {
|
|
@@ -1099,6 +1149,7 @@ var ncDurableRecordKinds = require_clip_model_registry.defineRecordKinds({
|
|
|
1099
1149
|
"state-summary-stamps": ledger("digest delivery stamps per rule kind"),
|
|
1100
1150
|
meta: ledger("outbox boot-reconcile watermark and other single-key meta rows", "settings", { none: "one fixed key (watermark), overwritten in place, never deleted" }),
|
|
1101
1151
|
snoozes: ledger("operator opens or closes a snooze window (write-through)"),
|
|
1152
|
+
"detection-verdicts": ledger("operator taps Correct or Wrong on a detection notification; one row per track"),
|
|
1102
1153
|
"snooze-suppressed": {
|
|
1103
1154
|
...QUERIED,
|
|
1104
1155
|
class: "ledger",
|
|
@@ -9234,6 +9285,9 @@ var en_default = {
|
|
|
9234
9285
|
"action.addon-update.uptodate": "Already up to date on {{nodeId}} — nothing to do",
|
|
9235
9286
|
"action.addon-update.unknown": "Could not read what needs updating on {{nodeId}} — nothing was changed. Open CamStack.",
|
|
9236
9287
|
"button.server-update.apply": "Update server on {{nodeId}}",
|
|
9288
|
+
"button.verdict.correct": "Correct",
|
|
9289
|
+
"button.verdict.wrong": "Wrong",
|
|
9290
|
+
"button.verdict.wrongAs": "Wrong: it is {{class}}",
|
|
9237
9291
|
"action.server-update.title": "Server update",
|
|
9238
9292
|
"action.server-update.started": "Updating server on {{nodeId}}…",
|
|
9239
9293
|
"action.server-update.restarting": "Server update staged on {{nodeId}} — the node is restarting. You will hear when it comes back.",
|
|
@@ -9387,7 +9441,7 @@ var it_default = {
|
|
|
9387
9441
|
"occupancy.op.free": "libera",
|
|
9388
9442
|
"system.device-online.title": "Di nuovo online — {{deviceName}}",
|
|
9389
9443
|
"system.device-online.body": "Il dispositivo {{deviceName}} è di nuovo online",
|
|
9390
|
-
"system.device-offline.title": "
|
|
9444
|
+
"system.device-offline.title": "Non raggiungibile — {{deviceName}}",
|
|
9391
9445
|
"system.device-offline.body": "Il dispositivo {{deviceName}} è offline",
|
|
9392
9446
|
"system.device-disabled.title": "Spento — {{deviceName}}",
|
|
9393
9447
|
"system.device-disabled.body": "Il dispositivo {{deviceName}} è stato spento",
|
|
@@ -9403,7 +9457,7 @@ var it_default = {
|
|
|
9403
9457
|
"system.stream-online.title": "Stream di nuovo online — {{deviceName}}",
|
|
9404
9458
|
"system.stream-online.body": "Lo stream della camera {{deviceName}} è di nuovo online",
|
|
9405
9459
|
"system.stream-online.body.profiled": "Lo stream {{profile}} della camera {{deviceName}} è di nuovo online",
|
|
9406
|
-
"system.stream-offline.title": "
|
|
9460
|
+
"system.stream-offline.title": "Flusso interrotto — {{deviceName}}",
|
|
9407
9461
|
"system.stream-offline.body": "Lo stream della camera {{deviceName}} è offline",
|
|
9408
9462
|
"system.stream-offline.body.profiled": "Lo stream {{profile}} della camera {{deviceName}} è offline",
|
|
9409
9463
|
"system.node-online.title": "Nodo di nuovo online — {{nodeName}}",
|
|
@@ -9434,7 +9488,7 @@ var it_default = {
|
|
|
9434
9488
|
"system.storage-location-low.body": "La location {{locationName}} è al {{freePercent}}% libero ({{reason}})",
|
|
9435
9489
|
"system.storage-location-critical.title": "Storage critico — {{locationName}}",
|
|
9436
9490
|
"system.storage-location-critical.body": "La location {{locationName}} è critica: {{reason}}",
|
|
9437
|
-
"system.storage-location-ok.title": "
|
|
9491
|
+
"system.storage-location-ok.title": "Archivio a posto — {{locationName}}",
|
|
9438
9492
|
"system.storage-location-ok.body": "La location {{locationName}} ha recuperato spazio libero",
|
|
9439
9493
|
"system.detection-blind.title": "Senza detection — {{deviceName}}",
|
|
9440
9494
|
"system.detection-blind.body": {
|
|
@@ -9515,8 +9569,8 @@ var it_default = {
|
|
|
9515
9569
|
"system.camera-disabled.body": "Il dispositivo {{deviceName}} è stato spento",
|
|
9516
9570
|
"system.camera-enabled.title": "Dispositivo riacceso",
|
|
9517
9571
|
"system.camera-enabled.body": "Il dispositivo {{deviceName}} è stato riacceso",
|
|
9518
|
-
"test.prefix": "[
|
|
9519
|
-
"timelapse.title": "
|
|
9572
|
+
"test.prefix": "[PROVA] {{body}}",
|
|
9573
|
+
"timelapse.title": "Sequenza — {{camera}}",
|
|
9520
9574
|
"timelapse.body.window": "{{rule}} — {{from}}→{{to}}",
|
|
9521
9575
|
"timelapse.body.tooManyEvents": "troppi eventi per il campionamento denso: video a cadenza uniforme",
|
|
9522
9576
|
"timelapse.body.denseRanges": {
|
|
@@ -9635,6 +9689,9 @@ var it_default = {
|
|
|
9635
9689
|
"action.addon-update.uptodate": "Già aggiornato su {{nodeId}} — niente da fare",
|
|
9636
9690
|
"action.addon-update.unknown": "Non è stato possibile leggere cosa aggiornare su {{nodeId}} — non è stato cambiato nulla. Apri CamStack.",
|
|
9637
9691
|
"button.server-update.apply": "Aggiorna server su {{nodeId}}",
|
|
9692
|
+
"button.verdict.correct": "Corretto",
|
|
9693
|
+
"button.verdict.wrong": "Sbagliato",
|
|
9694
|
+
"button.verdict.wrongAs": "Sbagliato: è {{class}}",
|
|
9638
9695
|
"action.server-update.title": "Aggiornamento server",
|
|
9639
9696
|
"action.server-update.started": "Aggiornamento server su {{nodeId}}…",
|
|
9640
9697
|
"action.server-update.restarting": "Aggiornamento server predisposto su {{nodeId}} — il nodo si riavvia. Ti avviseremo quando torna.",
|
|
@@ -9678,7 +9735,7 @@ var it_default = {
|
|
|
9678
9735
|
"summary.line.node-inference-unavailable": "{{nodeId}} — non può eseguire l'inferenza (da {{since}})",
|
|
9679
9736
|
"summary.line.addon-crash-loop": "{{nodeId}} — {{addons}} fermati dopo crash ripetuti (da {{since}})",
|
|
9680
9737
|
"summary.line.update": "{{packageName}} {{currentVersion}} → {{latestVersion}}",
|
|
9681
|
-
"summary.line.wrapper-update": "
|
|
9738
|
+
"summary.line.wrapper-update": "Contenitore {{currentVersion}} → {{latestVersion}}",
|
|
9682
9739
|
"summary.line.consumable.part": "{{item}} {{percentage}}%",
|
|
9683
9740
|
"summary.line.consumable.part.unmeasured": "{{item}} da sostituire",
|
|
9684
9741
|
"summary.line.consumable.tank": "{{item}} quasi vuota",
|
|
@@ -59797,6 +59854,25 @@ var TrackAlreadyTrainedError = class extends Error {
|
|
|
59797
59854
|
}
|
|
59798
59855
|
};
|
|
59799
59856
|
/**
|
|
59857
|
+
* Classify a failed `setTrackFlags` as one of the lifecycle's DELIBERATE
|
|
59858
|
+
* refusals, or `null` when it is a failure of the call itself.
|
|
59859
|
+
*
|
|
59860
|
+
* Needed because the caller that matters (a notification button) reaches the
|
|
59861
|
+
* method over `ctx.api`, where the error class does not survive the hop and
|
|
59862
|
+
* only the message does. The class check comes first; the message check is the
|
|
59863
|
+
* transported form, and `track-flags.spec.ts` pins it against the real
|
|
59864
|
+
* messages so a reworded refusal cannot silently turn into a generic failure.
|
|
59865
|
+
*/
|
|
59866
|
+
function retrainRefusalReason(err) {
|
|
59867
|
+
if (err instanceof TrackAlreadyTrainedError) return "trained";
|
|
59868
|
+
if (err instanceof StagingBudgetExceededError) return "budget";
|
|
59869
|
+
const message = err instanceof Error ? err.message : "";
|
|
59870
|
+
if (message.includes("is already trained")) return "trained";
|
|
59871
|
+
if (message.includes("in retrain staging")) return "budget";
|
|
59872
|
+
if (message.includes("unknown track")) return "unknown-track";
|
|
59873
|
+
return null;
|
|
59874
|
+
}
|
|
59875
|
+
/**
|
|
59800
59876
|
* The `debugNote` half of the patch — `{}` when nothing should be written.
|
|
59801
59877
|
*
|
|
59802
59878
|
* The note is not an independent column: it is the operator's sentence about
|
|
@@ -81938,6 +82014,10 @@ var NC_DEVICE_ROW_KINDS = [
|
|
|
81938
82014
|
{
|
|
81939
82015
|
key: "nc-artifacts",
|
|
81940
82016
|
kind: ncDurableRecordKinds.artifacts
|
|
82017
|
+
},
|
|
82018
|
+
{
|
|
82019
|
+
key: "nc-detection-verdicts",
|
|
82020
|
+
kind: ncDurableRecordKinds["detection-verdicts"]
|
|
81941
82021
|
}
|
|
81942
82022
|
];
|
|
81943
82023
|
/**
|
|
@@ -82357,6 +82437,82 @@ var NcSequenceStore = class NcSequenceStore {
|
|
|
82357
82437
|
}
|
|
82358
82438
|
};
|
|
82359
82439
|
//#endregion
|
|
82440
|
+
//#region src/notification-center/detection-verdict-ledger.ts
|
|
82441
|
+
/**
|
|
82442
|
+
* @durable class=ledger owner=notification-center
|
|
82443
|
+
* write="the operator taps Correct / Wrong on a detection notification; one row per track, the latest answer replaces the earlier one"
|
|
82444
|
+
* retention="none — bounded by the tracks a rule asked about; deleted with the camera by the device-removal hook"
|
|
82445
|
+
*/
|
|
82446
|
+
var NC_DETECTION_VERDICTS_COLLECTION = "notification-center:detection-verdicts";
|
|
82447
|
+
var NC_DETECTION_VERDICTS_COLUMNS = [
|
|
82448
|
+
{
|
|
82449
|
+
name: "id",
|
|
82450
|
+
type: "TEXT",
|
|
82451
|
+
primaryKey: true,
|
|
82452
|
+
notNull: true
|
|
82453
|
+
},
|
|
82454
|
+
{
|
|
82455
|
+
name: "ruleId",
|
|
82456
|
+
type: "TEXT",
|
|
82457
|
+
notNull: true
|
|
82458
|
+
},
|
|
82459
|
+
{
|
|
82460
|
+
name: "at",
|
|
82461
|
+
type: "INTEGER",
|
|
82462
|
+
notNull: true
|
|
82463
|
+
},
|
|
82464
|
+
(
|
|
82465
|
+
/** The FULL row (Zod-validated on read). */
|
|
82466
|
+
{
|
|
82467
|
+
name: "row",
|
|
82468
|
+
type: "JSON",
|
|
82469
|
+
notNull: true
|
|
82470
|
+
})
|
|
82471
|
+
];
|
|
82472
|
+
var NcDetectionVerdictRecordSchema = require_clip_model_registry.object({
|
|
82473
|
+
trackId: require_clip_model_registry.string().min(1),
|
|
82474
|
+
deviceId: require_clip_model_registry.number().int(),
|
|
82475
|
+
verdict: require_clip_model_registry._enum(["correct", "wrong"]),
|
|
82476
|
+
/** The class the operator said it really was. Only with `wrong`. */
|
|
82477
|
+
suggestedClass: require_clip_model_registry.string().min(1).optional(),
|
|
82478
|
+
/** Whose notification it was (the token's frozen owner), NOT who tapped. */
|
|
82479
|
+
userId: require_clip_model_registry.string().min(1),
|
|
82480
|
+
ruleId: require_clip_model_registry.string().min(1),
|
|
82481
|
+
ruleName: require_clip_model_registry.string(),
|
|
82482
|
+
at: require_clip_model_registry.number()
|
|
82483
|
+
});
|
|
82484
|
+
var NC_DETECTION_VERDICTS_SPEC = {
|
|
82485
|
+
collection: NC_DETECTION_VERDICTS_COLLECTION,
|
|
82486
|
+
columns: [...NC_DETECTION_VERDICTS_COLUMNS],
|
|
82487
|
+
writeMode: "write-through",
|
|
82488
|
+
keyOf: (row) => row.trackId,
|
|
82489
|
+
toValue: (row) => ({
|
|
82490
|
+
ruleId: row.ruleId,
|
|
82491
|
+
at: row.at,
|
|
82492
|
+
row
|
|
82493
|
+
}),
|
|
82494
|
+
fromRecord: (_key, data) => {
|
|
82495
|
+
const parsed = NcDetectionVerdictRecordSchema.safeParse(data["row"]);
|
|
82496
|
+
return parsed.success ? parsed.data : null;
|
|
82497
|
+
},
|
|
82498
|
+
deviceIdOf: (row) => row.deviceId,
|
|
82499
|
+
loadLimit: 1e4
|
|
82500
|
+
};
|
|
82501
|
+
var NcDetectionVerdictLedger = class {
|
|
82502
|
+
ledger;
|
|
82503
|
+
constructor(deps) {
|
|
82504
|
+
this.ledger = new DurableLedger({
|
|
82505
|
+
spec: NC_DETECTION_VERDICTS_SPEC,
|
|
82506
|
+
kind: deps.records.kind(ncDurableRecordKinds["detection-verdicts"]),
|
|
82507
|
+
logger: deps.logger
|
|
82508
|
+
});
|
|
82509
|
+
}
|
|
82510
|
+
/** Write-through; throws on a failed write so the tap reports it. */
|
|
82511
|
+
async record(row) {
|
|
82512
|
+
await this.ledger.put(NcDetectionVerdictRecordSchema.parse(row));
|
|
82513
|
+
}
|
|
82514
|
+
};
|
|
82515
|
+
//#endregion
|
|
82360
82516
|
//#region src/notification-center/device-prune.ts
|
|
82361
82517
|
function judgeList(list, isDead) {
|
|
82362
82518
|
const dead = list.filter(isDead);
|
|
@@ -83311,6 +83467,64 @@ var NcDeviceStateCache = class {
|
|
|
83311
83467
|
}
|
|
83312
83468
|
};
|
|
83313
83469
|
//#endregion
|
|
83470
|
+
//#region src/notification-center/detection-verdict-buttons.ts
|
|
83471
|
+
var VERDICT_CORRECT_ID = "detection-verdict-correct";
|
|
83472
|
+
var VERDICT_WRONG_ID = "detection-verdict-wrong";
|
|
83473
|
+
/** `detection-verdict-wrong:<class>` — Wrong, and what it really was. */
|
|
83474
|
+
var VERDICT_WRONG_AS_PREFIX = "detection-verdict-wrong:";
|
|
83475
|
+
/** The macro classes offered as "it was really…". Short on purpose. */
|
|
83476
|
+
var VERDICT_CLASS_CHOICES = [
|
|
83477
|
+
"person",
|
|
83478
|
+
"vehicle",
|
|
83479
|
+
"animal"
|
|
83480
|
+
];
|
|
83481
|
+
/** At most this many class buttons, and only when the channel has room. */
|
|
83482
|
+
var VERDICT_MAX_CLASS_CHOICES = 2;
|
|
83483
|
+
function verdictButtonRefusal(subject) {
|
|
83484
|
+
if (!subject.askVerdict) return "rule-off";
|
|
83485
|
+
if (subject.trackId === void 0) return "no-track";
|
|
83486
|
+
if (subject.ownerUserId === void 0) return "unowned-target";
|
|
83487
|
+
return null;
|
|
83488
|
+
}
|
|
83489
|
+
/** The classes worth offering for a subject of `className`. */
|
|
83490
|
+
function verdictClassChoices(className) {
|
|
83491
|
+
return VERDICT_CLASS_CHOICES.filter((c) => c !== className).slice(0, VERDICT_MAX_CLASS_CHOICES);
|
|
83492
|
+
}
|
|
83493
|
+
/**
|
|
83494
|
+
* Which buttons a channel with `maxActions` slots gets. `maxActions` absent
|
|
83495
|
+
* (the kind catalog did not answer) is treated as the common floor of three:
|
|
83496
|
+
* the two plain choices, never the class ones. The class choices appear only
|
|
83497
|
+
* when the channel declared room for a fourth button.
|
|
83498
|
+
*/
|
|
83499
|
+
function verdictButtonSpecs(input) {
|
|
83500
|
+
const base = [{
|
|
83501
|
+
actionId: VERDICT_CORRECT_ID,
|
|
83502
|
+
verdict: "correct"
|
|
83503
|
+
}, {
|
|
83504
|
+
actionId: VERDICT_WRONG_ID,
|
|
83505
|
+
verdict: "wrong"
|
|
83506
|
+
}];
|
|
83507
|
+
if (input.maxActions === void 0 || input.maxActions <= 3) return base;
|
|
83508
|
+
const room = input.maxActions - base.length;
|
|
83509
|
+
const extra = verdictClassChoices(input.className).slice(0, room).map((suggestedClass) => ({
|
|
83510
|
+
actionId: `${VERDICT_WRONG_AS_PREFIX}${suggestedClass}`,
|
|
83511
|
+
verdict: "wrong",
|
|
83512
|
+
suggestedClass
|
|
83513
|
+
}));
|
|
83514
|
+
return [...base, ...extra];
|
|
83515
|
+
}
|
|
83516
|
+
/** Is this one of our buttons (so a link-only channel can lift it into the body)? */
|
|
83517
|
+
function isVerdictAction(action) {
|
|
83518
|
+
return action.id === VERDICT_CORRECT_ID || action.id.startsWith(VERDICT_WRONG_ID);
|
|
83519
|
+
}
|
|
83520
|
+
/**
|
|
83521
|
+
* The body lines that carry the verdict choices on a channel with no buttons.
|
|
83522
|
+
* A bare URL per line: every chat and mail client linkifies it.
|
|
83523
|
+
*/
|
|
83524
|
+
function verdictLinkLines(actions) {
|
|
83525
|
+
return actions.filter(isVerdictAction).map((a) => `${a.label}: ${a.url ?? ""}`).join("\n");
|
|
83526
|
+
}
|
|
83527
|
+
//#endregion
|
|
83314
83528
|
//#region src/notification-center/state-summary/state-model.ts
|
|
83315
83529
|
/**
|
|
83316
83530
|
* The STATE SUMMARY's vocabulary (D647): its sections, and what one reported
|
|
@@ -84032,6 +84246,53 @@ function withTimeout$3(promise, ms) {
|
|
|
84032
84246
|
}
|
|
84033
84247
|
//#endregion
|
|
84034
84248
|
//#region src/notification-center/summary/mosaic-renderer.ts
|
|
84249
|
+
/**
|
|
84250
|
+
* The mosaic: N stored best-shots laid out as one image.
|
|
84251
|
+
*
|
|
84252
|
+
* ## The mosaic is ALWAYS CLEAN
|
|
84253
|
+
*
|
|
84254
|
+
* No detection boxes. Not "boxes off by default" — the media ladder in
|
|
84255
|
+
* `summary-selection.ts` refuses `fullFrameBoxed` outright and nothing here
|
|
84256
|
+
* draws a rectangle. The operator asked for it in those words, and there are
|
|
84257
|
+
* two further reasons worth writing down so nobody adds them back as a
|
|
84258
|
+
* convenience:
|
|
84259
|
+
*
|
|
84260
|
+
* - a burnt-in box is permanent. A tile is a durable artefact and a mosaic is
|
|
84261
|
+
* a thing an operator re-reads days later; a box drawn from a detection that
|
|
84262
|
+
* was wrong cannot be un-drawn;
|
|
84263
|
+
* - for P2 it is an attack surface. Anything rendered INTO the pixels is text
|
|
84264
|
+
* the model reads as scene content, and D121 is this repo's record of a
|
|
84265
|
+
* model treating in-frame text as instruction. Boxes stay DATA — the
|
|
84266
|
+
* artefact can carry them, the picture never does.
|
|
84267
|
+
*
|
|
84268
|
+
* ## …and by default it carries NO TEXT EITHER
|
|
84269
|
+
*
|
|
84270
|
+
* {@link mosaicTextPlan} is the ONE place that decides whether a word is drawn,
|
|
84271
|
+
* and with no `captions` on the rule the answer is "none": tile labels off,
|
|
84272
|
+
* title band off. Same argument as the boxes, one step further — the operator
|
|
84273
|
+
* asked for a wall of pictures, a burnt-in caption cannot be un-drawn, and the
|
|
84274
|
+
* label strip covers 14% of every tile with the part of the frame nearest the
|
|
84275
|
+
* ground, which is where feet, wheels and parcels are.
|
|
84276
|
+
*
|
|
84277
|
+
* Nothing is lost by it. The AI pass never sees this image:
|
|
84278
|
+
* `summary-producer.ts#analyse` hands the model the RAW per-track JPEGs and
|
|
84279
|
+
* `summary-ai.ts#buildSummaryAiPrompt` supplies camera name, class and time as
|
|
84280
|
+
* PROMPT TEXT. The tile rectangles stay exported (see {@link mosaicLayout}) so
|
|
84281
|
+
* a viewer can still say what a tile is without a word being burnt into it.
|
|
84282
|
+
*
|
|
84283
|
+
* ## sharp, and never ffmpeg `drawtext`
|
|
84284
|
+
*
|
|
84285
|
+
* Labels, WHEN ASKED FOR, are composited as SVG through sharp. D118 forbids `drawtext` because
|
|
84286
|
+
* the hub carries two ffmpeg builds that disagree about libfreetype, so a
|
|
84287
|
+
* caption would appear or vanish depending on which node rendered it. sharp is
|
|
84288
|
+
* a direct dependency of this addon and its text path is librsvg, which is the
|
|
84289
|
+
* same one the timelapse preview already uses.
|
|
84290
|
+
*
|
|
84291
|
+
* The geometry helpers are pure and exported: {@link mosaicGrid} and
|
|
84292
|
+
* {@link mosaicLayout} answer the tile rectangles WITHOUT touching a pixel,
|
|
84293
|
+
* which is what lets the layout be tested and what a future clickable-tile
|
|
84294
|
+
* viewer will read.
|
|
84295
|
+
*/
|
|
84035
84296
|
/** Widest mosaic produced. 1920 keeps a 3×3 grid at 640 px a tile — readable
|
|
84036
84297
|
* on a phone, and a ~200 KB JPEG that every notifier accepts. */
|
|
84037
84298
|
var MOSAIC_MAX_WIDTH = 1920;
|
|
@@ -84225,6 +84486,13 @@ function fitLine(text, usablePx, basePx, minPx) {
|
|
|
84225
84486
|
fontPx
|
|
84226
84487
|
};
|
|
84227
84488
|
}
|
|
84489
|
+
/**
|
|
84490
|
+
* The words drawn on a summary tile: camera, clock, and the class vocabulary
|
|
84491
|
+
* in the hub language. A class the vocabulary does not know stays the wire id.
|
|
84492
|
+
*/
|
|
84493
|
+
function summaryTileCaption(camera, clock, className, locale) {
|
|
84494
|
+
return `${camera} · ${clock} · ${require_clip_model_registry.eventKindLabel(className, locale)}`;
|
|
84495
|
+
}
|
|
84228
84496
|
/** The label strip drawn across the bottom of one tile. */
|
|
84229
84497
|
function tileLabelSvg(width, height, label) {
|
|
84230
84498
|
const bandHeight = Math.max(18, Math.round(height * LABEL_BAND_FRACTION));
|
|
@@ -85299,8 +85567,9 @@ function selectTriggerZoneCandidates(input) {
|
|
|
85299
85567
|
*
|
|
85300
85568
|
* - `scene-boxes` — the whole scene; the subject outlined THICK in the accent
|
|
85301
85569
|
* colour, every OTHER track visible at that instant outlined THIN, each
|
|
85302
|
-
* captioned with class
|
|
85303
|
-
* reads on any background. Boxes are the tracks'
|
|
85570
|
+
* captioned with the class vocabulary in the hub language ("Cane 87%") on a
|
|
85571
|
+
* dark plate that reads on any background. Boxes are the tracks' trail
|
|
85572
|
+
* boxes AT
|
|
85304
85573
|
* the source frame's timestamp — never the track envelope — scaled to the
|
|
85305
85574
|
* JPEG.
|
|
85306
85575
|
* - `crop` — the subject cut out of the clean frame around its box at that
|
|
@@ -85321,10 +85590,11 @@ function selectTriggerZoneCandidates(input) {
|
|
|
85321
85590
|
* ## Once per record
|
|
85322
85591
|
*
|
|
85323
85592
|
* N targets and every outbox retry resolve the same row; the render is
|
|
85324
|
-
* memoised per record + subject + mode + zone flag + camera
|
|
85325
|
-
* {@link NC_IMAGE_MEMO_TTL_MS}, so a 4K decode happens once, not once per
|
|
85326
|
-
* phone.
|
|
85327
|
-
*
|
|
85593
|
+
* memoised per record + subject + mode + zone flag + camera + hub language
|
|
85594
|
+
* for {@link NC_IMAGE_MEMO_TTL_MS}, so a 4K decode happens once, not once per
|
|
85595
|
+
* phone. The hub has one notification language, so every recipient of a row
|
|
85596
|
+
* shares that picture. It runs in the process that hosts the Notification
|
|
85597
|
+
* Center (the pipeline-analytics runner), which already carries `sharp`.
|
|
85328
85598
|
*/
|
|
85329
85599
|
/** How long a rendered picture is reused for the same record (N targets, retries). */
|
|
85330
85600
|
var NC_IMAGE_MEMO_TTL_MS = 15 * 6e4;
|
|
@@ -85356,12 +85626,37 @@ var SAME_SUBJECT_IOU = .8;
|
|
|
85356
85626
|
*/
|
|
85357
85627
|
var NC_IMAGE_SAME_CLASS_SUBJECT_IOU = .5;
|
|
85358
85628
|
var JPEG_QUALITY = 85;
|
|
85359
|
-
/** The caption of a box:
|
|
85629
|
+
/** The caption of a box: a name and confidence — "Cane 87%". The name is already chosen. */
|
|
85360
85630
|
function imageLabelText(name, confidence) {
|
|
85361
85631
|
if (confidence === void 0 || !Number.isFinite(confidence)) return name;
|
|
85362
85632
|
const pct = Math.round(Math.min(1, Math.max(0, confidence)) * 100);
|
|
85363
85633
|
return `${name} ${String(pct)}%`;
|
|
85364
85634
|
}
|
|
85635
|
+
/**
|
|
85636
|
+
* The name drawn on a box, in the hub language.
|
|
85637
|
+
*
|
|
85638
|
+
* A class id — the coarse class, or a finer one riding `label` — is
|
|
85639
|
+
* {@link eventKindLabel}. An identity or a plate stays the word the
|
|
85640
|
+
* recogniser produced. A class the vocabulary does not know stays the wire
|
|
85641
|
+
* id, which is what `eventKindLabel` already returns.
|
|
85642
|
+
*/
|
|
85643
|
+
function imageBoxName(input, locale) {
|
|
85644
|
+
if (input.labelKind === "identity" || input.labelKind === "plate") {
|
|
85645
|
+
const written = input.label?.trim();
|
|
85646
|
+
if (written !== void 0 && written.length > 0) return written;
|
|
85647
|
+
}
|
|
85648
|
+
const label = input.label?.trim();
|
|
85649
|
+
if (label !== void 0 && label.length > 0) {
|
|
85650
|
+
const key = label.toLowerCase();
|
|
85651
|
+
if (require_clip_model_registry.isEventKindLabelKey(key)) return require_clip_model_registry.eventKindLabel(key, locale);
|
|
85652
|
+
if (key !== input.className.trim().toLowerCase()) return label;
|
|
85653
|
+
}
|
|
85654
|
+
return require_clip_model_registry.eventKindLabel(input.className, locale);
|
|
85655
|
+
}
|
|
85656
|
+
/** Class vocabulary in `locale`, plus confidence — the string the overlay draws. */
|
|
85657
|
+
function imageBoxCaption(input, locale) {
|
|
85658
|
+
return imageLabelText(imageBoxName(input, locale), input.confidence);
|
|
85659
|
+
}
|
|
85365
85660
|
/** Stroke widths for an output of this size: the subject's is the thick one. */
|
|
85366
85661
|
function overlayStrokes(width, height) {
|
|
85367
85662
|
const subject = Math.max(4, Math.round(Math.min(width, height) / 110));
|
|
@@ -85660,8 +85955,8 @@ var NcNotificationImageStage = class {
|
|
|
85660
85955
|
}
|
|
85661
85956
|
/**
|
|
85662
85957
|
* The picture for this row from this source, rendered once per record +
|
|
85663
|
-
* subject + mode + zone flag + camera and reused for every
|
|
85664
|
-
* Null when the row ships no still.
|
|
85958
|
+
* subject + mode + zone flag + camera + language and reused for every
|
|
85959
|
+
* target and retry. Null when the row ships no still.
|
|
85665
85960
|
*/
|
|
85666
85961
|
async notificationImage(input) {
|
|
85667
85962
|
const payload = input.entry.payload;
|
|
@@ -85669,13 +85964,15 @@ var NcNotificationImageStage = class {
|
|
|
85669
85964
|
if (mode === "none") return null;
|
|
85670
85965
|
const zoneIds = input.withSubject ? payloadZoneOverlayIds(payload) : [];
|
|
85671
85966
|
const withPath = input.withSubject && payloadShowsSubjectPath(payload);
|
|
85967
|
+
const locale = input.locale ?? "en";
|
|
85672
85968
|
const key = [
|
|
85673
85969
|
input.entry.recordId,
|
|
85674
85970
|
payload.subject.trackId ?? "",
|
|
85675
85971
|
mode,
|
|
85676
85972
|
zoneIds.length > 0 ? "z" : "-",
|
|
85677
85973
|
withPath ? "p" : "-",
|
|
85678
|
-
String(input.camera)
|
|
85974
|
+
String(input.camera),
|
|
85975
|
+
locale
|
|
85679
85976
|
].join("|");
|
|
85680
85977
|
const now = this.deps.now();
|
|
85681
85978
|
const hit = this.memo.get(key);
|
|
@@ -85713,9 +86010,15 @@ var NcNotificationImageStage = class {
|
|
|
85713
86010
|
const trail = hasTrackedSubject(subjectTrackId) ? await this.subjectTrail(camera, subjectTrackId) : [];
|
|
85714
86011
|
const subjectBox = trailAt(trail, source.timestamp, NC_IMAGE_BOX_MATCH_MAX_MS)?.bbox ?? null;
|
|
85715
86012
|
const path = withPath ? this.subjectPath(input, trail) : [];
|
|
86013
|
+
const locale = input.locale ?? "en";
|
|
85716
86014
|
const subject = subjectBox !== null ? {
|
|
85717
86015
|
...subjectBox,
|
|
85718
|
-
label:
|
|
86016
|
+
label: imageBoxCaption({
|
|
86017
|
+
className: payload.subject.className,
|
|
86018
|
+
...payload.subject.label !== void 0 ? { label: payload.subject.label } : {},
|
|
86019
|
+
...payload.subject.labelKind !== void 0 ? { labelKind: payload.subject.labelKind } : {},
|
|
86020
|
+
...payload.subject.confidence !== void 0 ? { confidence: payload.subject.confidence } : {}
|
|
86021
|
+
}, locale),
|
|
85719
86022
|
className: payload.subject.className
|
|
85720
86023
|
} : null;
|
|
85721
86024
|
const othersDrawn = mode === "scene-boxes" ? otherTracks(await this.tracksAt(camera, source.timestamp), subjectTrackId, subjectBox, payload.subject.className) : [];
|
|
@@ -85730,7 +86033,11 @@ var NcNotificationImageStage = class {
|
|
|
85730
86033
|
});
|
|
85731
86034
|
const others = mode === "scene-boxes" ? othersDrawn.map((t) => ({
|
|
85732
86035
|
...t.bbox,
|
|
85733
|
-
label:
|
|
86036
|
+
label: imageBoxCaption({
|
|
86037
|
+
className: t.className,
|
|
86038
|
+
...t.label !== void 0 ? { label: t.label } : {},
|
|
86039
|
+
...t.confidence !== void 0 ? { confidence: t.confidence } : {}
|
|
86040
|
+
}, locale),
|
|
85734
86041
|
className: t.className,
|
|
85735
86042
|
...t.confidence !== void 0 ? { confidence: t.confidence } : {}
|
|
85736
86043
|
})) : [];
|
|
@@ -86926,6 +87233,8 @@ var NcDispatcher = class {
|
|
|
86926
87233
|
targetId: target.id,
|
|
86927
87234
|
...target.ownerUserId !== void 0 ? { ownerUserId: target.ownerUserId } : {},
|
|
86928
87235
|
...entry.payload.subject.className !== void 0 ? { className: entry.payload.subject.className } : {},
|
|
87236
|
+
...entry.payload.subject.trackId !== void 0 ? { trackId: entry.payload.subject.trackId } : {},
|
|
87237
|
+
...target.maxActions !== void 0 ? { maxActions: target.maxActions } : {},
|
|
86929
87238
|
...entry.payload.subject.systemEvent !== void 0 ? { systemEvent: entry.payload.subject.systemEvent } : {}
|
|
86930
87239
|
});
|
|
86931
87240
|
} catch (err) {
|
|
@@ -87021,9 +87330,13 @@ var NcDispatcher = class {
|
|
|
87021
87330
|
...summarySectionOf(systemEvent.data)
|
|
87022
87331
|
} } : {}
|
|
87023
87332
|
}) : null;
|
|
87024
|
-
const
|
|
87333
|
+
const minted = await this.mintActions(entry, target);
|
|
87334
|
+
const linkOnly = target.maxActions === 0;
|
|
87335
|
+
const verdictLinks = linkOnly ? verdictLinkLines(minted) : "";
|
|
87336
|
+
const actions = linkOnly ? minted.filter((a) => !isVerdictAction(a)) : minted;
|
|
87337
|
+
const withLinks = (text) => verdictLinks === "" ? text : `${text}\n\n${verdictLinks}`;
|
|
87025
87338
|
const notification = {
|
|
87026
|
-
body,
|
|
87339
|
+
body: withLinks(body),
|
|
87027
87340
|
title,
|
|
87028
87341
|
priority: clampPriority(paramNumber(entry.payload.params, "priority") ?? entry.payload.priority),
|
|
87029
87342
|
...params,
|
|
@@ -87045,7 +87358,7 @@ var NcDispatcher = class {
|
|
|
87045
87358
|
grouped: resolved.grouped,
|
|
87046
87359
|
withAiText: (aiText) => ({
|
|
87047
87360
|
...notification,
|
|
87048
|
-
body: renderBody(aiText)
|
|
87361
|
+
body: withLinks(renderBody(aiText))
|
|
87049
87362
|
})
|
|
87050
87363
|
};
|
|
87051
87364
|
}
|
|
@@ -87240,7 +87553,8 @@ var NcDispatcher = class {
|
|
|
87240
87553
|
annotated: isAnnotatedSceneKind(file.kind)
|
|
87241
87554
|
},
|
|
87242
87555
|
camera: cameraId,
|
|
87243
|
-
withSubject: false
|
|
87556
|
+
withSubject: false,
|
|
87557
|
+
locale: this.texts.language
|
|
87244
87558
|
});
|
|
87245
87559
|
if (image === null) continue;
|
|
87246
87560
|
out.push({
|
|
@@ -88029,7 +88343,8 @@ var NcDispatcher = class {
|
|
|
88029
88343
|
annotated: isAnnotatedSceneKind(file.kind)
|
|
88030
88344
|
},
|
|
88031
88345
|
camera: primaryImageCamera(entry.payload),
|
|
88032
|
-
withSubject: true
|
|
88346
|
+
withSubject: true,
|
|
88347
|
+
locale: this.texts.language
|
|
88033
88348
|
});
|
|
88034
88349
|
if (image === null) return null;
|
|
88035
88350
|
return {
|
|
@@ -94749,7 +95064,7 @@ var NcSummaryProducer = class {
|
|
|
94749
95064
|
purpose: "summary",
|
|
94750
95065
|
tiles: tiles.map(({ candidate, jpeg }) => ({
|
|
94751
95066
|
jpeg,
|
|
94752
|
-
...plan.tileLabels ? { label:
|
|
95067
|
+
...plan.tileLabels ? { label: summaryTileCaption(names.get(candidate.deviceId) ?? String(candidate.deviceId), clockOf$2(candidate.firstSeen), candidate.className, this.texts.language) } : {},
|
|
94753
95068
|
deviceId: candidate.deviceId,
|
|
94754
95069
|
sourceId: candidate.trackId
|
|
94755
95070
|
})),
|
|
@@ -99656,6 +99971,55 @@ async function mintButtons(host, input) {
|
|
|
99656
99971
|
const announcement = stored === null && input.ruleId === NC_ALARM_ANNOUNCE_RULE_ID;
|
|
99657
99972
|
if (stored === null && !announcement) return [];
|
|
99658
99973
|
const drafts = [];
|
|
99974
|
+
const verdictRefusal = verdictButtonRefusal({
|
|
99975
|
+
askVerdict: stored?.askVerdict === true,
|
|
99976
|
+
...input.trackId !== void 0 ? { trackId: input.trackId } : {},
|
|
99977
|
+
...input.ownerUserId !== void 0 ? { ownerUserId: input.ownerUserId } : {}
|
|
99978
|
+
});
|
|
99979
|
+
if (stored?.askVerdict === true && verdictRefusal !== null) host.logger.info("detection notification carries no verdict buttons", {
|
|
99980
|
+
tags: { deviceId: input.deviceId },
|
|
99981
|
+
meta: {
|
|
99982
|
+
ruleId: input.ruleId,
|
|
99983
|
+
reason: verdictRefusal,
|
|
99984
|
+
targetId: input.targetId
|
|
99985
|
+
}
|
|
99986
|
+
});
|
|
99987
|
+
const verdictTrackId = input.trackId;
|
|
99988
|
+
const verdictOwner = input.ownerUserId;
|
|
99989
|
+
const verdictButtons = verdictRefusal !== null || verdictTrackId === void 0 || verdictOwner === void 0 ? [] : verdictButtonSpecs({
|
|
99990
|
+
className: input.className,
|
|
99991
|
+
maxActions: input.maxActions
|
|
99992
|
+
}).map((spec) => {
|
|
99993
|
+
const id = (0, node_crypto.randomUUID)();
|
|
99994
|
+
drafts.push({
|
|
99995
|
+
id,
|
|
99996
|
+
ttlMs: actionTtlMs({ kind: "detection-verdict" }),
|
|
99997
|
+
grant: (expiresAt) => ({
|
|
99998
|
+
kind: "detection-verdict",
|
|
99999
|
+
id,
|
|
100000
|
+
verdict: spec.verdict,
|
|
100001
|
+
trackId: verdictTrackId,
|
|
100002
|
+
ruleId: input.ruleId,
|
|
100003
|
+
ruleName: input.ruleName,
|
|
100004
|
+
userId: verdictOwner,
|
|
100005
|
+
...spec.suggestedClass !== void 0 ? { suggestedClass: spec.suggestedClass } : {},
|
|
100006
|
+
...input.maxActions === 0 ? { viaLink: true } : {},
|
|
100007
|
+
deviceId: input.deviceId,
|
|
100008
|
+
actionId: spec.actionId,
|
|
100009
|
+
targetId: input.targetId,
|
|
100010
|
+
expiresAt
|
|
100011
|
+
})
|
|
100012
|
+
});
|
|
100013
|
+
return {
|
|
100014
|
+
id: spec.actionId,
|
|
100015
|
+
label: spec.suggestedClass === void 0 ? host.texts.text({ key: spec.verdict === "correct" ? "button.verdict.correct" : "button.verdict.wrong" }) : host.texts.text({
|
|
100016
|
+
key: "button.verdict.wrongAs",
|
|
100017
|
+
vars: { class: spec.suggestedClass }
|
|
100018
|
+
}),
|
|
100019
|
+
url: id,
|
|
100020
|
+
mode: "background"
|
|
100021
|
+
};
|
|
100022
|
+
});
|
|
99659
100023
|
const sequenceButtons = host.actionRunner === null || stored === null ? [] : buildActionButtons({
|
|
99660
100024
|
actions: stored.actions,
|
|
99661
100025
|
knownSequenceIds: host.sequences.knownIds(),
|
|
@@ -99868,6 +100232,7 @@ async function mintButtons(host, input) {
|
|
|
99868
100232
|
}
|
|
99869
100233
|
});
|
|
99870
100234
|
const out = [
|
|
100235
|
+
...verdictButtons,
|
|
99871
100236
|
...sequenceButtons,
|
|
99872
100237
|
...snoozeButtons,
|
|
99873
100238
|
...alarmButtons,
|
|
@@ -100091,7 +100456,52 @@ async function runGrantedAction(host, grant) {
|
|
|
100091
100456
|
case "sequence": return host.runGrantedSequence(grant);
|
|
100092
100457
|
case "addon-update": return host.applyGrantedAddonUpdate(grant);
|
|
100093
100458
|
case "server-update": return host.applyGrantedServerUpdate(grant);
|
|
100459
|
+
case "detection-verdict": return host.applyGrantedDetectionVerdict(grant);
|
|
100460
|
+
}
|
|
100461
|
+
}
|
|
100462
|
+
/** What a person is told when the retrain lifecycle refuses the mark. */
|
|
100463
|
+
var VERDICT_REFUSAL_COPY = {
|
|
100464
|
+
trained: "This track was already used for training — nothing was changed",
|
|
100465
|
+
budget: "This camera already holds as many tracks for retraining as it may — nothing was changed",
|
|
100466
|
+
"unknown-track": "This track is no longer available — nothing was changed"
|
|
100467
|
+
};
|
|
100468
|
+
/**
|
|
100469
|
+
* Mark the track one notification reported for retraining, and record who was
|
|
100470
|
+
* asked and when.
|
|
100471
|
+
*
|
|
100472
|
+
* Order matters. The MARK goes first and is the authority: a refusal ("already
|
|
100473
|
+
* trained", "budget full") means the operator's answer changed nothing, so no
|
|
100474
|
+
* ledger row is written for an answer that was not applied. The token is
|
|
100475
|
+
* already spent (D47) — the refusal is shown as the reason, not a retry.
|
|
100476
|
+
*/
|
|
100477
|
+
async function applyGrantedDetectionVerdict(host, grant) {
|
|
100478
|
+
const mark = host.deps.markTrackForRetrain;
|
|
100479
|
+
if (mark === void 0) throw new Error("no retrain mark is wired on this node");
|
|
100480
|
+
const outcome = await mark({
|
|
100481
|
+
deviceId: grant.deviceId,
|
|
100482
|
+
trackId: grant.trackId
|
|
100483
|
+
});
|
|
100484
|
+
if (!outcome.ok) {
|
|
100485
|
+
host.logger.info("detection verdict changed nothing — the retrain flow refused the mark", {
|
|
100486
|
+
tags: { deviceId: grant.deviceId },
|
|
100487
|
+
meta: {
|
|
100488
|
+
trackId: grant.trackId,
|
|
100489
|
+
reason: outcome.reason,
|
|
100490
|
+
verdict: grant.verdict
|
|
100491
|
+
}
|
|
100492
|
+
});
|
|
100493
|
+
throw new NcActionRefusedError(VERDICT_REFUSAL_COPY[outcome.reason]);
|
|
100094
100494
|
}
|
|
100495
|
+
await host.verdicts.record({
|
|
100496
|
+
trackId: grant.trackId,
|
|
100497
|
+
deviceId: grant.deviceId,
|
|
100498
|
+
verdict: grant.verdict,
|
|
100499
|
+
...grant.suggestedClass !== void 0 ? { suggestedClass: grant.suggestedClass } : {},
|
|
100500
|
+
userId: grant.userId,
|
|
100501
|
+
ruleId: grant.ruleId,
|
|
100502
|
+
ruleName: grant.ruleName,
|
|
100503
|
+
at: host.now()
|
|
100504
|
+
});
|
|
100095
100505
|
}
|
|
100096
100506
|
/**
|
|
100097
100507
|
* Apply the addon updates one notification was about.
|
|
@@ -105269,6 +105679,7 @@ var NotificationCenter = class {
|
|
|
105269
105679
|
* evaluating one.
|
|
105270
105680
|
*/
|
|
105271
105681
|
pruneLedger;
|
|
105682
|
+
verdicts;
|
|
105272
105683
|
/** D905: which rules cannot work, announced once per distinct reason. */
|
|
105273
105684
|
ruleHealth = new NcRuleHealthMonitor();
|
|
105274
105685
|
/** The prune itself — see `device-prune-runner.ts`. */
|
|
@@ -105497,6 +105908,11 @@ var NotificationCenter = class {
|
|
|
105497
105908
|
applyGrantedArmAnyway: (panel, grant) => self.applyGrantedArmAnyway(panel, grant),
|
|
105498
105909
|
applyGrantedBypassInput: (panel, grant) => self.applyGrantedBypassInput(panel, grant),
|
|
105499
105910
|
applyGrantedServerUpdate: (grant) => self.applyGrantedServerUpdate(grant),
|
|
105911
|
+
applyGrantedDetectionVerdict: (grant) => self.applyGrantedDetectionVerdict(grant),
|
|
105912
|
+
now: self.now,
|
|
105913
|
+
get verdicts() {
|
|
105914
|
+
return self.verdicts;
|
|
105915
|
+
},
|
|
105500
105916
|
applyGrantedSnooze: (grant) => self.applyGrantedSnooze(grant),
|
|
105501
105917
|
get deps() {
|
|
105502
105918
|
return self.deps;
|
|
@@ -105547,6 +105963,9 @@ var NotificationCenter = class {
|
|
|
105547
105963
|
async applyGrantedAddonUpdate(grant) {
|
|
105548
105964
|
return applyGrantedAddonUpdate(this.grantedActionsHost(), grant);
|
|
105549
105965
|
}
|
|
105966
|
+
async applyGrantedDetectionVerdict(grant) {
|
|
105967
|
+
return applyGrantedDetectionVerdict(this.grantedActionsHost(), grant);
|
|
105968
|
+
}
|
|
105550
105969
|
async applyGrantedServerUpdate(grant) {
|
|
105551
105970
|
return applyGrantedServerUpdate(this.grantedActionsHost(), grant);
|
|
105552
105971
|
}
|
|
@@ -105786,6 +106205,10 @@ var NotificationCenter = class {
|
|
|
105786
106205
|
}
|
|
105787
106206
|
this.summaryRules = NcSummaryStore.of(deps.records, this.logger.child("summary"), deps.now);
|
|
105788
106207
|
this.calendarEventsRules = NcCalendarEventsStore.of(deps.records, this.logger.child("calendar-events"), deps.now);
|
|
106208
|
+
this.verdicts = new NcDetectionVerdictLedger({
|
|
106209
|
+
...durableStore,
|
|
106210
|
+
logger: this.logger.child("detection-verdicts")
|
|
106211
|
+
});
|
|
105789
106212
|
this.pruneLedger = new NcPruneLedger({
|
|
105790
106213
|
...durableStore,
|
|
105791
106214
|
logger: this.logger.child("device-prune")
|
|
@@ -108393,6 +108816,23 @@ var BuildNotificationCenter = class extends BuildNotificationCenterBase {
|
|
|
108393
108816
|
};
|
|
108394
108817
|
}
|
|
108395
108818
|
},
|
|
108819
|
+
markTrackForRetrain: async ({ deviceId, trackId }) => {
|
|
108820
|
+
try {
|
|
108821
|
+
await this.ctx.api.pipelineAnalytics.setTrackFlags.mutate({
|
|
108822
|
+
deviceId,
|
|
108823
|
+
trackId,
|
|
108824
|
+
flags: { markForTrain: true }
|
|
108825
|
+
});
|
|
108826
|
+
return { ok: true };
|
|
108827
|
+
} catch (err) {
|
|
108828
|
+
const reason = retrainRefusalReason(err);
|
|
108829
|
+
if (reason === null) throw err;
|
|
108830
|
+
return {
|
|
108831
|
+
ok: false,
|
|
108832
|
+
reason
|
|
108833
|
+
};
|
|
108834
|
+
}
|
|
108835
|
+
},
|
|
108396
108836
|
readDeviceStates: async (ids) => {
|
|
108397
108837
|
const out = /* @__PURE__ */ new Map();
|
|
108398
108838
|
for (const id of ids) try {
|
|
@@ -108464,9 +108904,11 @@ var BuildNotificationCenter = class extends BuildNotificationCenterBase {
|
|
|
108464
108904
|
const targets = await api.notificationOutput.listTargets.query({});
|
|
108465
108905
|
const kinds = await api.notificationOutput.listTargetKinds.query({}).catch(() => []);
|
|
108466
108906
|
const bodyMaxLenByKind = new Map(kinds.map((k) => [k.kind, k.caps.bodyMaxLen]));
|
|
108907
|
+
const maxActionsByKind = new Map(kinds.map((k) => [k.kind, k.caps.actions]));
|
|
108467
108908
|
return targets.map((t) => {
|
|
108468
108909
|
const owner = t.config["ownerUserId"];
|
|
108469
108910
|
const bodyMaxLen = bodyMaxLenByKind.get(t.kind);
|
|
108911
|
+
const maxActions = maxActionsByKind.get(t.kind);
|
|
108470
108912
|
return {
|
|
108471
108913
|
id: t.id,
|
|
108472
108914
|
addonId: t.addonId,
|
|
@@ -108474,7 +108916,8 @@ var BuildNotificationCenter = class extends BuildNotificationCenterBase {
|
|
|
108474
108916
|
kind: t.kind,
|
|
108475
108917
|
enabled: t.enabled,
|
|
108476
108918
|
...typeof owner === "string" && owner.length > 0 ? { ownerUserId: owner } : {},
|
|
108477
|
-
...typeof bodyMaxLen === "number" ? { bodyMaxLen } : {}
|
|
108919
|
+
...typeof bodyMaxLen === "number" ? { bodyMaxLen } : {},
|
|
108920
|
+
...typeof maxActions === "number" ? { maxActions } : {}
|
|
108478
108921
|
};
|
|
108479
108922
|
});
|
|
108480
108923
|
},
|