@camstack/addon-post-analysis 1.2.641 → 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.
@@ -48242,7 +48242,20 @@ var NcRuleInputSchema = object({
48242
48242
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
48243
48243
  * path does not run Zod defaults.
48244
48244
  */
48245
- groupingSec: number().int().min(5).max(60).optional()
48245
+ groupingSec: number().int().min(5).max(60).optional(),
48246
+ /**
48247
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
48248
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
48249
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
48250
+ * same mark the Library writes, and records whose notification it was and
48251
+ * when. A channel with no buttons gets the same two choices as links.
48252
+ *
48253
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
48254
+ * cap path): absent and `false` both mean off, which is what every rule
48255
+ * authored before this field meant. An editor turns it off by writing
48256
+ * `false`, so it needs no `clear` key.
48257
+ */
48258
+ askVerdict: boolean().optional()
48246
48259
  });
48247
48260
  /**
48248
48261
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -48273,7 +48273,20 @@ var NcRuleInputSchema = object({
48273
48273
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
48274
48274
  * path does not run Zod defaults.
48275
48275
  */
48276
- groupingSec: number().int().min(5).max(60).optional()
48276
+ groupingSec: number().int().min(5).max(60).optional(),
48277
+ /**
48278
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
48279
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
48280
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
48281
+ * same mark the Library writes, and records whose notification it was and
48282
+ * when. A channel with no buttons gets the same two choices as links.
48283
+ *
48284
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
48285
+ * cap path): absent and `false` both mean off, which is what every rule
48286
+ * authored before this field meant. An editor turns it off by writing
48287
+ * `false`, so it needs no `clear` key.
48288
+ */
48289
+ askVerdict: boolean().optional()
48277
48290
  });
48278
48291
  /**
48279
48292
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -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-Qr0XipkX.js");
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);
8
8
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Qi as BaseAddon, _r as embeddingRequirementsUnavailableError, a as CLIP_EMBEDDING_STEP_ID, da as normalizeNodeId, gi as resolvePoolMemoryPolicy, gr as embeddingRequirementsFailedError, i as CLIP_TEXT_MODELS, l as resolveClusterModelPin, mr as embeddingModelFailedError, n as DEFAULT_CLIP_MODEL, ni as parseProcStatus, pr as embeddingEncoderCapability, r as CLIP_IMAGE_MODELS, t as BUILTIN_CLIP_MODELS, tn as PoolMemoryWatchdog } from "../clip-model-registry-Cw7Iy7Qi.mjs";
1
+ import { Qi as BaseAddon, _r as embeddingRequirementsUnavailableError, a as CLIP_EMBEDDING_STEP_ID, da as normalizeNodeId, gi as resolvePoolMemoryPolicy, gr as embeddingRequirementsFailedError, i as CLIP_TEXT_MODELS, l as resolveClusterModelPin, mr as embeddingModelFailedError, n as DEFAULT_CLIP_MODEL, ni as parseProcStatus, pr as embeddingEncoderCapability, r as CLIP_IMAGE_MODELS, t as BUILTIN_CLIP_MODELS, tn as PoolMemoryWatchdog } from "../clip-model-registry-CoH7ORtC.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";
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.422",
21
+ version: "1.2.423",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_pipeline_analytics_widgets",
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.422",
39
+ version: "1.2.423",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  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_clip_model_registry = require("../clip-model-registry-Qr0XipkX.js");
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") ?? "")}&amp;sig=${encodeURIComponent(url.searchParams.get("sig") ?? "")}&amp;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 background = (req.method ?? "GET").toUpperCase() === "POST";
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.",
@@ -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.",
@@ -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
@@ -87019,6 +87233,8 @@ var NcDispatcher = class {
87019
87233
  targetId: target.id,
87020
87234
  ...target.ownerUserId !== void 0 ? { ownerUserId: target.ownerUserId } : {},
87021
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 } : {},
87022
87238
  ...entry.payload.subject.systemEvent !== void 0 ? { systemEvent: entry.payload.subject.systemEvent } : {}
87023
87239
  });
87024
87240
  } catch (err) {
@@ -87114,9 +87330,13 @@ var NcDispatcher = class {
87114
87330
  ...summarySectionOf(systemEvent.data)
87115
87331
  } } : {}
87116
87332
  }) : null;
87117
- const actions = await this.mintActions(entry, target);
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}`;
87118
87338
  const notification = {
87119
- body,
87339
+ body: withLinks(body),
87120
87340
  title,
87121
87341
  priority: clampPriority(paramNumber(entry.payload.params, "priority") ?? entry.payload.priority),
87122
87342
  ...params,
@@ -87138,7 +87358,7 @@ var NcDispatcher = class {
87138
87358
  grouped: resolved.grouped,
87139
87359
  withAiText: (aiText) => ({
87140
87360
  ...notification,
87141
- body: renderBody(aiText)
87361
+ body: withLinks(renderBody(aiText))
87142
87362
  })
87143
87363
  };
87144
87364
  }
@@ -99751,6 +99971,55 @@ async function mintButtons(host, input) {
99751
99971
  const announcement = stored === null && input.ruleId === NC_ALARM_ANNOUNCE_RULE_ID;
99752
99972
  if (stored === null && !announcement) return [];
99753
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
+ });
99754
100023
  const sequenceButtons = host.actionRunner === null || stored === null ? [] : buildActionButtons({
99755
100024
  actions: stored.actions,
99756
100025
  knownSequenceIds: host.sequences.knownIds(),
@@ -99963,6 +100232,7 @@ async function mintButtons(host, input) {
99963
100232
  }
99964
100233
  });
99965
100234
  const out = [
100235
+ ...verdictButtons,
99966
100236
  ...sequenceButtons,
99967
100237
  ...snoozeButtons,
99968
100238
  ...alarmButtons,
@@ -100186,8 +100456,53 @@ async function runGrantedAction(host, grant) {
100186
100456
  case "sequence": return host.runGrantedSequence(grant);
100187
100457
  case "addon-update": return host.applyGrantedAddonUpdate(grant);
100188
100458
  case "server-update": return host.applyGrantedServerUpdate(grant);
100459
+ case "detection-verdict": return host.applyGrantedDetectionVerdict(grant);
100189
100460
  }
100190
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]);
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
+ });
100505
+ }
100191
100506
  /**
100192
100507
  * Apply the addon updates one notification was about.
100193
100508
  *
@@ -105364,6 +105679,7 @@ var NotificationCenter = class {
105364
105679
  * evaluating one.
105365
105680
  */
105366
105681
  pruneLedger;
105682
+ verdicts;
105367
105683
  /** D905: which rules cannot work, announced once per distinct reason. */
105368
105684
  ruleHealth = new NcRuleHealthMonitor();
105369
105685
  /** The prune itself — see `device-prune-runner.ts`. */
@@ -105592,6 +105908,11 @@ var NotificationCenter = class {
105592
105908
  applyGrantedArmAnyway: (panel, grant) => self.applyGrantedArmAnyway(panel, grant),
105593
105909
  applyGrantedBypassInput: (panel, grant) => self.applyGrantedBypassInput(panel, grant),
105594
105910
  applyGrantedServerUpdate: (grant) => self.applyGrantedServerUpdate(grant),
105911
+ applyGrantedDetectionVerdict: (grant) => self.applyGrantedDetectionVerdict(grant),
105912
+ now: self.now,
105913
+ get verdicts() {
105914
+ return self.verdicts;
105915
+ },
105595
105916
  applyGrantedSnooze: (grant) => self.applyGrantedSnooze(grant),
105596
105917
  get deps() {
105597
105918
  return self.deps;
@@ -105642,6 +105963,9 @@ var NotificationCenter = class {
105642
105963
  async applyGrantedAddonUpdate(grant) {
105643
105964
  return applyGrantedAddonUpdate(this.grantedActionsHost(), grant);
105644
105965
  }
105966
+ async applyGrantedDetectionVerdict(grant) {
105967
+ return applyGrantedDetectionVerdict(this.grantedActionsHost(), grant);
105968
+ }
105645
105969
  async applyGrantedServerUpdate(grant) {
105646
105970
  return applyGrantedServerUpdate(this.grantedActionsHost(), grant);
105647
105971
  }
@@ -105881,6 +106205,10 @@ var NotificationCenter = class {
105881
106205
  }
105882
106206
  this.summaryRules = NcSummaryStore.of(deps.records, this.logger.child("summary"), deps.now);
105883
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
+ });
105884
106212
  this.pruneLedger = new NcPruneLedger({
105885
106213
  ...durableStore,
105886
106214
  logger: this.logger.child("device-prune")
@@ -108488,6 +108816,23 @@ var BuildNotificationCenter = class extends BuildNotificationCenterBase {
108488
108816
  };
108489
108817
  }
108490
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
+ },
108491
108836
  readDeviceStates: async (ids) => {
108492
108837
  const out = /* @__PURE__ */ new Map();
108493
108838
  for (const id of ids) try {
@@ -108559,9 +108904,11 @@ var BuildNotificationCenter = class extends BuildNotificationCenterBase {
108559
108904
  const targets = await api.notificationOutput.listTargets.query({});
108560
108905
  const kinds = await api.notificationOutput.listTargetKinds.query({}).catch(() => []);
108561
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]));
108562
108908
  return targets.map((t) => {
108563
108909
  const owner = t.config["ownerUserId"];
108564
108910
  const bodyMaxLen = bodyMaxLenByKind.get(t.kind);
108911
+ const maxActions = maxActionsByKind.get(t.kind);
108565
108912
  return {
108566
108913
  id: t.id,
108567
108914
  addonId: t.addonId,
@@ -108569,7 +108916,8 @@ var BuildNotificationCenter = class extends BuildNotificationCenterBase {
108569
108916
  kind: t.kind,
108570
108917
  enabled: t.enabled,
108571
108918
  ...typeof owner === "string" && owner.length > 0 ? { ownerUserId: owner } : {},
108572
- ...typeof bodyMaxLen === "number" ? { bodyMaxLen } : {}
108919
+ ...typeof bodyMaxLen === "number" ? { bodyMaxLen } : {},
108920
+ ...typeof maxActions === "number" ? { maxActions } : {}
108573
108921
  };
108574
108922
  });
108575
108923
  },
@@ -1,4 +1,4 @@
1
- import { $ as MediaFileKindEnum, $i as CAP_ADVANCE_FIELDS, $n as cloneAlarmSettings, $r as paintHex, $t as OpsLogEntrySchema, A as DeclaredDevices, Aa as EventCategory, Ai as subKindsOf, An as TrackSideSchema, Ar as isAudioClassName, At as NcAlarmSensorsInputSchema, B as FULL_IMAGE_BBOX, Bi as templateCarriesSummary, Bn as applyModeDelaysEdits, Br as isRootSuppliableStepId, Bt as NcRuleUpdateInputSchema, C as ClearTrackRelationResultSchema, Ca as record, Ci as ruleKindOf, Cn as TimelapseRuleInputSchema, Cr as failureContributionCapability, Ct as NC_SNOOZE_MAX_MINUTES, D as DEFAULT_TIMELAPSE_PREVIEW_TEXT, Da as tuple, Di as servedTemplateVars, Dn as TrackPuritySchema, Dr as isAlertEpisodeOpen, Dt as NC_TEMPLATE_VARS, E as DEFAULT_EVENT_COLOR, Ea as templateLiteral, Ei as sensorRaisedOf, En as TrackPlacementSchema, Er as isAlarmControllableCap, Et as NC_TAXONOMY, F as EVENT_DENSITY_BATCH_MAX, Fi as systemEventDeliveryProblems, Fn as alarmPanelCapability, Fr as isEventOwnerType, Ft as NcRuleActionSchema, G as ListTrackRelationsResultSchema, Gi as zoneAnalyticsCapability, Gn as audioMetricsCapability, Gr as kebabToCamel, Gt as NcSnoozeSuppressedSchema, H as LABEL_SUPPLY_MACRO, Hi as vectorDimFromBase64, Hn as applySecureOnArmEdit, Hr as isSystemDelivery, Ht as NcSharedSequenceSchema, I as EVENT_KIND_BY_CAP, Ii as systemEventFamilyProblems, In as alertEpisodeId, Ir as isKnownTemplateVar, It as NcRuleInputSchema, J as MAX_BIRTH_DECISION_RECORDS, Ji as evaluateSensorEdge, Jn as buildSubjectPath, Jr as memoizePinnedFfmpeg, Jt as NcTaxonomySchema, K as LockStateSchema, Ki as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, Kn as audioModeOf, Kr as linkedStepOf, Kt as NcStateSummarySectionIdSchema, L as EVENT_OWNER_TYPES, Li as systemEventFilterApplies, Ln as alertEpisodeKind, Lr as isLabelSupplyStepId, Lt as NcRulePatchSchema, M as EMBEDDING_MODEL_FAILED, Mi as subjectPathSvgPoints, Mn as activateAlarmProfile, Mr as isDetectionMacroClass, Mt as NcAlarmSettingsPatchSchema, N as EMBEDDING_REQUIREMENTS_FAILED, Ni as summarizeReview, Nn as addonWidgetsSourceCapability, Nr as isDrawableSubjectPath, Nt as NcConditionDescriptorSchema, O as DETECTION_MACRO_CLASSES, Oa as union, Oi as startMoverUnderClearance, On as TrackRelationKindSchema, Or as isAlertEpisodeSeverity, Ot as NcAlarmConfigSchema, P as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Pi as syncActiveAlarmProfileSettings, Pn as alarmDeviceSecureOnArm, Pr as isEventKindLabelKey, Pt as NcConditionsSchema, Q as MOTION_CLOSE_AFTER_MS, Qi as BaseAddon, Qn as classifyRetrainReview, Qr as occupancyScope, Qt as OPS_LOG_DEFAULT_LIMIT, R as EXPORT_DENSE_MAX_RANGES, Ri as systemEventSummaryScheduleOf, Rn as alertEpisodePayload, Rr as isMediaOwnerType, Rt as NcRuleSchema, S as ClearTrackRelationInputSchema, Sa as partialRecord, Si as ruleGroupingApplies, Sn as TRACK_RELATIONS_DEFAULT_LIMIT, Sr as faceGalleryCapability, St as NC_SENSOR_TRANSITION_KINDS, T as DEFAULT_DETAIL_CROP_CONVENTION, Ta as string, Ti as secureActuationFor, Tn as TimelapseRuleSchema, Tr as gifFromMp4Args, Tt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, U as LabelAttributionSchema, Ui as viewerLinkBaseOf, Un as armRemovalReconcile, Ur as isSystemEventSummaryRule, Ut as NcSnoozeInputSchema, V as FailureCounters, Vi as templateVarsFor, Vn as applyNonBlockingEdit, Vr as isScheduleActive, Vt as NcScheduleSchema, W as ListTrackRelationsInputSchema, Wi as withSystemEventClasses, Wn as assertTimelapseCadences, Wr as isVectorIndexDimConflict, Wt as NcSnoozeSchema, X as MAX_TRACK_PURITIES, Xi as DeviceType, Xn as bundledCamstack, Xr as normalizeNcMediaPolicy, Xt as NcTemplatePreviewSchema, Y as MAX_BIRTH_LATENCY_PROXY_MS, Yi as isSourceCap, Yn as buildViewerLink, Yr as ncMediaPolicyError, Yt as NcTemplatePreviewInputSchema, Z as MAX_TRACK_RELATIONS, Zi as errMsg, Zn as calendarEventLocalSpan, Zr as notificationRulesCapability, Zt as NcTemplateVarDescriptorSchema, _ as BIRTH_DECISION_DEFAULT_LIMIT, _a as discriminatedUnion, _i as retrainRevisionConflictMessage, _n as SceneMonitorSchema, _t as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, a as CLIP_EMBEDDING_STEP_ID, aa as customAction, ai as plateGalleryCapability, an as RetrainDismissedSchema, ar as deleteAlarmProfile, at as NC_ALARM_NOTIFY_FIELDS, b as BucketAccumulator, ba as number, bi as ruleBelongsToAlarm$1, bn as SetTrackRelationInputSchema, br as eventKindLabel, bt as NC_GROUPING_MAX_SEC, c as readClusterModelPinStrict, ca as hydrateSchema, ci as readMotionTriggeredBy, cn as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, cr as deriveRecordingMode, ct as NC_AUDIO_CONFIRM_HITS_DEFAULT, d as APPEARANCE_BLOCKS, di as resolveClipSearchMinScore, dn as SCENE_DEFAULT_CHECK_INTERVAL_SEC, dr as deviceAttentionKind, dt as NC_CLIP_PRE_ROLL_DEFAULT_SEC, ea as CamProfileSchema, ei as paletteResultSchema, en as PackageLlmVerdictSchema, er as consumableFormOf, et as MotionTriggeredBySchema, f as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, fa as sleep$1, fi as resolveLocationMode, fn as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, fr as deviceRemovalHookCapability, ft as NC_CLIP_RETENTION_MAX_SEC, g as ArchivedDebugNoteSchema, ga as boolean, gn as SCENE_RESET_RECAPTURES, gt as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, h as AlarmArmModeSchema, ha as array, hi as resolveNcMediaImage, hn as SCENE_DIVERGED, hr as embeddingRefusalOf, ht as NC_CONFIRM_DEFAULT_TIMEOUT_MS, ia as createEvent, ii as pipelineAnalyticsCapability, in as RebuildObjectEmbeddingsRefusalSchema, ir as createAlarmProfile, it as NC_ALARM_DELAY_MAX_SEC, j as DetectionSourceSchema, ji as subjectPathSampleFromBox, jn as TrackSourceSchema, jr as isDefaultAlarmProfileId, jt as NcAlarmSensorsSchema, k as DETECTION_PIPELINE_CAP_NAME, ka as unknown, ki as storageOccupancyCapability, kn as TrackRelationSchema, kr as isAppLinkBase, kt as NcAlarmPostTriggeredActionSchema, l as resolveClusterModelPin, la as isDeviceScopedCap, li as readTimelapseGeneratedAt, ln as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, lr as describeSystemEventDeliveryProblem, lt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, m as AUDIO_MACRO_LABELS, ma as _enum, mi as resolveModeNotifyPolicy, mn as SCENE_DEFAULT_UNCOVERED_POLICY, mt as NC_CONFIRM_DEFAULT_MAX_IMAGE_PX, na as RECORD_MAX_DATA_BYTES, nn as RECORDING_EXPORT_MAX_READ_BYTES, nr as coreBlockIdFromAddonId, nt as NC_ALARM_DEFAULT_TRIGGERED_DURATION_SEC, o as CLUSTER_MODEL_OWNER_ADDON_ID, oa as defineCustomActions, oi as projectSubjectPath, on as RetrainOriginSchema, or as deriveDetailCropRect, ot as NC_ALARM_SYSTEM_EVENT_KINDS, p as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, pa as ZodObject, pi as resolveModeDelays, pn as SCENE_DEFAULT_QUIET_SECONDS, pt as NC_CONDITION_CATALOG, q as MAX_ARCHIVED_DEBUG_NOTES, qi as canonicalHash, qn as buildEventKindDescriptor, qr as mayWriteToLocation, qt as NcSystemEventKindSchema, ra as RECORD_QUERY_MAX, ri as pickFaceRescoreIntervalMinutes, rn as RECORDING_SOURCE_CAMSTACK_ADDON, rr as cosineSimilarity$1, rt as NC_ALARM_DELAY_FIELDS, s as FACE_EMBEDDING_STEP_ID, sa as defineRecordKinds, si as readDeviceStateFrom, sn as RetrainStatusSchema, sr as derivePostTriggeredAction, st as NC_ALARM_TRIGGERED_MAX_SEC, t as BUILTIN_CLIP_MODELS, ta as InvalidRecordError, ti as parseAlertEpisodePayload, tr as consumableLowPercentOf, tt as NC_ALARM_DEFAULT_PROFILE_ID, u as APPEARANCE_BIN_COUNT, ua as nodePin, ui as renameAlarmProfile, un as SCENE_DEFAULT_ANCHOR_THRESHOLD, ur as describeSystemEventFamilyProblem, ut as NC_CLIP_POST_ROLL_DEFAULT_SEC, v as BaseDevice, va as literal, vi as rootSupplyServes, vn as SetTrackPurityInputSchema, vr as encodeVectorBase64, vt as NC_DEFAULT_SNOOZE_MINUTES, w as CoverStateSchema, wa as strictObject, wi as sceneMonitorCapability, wn as TimelapseRulePatchSchema, wr as gifDeriveSpeed, wt as NC_STATE_SUMMARY_SECTION_IDS, x as COCO_TO_MACRO, xa as object, xi as ruleDeviceCompatCapQueries, xn as TIMELAPSE_DENSE_FLOOR_SEC, xr as evictionPolicyOfLocation, xt as NC_GROUPING_MIN_SEC, y as BirthDecisionRecordSchema, ya as looseObject, yi as ruleAutoTriggersAlarm, yn as SetTrackPurityResultSchema, yr as ensureAlarmProfiles, yt as NC_GROUPING_DEFAULT_SEC, z as FIRST_LEVEL_MACRO_CLASSES, zi as systemEventSummarySplitOf, zn as applyBlockingEdit, zr as isRetiredClearKey, zt as NcRuleTargetSchema } from "../clip-model-registry-Cw7Iy7Qi.mjs";
1
+ import { $ as MediaFileKindEnum, $i as CAP_ADVANCE_FIELDS, $n as cloneAlarmSettings, $r as paintHex, $t as OpsLogEntrySchema, A as DeclaredDevices, Aa as EventCategory, Ai as subKindsOf, An as TrackSideSchema, Ar as isAudioClassName, At as NcAlarmSensorsInputSchema, B as FULL_IMAGE_BBOX, Bi as templateCarriesSummary, Bn as applyModeDelaysEdits, Br as isRootSuppliableStepId, Bt as NcRuleUpdateInputSchema, C as ClearTrackRelationResultSchema, Ca as record, Ci as ruleKindOf, Cn as TimelapseRuleInputSchema, Cr as failureContributionCapability, Ct as NC_SNOOZE_MAX_MINUTES, D as DEFAULT_TIMELAPSE_PREVIEW_TEXT, Da as tuple, Di as servedTemplateVars, Dn as TrackPuritySchema, Dr as isAlertEpisodeOpen, Dt as NC_TEMPLATE_VARS, E as DEFAULT_EVENT_COLOR, Ea as templateLiteral, Ei as sensorRaisedOf, En as TrackPlacementSchema, Er as isAlarmControllableCap, Et as NC_TAXONOMY, F as EVENT_DENSITY_BATCH_MAX, Fi as systemEventDeliveryProblems, Fn as alarmPanelCapability, Fr as isEventOwnerType, Ft as NcRuleActionSchema, G as ListTrackRelationsResultSchema, Gi as zoneAnalyticsCapability, Gn as audioMetricsCapability, Gr as kebabToCamel, Gt as NcSnoozeSuppressedSchema, H as LABEL_SUPPLY_MACRO, Hi as vectorDimFromBase64, Hn as applySecureOnArmEdit, Hr as isSystemDelivery, Ht as NcSharedSequenceSchema, I as EVENT_KIND_BY_CAP, Ii as systemEventFamilyProblems, In as alertEpisodeId, Ir as isKnownTemplateVar, It as NcRuleInputSchema, J as MAX_BIRTH_DECISION_RECORDS, Ji as evaluateSensorEdge, Jn as buildSubjectPath, Jr as memoizePinnedFfmpeg, Jt as NcTaxonomySchema, K as LockStateSchema, Ki as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, Kn as audioModeOf, Kr as linkedStepOf, Kt as NcStateSummarySectionIdSchema, L as EVENT_OWNER_TYPES, Li as systemEventFilterApplies, Ln as alertEpisodeKind, Lr as isLabelSupplyStepId, Lt as NcRulePatchSchema, M as EMBEDDING_MODEL_FAILED, Mi as subjectPathSvgPoints, Mn as activateAlarmProfile, Mr as isDetectionMacroClass, Mt as NcAlarmSettingsPatchSchema, N as EMBEDDING_REQUIREMENTS_FAILED, Ni as summarizeReview, Nn as addonWidgetsSourceCapability, Nr as isDrawableSubjectPath, Nt as NcConditionDescriptorSchema, O as DETECTION_MACRO_CLASSES, Oa as union, Oi as startMoverUnderClearance, On as TrackRelationKindSchema, Or as isAlertEpisodeSeverity, Ot as NcAlarmConfigSchema, P as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Pi as syncActiveAlarmProfileSettings, Pn as alarmDeviceSecureOnArm, Pr as isEventKindLabelKey, Pt as NcConditionsSchema, Q as MOTION_CLOSE_AFTER_MS, Qi as BaseAddon, Qn as classifyRetrainReview, Qr as occupancyScope, Qt as OPS_LOG_DEFAULT_LIMIT, R as EXPORT_DENSE_MAX_RANGES, Ri as systemEventSummaryScheduleOf, Rn as alertEpisodePayload, Rr as isMediaOwnerType, Rt as NcRuleSchema, S as ClearTrackRelationInputSchema, Sa as partialRecord, Si as ruleGroupingApplies, Sn as TRACK_RELATIONS_DEFAULT_LIMIT, Sr as faceGalleryCapability, St as NC_SENSOR_TRANSITION_KINDS, T as DEFAULT_DETAIL_CROP_CONVENTION, Ta as string, Ti as secureActuationFor, Tn as TimelapseRuleSchema, Tr as gifFromMp4Args, Tt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, U as LabelAttributionSchema, Ui as viewerLinkBaseOf, Un as armRemovalReconcile, Ur as isSystemEventSummaryRule, Ut as NcSnoozeInputSchema, V as FailureCounters, Vi as templateVarsFor, Vn as applyNonBlockingEdit, Vr as isScheduleActive, Vt as NcScheduleSchema, W as ListTrackRelationsInputSchema, Wi as withSystemEventClasses, Wn as assertTimelapseCadences, Wr as isVectorIndexDimConflict, Wt as NcSnoozeSchema, X as MAX_TRACK_PURITIES, Xi as DeviceType, Xn as bundledCamstack, Xr as normalizeNcMediaPolicy, Xt as NcTemplatePreviewSchema, Y as MAX_BIRTH_LATENCY_PROXY_MS, Yi as isSourceCap, Yn as buildViewerLink, Yr as ncMediaPolicyError, Yt as NcTemplatePreviewInputSchema, Z as MAX_TRACK_RELATIONS, Zi as errMsg, Zn as calendarEventLocalSpan, Zr as notificationRulesCapability, Zt as NcTemplateVarDescriptorSchema, _ as BIRTH_DECISION_DEFAULT_LIMIT, _a as discriminatedUnion, _i as retrainRevisionConflictMessage, _n as SceneMonitorSchema, _t as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, a as CLIP_EMBEDDING_STEP_ID, aa as customAction, ai as plateGalleryCapability, an as RetrainDismissedSchema, ar as deleteAlarmProfile, at as NC_ALARM_NOTIFY_FIELDS, b as BucketAccumulator, ba as number, bi as ruleBelongsToAlarm$1, bn as SetTrackRelationInputSchema, br as eventKindLabel, bt as NC_GROUPING_MAX_SEC, c as readClusterModelPinStrict, ca as hydrateSchema, ci as readMotionTriggeredBy, cn as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, cr as deriveRecordingMode, ct as NC_AUDIO_CONFIRM_HITS_DEFAULT, d as APPEARANCE_BLOCKS, di as resolveClipSearchMinScore, dn as SCENE_DEFAULT_CHECK_INTERVAL_SEC, dr as deviceAttentionKind, dt as NC_CLIP_PRE_ROLL_DEFAULT_SEC, ea as CamProfileSchema, ei as paletteResultSchema, en as PackageLlmVerdictSchema, er as consumableFormOf, et as MotionTriggeredBySchema, f as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, fa as sleep$1, fi as resolveLocationMode, fn as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, fr as deviceRemovalHookCapability, ft as NC_CLIP_RETENTION_MAX_SEC, g as ArchivedDebugNoteSchema, ga as boolean, gn as SCENE_RESET_RECAPTURES, gt as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, h as AlarmArmModeSchema, ha as array, hi as resolveNcMediaImage, hn as SCENE_DIVERGED, hr as embeddingRefusalOf, ht as NC_CONFIRM_DEFAULT_TIMEOUT_MS, ia as createEvent, ii as pipelineAnalyticsCapability, in as RebuildObjectEmbeddingsRefusalSchema, ir as createAlarmProfile, it as NC_ALARM_DELAY_MAX_SEC, j as DetectionSourceSchema, ji as subjectPathSampleFromBox, jn as TrackSourceSchema, jr as isDefaultAlarmProfileId, jt as NcAlarmSensorsSchema, k as DETECTION_PIPELINE_CAP_NAME, ka as unknown, ki as storageOccupancyCapability, kn as TrackRelationSchema, kr as isAppLinkBase, kt as NcAlarmPostTriggeredActionSchema, l as resolveClusterModelPin, la as isDeviceScopedCap, li as readTimelapseGeneratedAt, ln as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, lr as describeSystemEventDeliveryProblem, lt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, m as AUDIO_MACRO_LABELS, ma as _enum, mi as resolveModeNotifyPolicy, mn as SCENE_DEFAULT_UNCOVERED_POLICY, mt as NC_CONFIRM_DEFAULT_MAX_IMAGE_PX, na as RECORD_MAX_DATA_BYTES, nn as RECORDING_EXPORT_MAX_READ_BYTES, nr as coreBlockIdFromAddonId, nt as NC_ALARM_DEFAULT_TRIGGERED_DURATION_SEC, o as CLUSTER_MODEL_OWNER_ADDON_ID, oa as defineCustomActions, oi as projectSubjectPath, on as RetrainOriginSchema, or as deriveDetailCropRect, ot as NC_ALARM_SYSTEM_EVENT_KINDS, p as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, pa as ZodObject, pi as resolveModeDelays, pn as SCENE_DEFAULT_QUIET_SECONDS, pt as NC_CONDITION_CATALOG, q as MAX_ARCHIVED_DEBUG_NOTES, qi as canonicalHash, qn as buildEventKindDescriptor, qr as mayWriteToLocation, qt as NcSystemEventKindSchema, ra as RECORD_QUERY_MAX, ri as pickFaceRescoreIntervalMinutes, rn as RECORDING_SOURCE_CAMSTACK_ADDON, rr as cosineSimilarity$1, rt as NC_ALARM_DELAY_FIELDS, s as FACE_EMBEDDING_STEP_ID, sa as defineRecordKinds, si as readDeviceStateFrom, sn as RetrainStatusSchema, sr as derivePostTriggeredAction, st as NC_ALARM_TRIGGERED_MAX_SEC, t as BUILTIN_CLIP_MODELS, ta as InvalidRecordError, ti as parseAlertEpisodePayload, tr as consumableLowPercentOf, tt as NC_ALARM_DEFAULT_PROFILE_ID, u as APPEARANCE_BIN_COUNT, ua as nodePin, ui as renameAlarmProfile, un as SCENE_DEFAULT_ANCHOR_THRESHOLD, ur as describeSystemEventFamilyProblem, ut as NC_CLIP_POST_ROLL_DEFAULT_SEC, v as BaseDevice, va as literal, vi as rootSupplyServes, vn as SetTrackPurityInputSchema, vr as encodeVectorBase64, vt as NC_DEFAULT_SNOOZE_MINUTES, w as CoverStateSchema, wa as strictObject, wi as sceneMonitorCapability, wn as TimelapseRulePatchSchema, wr as gifDeriveSpeed, wt as NC_STATE_SUMMARY_SECTION_IDS, x as COCO_TO_MACRO, xa as object, xi as ruleDeviceCompatCapQueries, xn as TIMELAPSE_DENSE_FLOOR_SEC, xr as evictionPolicyOfLocation, xt as NC_GROUPING_MIN_SEC, y as BirthDecisionRecordSchema, ya as looseObject, yi as ruleAutoTriggersAlarm, yn as SetTrackPurityResultSchema, yr as ensureAlarmProfiles, yt as NC_GROUPING_DEFAULT_SEC, z as FIRST_LEVEL_MACRO_CLASSES, zi as systemEventSummarySplitOf, zn as applyBlockingEdit, zr as isRetiredClearKey, zt as NcRuleTargetSchema } from "../clip-model-registry-CoH7ORtC.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs$1 from "node:fs";
4
4
  import { createWriteStream, promises } from "node:fs";
@@ -750,6 +750,8 @@ var DEFAULT_ACTION_TTL_MS = 15 * 6e4;
750
750
  * notification that is not fresh.
751
751
  */
752
752
  var SNOOZE_ACTION_TTL_MS = 60 * 6e4;
753
+ /** How long a Correct / Wrong button stays live: one day. */
754
+ var VERDICT_ACTION_TTL_MS = 1440 * 6e4;
753
755
  /**
754
756
  * How long a DISARM button stays live: five minutes.
755
757
  *
@@ -777,6 +779,7 @@ function actionTtlMs(subject) {
777
779
  case "sequence": return DEFAULT_ACTION_TTL_MS;
778
780
  case "addon-update": return DEFAULT_ACTION_TTL_MS;
779
781
  case "server-update": return DEFAULT_ACTION_TTL_MS;
782
+ case "detection-verdict": return VERDICT_ACTION_TTL_MS;
780
783
  }
781
784
  }
782
785
  var NcActionTokenRegistry = class {
@@ -820,6 +823,30 @@ var NcActionTokenRegistry = class {
820
823
  grant: found.grant
821
824
  };
822
825
  }
826
+ /**
827
+ * Read the grant without spending it. ONLY the confirm page of a link-only
828
+ * channel may call this: it exists so a link preview that opens the URL
829
+ * cannot spend the token, and it never runs anything.
830
+ */
831
+ peek(id) {
832
+ const found = this.grants.get(id);
833
+ if (found === void 0) return {
834
+ ok: false,
835
+ reason: "unknown"
836
+ };
837
+ if (found.used) return {
838
+ ok: false,
839
+ reason: "already-used"
840
+ };
841
+ if (found.grant.expiresAt <= this.now()) return {
842
+ ok: false,
843
+ reason: "expired"
844
+ };
845
+ return {
846
+ ok: true,
847
+ grant: found.grant
848
+ };
849
+ }
823
850
  /** Drop records that can no longer be claimed. Memory hygiene only — an
824
851
  * expired record is already refused by {@link claim}, so a sweep that never
825
852
  * ran would cost bytes, never safety. */
@@ -861,6 +888,7 @@ function describeGrant(grant) {
861
888
  return grant.action === "disarm" ? "Alarm disarmed" : `Alarm armed${grant.mode !== void 0 ? ` (${grant.mode})` : ""}`;
862
889
  case "addon-update": return `${grant.packages.length} addon update(s) on ${grant.nodeId}`;
863
890
  case "server-update": return `Server update on ${grant.nodeId}`;
891
+ 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`;
864
892
  }
865
893
  }
866
894
  /** The audit line's `meta`, per family. A snooze that logged a `sequence` it
@@ -914,6 +942,15 @@ function grantMeta(grant) {
914
942
  ...common,
915
943
  nodeId: grant.nodeId
916
944
  };
945
+ case "detection-verdict": return {
946
+ ...common,
947
+ ruleId: grant.ruleId,
948
+ rule: grant.ruleName,
949
+ userId: grant.userId,
950
+ trackId: grant.trackId,
951
+ verdict: grant.verdict,
952
+ suggestedClass: grant.suggestedClass ?? null
953
+ };
917
954
  }
918
955
  }
919
956
  /** What the human standing in front of the page is told. */
@@ -922,6 +959,11 @@ var FAILURE_COPY = {
922
959
  expired: "This button has expired. Open CamStack to do it there.",
923
960
  "already-used": "Already done — this button had been used."
924
961
  };
962
+ /** The question a link-only channel lands on, answered by a POST form. */
963
+ function confirmPage(url, grant) {
964
+ const action = `${url.pathname}?exp=${encodeURIComponent(url.searchParams.get("exp") ?? "")}&amp;sig=${encodeURIComponent(url.searchParams.get("sig") ?? "")}&amp;confirm=1`;
965
+ 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>`;
966
+ }
925
967
  function page(title, detail) {
926
968
  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>`;
927
969
  }
@@ -935,7 +977,8 @@ var NcActionPlane = class {
935
977
  handler = async (req, res) => {
936
978
  const url = new URL(req.url ?? "/", "http://placeholder");
937
979
  const id = decodeURIComponent(url.pathname.split("/").filter(Boolean).pop() ?? "");
938
- const background = (req.method ?? "GET").toUpperCase() === "POST";
980
+ const posted = (req.method ?? "GET").toUpperCase() === "POST";
981
+ const background = posted && url.searchParams.get("confirm") !== "1";
939
982
  if (!verifyActionSignature({
940
983
  secret: this.deps.secret,
941
984
  id,
@@ -947,6 +990,13 @@ var NcActionPlane = class {
947
990
  this.reply(res, background, 404, "Not found", "This link is not valid.");
948
991
  return;
949
992
  }
993
+ if (!posted) {
994
+ const peeked = this.deps.registry.peek(id);
995
+ if (peeked.ok && peeked.grant.kind === "detection-verdict" && peeked.grant.viaLink === true) {
996
+ this.sendBody(res, 200, "text/html; charset=utf-8", confirmPage(url, peeked.grant));
997
+ return;
998
+ }
999
+ }
950
1000
  const claim = this.deps.registry.claim(id);
951
1001
  if (!claim.ok) {
952
1002
  this.deps.logger.info("nc action callback not claimable", { meta: {
@@ -1092,6 +1142,7 @@ var ncDurableRecordKinds = defineRecordKinds({
1092
1142
  "state-summary-stamps": ledger("digest delivery stamps per rule kind"),
1093
1143
  meta: ledger("outbox boot-reconcile watermark and other single-key meta rows", "settings", { none: "one fixed key (watermark), overwritten in place, never deleted" }),
1094
1144
  snoozes: ledger("operator opens or closes a snooze window (write-through)"),
1145
+ "detection-verdicts": ledger("operator taps Correct or Wrong on a detection notification; one row per track"),
1095
1146
  "snooze-suppressed": {
1096
1147
  ...QUERIED,
1097
1148
  class: "ledger",
@@ -9226,6 +9277,9 @@ var en_default = {
9226
9277
  "action.addon-update.uptodate": "Already up to date on {{nodeId}} — nothing to do",
9227
9278
  "action.addon-update.unknown": "Could not read what needs updating on {{nodeId}} — nothing was changed. Open CamStack.",
9228
9279
  "button.server-update.apply": "Update server on {{nodeId}}",
9280
+ "button.verdict.correct": "Correct",
9281
+ "button.verdict.wrong": "Wrong",
9282
+ "button.verdict.wrongAs": "Wrong: it is {{class}}",
9229
9283
  "action.server-update.title": "Server update",
9230
9284
  "action.server-update.started": "Updating server on {{nodeId}}…",
9231
9285
  "action.server-update.restarting": "Server update staged on {{nodeId}} — the node is restarting. You will hear when it comes back.",
@@ -9627,6 +9681,9 @@ var it_default = {
9627
9681
  "action.addon-update.uptodate": "Già aggiornato su {{nodeId}} — niente da fare",
9628
9682
  "action.addon-update.unknown": "Non è stato possibile leggere cosa aggiornare su {{nodeId}} — non è stato cambiato nulla. Apri CamStack.",
9629
9683
  "button.server-update.apply": "Aggiorna server su {{nodeId}}",
9684
+ "button.verdict.correct": "Corretto",
9685
+ "button.verdict.wrong": "Sbagliato",
9686
+ "button.verdict.wrongAs": "Sbagliato: è {{class}}",
9630
9687
  "action.server-update.title": "Aggiornamento server",
9631
9688
  "action.server-update.started": "Aggiornamento server su {{nodeId}}…",
9632
9689
  "action.server-update.restarting": "Aggiornamento server predisposto su {{nodeId}} — il nodo si riavvia. Ti avviseremo quando torna.",
@@ -59702,6 +59759,25 @@ var TrackAlreadyTrainedError = class extends Error {
59702
59759
  }
59703
59760
  };
59704
59761
  /**
59762
+ * Classify a failed `setTrackFlags` as one of the lifecycle's DELIBERATE
59763
+ * refusals, or `null` when it is a failure of the call itself.
59764
+ *
59765
+ * Needed because the caller that matters (a notification button) reaches the
59766
+ * method over `ctx.api`, where the error class does not survive the hop and
59767
+ * only the message does. The class check comes first; the message check is the
59768
+ * transported form, and `track-flags.spec.ts` pins it against the real
59769
+ * messages so a reworded refusal cannot silently turn into a generic failure.
59770
+ */
59771
+ function retrainRefusalReason(err) {
59772
+ if (err instanceof TrackAlreadyTrainedError) return "trained";
59773
+ if (err instanceof StagingBudgetExceededError) return "budget";
59774
+ const message = err instanceof Error ? err.message : "";
59775
+ if (message.includes("is already trained")) return "trained";
59776
+ if (message.includes("in retrain staging")) return "budget";
59777
+ if (message.includes("unknown track")) return "unknown-track";
59778
+ return null;
59779
+ }
59780
+ /**
59705
59781
  * The `debugNote` half of the patch — `{}` when nothing should be written.
59706
59782
  *
59707
59783
  * The note is not an independent column: it is the operator's sentence about
@@ -81843,6 +81919,10 @@ var NC_DEVICE_ROW_KINDS = [
81843
81919
  {
81844
81920
  key: "nc-artifacts",
81845
81921
  kind: ncDurableRecordKinds.artifacts
81922
+ },
81923
+ {
81924
+ key: "nc-detection-verdicts",
81925
+ kind: ncDurableRecordKinds["detection-verdicts"]
81846
81926
  }
81847
81927
  ];
81848
81928
  /**
@@ -82262,6 +82342,82 @@ var NcSequenceStore = class NcSequenceStore {
82262
82342
  }
82263
82343
  };
82264
82344
  //#endregion
82345
+ //#region src/notification-center/detection-verdict-ledger.ts
82346
+ /**
82347
+ * @durable class=ledger owner=notification-center
82348
+ * write="the operator taps Correct / Wrong on a detection notification; one row per track, the latest answer replaces the earlier one"
82349
+ * retention="none — bounded by the tracks a rule asked about; deleted with the camera by the device-removal hook"
82350
+ */
82351
+ var NC_DETECTION_VERDICTS_COLLECTION = "notification-center:detection-verdicts";
82352
+ var NC_DETECTION_VERDICTS_COLUMNS = [
82353
+ {
82354
+ name: "id",
82355
+ type: "TEXT",
82356
+ primaryKey: true,
82357
+ notNull: true
82358
+ },
82359
+ {
82360
+ name: "ruleId",
82361
+ type: "TEXT",
82362
+ notNull: true
82363
+ },
82364
+ {
82365
+ name: "at",
82366
+ type: "INTEGER",
82367
+ notNull: true
82368
+ },
82369
+ (
82370
+ /** The FULL row (Zod-validated on read). */
82371
+ {
82372
+ name: "row",
82373
+ type: "JSON",
82374
+ notNull: true
82375
+ })
82376
+ ];
82377
+ var NcDetectionVerdictRecordSchema = object({
82378
+ trackId: string().min(1),
82379
+ deviceId: number().int(),
82380
+ verdict: _enum(["correct", "wrong"]),
82381
+ /** The class the operator said it really was. Only with `wrong`. */
82382
+ suggestedClass: string().min(1).optional(),
82383
+ /** Whose notification it was (the token's frozen owner), NOT who tapped. */
82384
+ userId: string().min(1),
82385
+ ruleId: string().min(1),
82386
+ ruleName: string(),
82387
+ at: number()
82388
+ });
82389
+ var NC_DETECTION_VERDICTS_SPEC = {
82390
+ collection: NC_DETECTION_VERDICTS_COLLECTION,
82391
+ columns: [...NC_DETECTION_VERDICTS_COLUMNS],
82392
+ writeMode: "write-through",
82393
+ keyOf: (row) => row.trackId,
82394
+ toValue: (row) => ({
82395
+ ruleId: row.ruleId,
82396
+ at: row.at,
82397
+ row
82398
+ }),
82399
+ fromRecord: (_key, data) => {
82400
+ const parsed = NcDetectionVerdictRecordSchema.safeParse(data["row"]);
82401
+ return parsed.success ? parsed.data : null;
82402
+ },
82403
+ deviceIdOf: (row) => row.deviceId,
82404
+ loadLimit: 1e4
82405
+ };
82406
+ var NcDetectionVerdictLedger = class {
82407
+ ledger;
82408
+ constructor(deps) {
82409
+ this.ledger = new DurableLedger({
82410
+ spec: NC_DETECTION_VERDICTS_SPEC,
82411
+ kind: deps.records.kind(ncDurableRecordKinds["detection-verdicts"]),
82412
+ logger: deps.logger
82413
+ });
82414
+ }
82415
+ /** Write-through; throws on a failed write so the tap reports it. */
82416
+ async record(row) {
82417
+ await this.ledger.put(NcDetectionVerdictRecordSchema.parse(row));
82418
+ }
82419
+ };
82420
+ //#endregion
82265
82421
  //#region src/notification-center/device-prune.ts
82266
82422
  function judgeList(list, isDead) {
82267
82423
  const dead = list.filter(isDead);
@@ -83216,6 +83372,64 @@ var NcDeviceStateCache = class {
83216
83372
  }
83217
83373
  };
83218
83374
  //#endregion
83375
+ //#region src/notification-center/detection-verdict-buttons.ts
83376
+ var VERDICT_CORRECT_ID = "detection-verdict-correct";
83377
+ var VERDICT_WRONG_ID = "detection-verdict-wrong";
83378
+ /** `detection-verdict-wrong:<class>` — Wrong, and what it really was. */
83379
+ var VERDICT_WRONG_AS_PREFIX = "detection-verdict-wrong:";
83380
+ /** The macro classes offered as "it was really…". Short on purpose. */
83381
+ var VERDICT_CLASS_CHOICES = [
83382
+ "person",
83383
+ "vehicle",
83384
+ "animal"
83385
+ ];
83386
+ /** At most this many class buttons, and only when the channel has room. */
83387
+ var VERDICT_MAX_CLASS_CHOICES = 2;
83388
+ function verdictButtonRefusal(subject) {
83389
+ if (!subject.askVerdict) return "rule-off";
83390
+ if (subject.trackId === void 0) return "no-track";
83391
+ if (subject.ownerUserId === void 0) return "unowned-target";
83392
+ return null;
83393
+ }
83394
+ /** The classes worth offering for a subject of `className`. */
83395
+ function verdictClassChoices(className) {
83396
+ return VERDICT_CLASS_CHOICES.filter((c) => c !== className).slice(0, VERDICT_MAX_CLASS_CHOICES);
83397
+ }
83398
+ /**
83399
+ * Which buttons a channel with `maxActions` slots gets. `maxActions` absent
83400
+ * (the kind catalog did not answer) is treated as the common floor of three:
83401
+ * the two plain choices, never the class ones. The class choices appear only
83402
+ * when the channel declared room for a fourth button.
83403
+ */
83404
+ function verdictButtonSpecs(input) {
83405
+ const base = [{
83406
+ actionId: VERDICT_CORRECT_ID,
83407
+ verdict: "correct"
83408
+ }, {
83409
+ actionId: VERDICT_WRONG_ID,
83410
+ verdict: "wrong"
83411
+ }];
83412
+ if (input.maxActions === void 0 || input.maxActions <= 3) return base;
83413
+ const room = input.maxActions - base.length;
83414
+ const extra = verdictClassChoices(input.className).slice(0, room).map((suggestedClass) => ({
83415
+ actionId: `${VERDICT_WRONG_AS_PREFIX}${suggestedClass}`,
83416
+ verdict: "wrong",
83417
+ suggestedClass
83418
+ }));
83419
+ return [...base, ...extra];
83420
+ }
83421
+ /** Is this one of our buttons (so a link-only channel can lift it into the body)? */
83422
+ function isVerdictAction(action) {
83423
+ return action.id === VERDICT_CORRECT_ID || action.id.startsWith(VERDICT_WRONG_ID);
83424
+ }
83425
+ /**
83426
+ * The body lines that carry the verdict choices on a channel with no buttons.
83427
+ * A bare URL per line: every chat and mail client linkifies it.
83428
+ */
83429
+ function verdictLinkLines(actions) {
83430
+ return actions.filter(isVerdictAction).map((a) => `${a.label}: ${a.url ?? ""}`).join("\n");
83431
+ }
83432
+ //#endregion
83219
83433
  //#region src/notification-center/state-summary/state-model.ts
83220
83434
  /**
83221
83435
  * The STATE SUMMARY's vocabulary (D647): its sections, and what one reported
@@ -86924,6 +87138,8 @@ var NcDispatcher = class {
86924
87138
  targetId: target.id,
86925
87139
  ...target.ownerUserId !== void 0 ? { ownerUserId: target.ownerUserId } : {},
86926
87140
  ...entry.payload.subject.className !== void 0 ? { className: entry.payload.subject.className } : {},
87141
+ ...entry.payload.subject.trackId !== void 0 ? { trackId: entry.payload.subject.trackId } : {},
87142
+ ...target.maxActions !== void 0 ? { maxActions: target.maxActions } : {},
86927
87143
  ...entry.payload.subject.systemEvent !== void 0 ? { systemEvent: entry.payload.subject.systemEvent } : {}
86928
87144
  });
86929
87145
  } catch (err) {
@@ -87019,9 +87235,13 @@ var NcDispatcher = class {
87019
87235
  ...summarySectionOf(systemEvent.data)
87020
87236
  } } : {}
87021
87237
  }) : null;
87022
- const actions = await this.mintActions(entry, target);
87238
+ const minted = await this.mintActions(entry, target);
87239
+ const linkOnly = target.maxActions === 0;
87240
+ const verdictLinks = linkOnly ? verdictLinkLines(minted) : "";
87241
+ const actions = linkOnly ? minted.filter((a) => !isVerdictAction(a)) : minted;
87242
+ const withLinks = (text) => verdictLinks === "" ? text : `${text}\n\n${verdictLinks}`;
87023
87243
  const notification = {
87024
- body,
87244
+ body: withLinks(body),
87025
87245
  title,
87026
87246
  priority: clampPriority(paramNumber(entry.payload.params, "priority") ?? entry.payload.priority),
87027
87247
  ...params,
@@ -87043,7 +87263,7 @@ var NcDispatcher = class {
87043
87263
  grouped: resolved.grouped,
87044
87264
  withAiText: (aiText) => ({
87045
87265
  ...notification,
87046
- body: renderBody(aiText)
87266
+ body: withLinks(renderBody(aiText))
87047
87267
  })
87048
87268
  };
87049
87269
  }
@@ -99656,6 +99876,55 @@ async function mintButtons(host, input) {
99656
99876
  const announcement = stored === null && input.ruleId === NC_ALARM_ANNOUNCE_RULE_ID;
99657
99877
  if (stored === null && !announcement) return [];
99658
99878
  const drafts = [];
99879
+ const verdictRefusal = verdictButtonRefusal({
99880
+ askVerdict: stored?.askVerdict === true,
99881
+ ...input.trackId !== void 0 ? { trackId: input.trackId } : {},
99882
+ ...input.ownerUserId !== void 0 ? { ownerUserId: input.ownerUserId } : {}
99883
+ });
99884
+ if (stored?.askVerdict === true && verdictRefusal !== null) host.logger.info("detection notification carries no verdict buttons", {
99885
+ tags: { deviceId: input.deviceId },
99886
+ meta: {
99887
+ ruleId: input.ruleId,
99888
+ reason: verdictRefusal,
99889
+ targetId: input.targetId
99890
+ }
99891
+ });
99892
+ const verdictTrackId = input.trackId;
99893
+ const verdictOwner = input.ownerUserId;
99894
+ const verdictButtons = verdictRefusal !== null || verdictTrackId === void 0 || verdictOwner === void 0 ? [] : verdictButtonSpecs({
99895
+ className: input.className,
99896
+ maxActions: input.maxActions
99897
+ }).map((spec) => {
99898
+ const id = randomUUID();
99899
+ drafts.push({
99900
+ id,
99901
+ ttlMs: actionTtlMs({ kind: "detection-verdict" }),
99902
+ grant: (expiresAt) => ({
99903
+ kind: "detection-verdict",
99904
+ id,
99905
+ verdict: spec.verdict,
99906
+ trackId: verdictTrackId,
99907
+ ruleId: input.ruleId,
99908
+ ruleName: input.ruleName,
99909
+ userId: verdictOwner,
99910
+ ...spec.suggestedClass !== void 0 ? { suggestedClass: spec.suggestedClass } : {},
99911
+ ...input.maxActions === 0 ? { viaLink: true } : {},
99912
+ deviceId: input.deviceId,
99913
+ actionId: spec.actionId,
99914
+ targetId: input.targetId,
99915
+ expiresAt
99916
+ })
99917
+ });
99918
+ return {
99919
+ id: spec.actionId,
99920
+ label: spec.suggestedClass === void 0 ? host.texts.text({ key: spec.verdict === "correct" ? "button.verdict.correct" : "button.verdict.wrong" }) : host.texts.text({
99921
+ key: "button.verdict.wrongAs",
99922
+ vars: { class: spec.suggestedClass }
99923
+ }),
99924
+ url: id,
99925
+ mode: "background"
99926
+ };
99927
+ });
99659
99928
  const sequenceButtons = host.actionRunner === null || stored === null ? [] : buildActionButtons({
99660
99929
  actions: stored.actions,
99661
99930
  knownSequenceIds: host.sequences.knownIds(),
@@ -99868,6 +100137,7 @@ async function mintButtons(host, input) {
99868
100137
  }
99869
100138
  });
99870
100139
  const out = [
100140
+ ...verdictButtons,
99871
100141
  ...sequenceButtons,
99872
100142
  ...snoozeButtons,
99873
100143
  ...alarmButtons,
@@ -100091,8 +100361,53 @@ async function runGrantedAction(host, grant) {
100091
100361
  case "sequence": return host.runGrantedSequence(grant);
100092
100362
  case "addon-update": return host.applyGrantedAddonUpdate(grant);
100093
100363
  case "server-update": return host.applyGrantedServerUpdate(grant);
100364
+ case "detection-verdict": return host.applyGrantedDetectionVerdict(grant);
100094
100365
  }
100095
100366
  }
100367
+ /** What a person is told when the retrain lifecycle refuses the mark. */
100368
+ var VERDICT_REFUSAL_COPY = {
100369
+ trained: "This track was already used for training — nothing was changed",
100370
+ budget: "This camera already holds as many tracks for retraining as it may — nothing was changed",
100371
+ "unknown-track": "This track is no longer available — nothing was changed"
100372
+ };
100373
+ /**
100374
+ * Mark the track one notification reported for retraining, and record who was
100375
+ * asked and when.
100376
+ *
100377
+ * Order matters. The MARK goes first and is the authority: a refusal ("already
100378
+ * trained", "budget full") means the operator's answer changed nothing, so no
100379
+ * ledger row is written for an answer that was not applied. The token is
100380
+ * already spent (D47) — the refusal is shown as the reason, not a retry.
100381
+ */
100382
+ async function applyGrantedDetectionVerdict(host, grant) {
100383
+ const mark = host.deps.markTrackForRetrain;
100384
+ if (mark === void 0) throw new Error("no retrain mark is wired on this node");
100385
+ const outcome = await mark({
100386
+ deviceId: grant.deviceId,
100387
+ trackId: grant.trackId
100388
+ });
100389
+ if (!outcome.ok) {
100390
+ host.logger.info("detection verdict changed nothing — the retrain flow refused the mark", {
100391
+ tags: { deviceId: grant.deviceId },
100392
+ meta: {
100393
+ trackId: grant.trackId,
100394
+ reason: outcome.reason,
100395
+ verdict: grant.verdict
100396
+ }
100397
+ });
100398
+ throw new NcActionRefusedError(VERDICT_REFUSAL_COPY[outcome.reason]);
100399
+ }
100400
+ await host.verdicts.record({
100401
+ trackId: grant.trackId,
100402
+ deviceId: grant.deviceId,
100403
+ verdict: grant.verdict,
100404
+ ...grant.suggestedClass !== void 0 ? { suggestedClass: grant.suggestedClass } : {},
100405
+ userId: grant.userId,
100406
+ ruleId: grant.ruleId,
100407
+ ruleName: grant.ruleName,
100408
+ at: host.now()
100409
+ });
100410
+ }
100096
100411
  /**
100097
100412
  * Apply the addon updates one notification was about.
100098
100413
  *
@@ -105269,6 +105584,7 @@ var NotificationCenter = class {
105269
105584
  * evaluating one.
105270
105585
  */
105271
105586
  pruneLedger;
105587
+ verdicts;
105272
105588
  /** D905: which rules cannot work, announced once per distinct reason. */
105273
105589
  ruleHealth = new NcRuleHealthMonitor();
105274
105590
  /** The prune itself — see `device-prune-runner.ts`. */
@@ -105497,6 +105813,11 @@ var NotificationCenter = class {
105497
105813
  applyGrantedArmAnyway: (panel, grant) => self.applyGrantedArmAnyway(panel, grant),
105498
105814
  applyGrantedBypassInput: (panel, grant) => self.applyGrantedBypassInput(panel, grant),
105499
105815
  applyGrantedServerUpdate: (grant) => self.applyGrantedServerUpdate(grant),
105816
+ applyGrantedDetectionVerdict: (grant) => self.applyGrantedDetectionVerdict(grant),
105817
+ now: self.now,
105818
+ get verdicts() {
105819
+ return self.verdicts;
105820
+ },
105500
105821
  applyGrantedSnooze: (grant) => self.applyGrantedSnooze(grant),
105501
105822
  get deps() {
105502
105823
  return self.deps;
@@ -105547,6 +105868,9 @@ var NotificationCenter = class {
105547
105868
  async applyGrantedAddonUpdate(grant) {
105548
105869
  return applyGrantedAddonUpdate(this.grantedActionsHost(), grant);
105549
105870
  }
105871
+ async applyGrantedDetectionVerdict(grant) {
105872
+ return applyGrantedDetectionVerdict(this.grantedActionsHost(), grant);
105873
+ }
105550
105874
  async applyGrantedServerUpdate(grant) {
105551
105875
  return applyGrantedServerUpdate(this.grantedActionsHost(), grant);
105552
105876
  }
@@ -105786,6 +106110,10 @@ var NotificationCenter = class {
105786
106110
  }
105787
106111
  this.summaryRules = NcSummaryStore.of(deps.records, this.logger.child("summary"), deps.now);
105788
106112
  this.calendarEventsRules = NcCalendarEventsStore.of(deps.records, this.logger.child("calendar-events"), deps.now);
106113
+ this.verdicts = new NcDetectionVerdictLedger({
106114
+ ...durableStore,
106115
+ logger: this.logger.child("detection-verdicts")
106116
+ });
105789
106117
  this.pruneLedger = new NcPruneLedger({
105790
106118
  ...durableStore,
105791
106119
  logger: this.logger.child("device-prune")
@@ -108393,6 +108721,23 @@ var BuildNotificationCenter = class extends BuildNotificationCenterBase {
108393
108721
  };
108394
108722
  }
108395
108723
  },
108724
+ markTrackForRetrain: async ({ deviceId, trackId }) => {
108725
+ try {
108726
+ await this.ctx.api.pipelineAnalytics.setTrackFlags.mutate({
108727
+ deviceId,
108728
+ trackId,
108729
+ flags: { markForTrain: true }
108730
+ });
108731
+ return { ok: true };
108732
+ } catch (err) {
108733
+ const reason = retrainRefusalReason(err);
108734
+ if (reason === null) throw err;
108735
+ return {
108736
+ ok: false,
108737
+ reason
108738
+ };
108739
+ }
108740
+ },
108396
108741
  readDeviceStates: async (ids) => {
108397
108742
  const out = /* @__PURE__ */ new Map();
108398
108743
  for (const id of ids) try {
@@ -108464,9 +108809,11 @@ var BuildNotificationCenter = class extends BuildNotificationCenterBase {
108464
108809
  const targets = await api.notificationOutput.listTargets.query({});
108465
108810
  const kinds = await api.notificationOutput.listTargetKinds.query({}).catch(() => []);
108466
108811
  const bodyMaxLenByKind = new Map(kinds.map((k) => [k.kind, k.caps.bodyMaxLen]));
108812
+ const maxActionsByKind = new Map(kinds.map((k) => [k.kind, k.caps.actions]));
108467
108813
  return targets.map((t) => {
108468
108814
  const owner = t.config["ownerUserId"];
108469
108815
  const bodyMaxLen = bodyMaxLenByKind.get(t.kind);
108816
+ const maxActions = maxActionsByKind.get(t.kind);
108470
108817
  return {
108471
108818
  id: t.id,
108472
108819
  addonId: t.addonId,
@@ -108474,7 +108821,8 @@ var BuildNotificationCenter = class extends BuildNotificationCenterBase {
108474
108821
  kind: t.kind,
108475
108822
  enabled: t.enabled,
108476
108823
  ...typeof owner === "string" && owner.length > 0 ? { ownerUserId: owner } : {},
108477
- ...typeof bodyMaxLen === "number" ? { bodyMaxLen } : {}
108824
+ ...typeof bodyMaxLen === "number" ? { bodyMaxLen } : {},
108825
+ ...typeof maxActions === "number" ? { maxActions } : {}
108478
108826
  };
108479
108827
  });
108480
108828
  },
@@ -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-Cct0TMzC.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Dhan9FH4.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.641",
3
+ "version": "1.2.642",
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",