@camstack/addon-post-analysis 1.2.621 → 1.2.623
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/THIRD_PARTY_MODELS.md +2 -2
- package/dist/{clip-model-registry-BBzoIoKO.mjs → clip-model-registry-C58fyELd.mjs} +412 -66
- package/dist/{clip-model-registry-CcgzpELQ.js → clip-model-registry-KKT83z9U.js} +429 -65
- 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-CkNZAMox.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-_ahOxwNA.mjs} +3 -3
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-V5-s2hmZ.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-CssYIiFD.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B1HZu0i3.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-wlnNaaYX.mjs → hostInit-CdRVx4vF.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +888 -155
- package/dist/pipeline-analytics/index.mjs +888 -155
- 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-BSMSev-b.mjs +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as MediaFileKindEnum, $i as
|
|
1
|
+
import { $ as MediaFileKindEnum, $i as defineCustomActions, $n as cosineSimilarity$1, $r as plateGalleryCapability, $t as OpsLogEntrySchema, A as DeclaredDevices, Ai as systemEventFamilyProblems, An as activateAlarmProfile, Ar as isEventKindLabelKey, At as NcAlarmSensorsInputSchema, B as FULL_IMAGE_BBOX, Bi as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, Bn as applySecureOnArmEdit, Br as isVectorIndexDimConflict, Bt as NcRuleUpdateInputSchema, C as ClearTrackRelationResultSchema, Ca as EventCategory, Ci as storageOccupancyCapability, Cn as TimelapseRulePatchSchema, Cr as isAlertEpisodeOpen, Ct as NC_SNOOZE_MAX_MINUTES, D as DEFAULT_TIMELAPSE_PREVIEW_TEXT, Di as summarizeReview, Dn as TrackRelationSchema, Dr as isDefaultAlarmProfileId, Dt as NC_TEMPLATE_VARS, E as DEFAULT_EVENT_COLOR, Ei as subjectPathSvgPoints, En as TrackRelationKindSchema, Er as isAudioClassName, Et as NC_TAXONOMY, F as EVENT_DENSITY_BATCH_MAX, Fi as templateVarsFor, Fn as alertEpisodeKind, Fr as isRetiredClearKey, Ft as NcRuleActionSchema, G as ListTrackRelationsResultSchema, Gi as errMsg, Gn as buildEventKindDescriptor, Gr as ncMediaPolicyError, Gt as NcSnoozeSuppressedSchema, H as LABEL_SUPPLY_MACRO, Hi as evaluateSensorEdge, Hn as assertTimelapseCadences, Hr as linkedStepOf, Ht as NcSharedSequenceSchema, I as EVENT_KIND_BY_CAP, Ii as vectorDimFromBase64, In as alertEpisodePayload, Ir as isRootSuppliableStepId, It as NcRuleInputSchema, J as MAX_BIRTH_DECISION_RECORDS, Ji as InvalidRecordError, Jn as calendarEventLocalSpan, Jr as occupancyScope, Jt as NcTaxonomySchema, K as LockStateSchema, Ki as BaseAddon, Kn as buildSubjectPath, Kr as normalizeNcMediaPolicy, Kt as NcStateSummarySectionIdSchema, L as EVENT_OWNER_TYPES, Li as viewerLinkBaseOf, Ln as applyBlockingEdit, Lr as isScheduleActive, Lt as NcRulePatchSchema, M as EMBEDDING_MODEL_FAILED, Mi as systemEventSummaryScheduleOf, Mn as alarmDeviceSecureOnArm, Mr as isKnownTemplateVar, Mt as NcAlarmSettingsPatchSchema, N as EMBEDDING_REQUIREMENTS_FAILED, Ni as systemEventSummarySplitOf, Nn as alarmPanelCapability, Nr as isLabelSupplyStepId, Nt as NcConditionDescriptorSchema, O as DETECTION_MACRO_CLASSES, Oi as syncActiveAlarmProfileSettings, On as TrackSideSchema, Or as isDetectionMacroClass, Ot as NcAlarmConfigSchema, P as EMBEDDING_REQUIREMENTS_UNAVAILABLE, Pi as templateCarriesSummary, Pn as alertEpisodeId, Pr as isMediaOwnerType, Pt as NcConditionsSchema, Q as MOTION_CLOSE_AFTER_MS, Qi as customAction, Qn as coreBlockIdFromAddonId, Qr as pipelineAnalyticsCapability, Qt as OPS_LOG_DEFAULT_LIMIT, R as EXPORT_DENSE_MAX_RANGES, Ri as withSystemEventClasses, Rn as applyModeDelaysEdits, Rr as isSystemDelivery, Rt as NcRuleSchema, S as ClearTrackRelationInputSchema, Sa as unknown, Si as startMoverUnderClearance, Sn as TimelapseRuleInputSchema, Sr as isAlarmControllableCap, St as NC_SENSOR_TRANSITION_KINDS, T as DEFAULT_DETAIL_CROP_CONVENTION, Ti as subjectPathSampleFromBox, Tn as TrackPuritySchema, Tr as isAppLinkBase, Tt as NC_TANK_SYSTEM_EVENT_KINDS, U as LabelAttributionSchema, Ui as isSourceCap, Un as audioMetricsCapability, Ur as mayWriteToLocation, Ut as NcSnoozeInputSchema, V as FailureCounters, Vi as canonicalHash, Vn as armRemovalReconcile, Vr as kebabToCamel, Vt as NcScheduleSchema, W as ListTrackRelationsInputSchema, Wi as DeviceType, Wn as audioModeOf, Wr as memoizePinnedFfmpeg, Wt as NcSnoozeSchema, X as MAX_TRACK_PURITIES, Xi as RECORD_QUERY_MAX, Xn as cloneAlarmSettings, Xt as NcTemplatePreviewSchema, Y as MAX_BIRTH_LATENCY_PROXY_MS, Yi as RECORD_MAX_DATA_BYTES, Yn as classifyRetrainReview, Yr as parseAlertEpisodePayload, Yt as NcTemplatePreviewInputSchema, Z as MAX_TRACK_RELATIONS, Zi as createEvent, Zn as consumableLowPercentOf, Zr as pickFaceRescoreIntervalMinutes, Zt as NcTemplateVarDescriptorSchema, _ as BIRTH_DECISION_DEFAULT_LIMIT, _a as strictObject, _i as ruleKindOf, _n as SetTrackPurityInputSchema, _r as evictionPolicyOfLocation, _t as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, a as CLIP_EMBEDDING_STEP_ID, aa as sleep$1, ai as resolveClipSearchMinScore, an as RetrainOriginSchema, ar as describeSystemEventDeliveryProblem, at as NC_ALARM_NOTIFY_FIELDS, b as BucketAccumulator, ba as tuple, bi as sensorRaisedOf, bn as TIMELAPSE_DENSE_FLOOR_SEC, br as gifDeriveSpeed, bt as NC_GROUPING_MAX_SEC, c as readClusterModelPinStrict, ca as array, ci as resolveModeNotifyPolicy, cn as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, cr as deviceRemovalHookCapability, ct as NC_AUDIO_CONFIRM_HITS_DEFAULT, d as APPEARANCE_BLOCKS, da as literal, di as retrainRevisionConflictMessage, dn as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, dr as embeddingRefusalOf, dt as NC_CLIP_PRE_ROLL_DEFAULT_SEC, ea as defineRecordKinds, ei as projectSubjectPath, er as createAlarmProfile, et as MotionTriggeredBySchema, f as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, fa as looseObject, fi as rootSupplyServes, fn as SCENE_DEFAULT_QUIET_SECONDS, ft as NC_CLIP_RETENTION_MAX_SEC, g as ArchivedDebugNoteSchema, ga as record, gi as ruleGroupingApplies, gn as SceneMonitorSchema, gr as eventKindLabel, gt as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, h as AlarmArmModeSchema, ha as partialRecord, hi as ruleDeviceCompatCapQueries, hn as SCENE_RESET_RECAPTURES, hr as ensureAlarmProfiles, ht as NC_CONFIRM_DEFAULT_TIMEOUT_MS, ii as renameAlarmProfile, in as RetrainDismissedSchema, ir as deriveRecordingMode, it as NC_ALARM_DELAY_MAX_SEC, j as DetectionSourceSchema, ji as systemEventFilterApplies, jn as addonWidgetsSourceCapability, jr as isEventOwnerType, jt as NcAlarmSensorsSchema, k as DETECTION_PIPELINE_CAP_NAME, ki as systemEventDeliveryProblems, kn as TrackSourceSchema, kr as isDrawableSubjectPath, kt as NcAlarmPostTriggeredActionSchema, l as resolveClusterModelPin, la as boolean, li as resolveNcMediaImage, ln as SCENE_DEFAULT_ANCHOR_THRESHOLD, lt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, m as AUDIO_MACRO_LABELS, ma as object, mi as ruleBelongsToAlarm$1, mn as SCENE_DIVERGED, mr as encodeVectorBase64, mt as NC_CONFIRM_DEFAULT_MAX_IMAGE_PX, na as isDeviceScopedCap, ni as readMotionTriggeredBy, nn as RECORDING_SOURCE_CAMSTACK_ADDON, nr as deriveDetailCropRect, nt as NC_ALARM_DEFAULT_TRIGGERED_DURATION_SEC, o as CLUSTER_MODEL_OWNER_ADDON_ID, oa as ZodObject, oi as resolveLocationMode, on as RetrainStatusSchema, or as describeSystemEventFamilyProblem, ot as NC_ALARM_SYSTEM_EVENT_KINDS, p as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, pa as number, pi as ruleAutoTriggersAlarm, pn as SCENE_DEFAULT_UNCOVERED_POLICY, pt as NC_CONDITION_CATALOG, q as MAX_ARCHIVED_DEBUG_NOTES, qi as CamProfileSchema, qn as buildViewerLink, qr as notificationRulesCapability, qt as NcSystemEventKindSchema, ra as nodePin, ri as readTimelapseGeneratedAt, rn as RebuildObjectEmbeddingsRefusalSchema, rr as derivePostTriggeredAction, rt as NC_ALARM_DELAY_FIELDS, s as FACE_EMBEDDING_STEP_ID, sa as _enum, si as resolveModeDelays, sn as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, sr as deviceAttentionKind, st as NC_ALARM_TRIGGERED_MAX_SEC, t as BUILTIN_CLIP_MODELS, ta as hydrateSchema, ti as readDeviceStateFrom, tn as RECORDING_EXPORT_MAX_READ_BYTES, tr as deleteAlarmProfile, tt as NC_ALARM_DEFAULT_PROFILE_ID, u as APPEARANCE_BIN_COUNT, ua as discriminatedUnion, un as SCENE_DEFAULT_CHECK_INTERVAL_SEC, ut as NC_CLIP_POST_ROLL_DEFAULT_SEC, v as BaseDevice, va as string, vi as sceneMonitorCapability, vn as SetTrackPurityResultSchema, vr as faceGalleryCapability, vt as NC_DEFAULT_SNOOZE_MINUTES, w as CoverStateSchema, wi as subKindsOf, wn as TimelapseRuleSchema, wr as isAlertEpisodeSeverity, wt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, x as COCO_TO_MACRO, xa as union, xi as servedTemplateVars, xn as TRACK_RELATIONS_DEFAULT_LIMIT, xr as gifFromMp4Args, xt as NC_GROUPING_MIN_SEC, y as BirthDecisionRecordSchema, ya as templateLiteral, yi as secureActuationFor, yn as SetTrackRelationInputSchema, yr as failureContributionCapability, yt as NC_GROUPING_DEFAULT_SEC, z as FIRST_LEVEL_MACRO_CLASSES, zi as zoneAnalyticsCapability, zn as applyNonBlockingEdit, zr as isSystemEventSummaryRule, zt as NcRuleTargetSchema } from "../clip-model-registry-C58fyELd.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";
|
|
@@ -5394,40 +5394,118 @@ async function notificationLinkEndpoints(api, logger) {
|
|
|
5394
5394
|
*/
|
|
5395
5395
|
var NC_TRACKS_AT_INSTANT_LIMIT = 25;
|
|
5396
5396
|
/**
|
|
5397
|
-
*
|
|
5398
|
-
*
|
|
5399
|
-
*
|
|
5397
|
+
* How far outside its own life a track may still be drawn: one detection tick.
|
|
5398
|
+
*
|
|
5399
|
+
* The box TOLERANCE (`toleranceMs`, 750 ms) is about how sparse a trail is —
|
|
5400
|
+
* a live track keeps a position every few frames — and it must never stretch
|
|
5401
|
+
* a LIFE. A track born after the frame was not on it: on 2026-10-22 the
|
|
5402
|
+
* tracker split a dog on camera 615 and the second piece, born 676 ms after
|
|
5403
|
+
* the key frame, was drawn at its first box as a second box on the dog. One
|
|
5404
|
+
* tick (~110–150 ms at that camera's detection rate, rounded up) absorbs the
|
|
5405
|
+
* gap between a frame and the detection that confirmed it, and nothing more.
|
|
5406
|
+
*/
|
|
5407
|
+
var NC_TRACK_LIFE_SLACK_MS = 200;
|
|
5408
|
+
/**
|
|
5409
|
+
* A position this close to the instant is drawn as it is: one detection tick,
|
|
5410
|
+
* the same notion as {@link NC_TRACK_LIFE_SLACK_MS}. Anything farther is a
|
|
5411
|
+
* position of a MOVED subject, so it is either interpolated (a position on
|
|
5412
|
+
* each side inside the tolerance) or the track is left out — never drawn at a
|
|
5413
|
+
* later place than it was at the picture's instant.
|
|
5414
|
+
*/
|
|
5415
|
+
var NC_BOX_EXACT_MS = NC_TRACK_LIFE_SLACK_MS;
|
|
5416
|
+
/** The box at `timestamp`: a near position as is, else the bracketing pair interpolated. */
|
|
5417
|
+
function boxAt(positions, timestamp, toleranceMs) {
|
|
5418
|
+
let nearest = null;
|
|
5419
|
+
let before = null;
|
|
5420
|
+
let after = null;
|
|
5421
|
+
for (const p of positions) {
|
|
5422
|
+
const d = p.timestamp - timestamp;
|
|
5423
|
+
if (nearest === null || Math.abs(d) < Math.abs(nearest.timestamp - timestamp)) nearest = p;
|
|
5424
|
+
if (d <= 0 && (before === null || p.timestamp > before.timestamp)) before = p;
|
|
5425
|
+
if (d >= 0 && (after === null || p.timestamp < after.timestamp)) after = p;
|
|
5426
|
+
}
|
|
5427
|
+
if (nearest === null) return null;
|
|
5428
|
+
const dt = nearest.timestamp - timestamp;
|
|
5429
|
+
if (Math.abs(dt) <= NC_BOX_EXACT_MS) return {
|
|
5430
|
+
bbox: nearest.bbox,
|
|
5431
|
+
score: nearest.score,
|
|
5432
|
+
dtMs: dt,
|
|
5433
|
+
interpolated: false
|
|
5434
|
+
};
|
|
5435
|
+
if (before === null || after === null || timestamp - before.timestamp > toleranceMs || after.timestamp - timestamp > toleranceMs) return null;
|
|
5436
|
+
const span = after.timestamp - before.timestamp;
|
|
5437
|
+
const f = span === 0 ? 0 : (timestamp - before.timestamp) / span;
|
|
5438
|
+
const lerp = (a, b) => a + (b - a) * f;
|
|
5439
|
+
return {
|
|
5440
|
+
bbox: {
|
|
5441
|
+
x: lerp(before.bbox.x, after.bbox.x),
|
|
5442
|
+
y: lerp(before.bbox.y, after.bbox.y),
|
|
5443
|
+
w: lerp(before.bbox.w, after.bbox.w),
|
|
5444
|
+
h: lerp(before.bbox.h, after.bbox.h)
|
|
5445
|
+
},
|
|
5446
|
+
score: nearest.score,
|
|
5447
|
+
dtMs: dt,
|
|
5448
|
+
interpolated: true
|
|
5449
|
+
};
|
|
5450
|
+
}
|
|
5451
|
+
/**
|
|
5452
|
+
* Every source track ALIVE at `timestamp` (± {@link NC_TRACK_LIFE_SLACK_MS})
|
|
5453
|
+
* with a box AT that instant: a trail position within {@link NC_BOX_EXACT_MS},
|
|
5454
|
+
* else the linear interpolation of the positions bracketing it (each within
|
|
5455
|
+
* `toleranceMs`). A track with neither is not drawn — `onDrop` names it.
|
|
5456
|
+
* Deduplicated by track id (a live copy and a persisted copy are one track).
|
|
5457
|
+
* Pure.
|
|
5400
5458
|
*/
|
|
5401
|
-
function tracksAtInstant(sources, timestamp, toleranceMs, fallbackDims) {
|
|
5459
|
+
function tracksAtInstant(sources, timestamp, toleranceMs, fallbackDims, onDrop) {
|
|
5402
5460
|
const out = /* @__PURE__ */ new Map();
|
|
5403
5461
|
for (const track of sources) {
|
|
5404
5462
|
if (out.has(track.trackId)) continue;
|
|
5405
|
-
if (track.firstSeen > timestamp +
|
|
5463
|
+
if (track.firstSeen > timestamp + NC_TRACK_LIFE_SLACK_MS || track.lastSeen < timestamp - NC_TRACK_LIFE_SLACK_MS) {
|
|
5464
|
+
onDrop?.({
|
|
5465
|
+
trackId: track.trackId,
|
|
5466
|
+
className: track.className,
|
|
5467
|
+
reason: "outside-life"
|
|
5468
|
+
});
|
|
5469
|
+
continue;
|
|
5470
|
+
}
|
|
5406
5471
|
const w = track.frameWidth ?? fallbackDims?.w ?? 0;
|
|
5407
5472
|
const h = track.frameHeight ?? fallbackDims?.h ?? 0;
|
|
5408
|
-
if (w <= 0 || h <= 0)
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5473
|
+
if (w <= 0 || h <= 0) {
|
|
5474
|
+
onDrop?.({
|
|
5475
|
+
trackId: track.trackId,
|
|
5476
|
+
className: track.className,
|
|
5477
|
+
reason: "no-frame-dims"
|
|
5478
|
+
});
|
|
5479
|
+
continue;
|
|
5480
|
+
}
|
|
5481
|
+
const at = boxAt(track.positions, timestamp, toleranceMs);
|
|
5482
|
+
if (at === null) {
|
|
5483
|
+
const nearestDtMs = track.positions.reduce((best, p) => {
|
|
5484
|
+
const d = p.timestamp - timestamp;
|
|
5485
|
+
return best === void 0 || Math.abs(d) < Math.abs(best) ? d : best;
|
|
5486
|
+
}, void 0);
|
|
5487
|
+
onDrop?.({
|
|
5488
|
+
trackId: track.trackId,
|
|
5489
|
+
className: track.className,
|
|
5490
|
+
reason: "no-position-near-instant",
|
|
5491
|
+
...nearestDtMs !== void 0 ? { nearestDtMs } : {}
|
|
5492
|
+
});
|
|
5493
|
+
continue;
|
|
5417
5494
|
}
|
|
5418
|
-
if (best === null || bestDelta > toleranceMs) continue;
|
|
5419
5495
|
const label = track.subLabel ?? track.label;
|
|
5420
5496
|
out.set(track.trackId, {
|
|
5421
5497
|
trackId: track.trackId,
|
|
5422
5498
|
className: track.className,
|
|
5423
5499
|
...label !== void 0 && label !== track.className ? { label } : {},
|
|
5424
|
-
...
|
|
5500
|
+
...at.score !== void 0 ? { confidence: at.score } : {},
|
|
5425
5501
|
bbox: {
|
|
5426
|
-
x:
|
|
5427
|
-
y:
|
|
5428
|
-
w:
|
|
5429
|
-
h:
|
|
5430
|
-
}
|
|
5502
|
+
x: at.bbox.x / w,
|
|
5503
|
+
y: at.bbox.y / h,
|
|
5504
|
+
w: at.bbox.w / w,
|
|
5505
|
+
h: at.bbox.h / h
|
|
5506
|
+
},
|
|
5507
|
+
dtMs: at.dtMs,
|
|
5508
|
+
interpolated: at.interpolated
|
|
5431
5509
|
});
|
|
5432
5510
|
}
|
|
5433
5511
|
return [...out.values()];
|
|
@@ -19329,11 +19407,16 @@ var DebugNoteArchiveStore = class {
|
|
|
19329
19407
|
};
|
|
19330
19408
|
//#endregion
|
|
19331
19409
|
//#region src/pipeline-analytics/store/event-ledger-collection.ts
|
|
19410
|
+
/**
|
|
19411
|
+
* One event ledger (motion, audio, sensor, faces, plates …) over its
|
|
19412
|
+
* `ctx.records` kind. Every node has `ctx.records`, so there is no other path.
|
|
19413
|
+
*/
|
|
19332
19414
|
/** Matches {@link EventStore.LEDGER_PAGE_SIZE} — kept local to avoid a circular import. */
|
|
19333
19415
|
var LEDGER_PAGE_SIZE = 500;
|
|
19334
19416
|
var EventLedgerCollection = class EventLedgerCollection {
|
|
19335
19417
|
collection;
|
|
19336
19418
|
kind;
|
|
19419
|
+
records;
|
|
19337
19420
|
autoIntegerId;
|
|
19338
19421
|
legacyAtField;
|
|
19339
19422
|
deviceIdMode;
|
|
@@ -19349,6 +19432,7 @@ var EventLedgerCollection = class EventLedgerCollection {
|
|
|
19349
19432
|
this.logger = deps.logger;
|
|
19350
19433
|
this.deriveBody = deps.deriveBody;
|
|
19351
19434
|
this.indexFields = deps.definition.declaration.indexes.map((i) => i.fields);
|
|
19435
|
+
this.records = deps.records;
|
|
19352
19436
|
const handle = deps.records.kind(deps.definition);
|
|
19353
19437
|
this.kind = handle;
|
|
19354
19438
|
}
|
|
@@ -19367,6 +19451,31 @@ var EventLedgerCollection = class EventLedgerCollection {
|
|
|
19367
19451
|
}
|
|
19368
19452
|
await this.kind.put(put);
|
|
19369
19453
|
}
|
|
19454
|
+
/**
|
|
19455
|
+
* `upsertRow` as an op of {@link EventLedgerCollection.transact}. A row that
|
|
19456
|
+
* cannot be mapped THROWS here: inside a transaction a silently skipped write
|
|
19457
|
+
* would let the rest of the call commit without it.
|
|
19458
|
+
*/
|
|
19459
|
+
async txUpsert(id, value) {
|
|
19460
|
+
const put = this.buildRecordPut(id, value);
|
|
19461
|
+
if (put === null) throw new InvalidRecordError(`${this.collection}/${id}: the record could not be mapped`);
|
|
19462
|
+
return this.kind.txPut(put);
|
|
19463
|
+
}
|
|
19464
|
+
/** `update` as an op of {@link EventLedgerCollection.transact}. */
|
|
19465
|
+
txUpdate(id, patch) {
|
|
19466
|
+
return this.kind.txPatch(id, this.derived(patch, "patch"));
|
|
19467
|
+
}
|
|
19468
|
+
/** `deleteKey` as an op of {@link EventLedgerCollection.transact}. */
|
|
19469
|
+
txDelete(id) {
|
|
19470
|
+
return this.kind.txDelete(id);
|
|
19471
|
+
}
|
|
19472
|
+
/**
|
|
19473
|
+
* Run ops (built by the `tx*` methods of ledgers whose kinds share a
|
|
19474
|
+
* `transactFamily`) in ONE transaction: all land or none does.
|
|
19475
|
+
*/
|
|
19476
|
+
transact(ops) {
|
|
19477
|
+
return this.records.transact(ops);
|
|
19478
|
+
}
|
|
19370
19479
|
async insert(record) {
|
|
19371
19480
|
const idNum = this.autoIntegerId ? await this.kind.allocateId() : null;
|
|
19372
19481
|
const id = idNum !== null ? String(idNum) : typeof record["id"] === "string" ? record["id"] : null;
|
|
@@ -20443,8 +20552,8 @@ var FACE_REF_COLUMNS = ["deviceId"];
|
|
|
20443
20552
|
* D312) a predicate that silently matched nothing. The faces ledger therefore derives a
|
|
20444
20553
|
* boolean `recognized` from every write that touches `recognizedIdentityId`
|
|
20445
20554
|
* (`face-recognized.ts`), and the views are plain ranges over the `recognized_ts` /
|
|
20446
|
-
* `assigned_recognized_ts` indexes.
|
|
20447
|
-
*
|
|
20555
|
+
* `assigned_recognized_ts` indexes. Rows written before the field existed were
|
|
20556
|
+
* backfilled offline on the operator hubs on 2026-10-22 (the one-shot script is deleted).
|
|
20448
20557
|
*
|
|
20449
20558
|
* `assigned` is `BOOLEAN NOT NULL`; the backend serialises `false` to `0` at
|
|
20450
20559
|
* the bind (`serializeColumnValue`) and decodes `0` back to `false` on read, so
|
|
@@ -22308,8 +22417,9 @@ var RetrainAnnotationStore = class {
|
|
|
22308
22417
|
* The old rows' deletion, the new rows, the frame's `dismissed` set and its
|
|
22309
22418
|
* bumped `revision` run under a per-frame commit lock, guarded by
|
|
22310
22419
|
* `commit.fromRevision`: a concurrent save in this process leaves one winner
|
|
22311
|
-
* and one `conflict` — never a blend. The
|
|
22312
|
-
*
|
|
22420
|
+
* and one `conflict` — never a blend. The deletion, the new rows and the frame
|
|
22421
|
+
* row are ONE records `transact` (the four retrain kinds share a family): a
|
|
22422
|
+
* failure leaves the frame exactly as it was.
|
|
22313
22423
|
*
|
|
22314
22424
|
* Returns the persisted rows so the caller renders ids it can address.
|
|
22315
22425
|
*/
|
|
@@ -22426,21 +22536,14 @@ var RetrainAnnotationStore = class {
|
|
|
22426
22536
|
}
|
|
22427
22537
|
return stamped;
|
|
22428
22538
|
}
|
|
22429
|
-
/**
|
|
22430
|
-
|
|
22431
|
-
|
|
22432
|
-
|
|
22433
|
-
|
|
22434
|
-
|
|
22435
|
-
|
|
22436
|
-
|
|
22437
|
-
meta: {
|
|
22438
|
-
annotationId: row.id,
|
|
22439
|
-
error: String(err)
|
|
22440
|
-
}
|
|
22441
|
-
});
|
|
22442
|
-
}
|
|
22443
|
-
return rows.length;
|
|
22539
|
+
/**
|
|
22540
|
+
* The delete ops of every annotation of a frame, for ONE transaction with
|
|
22541
|
+
* the frame row and its memberships (RetrainService.removeFrame). A failed
|
|
22542
|
+
* read THROWS: "no annotations" must never be the answer to an unreadable
|
|
22543
|
+
* store, because the frame row would then be deleted over live boxes.
|
|
22544
|
+
*/
|
|
22545
|
+
async deleteOpsForFrame(mediaKey) {
|
|
22546
|
+
return (await this.annotations.query({ where: { mediaKey } })).map((row) => this.annotations.txDelete(row.id));
|
|
22444
22547
|
}
|
|
22445
22548
|
async replaceForFrameOnRecords(mediaKey, rows, commit, nextRevision) {
|
|
22446
22549
|
const frame = await this.frames.get(mediaKey);
|
|
@@ -22453,12 +22556,14 @@ var RetrainAnnotationStore = class {
|
|
|
22453
22556
|
kind: "conflict",
|
|
22454
22557
|
currentRevision
|
|
22455
22558
|
};
|
|
22456
|
-
|
|
22457
|
-
for (const
|
|
22458
|
-
await this.
|
|
22559
|
+
const ops = [];
|
|
22560
|
+
for (const existing of await this.annotations.query({ where: { mediaKey } })) ops.push(this.annotations.txDelete(existing.id));
|
|
22561
|
+
for (const row of rows) ops.push(await this.annotations.txUpsert(row.id, this.toRecord(row)));
|
|
22562
|
+
ops.push(await this.frames.txUpdate(mediaKey, {
|
|
22459
22563
|
revision: nextRevision,
|
|
22460
22564
|
...commit.dismissed !== void 0 ? { dismissed: [...commit.dismissed] } : {}
|
|
22461
|
-
});
|
|
22565
|
+
}));
|
|
22566
|
+
await this.frames.transact(ops);
|
|
22462
22567
|
return {
|
|
22463
22568
|
kind: "saved",
|
|
22464
22569
|
rows,
|
|
@@ -22476,8 +22581,10 @@ var RetrainAnnotationStore = class {
|
|
|
22476
22581
|
kind: "conflict",
|
|
22477
22582
|
currentRevision
|
|
22478
22583
|
};
|
|
22479
|
-
|
|
22480
|
-
await this.
|
|
22584
|
+
const ops = [];
|
|
22585
|
+
for (const row of eligible) ops.push(await this.annotations.txUpdate(row.id, { origin: origins.get(row.id) ?? null }));
|
|
22586
|
+
ops.push(await this.frames.txUpdate(mediaKey, { revision: nextRevision }));
|
|
22587
|
+
await this.frames.transact(ops);
|
|
22481
22588
|
return {
|
|
22482
22589
|
kind: "updated",
|
|
22483
22590
|
updated: eligible.length,
|
|
@@ -22648,16 +22755,16 @@ var RetrainCollectionStore = class {
|
|
|
22648
22755
|
}, counts.get(input.id) ?? 0)
|
|
22649
22756
|
};
|
|
22650
22757
|
}
|
|
22651
|
-
/** Remove the collection and its memberships
|
|
22652
|
-
*
|
|
22758
|
+
/** Remove the collection and its memberships in ONE transaction: a failure
|
|
22759
|
+
* leaves the collection with all its members, never memberships pointing at
|
|
22760
|
+
* a name that no longer exists, nor a collection that silently lost some. */
|
|
22653
22761
|
async delete(id) {
|
|
22654
22762
|
if (await this.readRow(id) === null) return {
|
|
22655
22763
|
deleted: false,
|
|
22656
22764
|
memberFrameIds: []
|
|
22657
22765
|
};
|
|
22658
22766
|
const members = await this.memberships.query({ where: { collectionId: id } });
|
|
22659
|
-
|
|
22660
|
-
await this.collections.deleteKey(id);
|
|
22767
|
+
await this.collections.transact([...members.map((m) => this.memberships.txDelete(m.id)), this.collections.txDelete(id)]);
|
|
22661
22768
|
this.logger.info("retrain collection deleted", { meta: {
|
|
22662
22769
|
collectionId: id,
|
|
22663
22770
|
members: members.length
|
|
@@ -22686,11 +22793,29 @@ var RetrainCollectionStore = class {
|
|
|
22686
22793
|
await this.memberships.deleteKey(key);
|
|
22687
22794
|
return true;
|
|
22688
22795
|
}
|
|
22689
|
-
/**
|
|
22690
|
-
|
|
22691
|
-
|
|
22692
|
-
|
|
22693
|
-
|
|
22796
|
+
/**
|
|
22797
|
+
* The delete ops of every membership of a frame, for ONE transaction with
|
|
22798
|
+
* the frame row and its annotations (RetrainService.removeFrame). A failed
|
|
22799
|
+
* read THROWS: the frame row must not be deleted over memberships nobody
|
|
22800
|
+
* could list.
|
|
22801
|
+
*/
|
|
22802
|
+
async forgetFrameOps(frameId) {
|
|
22803
|
+
return (await this.memberships.query({ where: { frameId } })).map((r) => this.memberships.txDelete(r.id));
|
|
22804
|
+
}
|
|
22805
|
+
/**
|
|
22806
|
+
* The op that adds a frame to a collection, for ONE transaction with the
|
|
22807
|
+
* frame's own row (a still stored from the recording). `null` when it is
|
|
22808
|
+
* already a member: there is nothing to write.
|
|
22809
|
+
*/
|
|
22810
|
+
async addFrameOp(input) {
|
|
22811
|
+
const key = membershipKey(input.collectionId, input.frameId);
|
|
22812
|
+
if (await this.memberships.get(key) !== null) return null;
|
|
22813
|
+
return this.memberships.txUpsert(key, {
|
|
22814
|
+
collectionId: input.collectionId,
|
|
22815
|
+
frameId: input.frameId,
|
|
22816
|
+
deviceId: input.deviceId,
|
|
22817
|
+
addedAt: this.now()
|
|
22818
|
+
});
|
|
22694
22819
|
}
|
|
22695
22820
|
/** The frames of a collection, in the order they were added. */
|
|
22696
22821
|
async listFrameIds(collectionId) {
|
|
@@ -23282,7 +23407,7 @@ var RetrainFrameStore = class {
|
|
|
23282
23407
|
* returned untouched; the blob lands before the row; a refusal is named.
|
|
23283
23408
|
* `capturedAt` IS the instant — the camera saw this frame then.
|
|
23284
23409
|
*/
|
|
23285
|
-
async storeRecordingStill(input) {
|
|
23410
|
+
async storeRecordingStill(input, withRow = []) {
|
|
23286
23411
|
const frameId = recordingFrameId(input.deviceId, input.epochMs);
|
|
23287
23412
|
const existing = await this.get(frameId);
|
|
23288
23413
|
if (existing !== null) return {
|
|
@@ -23329,7 +23454,7 @@ var RetrainFrameStore = class {
|
|
|
23329
23454
|
relativePath: path,
|
|
23330
23455
|
data: input.bytes
|
|
23331
23456
|
});
|
|
23332
|
-
await this.ledger.
|
|
23457
|
+
const row = await this.ledger.txUpsert(frameId, {
|
|
23333
23458
|
deviceId: frame.deviceId,
|
|
23334
23459
|
trackId: "",
|
|
23335
23460
|
sourceMediaId: 0,
|
|
@@ -23346,6 +23471,7 @@ var RetrainFrameStore = class {
|
|
|
23346
23471
|
origin: "recording",
|
|
23347
23472
|
epochMs: input.epochMs
|
|
23348
23473
|
});
|
|
23474
|
+
await this.ledger.transact([row, ...withRow]);
|
|
23349
23475
|
};
|
|
23350
23476
|
try {
|
|
23351
23477
|
if (this.writeGate) await this.writeGate.run(persist);
|
|
@@ -23399,22 +23525,33 @@ var RetrainFrameStore = class {
|
|
|
23399
23525
|
/**
|
|
23400
23526
|
* Drop a copy and its blob — the operator un-selecting a frame.
|
|
23401
23527
|
*
|
|
23402
|
-
* The
|
|
23403
|
-
*
|
|
23404
|
-
*
|
|
23528
|
+
* The ROW goes first, in one transaction with `withRow` (the frame's
|
|
23529
|
+
* annotations and collection memberships, which the caller builds): a failure
|
|
23530
|
+
* leaves the frame, its boxes and its memberships all in place, and throws.
|
|
23531
|
+
* The blob goes after: if that fails the row is already gone and the bytes
|
|
23532
|
+
* are reclaimable junk, logged, never a row pointing at missing media.
|
|
23405
23533
|
*/
|
|
23406
|
-
async deleteFrame(frame) {
|
|
23534
|
+
async deleteFrame(frame, withRow = []) {
|
|
23407
23535
|
const remove = async () => {
|
|
23536
|
+
try {
|
|
23537
|
+
await this.ledger.transact([...withRow, this.ledger.txDelete(frame.frameId)]);
|
|
23538
|
+
} catch (err) {
|
|
23539
|
+
this.logger.error("retrain frame delete failed; nothing was removed", {
|
|
23540
|
+
tags: { deviceId: frame.deviceId },
|
|
23541
|
+
meta: {
|
|
23542
|
+
frameId: frame.frameId,
|
|
23543
|
+
error: String(err)
|
|
23544
|
+
}
|
|
23545
|
+
});
|
|
23546
|
+
throw err;
|
|
23547
|
+
}
|
|
23408
23548
|
try {
|
|
23409
23549
|
await this.storage.delete({
|
|
23410
23550
|
location: frame.locationId ?? LEGACY_RETRAIN_MEDIA_LOCATION,
|
|
23411
23551
|
relativePath: framePath(frame)
|
|
23412
23552
|
});
|
|
23413
|
-
} catch {}
|
|
23414
|
-
try {
|
|
23415
|
-
await this.ledger.deleteKey(frame.frameId);
|
|
23416
23553
|
} catch (err) {
|
|
23417
|
-
this.logger.warn("
|
|
23554
|
+
this.logger.warn("retrain frame removed but its blob could not be deleted", {
|
|
23418
23555
|
tags: { deviceId: frame.deviceId },
|
|
23419
23556
|
meta: {
|
|
23420
23557
|
frameId: frame.frameId,
|
|
@@ -28392,7 +28529,9 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
28392
28529
|
const blindForMs = numberField(data, "blindForMs");
|
|
28393
28530
|
if (deviceId === void 0 || blindForMs === void 0) return null;
|
|
28394
28531
|
const identity = lookup(deviceId);
|
|
28395
|
-
const
|
|
28532
|
+
const motionMs = numberField(data, "motionMs") ?? blindForMs;
|
|
28533
|
+
const sessions = numberField(data, "sessions") ?? 1;
|
|
28534
|
+
const minutes = Math.max(1, Math.round(motionMs / 6e4));
|
|
28396
28535
|
return systemSubject({
|
|
28397
28536
|
facts,
|
|
28398
28537
|
kind: "detection-blind",
|
|
@@ -28401,13 +28540,39 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
28401
28540
|
bodyKey: "system.detection-blind.body",
|
|
28402
28541
|
textParams: {
|
|
28403
28542
|
...deviceTextParams(deviceId, identity),
|
|
28404
|
-
minutes: String(minutes)
|
|
28543
|
+
minutes: String(minutes),
|
|
28544
|
+
sessions: String(sessions)
|
|
28405
28545
|
},
|
|
28406
28546
|
textCount: minutes,
|
|
28407
28547
|
deviceId,
|
|
28408
28548
|
...identity !== void 0 ? { deviceType: identity.type } : {}
|
|
28409
28549
|
});
|
|
28410
28550
|
}
|
|
28551
|
+
case "pipeline.motion-noisy":
|
|
28552
|
+
case "pipeline.motion-calm": {
|
|
28553
|
+
const deviceId = numberField(data, "deviceId");
|
|
28554
|
+
const dutyPct = numberField(data, "dutyPct");
|
|
28555
|
+
const sessions = numberField(data, "sessions");
|
|
28556
|
+
const windowMs = numberField(data, "windowMs");
|
|
28557
|
+
if (deviceId === void 0 || dutyPct === void 0 || sessions === void 0 || windowMs === void 0) return null;
|
|
28558
|
+
const noisy = event.category === "pipeline.motion-noisy";
|
|
28559
|
+
const identity = lookup(deviceId);
|
|
28560
|
+
return systemSubject({
|
|
28561
|
+
facts,
|
|
28562
|
+
kind: noisy ? "motion-noisy" : "motion-calm",
|
|
28563
|
+
subject: `motion-noise:${deviceId}`,
|
|
28564
|
+
titleKey: noisy ? "system.motion-noisy.title" : "system.motion-calm.title",
|
|
28565
|
+
bodyKey: noisy ? "system.motion-noisy.body" : "system.motion-calm.body",
|
|
28566
|
+
textParams: {
|
|
28567
|
+
...deviceTextParams(deviceId, identity),
|
|
28568
|
+
minutes: String(Math.max(1, Math.round(windowMs / 6e4))),
|
|
28569
|
+
duty: String(dutyPct),
|
|
28570
|
+
sessions: String(sessions)
|
|
28571
|
+
},
|
|
28572
|
+
deviceId,
|
|
28573
|
+
...identity !== void 0 ? { deviceType: identity.type } : {}
|
|
28574
|
+
});
|
|
28575
|
+
}
|
|
28411
28576
|
case "recording.health.stalled": {
|
|
28412
28577
|
const deviceId = numberField(data, "deviceId");
|
|
28413
28578
|
const idleMs = numberField(data, "idleMs");
|
|
@@ -28489,6 +28654,33 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
28489
28654
|
...nodeId !== void 0 ? { nodeId } : {}
|
|
28490
28655
|
});
|
|
28491
28656
|
}
|
|
28657
|
+
case "notification.rule-broken":
|
|
28658
|
+
case "notification.rule-repaired": {
|
|
28659
|
+
const ruleId = stringField(data, "ruleId");
|
|
28660
|
+
if (ruleId === void 0) return null;
|
|
28661
|
+
const broken = event.category === "notification.rule-broken";
|
|
28662
|
+
const kind = broken ? "rule-broken" : "rule-repaired";
|
|
28663
|
+
const ruleName = stringField(data, "ruleName") ?? ruleId;
|
|
28664
|
+
const reason = stringField(data, "reason") ?? "unknown";
|
|
28665
|
+
const ids = data["offendingIds"];
|
|
28666
|
+
const offendingIds = Array.isArray(ids) ? ids.filter((id) => typeof id === "string").join(", ") : "";
|
|
28667
|
+
return systemSubject({
|
|
28668
|
+
facts,
|
|
28669
|
+
kind,
|
|
28670
|
+
subject: `rule-health:${ruleId}`,
|
|
28671
|
+
titleKey: `system.${kind}.title`,
|
|
28672
|
+
bodyKey: broken ? `system.rule-broken.body.${reason}` : "system.rule-repaired.body",
|
|
28673
|
+
textParams: broken ? {
|
|
28674
|
+
ruleId,
|
|
28675
|
+
ruleName,
|
|
28676
|
+
brokenReason: reason,
|
|
28677
|
+
offendingIds
|
|
28678
|
+
} : {
|
|
28679
|
+
ruleId,
|
|
28680
|
+
ruleName
|
|
28681
|
+
}
|
|
28682
|
+
});
|
|
28683
|
+
}
|
|
28492
28684
|
case "update.available": return incomingFromUpdateAvailable(facts, data);
|
|
28493
28685
|
case "addon.updated": {
|
|
28494
28686
|
const update = parseAddonUpdateComplete(data);
|
|
@@ -28498,7 +28690,7 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
28498
28690
|
case "system.restarting": return incomingFromNodeRestarting(facts, data);
|
|
28499
28691
|
case "recording.export.completed": return incomingFromExportCompleted(facts, data);
|
|
28500
28692
|
case "backup.run-completed": return incomingFromBackupCompleted(facts, data);
|
|
28501
|
-
case "alert.episode": return incomingFromAlertEpisode(facts);
|
|
28693
|
+
case "alert.episode": return incomingFromAlertEpisode(facts, lookup);
|
|
28502
28694
|
default: return null;
|
|
28503
28695
|
}
|
|
28504
28696
|
}
|
|
@@ -28514,9 +28706,11 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
28514
28706
|
* Whether an edge is notified at all (once per episode, reconciled from the
|
|
28515
28707
|
* alerts store) is decided before this point, by `NcAlertEpisodeLedger`.
|
|
28516
28708
|
*/
|
|
28517
|
-
function incomingFromAlertEpisode(facts) {
|
|
28709
|
+
function incomingFromAlertEpisode(facts, lookup) {
|
|
28518
28710
|
const payload = parseAlertEpisodePayload(facts.data);
|
|
28519
28711
|
if (payload === null) return null;
|
|
28712
|
+
const attention = incomingFromDeviceAttention(facts, payload, lookup);
|
|
28713
|
+
if (attention !== null) return attention;
|
|
28520
28714
|
const kind = alertEpisodeKind(payload.severity, payload.phase);
|
|
28521
28715
|
if (kind === null) return null;
|
|
28522
28716
|
return systemSubject({
|
|
@@ -28537,6 +28731,38 @@ function incomingFromAlertEpisode(facts) {
|
|
|
28537
28731
|
...payload.nodeId !== void 0 ? { nodeId: payload.nodeId } : {}
|
|
28538
28732
|
});
|
|
28539
28733
|
}
|
|
28734
|
+
/**
|
|
28735
|
+
* A CRITICAL alert whose source is a DEVICE (D911) is announced in the devices
|
|
28736
|
+
* family — `device-attention-required` / `-resolved`, carrying the device id so
|
|
28737
|
+
* a rule's device filters apply — and NOT as `alert-critical`, so one alert
|
|
28738
|
+
* never notifies twice through two families. A device source whose id is not a
|
|
28739
|
+
* device id falls through to the generic alert kinds.
|
|
28740
|
+
*/
|
|
28741
|
+
function incomingFromDeviceAttention(facts, payload, lookup) {
|
|
28742
|
+
if (payload.sourceType !== "device" || payload.sourceId === void 0) return null;
|
|
28743
|
+
const kind = deviceAttentionKind(payload.severity, payload.phase);
|
|
28744
|
+
if (kind === null) return null;
|
|
28745
|
+
const deviceId = Number(payload.sourceId);
|
|
28746
|
+
if (!Number.isInteger(deviceId)) return null;
|
|
28747
|
+
const identity = lookup?.(deviceId);
|
|
28748
|
+
return systemSubject({
|
|
28749
|
+
facts,
|
|
28750
|
+
kind,
|
|
28751
|
+
subject: `alert:${payload.episodeId}`,
|
|
28752
|
+
titleKey: `system.${kind}.title`,
|
|
28753
|
+
bodyKey: `system.${kind}.body`,
|
|
28754
|
+
textParams: {
|
|
28755
|
+
...deviceTextParams(deviceId, identity),
|
|
28756
|
+
title: payload.title,
|
|
28757
|
+
message: payload.message,
|
|
28758
|
+
category: payload.category,
|
|
28759
|
+
actionUrl: payload.actionUrl ?? ""
|
|
28760
|
+
},
|
|
28761
|
+
deviceId,
|
|
28762
|
+
...identity !== void 0 ? { deviceType: identity.type } : {},
|
|
28763
|
+
...payload.nodeId !== void 0 ? { nodeId: payload.nodeId } : {}
|
|
28764
|
+
});
|
|
28765
|
+
}
|
|
28540
28766
|
/** The shells whose update has its own sentence. Anything else gets the plain
|
|
28541
28767
|
* one: a body that names a shell nobody observed is worse than a vague body. */
|
|
28542
28768
|
var WRAPPER_BODY_KINDS = new Set([
|
|
@@ -30494,8 +30720,8 @@ var en_default = {
|
|
|
30494
30720
|
"occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
|
|
30495
30721
|
"occupancy.op.occupied": "occupied",
|
|
30496
30722
|
"occupancy.op.free": "free",
|
|
30497
|
-
"system.device-online.title": "
|
|
30498
|
-
"system.device-online.body": "Device {{deviceName}} is online",
|
|
30723
|
+
"system.device-online.title": "Back online — {{deviceName}}",
|
|
30724
|
+
"system.device-online.body": "Device {{deviceName}} is back online",
|
|
30499
30725
|
"system.device-offline.title": "Offline — {{deviceName}}",
|
|
30500
30726
|
"system.device-offline.body": "Device {{deviceName}} is offline",
|
|
30501
30727
|
"system.device-disabled.title": "Switched off — {{deviceName}}",
|
|
@@ -30508,14 +30734,14 @@ var en_default = {
|
|
|
30508
30734
|
"system.device-battery-normal.title": "Battery back to normal — {{deviceName}}",
|
|
30509
30735
|
"system.device-battery-normal.body": "{{deviceName}} is back to {{percentage}}% battery",
|
|
30510
30736
|
"system.device-battery-normal.body.binary": "{{deviceName}} no longer reports a low battery",
|
|
30511
|
-
"system.stream-online.title": "Stream online — {{deviceName}}",
|
|
30512
|
-
"system.stream-online.body": "Stream for camera {{deviceName}} is online",
|
|
30513
|
-
"system.stream-online.body.profiled": "{{profile}} stream for camera {{deviceName}} is online",
|
|
30737
|
+
"system.stream-online.title": "Stream back online — {{deviceName}}",
|
|
30738
|
+
"system.stream-online.body": "Stream for camera {{deviceName}} is back online",
|
|
30739
|
+
"system.stream-online.body.profiled": "{{profile}} stream for camera {{deviceName}} is back online",
|
|
30514
30740
|
"system.stream-offline.title": "Stream offline — {{deviceName}}",
|
|
30515
30741
|
"system.stream-offline.body": "Stream for camera {{deviceName}} is offline",
|
|
30516
30742
|
"system.stream-offline.body.profiled": "{{profile}} stream for camera {{deviceName}} is offline",
|
|
30517
|
-
"system.node-online.title": "Node online — {{nodeName}}",
|
|
30518
|
-
"system.node-online.body.agent": "Agent {{nodeName}} is online",
|
|
30743
|
+
"system.node-online.title": "Node back online — {{nodeName}}",
|
|
30744
|
+
"system.node-online.body.agent": "Agent {{nodeName}} is back online",
|
|
30519
30745
|
"system.node-online.body.agent.after": "Agent {{nodeName}} is back online after {{offlineFor}} offline",
|
|
30520
30746
|
"system.node-offline.title": "Node offline — {{nodeName}}",
|
|
30521
30747
|
"system.node-offline.body.agent": "Agent {{nodeName}} is offline",
|
|
@@ -30527,18 +30753,26 @@ var en_default = {
|
|
|
30527
30753
|
"system.addon-crash-loop.body": "The {{addons}} addon on node {{nodeId}} crashed {{count}} times in a row and will NOT be restarted. Everything it provides is offline until it is repaired.",
|
|
30528
30754
|
"system.detection-blind.title": "Detecting nothing — {{deviceName}}",
|
|
30529
30755
|
"system.detection-blind.body": {
|
|
30530
|
-
"one": "Camera {{deviceName}}
|
|
30531
|
-
"other": "Camera {{deviceName}}
|
|
30756
|
+
"one": "Camera {{deviceName}} moved for {{minutes}} minute over {{sessions}} sessions and the detector produced nothing at all, not even a weak candidate",
|
|
30757
|
+
"other": "Camera {{deviceName}} moved for {{minutes}} minutes over {{sessions}} sessions and the detector produced nothing at all, not even a weak candidate"
|
|
30532
30758
|
},
|
|
30759
|
+
"system.motion-noisy.title": "Constant motion, no subject — {{deviceName}}",
|
|
30760
|
+
"system.motion-noisy.body": "Camera {{deviceName}} has been in motion {{duty}}% of the last {{minutes}} minutes ({{sessions}} sessions) without a single detection. The detector is fine — tune this camera's motion zones or sensitivity.",
|
|
30761
|
+
"system.motion-calm.title": "Motion calm again — {{deviceName}}",
|
|
30762
|
+
"system.motion-calm.body": "Camera {{deviceName}} is no longer in constant motion ({{duty}}% of the last {{minutes}} minutes, {{sessions}} sessions)",
|
|
30533
30763
|
"system.recording-stalled.title": "Recording stalled — {{deviceName}}",
|
|
30534
30764
|
"system.recording-stalled.body": {
|
|
30535
30765
|
"one": "Camera {{deviceName}} has not written a recording segment for {{minutes}} minute",
|
|
30536
30766
|
"other": "Camera {{deviceName}} has not written a recording segment for {{minutes}} minutes"
|
|
30537
30767
|
},
|
|
30538
|
-
"system.recording-healthy.title": "Recording
|
|
30768
|
+
"system.recording-healthy.title": "Recording again — {{deviceName}}",
|
|
30539
30769
|
"system.recording-healthy.body": "Camera {{deviceName}} is writing recording segments again",
|
|
30540
30770
|
"system.recording-degraded.title": "Recording degraded — {{deviceName}}",
|
|
30541
30771
|
"system.recording-degraded.body": "Camera {{deviceName}} exhausted its writer restart budget ({{message}})",
|
|
30772
|
+
"system.device-attention-required.title": "{{title}}",
|
|
30773
|
+
"system.device-attention-required.body": "{{message}}",
|
|
30774
|
+
"system.device-attention-resolved.title": "Resolved: {{title}}",
|
|
30775
|
+
"system.device-attention-resolved.body": "{{deviceName}} no longer needs attention",
|
|
30542
30776
|
"system.storage-location-low.title": "Storage low — {{locationName}}",
|
|
30543
30777
|
"system.storage-location-low.body": "Location {{locationName}} is down to {{freePercent}}% free ({{reason}})",
|
|
30544
30778
|
"system.storage-location-critical.title": "Storage critical — {{locationName}}",
|
|
@@ -30796,7 +31030,13 @@ var en_default = {
|
|
|
30796
31030
|
"calendar-events.line.timed": "• {{when}} {{title}}{{where}}",
|
|
30797
31031
|
"calendar-events.line.allDay": "• All day {{title}}{{where}}",
|
|
30798
31032
|
"calendar-events.where": " @ {{location}}",
|
|
30799
|
-
"calendar-events.more": "…and {{count}} more"
|
|
31033
|
+
"calendar-events.more": "…and {{count}} more",
|
|
31034
|
+
"system.rule-broken.title": "Notification rule broken — {{ruleName}}",
|
|
31035
|
+
"system.rule-broken.body.unparseable": "The rule \"{{ruleName}}\" cannot be loaded and sends nothing. Open it and save it again.",
|
|
31036
|
+
"system.rule-broken.body.device-deleted": "The rule \"{{ruleName}}\" still names a deleted device ({{offendingIds}}) and was switched off. Pick the device again.",
|
|
31037
|
+
"system.rule-broken.body.target-deleted": "The rule \"{{ruleName}}\" still sends to a deleted destination ({{offendingIds}}) and was switched off. Pick a destination again.",
|
|
31038
|
+
"system.rule-repaired.title": "Notification rule works again — {{ruleName}}",
|
|
31039
|
+
"system.rule-repaired.body": "The rule \"{{ruleName}}\" loads and names nothing deleted any more."
|
|
30800
31040
|
};
|
|
30801
31041
|
//#endregion
|
|
30802
31042
|
//#region src/notification-center/texts/it.json
|
|
@@ -30881,8 +31121,8 @@ var it_default = {
|
|
|
30881
31121
|
"occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
|
|
30882
31122
|
"occupancy.op.occupied": "occupata",
|
|
30883
31123
|
"occupancy.op.free": "libera",
|
|
30884
|
-
"system.device-online.title": "
|
|
30885
|
-
"system.device-online.body": "Il dispositivo {{deviceName}} è online",
|
|
31124
|
+
"system.device-online.title": "Di nuovo online — {{deviceName}}",
|
|
31125
|
+
"system.device-online.body": "Il dispositivo {{deviceName}} è di nuovo online",
|
|
30886
31126
|
"system.device-offline.title": "Offline — {{deviceName}}",
|
|
30887
31127
|
"system.device-offline.body": "Il dispositivo {{deviceName}} è offline",
|
|
30888
31128
|
"system.device-disabled.title": "Spento — {{deviceName}}",
|
|
@@ -30895,14 +31135,14 @@ var it_default = {
|
|
|
30895
31135
|
"system.device-battery-normal.title": "Batteria rientrata — {{deviceName}}",
|
|
30896
31136
|
"system.device-battery-normal.body": "{{deviceName}} è risalito al {{percentage}}% di batteria",
|
|
30897
31137
|
"system.device-battery-normal.body.binary": "{{deviceName}} non segnala più la batteria scarica",
|
|
30898
|
-
"system.stream-online.title": "Stream online — {{deviceName}}",
|
|
30899
|
-
"system.stream-online.body": "Lo stream della camera {{deviceName}} è online",
|
|
30900
|
-
"system.stream-online.body.profiled": "Lo stream {{profile}} della camera {{deviceName}} è online",
|
|
31138
|
+
"system.stream-online.title": "Stream di nuovo online — {{deviceName}}",
|
|
31139
|
+
"system.stream-online.body": "Lo stream della camera {{deviceName}} è di nuovo online",
|
|
31140
|
+
"system.stream-online.body.profiled": "Lo stream {{profile}} della camera {{deviceName}} è di nuovo online",
|
|
30901
31141
|
"system.stream-offline.title": "Stream offline — {{deviceName}}",
|
|
30902
31142
|
"system.stream-offline.body": "Lo stream della camera {{deviceName}} è offline",
|
|
30903
31143
|
"system.stream-offline.body.profiled": "Lo stream {{profile}} della camera {{deviceName}} è offline",
|
|
30904
|
-
"system.node-online.title": "Nodo online — {{nodeName}}",
|
|
30905
|
-
"system.node-online.body.agent": "L'agent {{nodeName}} è online",
|
|
31144
|
+
"system.node-online.title": "Nodo di nuovo online — {{nodeName}}",
|
|
31145
|
+
"system.node-online.body.agent": "L'agent {{nodeName}} è di nuovo online",
|
|
30906
31146
|
"system.node-online.body.agent.after": "L'agent {{nodeName}} è di nuovo online dopo {{offlineFor}} offline",
|
|
30907
31147
|
"system.node-offline.title": "Nodo offline — {{nodeName}}",
|
|
30908
31148
|
"system.node-offline.body.agent": "L'agent {{nodeName}} è offline",
|
|
@@ -30917,10 +31157,14 @@ var it_default = {
|
|
|
30917
31157
|
"one": "La camera {{deviceName}} non scrive un segmento da {{minutes}} minuto",
|
|
30918
31158
|
"other": "La camera {{deviceName}} non scrive un segmento da {{minutes}} minuti"
|
|
30919
31159
|
},
|
|
30920
|
-
"system.recording-healthy.title": "
|
|
31160
|
+
"system.recording-healthy.title": "Registra di nuovo — {{deviceName}}",
|
|
30921
31161
|
"system.recording-healthy.body": "La camera {{deviceName}} scrive di nuovo i segmenti",
|
|
30922
31162
|
"system.recording-degraded.title": "Registrazione degradata — {{deviceName}}",
|
|
30923
31163
|
"system.recording-degraded.body": "La camera {{deviceName}} ha esaurito i riavvii del writer ({{message}})",
|
|
31164
|
+
"system.device-attention-required.title": "{{title}}",
|
|
31165
|
+
"system.device-attention-required.body": "{{message}}",
|
|
31166
|
+
"system.device-attention-resolved.title": "Risolto: {{title}}",
|
|
31167
|
+
"system.device-attention-resolved.body": "{{deviceName}} non richiede più intervento",
|
|
30924
31168
|
"system.storage-location-low.title": "Storage basso — {{locationName}}",
|
|
30925
31169
|
"system.storage-location-low.body": "La location {{locationName}} è al {{freePercent}}% libero ({{reason}})",
|
|
30926
31170
|
"system.storage-location-critical.title": "Storage critico — {{locationName}}",
|
|
@@ -30929,9 +31173,13 @@ var it_default = {
|
|
|
30929
31173
|
"system.storage-location-ok.body": "La location {{locationName}} ha recuperato spazio libero",
|
|
30930
31174
|
"system.detection-blind.title": "Senza detection — {{deviceName}}",
|
|
30931
31175
|
"system.detection-blind.body": {
|
|
30932
|
-
"one": "La camera {{deviceName}}
|
|
30933
|
-
"other": "La camera {{deviceName}}
|
|
31176
|
+
"one": "La camera {{deviceName}} ha avuto movimento per {{minutes}} minuto in {{sessions}} sessioni e il detector non ha prodotto nulla, nemmeno un candidato debole",
|
|
31177
|
+
"other": "La camera {{deviceName}} ha avuto movimento per {{minutes}} minuti in {{sessions}} sessioni e il detector non ha prodotto nulla, nemmeno un candidato debole"
|
|
30934
31178
|
},
|
|
31179
|
+
"system.motion-noisy.title": "Movimento continuo senza soggetto — {{deviceName}}",
|
|
31180
|
+
"system.motion-noisy.body": "La camera {{deviceName}} è stata in movimento il {{duty}}% degli ultimi {{minutes}} minuti ({{sessions}} sessioni) senza alcun rilevamento. Il detector funziona — regola le zone di movimento o la sensibilità di questa camera.",
|
|
31181
|
+
"system.motion-calm.title": "Movimento di nuovo calmo — {{deviceName}}",
|
|
31182
|
+
"system.motion-calm.body": "La camera {{deviceName}} non è più in movimento continuo ({{duty}}% degli ultimi {{minutes}} minuti, {{sessions}} sessioni)",
|
|
30935
31183
|
"system.addon-update-available.title": {
|
|
30936
31184
|
"one": "{{count}} aggiornamento addon disponibile",
|
|
30937
31185
|
"other": "{{count}} aggiornamenti addon disponibili"
|
|
@@ -31183,7 +31431,13 @@ var it_default = {
|
|
|
31183
31431
|
"calendar-events.line.timed": "• {{when}} {{title}}{{where}}",
|
|
31184
31432
|
"calendar-events.line.allDay": "• Tutto il giorno {{title}}{{where}}",
|
|
31185
31433
|
"calendar-events.where": " @ {{location}}",
|
|
31186
|
-
"calendar-events.more": "…e altri {{count}}"
|
|
31434
|
+
"calendar-events.more": "…e altri {{count}}",
|
|
31435
|
+
"system.rule-broken.title": "Regola di notifica rotta — {{ruleName}}",
|
|
31436
|
+
"system.rule-broken.body.unparseable": "La regola \"{{ruleName}}\" non si carica e non invia nulla. Aprila e salvala di nuovo.",
|
|
31437
|
+
"system.rule-broken.body.device-deleted": "La regola \"{{ruleName}}\" usa ancora un dispositivo eliminato ({{offendingIds}}) ed è stata disattivata. Scegli di nuovo il dispositivo.",
|
|
31438
|
+
"system.rule-broken.body.target-deleted": "La regola \"{{ruleName}}\" invia ancora a una destinazione eliminata ({{offendingIds}}) ed è stata disattivata. Scegli di nuovo una destinazione.",
|
|
31439
|
+
"system.rule-repaired.title": "Regola di notifica di nuovo funzionante — {{ruleName}}",
|
|
31440
|
+
"system.rule-repaired.body": "La regola \"{{ruleName}}\" si carica e non usa più nulla di eliminato."
|
|
31187
31441
|
};
|
|
31188
31442
|
//#endregion
|
|
31189
31443
|
//#region src/notification-center/text-catalog.ts
|
|
@@ -33454,6 +33708,10 @@ function systemEventKeys(kind) {
|
|
|
33454
33708
|
titleKey: `system.${kind}.title`,
|
|
33455
33709
|
bodyKey: `system.${kind}.body.cleanWater`
|
|
33456
33710
|
};
|
|
33711
|
+
if (kind === "rule-broken") return {
|
|
33712
|
+
titleKey: `system.${kind}.title`,
|
|
33713
|
+
bodyKey: `system.${kind}.body.device-deleted`
|
|
33714
|
+
};
|
|
33457
33715
|
return {
|
|
33458
33716
|
titleKey: `system.${kind}.title`,
|
|
33459
33717
|
bodyKey: `system.${kind}.body`
|
|
@@ -33481,6 +33739,7 @@ function systemEventSubject(kind, scope) {
|
|
|
33481
33739
|
if (kind === "node-online" || kind === "node-offline") return `node:${scope.nodeId ?? "hub"}`;
|
|
33482
33740
|
if (kind === "node-inference-unavailable") return `node-inference:${scope.nodeId ?? "hub"}`;
|
|
33483
33741
|
if (kind === "detection-blind") return `detection-blind:${scope.deviceId ?? 0}`;
|
|
33742
|
+
if (kind === "motion-noisy" || kind === "motion-calm") return `motion-noise:${scope.deviceId ?? 0}`;
|
|
33484
33743
|
if (kind === "addon-crash-loop") return `addon-crash:${scope.nodeId ?? "hub"}:${TEST_CRASHED_RUNNER}`;
|
|
33485
33744
|
if (kind === "addon-update-available" || kind === "server-update-available") return `package:${scope.packageName ?? "@camstack/server"}`;
|
|
33486
33745
|
if (kind === "wrapper-update-available") return `wrapper:${scope.nodeId ?? "hub"}`;
|
|
@@ -33626,6 +33885,8 @@ function planRuleTest(input) {
|
|
|
33626
33885
|
seconds: "30",
|
|
33627
33886
|
devices: "cuda:0",
|
|
33628
33887
|
minutes: "15",
|
|
33888
|
+
duty: "55",
|
|
33889
|
+
sessions: "96",
|
|
33629
33890
|
addons: TEST_CRASHED_RUNNER,
|
|
33630
33891
|
list: "@camstack/addon-pipeline 1.0.1 (current 1.0.0)",
|
|
33631
33892
|
nodes: "hub",
|
|
@@ -39694,6 +39955,99 @@ var NcRecordedCutMemo = class {
|
|
|
39694
39955
|
}
|
|
39695
39956
|
};
|
|
39696
39957
|
//#endregion
|
|
39958
|
+
//#region src/notification-center/notification-label-layout.ts
|
|
39959
|
+
/**
|
|
39960
|
+
* Where each box's caption goes on a notification picture — pure geometry,
|
|
39961
|
+
* rects in, placements out (D897 follow-up, 2026-10-22).
|
|
39962
|
+
*
|
|
39963
|
+
* A caption drawn INSIDE its box used to cover the subject whole on a small
|
|
39964
|
+
* box, and a caption next to a crowd landed on a neighbour's box. The rule:
|
|
39965
|
+
*
|
|
39966
|
+
* 1. Try, in order: OUTSIDE above the box (inside the picture), OUTSIDE below
|
|
39967
|
+
* it, then INSIDE its top edge — the last only when the box is big enough
|
|
39968
|
+
* that the plate is a thin band of it: plate height ≤
|
|
39969
|
+
* {@link NC_LABEL_INSIDE_MAX_HEIGHT_FRACTION} of the box height and plate
|
|
39970
|
+
* width ≤ the box width. A fifth of the height leaves the head and torso of
|
|
39971
|
+
* an upright person, or most of a vehicle, visible; anything taller is the
|
|
39972
|
+
* caption hiding what it names.
|
|
39973
|
+
* 2. A candidate is refused when its plate intersects ANY other box drawn on
|
|
39974
|
+
* the picture, or a plate already placed.
|
|
39975
|
+
* 3. The subject is placed first; the other tracks follow by score, highest
|
|
39976
|
+
* first, so a crowd never costs the subject its caption.
|
|
39977
|
+
* 4. The font never shrinks to make a plate fit: no fitting candidate means
|
|
39978
|
+
* no caption for that box. A box without its caption still carries its
|
|
39979
|
+
* outline and colour.
|
|
39980
|
+
*
|
|
39981
|
+
* Coordinates are the picture's own pixels, whatever it is (a scene or a crop).
|
|
39982
|
+
*/
|
|
39983
|
+
/** The inside placement is allowed only when the plate is at most this fraction of the box height. */
|
|
39984
|
+
var NC_LABEL_INSIDE_MAX_HEIGHT_FRACTION = .2;
|
|
39985
|
+
/** A caption is never drawn smaller than this, in output pixels. */
|
|
39986
|
+
var NC_LABEL_MIN_FONT_PX = 14;
|
|
39987
|
+
/** The dark plate a caption sits on, for this text at this font size. */
|
|
39988
|
+
function labelPlateSize(label, fontSize) {
|
|
39989
|
+
return {
|
|
39990
|
+
w: Math.round(label.length * fontSize * .62 + fontSize * .8),
|
|
39991
|
+
h: Math.round(fontSize * 1.35)
|
|
39992
|
+
};
|
|
39993
|
+
}
|
|
39994
|
+
/** Positive-area overlap; rectangles that only touch do not intersect. */
|
|
39995
|
+
function intersects(a, b) {
|
|
39996
|
+
return a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
|
39997
|
+
}
|
|
39998
|
+
function captionOrder(boxes) {
|
|
39999
|
+
const rank = (b) => b.role === "subject" ? 0 : 1;
|
|
40000
|
+
return boxes.map((_, i) => i).sort((i, j) => {
|
|
40001
|
+
const a = boxes[i];
|
|
40002
|
+
const b = boxes[j];
|
|
40003
|
+
if (a === void 0 || b === void 0) return 0;
|
|
40004
|
+
return rank(a) - rank(b) || (b.score ?? 0) - (a.score ?? 0) || i - j;
|
|
40005
|
+
});
|
|
40006
|
+
}
|
|
40007
|
+
function candidates(box, plate, width, height) {
|
|
40008
|
+
if (plate.w > width) return [];
|
|
40009
|
+
const x = Math.min(box.x, Math.max(0, width - plate.w));
|
|
40010
|
+
const out = [];
|
|
40011
|
+
if (box.y - plate.h >= 0) out.push({
|
|
40012
|
+
x,
|
|
40013
|
+
y: box.y - plate.h,
|
|
40014
|
+
...plate,
|
|
40015
|
+
where: "above"
|
|
40016
|
+
});
|
|
40017
|
+
if (box.y + box.h + plate.h <= height) out.push({
|
|
40018
|
+
x,
|
|
40019
|
+
y: box.y + box.h,
|
|
40020
|
+
...plate,
|
|
40021
|
+
where: "below"
|
|
40022
|
+
});
|
|
40023
|
+
if (plate.h <= box.h * NC_LABEL_INSIDE_MAX_HEIGHT_FRACTION && plate.w <= box.w) out.push({
|
|
40024
|
+
x,
|
|
40025
|
+
y: box.y,
|
|
40026
|
+
...plate,
|
|
40027
|
+
where: "inside"
|
|
40028
|
+
});
|
|
40029
|
+
return out;
|
|
40030
|
+
}
|
|
40031
|
+
/** Place every caption by the rule above. */
|
|
40032
|
+
function layoutBoxLabels(input) {
|
|
40033
|
+
const { width, height, boxes } = input;
|
|
40034
|
+
const fontSize = Math.max(NC_LABEL_MIN_FONT_PX, input.fontSize);
|
|
40035
|
+
const placements = /* @__PURE__ */ new Map();
|
|
40036
|
+
let dropped = 0;
|
|
40037
|
+
for (const i of captionOrder(boxes)) {
|
|
40038
|
+
const box = boxes[i];
|
|
40039
|
+
if (box === void 0 || box.label.length === 0) continue;
|
|
40040
|
+
const chosen = candidates(box, labelPlateSize(box.label, fontSize), width, height).find((c) => !boxes.some((other, j) => j !== i && intersects(c, other)) && ![...placements.values()].some((p) => intersects(c, p)));
|
|
40041
|
+
if (chosen === void 0) dropped += 1;
|
|
40042
|
+
else placements.set(i, chosen);
|
|
40043
|
+
}
|
|
40044
|
+
return {
|
|
40045
|
+
placements: boxes.map((_, i) => placements.get(i) ?? null),
|
|
40046
|
+
placed: placements.size,
|
|
40047
|
+
dropped
|
|
40048
|
+
};
|
|
40049
|
+
}
|
|
40050
|
+
//#endregion
|
|
39697
40051
|
//#region src/notification-center/trigger-zone-still.ts
|
|
39698
40052
|
/**
|
|
39699
40053
|
* How far a candidate's instant may sit from the nearest trail point before the
|
|
@@ -39816,6 +40170,15 @@ var NC_IMAGE_OTHER_COLOR = "#f2f2f2";
|
|
|
39816
40170
|
var NC_IMAGE_ZONE_COLOR = "#29b6f6";
|
|
39817
40171
|
/** Two tracks whose boxes overlap this much at one instant are the same subject (a fused id). */
|
|
39818
40172
|
var SAME_SUBJECT_IOU = .8;
|
|
40173
|
+
/**
|
|
40174
|
+
* A box of the SUBJECT'S CLASS overlapping the subject this much at one
|
|
40175
|
+
* instant is the subject itself — an unfused piece L2 has not joined yet
|
|
40176
|
+
* (D683 fuses 10–20 s later; a push leaves ~6 s after the trigger). The
|
|
40177
|
+
* detector's per-class NMS (`NMS_IOU_DEFAULT` 0.45) keeps one of two
|
|
40178
|
+
* same-class boxes above 0.45 in a frame, so two REAL subjects of one class
|
|
40179
|
+
* cannot overlap past it; 0.5 leaves a margin over that.
|
|
40180
|
+
*/
|
|
40181
|
+
var NC_IMAGE_SAME_CLASS_SUBJECT_IOU = .5;
|
|
39819
40182
|
var JPEG_QUALITY = 85;
|
|
39820
40183
|
/** The caption of a box: class (or label) and confidence — "dog 87%". Language-free. */
|
|
39821
40184
|
function imageLabelText(name, confidence) {
|
|
@@ -39848,15 +40211,21 @@ function toPixels(box, width, height) {
|
|
|
39848
40211
|
h: Math.max(1, Math.min(Math.round(box.h * height), height - y))
|
|
39849
40212
|
};
|
|
39850
40213
|
}
|
|
40214
|
+
/** {@link notificationOverlay}'s SVG alone. */
|
|
40215
|
+
function notificationOverlaySvg(input) {
|
|
40216
|
+
return notificationOverlay(input).svg;
|
|
40217
|
+
}
|
|
39851
40218
|
/**
|
|
39852
|
-
* The SVG drawn over a
|
|
39853
|
-
*
|
|
39854
|
-
*
|
|
40219
|
+
* The SVG drawn over a picture of `width` × `height`. The subject is drawn
|
|
40220
|
+
* LAST so a crowd never hides it. Captions sit on a dark plate, white text,
|
|
40221
|
+
* where `layoutBoxLabels` puts them — outside above, outside below, inside a
|
|
40222
|
+
* box only when it covers a thin band of it, never on another box or caption,
|
|
40223
|
+
* and nowhere at all rather than over the subject.
|
|
39855
40224
|
*/
|
|
39856
|
-
function
|
|
40225
|
+
function notificationOverlay(input) {
|
|
39857
40226
|
const { width, height } = input;
|
|
39858
40227
|
const strokes = overlayStrokes(width, height);
|
|
39859
|
-
const fontSize = Math.max(
|
|
40228
|
+
const fontSize = Math.max(NC_LABEL_MIN_FONT_PX, Math.round(height / 36));
|
|
39860
40229
|
const parts = [];
|
|
39861
40230
|
for (const zone of input.zones) {
|
|
39862
40231
|
if (zone.length < 3) continue;
|
|
@@ -39868,19 +40237,29 @@ function notificationOverlaySvg(input) {
|
|
|
39868
40237
|
parts.push(`<polyline data-role="subject-path" points="${subjectPathSvgPoints(run, width, height)}" fill="none" stroke="${NC_IMAGE_SUBJECT_COLOR}" stroke-width="${String(strokes.subject)}" stroke-linejoin="round" stroke-linecap="round" stroke-opacity="0.9"/>`);
|
|
39869
40238
|
}
|
|
39870
40239
|
const ordered = [...input.boxes.filter((b) => b.role === "other"), ...input.boxes.filter((b) => b.role === "subject")];
|
|
39871
|
-
|
|
40240
|
+
const layout = layoutBoxLabels({
|
|
40241
|
+
width,
|
|
40242
|
+
height,
|
|
40243
|
+
fontSize,
|
|
40244
|
+
boxes: ordered
|
|
40245
|
+
});
|
|
40246
|
+
const plates = [];
|
|
40247
|
+
ordered.forEach((box, i) => {
|
|
39872
40248
|
const subject = box.role === "subject";
|
|
39873
40249
|
const color = subject ? NC_IMAGE_SUBJECT_COLOR : NC_IMAGE_OTHER_COLOR;
|
|
39874
40250
|
const stroke = subject ? strokes.subject : strokes.other;
|
|
39875
40251
|
parts.push(`<rect data-role="${box.role}" x="${String(box.x)}" y="${String(box.y)}" width="${String(box.w)}" height="${String(box.h)}" fill="none" stroke="${color}" stroke-width="${String(stroke)}"/>`);
|
|
39876
|
-
|
|
39877
|
-
|
|
39878
|
-
|
|
39879
|
-
|
|
39880
|
-
|
|
39881
|
-
|
|
39882
|
-
|
|
39883
|
-
|
|
40252
|
+
const plate = layout.placements[i];
|
|
40253
|
+
if (plate === null || plate === void 0) return;
|
|
40254
|
+
plates.push(`<rect data-role="${box.role}-plate" x="${String(plate.x)}" y="${String(plate.y)}" width="${String(plate.w)}" height="${String(plate.h)}" fill="#000000" fill-opacity="0.72" stroke="${color}" stroke-width="${String(subject ? 2 : 1)}"/>`, `<text data-role="${box.role}-label" x="${String(plate.x + Math.round(fontSize * .4))}" y="${String(plate.y + Math.round(plate.h * .75))}" font-family="DejaVu Sans, Helvetica, Arial, sans-serif" font-size="${String(fontSize)}" font-weight="${subject ? "700" : "400"}" fill="#ffffff">${escapeXml$2(box.label)}</text>`);
|
|
40255
|
+
});
|
|
40256
|
+
return {
|
|
40257
|
+
svg: `<svg xmlns="http://www.w3.org/2000/svg" width="${String(width)}" height="${String(height)}">${[...parts, ...plates].join("")}</svg>`,
|
|
40258
|
+
labels: {
|
|
40259
|
+
placed: layout.placed,
|
|
40260
|
+
dropped: layout.dropped
|
|
40261
|
+
}
|
|
40262
|
+
};
|
|
39884
40263
|
}
|
|
39885
40264
|
/** The pixel rectangle a `crop` cuts from a `width` × `height` frame, or null when too small. */
|
|
39886
40265
|
function subjectCropRect(box, width, height) {
|
|
@@ -39910,11 +40289,18 @@ function iou$4(a, b) {
|
|
|
39910
40289
|
}
|
|
39911
40290
|
/**
|
|
39912
40291
|
* The tracks to draw thin: every track at the instant except the subject —
|
|
39913
|
-
* the same id,
|
|
39914
|
-
* survivor's name)
|
|
39915
|
-
|
|
39916
|
-
|
|
39917
|
-
|
|
40292
|
+
* the same id, a box that is the subject's (a fused id reads under its
|
|
40293
|
+
* survivor's name), or a box of the subject's class over it past what NMS
|
|
40294
|
+
* lets two real subjects share (an unfused piece of the same subject).
|
|
40295
|
+
*/
|
|
40296
|
+
function otherTracks(tracks, subjectTrackId, subjectBox, subjectClassName) {
|
|
40297
|
+
return tracks.filter((t) => {
|
|
40298
|
+
if (t.trackId === subjectTrackId) return false;
|
|
40299
|
+
if (subjectBox === null) return true;
|
|
40300
|
+
const overlap = iou$4(t.bbox, subjectBox);
|
|
40301
|
+
if (overlap >= SAME_SUBJECT_IOU) return false;
|
|
40302
|
+
return !(t.className === subjectClassName && overlap >= NC_IMAGE_SAME_CLASS_SUBJECT_IOU);
|
|
40303
|
+
});
|
|
39918
40304
|
}
|
|
39919
40305
|
/**
|
|
39920
40306
|
* What picture a ROW asks for. A row written since D897 carries `mediaImage`;
|
|
@@ -39931,9 +40317,15 @@ function payloadImageMode(payload) {
|
|
|
39931
40317
|
if (payload.media === "keyFrame") return "clean";
|
|
39932
40318
|
return "crop";
|
|
39933
40319
|
}
|
|
39934
|
-
/**
|
|
40320
|
+
/**
|
|
40321
|
+
* The zone ids a row's picture outlines: the zone(s) THIS event triggered,
|
|
40322
|
+
* when it asked — never every zone the rule lists (a dog in Uscio drew Aiuola
|
|
40323
|
+
* too). A row that asked but froze no `matchedZoneIds` (written before the
|
|
40324
|
+
* field) draws none: an outline of a zone the subject never entered is a
|
|
40325
|
+
* picture that lies about its own reason.
|
|
40326
|
+
*/
|
|
39935
40327
|
function payloadZoneOverlayIds(payload) {
|
|
39936
|
-
if (payload.mediaShowTriggerZone === true) return payload.
|
|
40328
|
+
if (payload.mediaShowTriggerZone === true) return payload.matchedZoneIds ?? [];
|
|
39937
40329
|
if (payload.mediaImage === void 0) return payload.mediaZoneIds ?? [];
|
|
39938
40330
|
return [];
|
|
39939
40331
|
}
|
|
@@ -40022,7 +40414,8 @@ async function renderNotificationImage(req) {
|
|
|
40022
40414
|
const boxes = req.mode === "scene-boxes" ? [...req.others.map((o) => ({
|
|
40023
40415
|
...toPixels(o, outW, outH),
|
|
40024
40416
|
role: "other",
|
|
40025
|
-
label: o.label
|
|
40417
|
+
label: o.label,
|
|
40418
|
+
...o.confidence !== void 0 ? { score: o.confidence } : {}
|
|
40026
40419
|
})), ...req.subject !== null ? [{
|
|
40027
40420
|
...toPixels(req.subject, outW, outH),
|
|
40028
40421
|
role: "subject",
|
|
@@ -40040,16 +40433,18 @@ async function renderNotificationImage(req) {
|
|
|
40040
40433
|
height: outH,
|
|
40041
40434
|
fit: "fill"
|
|
40042
40435
|
});
|
|
40436
|
+
let labels;
|
|
40043
40437
|
if (boxes.length > 0 || zones.length > 0 || paths.length > 0) {
|
|
40044
|
-
const
|
|
40438
|
+
const overlay = notificationOverlay({
|
|
40045
40439
|
width: outW,
|
|
40046
40440
|
height: outH,
|
|
40047
40441
|
boxes,
|
|
40048
40442
|
zones,
|
|
40049
40443
|
paths
|
|
40050
40444
|
});
|
|
40445
|
+
if (boxes.length > 0) labels = overlay.labels;
|
|
40051
40446
|
pipeline = pipeline.composite([{
|
|
40052
|
-
input: Buffer.from(svg),
|
|
40447
|
+
input: Buffer.from(overlay.svg),
|
|
40053
40448
|
top: 0,
|
|
40054
40449
|
left: 0
|
|
40055
40450
|
}]);
|
|
@@ -40059,7 +40454,8 @@ async function renderNotificationImage(req) {
|
|
|
40059
40454
|
return {
|
|
40060
40455
|
bytes: toArrayBufferBytes(out),
|
|
40061
40456
|
name: `${rendered}.jpg`,
|
|
40062
|
-
rendered
|
|
40457
|
+
rendered,
|
|
40458
|
+
...labels !== void 0 ? { labels } : {}
|
|
40063
40459
|
};
|
|
40064
40460
|
}
|
|
40065
40461
|
var NcNotificationImageStage = class {
|
|
@@ -40133,10 +40529,29 @@ var NcNotificationImageStage = class {
|
|
|
40133
40529
|
...subjectBox,
|
|
40134
40530
|
label: imageLabelText(payload.subject.label ?? payload.subject.className, payload.subject.confidence)
|
|
40135
40531
|
} : null;
|
|
40136
|
-
const
|
|
40532
|
+
const othersDrawn = mode === "scene-boxes" ? otherTracks(await this.tracksAt(camera, source.timestamp), subjectTrackId, subjectBox, payload.subject.className) : [];
|
|
40533
|
+
if (mode === "scene-boxes") this.deps.logger.info("scene-boxes: other boxes drawn", {
|
|
40534
|
+
tags: { deviceId: camera },
|
|
40535
|
+
meta: {
|
|
40536
|
+
ruleId: entry.ruleId,
|
|
40537
|
+
recordId: entry.recordId,
|
|
40538
|
+
instant: source.timestamp,
|
|
40539
|
+
drawn: othersDrawn.map((t) => `${t.trackId}/${t.className}/dt=${t.dtMs ?? "?"}ms/${t.interpolated === true ? "interpolated" : "exact"}`).join(",")
|
|
40540
|
+
}
|
|
40541
|
+
});
|
|
40542
|
+
const others = mode === "scene-boxes" ? othersDrawn.map((t) => ({
|
|
40137
40543
|
...t.bbox,
|
|
40138
|
-
label: imageLabelText(t.label ?? t.className, t.confidence)
|
|
40544
|
+
label: imageLabelText(t.label ?? t.className, t.confidence),
|
|
40545
|
+
...t.confidence !== void 0 ? { confidence: t.confidence } : {}
|
|
40139
40546
|
})) : [];
|
|
40547
|
+
if (mode !== "crop" && input.withSubject && payload.mediaShowTriggerZone === true && payload.matchedZoneIds === void 0) this.deps.logger.info("trigger zone not drawn — the row never froze which zone fired", {
|
|
40548
|
+
tags: { deviceId: camera },
|
|
40549
|
+
meta: {
|
|
40550
|
+
ruleId: entry.ruleId,
|
|
40551
|
+
recordId: entry.recordId,
|
|
40552
|
+
ruleZoneIds: (payload.triggerZoneIds ?? []).join(",")
|
|
40553
|
+
}
|
|
40554
|
+
});
|
|
40140
40555
|
const zones = mode === "crop" || zoneIds.length === 0 ? [] : await this.zonePolygons(camera, zoneIds);
|
|
40141
40556
|
try {
|
|
40142
40557
|
const image = await this.render({
|
|
@@ -40149,6 +40564,7 @@ var NcNotificationImageStage = class {
|
|
|
40149
40564
|
});
|
|
40150
40565
|
const reason = image.rendered === mode ? void 0 : mode === "crop" ? "no-subject-box" : "no-tracks-at-instant";
|
|
40151
40566
|
if (reason !== void 0) this.reportFallback(input, mode, reason);
|
|
40567
|
+
if (image.labels !== void 0) this.reportLabels(input, image.labels);
|
|
40152
40568
|
return reason !== void 0 ? {
|
|
40153
40569
|
...image,
|
|
40154
40570
|
reason
|
|
@@ -40197,6 +40613,19 @@ var NcNotificationImageStage = class {
|
|
|
40197
40613
|
}
|
|
40198
40614
|
});
|
|
40199
40615
|
}
|
|
40616
|
+
/** ONE line per rendered picture: how many captions it carries and how many had no clear spot. */
|
|
40617
|
+
reportLabels(input, labels) {
|
|
40618
|
+
const camera = input.camera === "composite" ? input.entry.payload.subject.deviceId : input.camera;
|
|
40619
|
+
this.deps.logger.debug("notification picture captions laid out", {
|
|
40620
|
+
tags: { deviceId: camera },
|
|
40621
|
+
meta: {
|
|
40622
|
+
ruleId: input.entry.ruleId,
|
|
40623
|
+
recordId: input.entry.recordId,
|
|
40624
|
+
placed: labels.placed,
|
|
40625
|
+
dropped: labels.dropped
|
|
40626
|
+
}
|
|
40627
|
+
});
|
|
40628
|
+
}
|
|
40200
40629
|
/** The subject's trail, read once per render for its box AND its path. */
|
|
40201
40630
|
async subjectTrail(camera, trackId) {
|
|
40202
40631
|
const read = this.deps.getSubjectTrail;
|
|
@@ -46890,6 +47319,27 @@ function isRecord(value) {
|
|
|
46890
47319
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
46891
47320
|
}
|
|
46892
47321
|
/**
|
|
47322
|
+
* Incident 2026-10-22: a row stored with `groupingSec: 0` failed `NcRuleSchema` (min
|
|
47323
|
+
* {@link NC_GROUPING_MIN_SEC}) and the WHOLE rule was skipped as malformed on every load — a
|
|
47324
|
+
* system-event rule on which a window cannot even apply vanished from the engine and from
|
|
47325
|
+
* `listRules`. A non-positive (or non-numeric) window only ever meant "no window": dropped. A
|
|
47326
|
+
* positive one outside the range is clamped. One bad optional field never costs the rule.
|
|
47327
|
+
*/
|
|
47328
|
+
function repairStoredGroupingSec(value) {
|
|
47329
|
+
if (value === void 0) return { kind: "unchanged" };
|
|
47330
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return {
|
|
47331
|
+
kind: "repaired",
|
|
47332
|
+
from: value,
|
|
47333
|
+
to: null
|
|
47334
|
+
};
|
|
47335
|
+
const clamped = clampGroupingSec(value);
|
|
47336
|
+
return clamped === value ? { kind: "unchanged" } : {
|
|
47337
|
+
kind: "repaired",
|
|
47338
|
+
from: value,
|
|
47339
|
+
to: clamped
|
|
47340
|
+
};
|
|
47341
|
+
}
|
|
47342
|
+
/**
|
|
46893
47343
|
* Project a RAW stored rule value (`data['rule']`, before any Zod parse). A value that is not
|
|
46894
47344
|
* an object, or has no string `delivery`, is returned untouched: the schema parse that follows
|
|
46895
47345
|
* rejects it exactly as before, and a projection must never be what decides a row is valid.
|
|
@@ -46897,9 +47347,12 @@ function isRecord(value) {
|
|
|
46897
47347
|
function projectRawStoredRule(raw) {
|
|
46898
47348
|
if (!isRecord(raw) || typeof raw["delivery"] !== "string") return {
|
|
46899
47349
|
candidate: raw,
|
|
46900
|
-
projection: { kind: "unchanged" }
|
|
47350
|
+
projection: { kind: "unchanged" },
|
|
47351
|
+
groupingRepair: { kind: "unchanged" }
|
|
46901
47352
|
};
|
|
46902
|
-
const
|
|
47353
|
+
const groupingRepair = repairStoredGroupingSec(raw["groupingSec"]);
|
|
47354
|
+
const repaired = withRepairedGrouping(raw, groupingRepair);
|
|
47355
|
+
const groupingSec = repaired["groupingSec"];
|
|
46903
47356
|
const projection = projectRetiredRuleFields({
|
|
46904
47357
|
delivery: raw["delivery"],
|
|
46905
47358
|
conditions: isRecord(raw["conditions"]) ? raw["conditions"] : {},
|
|
@@ -46908,15 +47361,25 @@ function projectRawStoredRule(raw) {
|
|
|
46908
47361
|
...typeof groupingSec === "number" ? { groupingSec } : {}
|
|
46909
47362
|
});
|
|
46910
47363
|
if (projection.kind === "unchanged" || projection.groupingSec === void 0) return {
|
|
46911
|
-
candidate:
|
|
46912
|
-
projection
|
|
47364
|
+
candidate: repaired,
|
|
47365
|
+
projection,
|
|
47366
|
+
groupingRepair
|
|
46913
47367
|
};
|
|
46914
47368
|
return {
|
|
46915
47369
|
candidate: {
|
|
46916
|
-
...
|
|
47370
|
+
...repaired,
|
|
46917
47371
|
groupingSec: projection.groupingSec
|
|
46918
47372
|
},
|
|
46919
|
-
projection
|
|
47373
|
+
projection,
|
|
47374
|
+
groupingRepair
|
|
47375
|
+
};
|
|
47376
|
+
}
|
|
47377
|
+
function withRepairedGrouping(raw, repair) {
|
|
47378
|
+
if (repair.kind === "unchanged") return raw;
|
|
47379
|
+
const { groupingSec: _dropped, ...rest } = raw;
|
|
47380
|
+
return repair.to === null ? rest : {
|
|
47381
|
+
...rest,
|
|
47382
|
+
groupingSec: repair.to
|
|
46920
47383
|
};
|
|
46921
47384
|
}
|
|
46922
47385
|
/**
|
|
@@ -46969,16 +47432,44 @@ var LOAD_LIMIT = 1e4;
|
|
|
46969
47432
|
* Parse a raw body object (as returned from storage — may contain retired keys)
|
|
46970
47433
|
* into an `NcRule` applying the legacy field projection.
|
|
46971
47434
|
*/
|
|
46972
|
-
function bodyToRule(id, raw, onProjection) {
|
|
46973
|
-
const
|
|
47435
|
+
function bodyToRule(id, raw, onProjection, onGroupingRepair) {
|
|
47436
|
+
const parsed = parseStoredRule(id, raw, onProjection, onGroupingRepair);
|
|
47437
|
+
return parsed.ok ? parsed.rule : null;
|
|
47438
|
+
}
|
|
47439
|
+
function parseStoredRule(id, raw, onProjection, onGroupingRepair, onClassWidening) {
|
|
47440
|
+
const { candidate, projection, groupingRepair } = projectRawStoredRule(raw);
|
|
46974
47441
|
const parsed = NcRuleSchema.safeParse({
|
|
46975
47442
|
...candidate,
|
|
46976
47443
|
id
|
|
46977
47444
|
});
|
|
46978
|
-
if (!parsed.success)
|
|
47445
|
+
if (!parsed.success) {
|
|
47446
|
+
const issue = parsed.error.issues[0];
|
|
47447
|
+
return {
|
|
47448
|
+
ok: false,
|
|
47449
|
+
reason: issue === void 0 ? "invalid" : `${issue.path.join(".") || "(root)"}: ${issue.message}`
|
|
47450
|
+
};
|
|
47451
|
+
}
|
|
46979
47452
|
if (projection.kind === "projected") onProjection?.(id, projection);
|
|
46980
|
-
|
|
47453
|
+
if (groupingRepair.kind === "repaired") onGroupingRepair?.(id, groupingRepair);
|
|
47454
|
+
const media = withNormalizedMedia(parsed.data);
|
|
47455
|
+
const rule = withSystemEventClasses(media);
|
|
47456
|
+
if (rule !== media) onClassWidening?.(id, media, rule);
|
|
47457
|
+
return {
|
|
47458
|
+
ok: true,
|
|
47459
|
+
rule
|
|
47460
|
+
};
|
|
47461
|
+
}
|
|
47462
|
+
/**
|
|
47463
|
+
* D906: what a rule is stored as — its media policy folded (D897) and, on an
|
|
47464
|
+
* INSTANT system rule, every paired class it touches completed with both
|
|
47465
|
+
* edges. Applied on every WRITE; the load applies the same through
|
|
47466
|
+
* {@link parseStoredRule}, which also says once that it widened a rule.
|
|
47467
|
+
*/
|
|
47468
|
+
function normalizeStoredRule(rule) {
|
|
47469
|
+
return withSystemEventClasses(withNormalizedMedia(rule));
|
|
46981
47470
|
}
|
|
47471
|
+
/** At most this many skipped rows are NAMED in the WARN line; the count is always whole. */
|
|
47472
|
+
var SKIPPED_ROWS_NAMED_MAX = 10;
|
|
46982
47473
|
/**
|
|
46983
47474
|
* D897: the rule with its media policy in the stored shape — `image` set, the
|
|
46984
47475
|
* legacy `attach` / `frame` / `zoneCrop` folded into it. Applied on LOAD (a read
|
|
@@ -47055,6 +47546,8 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47055
47546
|
now;
|
|
47056
47547
|
newId;
|
|
47057
47548
|
identityIdsByName;
|
|
47549
|
+
/** Rows the last successful {@link load} skipped; `null` before one succeeded. */
|
|
47550
|
+
skipped = null;
|
|
47058
47551
|
/** Has a {@link load} ever completed? See the note there. */
|
|
47059
47552
|
loaded = false;
|
|
47060
47553
|
/** Legacy detection rules naming sounds already reported (D690) — once each. */
|
|
@@ -47066,6 +47559,10 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47066
47559
|
pendingProjections = /* @__PURE__ */ new Map();
|
|
47067
47560
|
/** Rules whose projection was already logged in this process (D692 §9) — once each. */
|
|
47068
47561
|
projectionLogged = /* @__PURE__ */ new Set();
|
|
47562
|
+
/** Rules whose out-of-range `groupingSec` repair was already logged — once each. */
|
|
47563
|
+
groupingRepairLogged = /* @__PURE__ */ new Set();
|
|
47564
|
+
/** Rules whose class widening (D906) was already logged — once each. */
|
|
47565
|
+
classWideningLogged = /* @__PURE__ */ new Set();
|
|
47069
47566
|
constructor(deps) {
|
|
47070
47567
|
this.logger = deps.logger;
|
|
47071
47568
|
this.now = deps.now ?? (() => Date.now());
|
|
@@ -47100,20 +47597,29 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47100
47597
|
try {
|
|
47101
47598
|
const { records } = await this.kind.query({ limit: LOAD_LIMIT });
|
|
47102
47599
|
const next = /* @__PURE__ */ new Map();
|
|
47103
|
-
|
|
47600
|
+
const skippedRows = [];
|
|
47104
47601
|
for (const row of records) {
|
|
47105
|
-
const
|
|
47602
|
+
const parsed = parseStoredRule(row.id, row.data, (ruleId, projection) => {
|
|
47106
47603
|
this.pendingProjections.set(ruleId, projection);
|
|
47107
|
-
});
|
|
47108
|
-
if (
|
|
47109
|
-
|
|
47604
|
+
}, (ruleId, repair) => this.logGroupingRepair(ruleId, repair), (ruleId, before, after) => this.logClassWidening(ruleId, before, after));
|
|
47605
|
+
if (!parsed.ok) {
|
|
47606
|
+
const name = row.data["name"];
|
|
47607
|
+
skippedRows.push({
|
|
47608
|
+
ruleId: row.id,
|
|
47609
|
+
name: typeof name === "string" ? name : null,
|
|
47610
|
+
reason: parsed.reason
|
|
47611
|
+
});
|
|
47110
47612
|
continue;
|
|
47111
47613
|
}
|
|
47112
|
-
next.set(row.id, rule);
|
|
47614
|
+
next.set(row.id, parsed.rule);
|
|
47113
47615
|
}
|
|
47114
47616
|
this.byId.clear();
|
|
47115
47617
|
for (const [id, rule] of next) this.byId.set(id, rule);
|
|
47116
|
-
|
|
47618
|
+
this.skipped = skippedRows;
|
|
47619
|
+
if (skippedRows.length > 0) this.logger.warn("notification rule rows skipped as malformed", { meta: {
|
|
47620
|
+
skipped: skippedRows.length,
|
|
47621
|
+
rows: skippedRows.slice(0, SKIPPED_ROWS_NAMED_MAX)
|
|
47622
|
+
} });
|
|
47117
47623
|
this.loaded = true;
|
|
47118
47624
|
} catch (err) {
|
|
47119
47625
|
this.logger.warn("notification rules load failed — keeping rules already in memory", { meta: {
|
|
@@ -47125,6 +47631,27 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47125
47631
|
for (const id of warnLegacyAudioClassRules([...this.byId.values()], this.audioClassWarned, this.logger)) this.audioClassWarned.add(id);
|
|
47126
47632
|
this.logger.debug("notification rules loaded", { meta: { rules: this.byId.size } });
|
|
47127
47633
|
}
|
|
47634
|
+
/** Say, once per process per rule, that an instant rule was read as its whole class (D906). */
|
|
47635
|
+
logClassWidening(ruleId, before, after) {
|
|
47636
|
+
if (this.classWideningLogged.has(ruleId)) return;
|
|
47637
|
+
this.classWideningLogged.add(ruleId);
|
|
47638
|
+
const had = new Set(before.conditions.systemEvent?.kinds ?? []);
|
|
47639
|
+
this.logger.info("instant system rule read as its whole event class — both edges", { meta: {
|
|
47640
|
+
ruleId,
|
|
47641
|
+
rule: after.name,
|
|
47642
|
+
added: (after.conditions.systemEvent?.kinds ?? []).filter((kind) => !had.has(kind))
|
|
47643
|
+
} });
|
|
47644
|
+
}
|
|
47645
|
+
/** Say, once per process per rule, that an out-of-range stored `groupingSec` was repaired. */
|
|
47646
|
+
logGroupingRepair(ruleId, repair) {
|
|
47647
|
+
if (this.groupingRepairLogged.has(ruleId)) return;
|
|
47648
|
+
this.groupingRepairLogged.add(ruleId);
|
|
47649
|
+
this.logger.warn("rule carries an out-of-range groupingSec — repaired on read", { meta: {
|
|
47650
|
+
ruleId,
|
|
47651
|
+
from: repair.from,
|
|
47652
|
+
to: repair.to
|
|
47653
|
+
} });
|
|
47654
|
+
}
|
|
47128
47655
|
/** Log, once per process per rule, what `bodyToRule` projected from a retired field. */
|
|
47129
47656
|
logPendingProjections() {
|
|
47130
47657
|
for (const [ruleId, projection] of this.pendingProjections) {
|
|
@@ -47193,6 +47720,13 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47193
47720
|
* camera's ring) must ask this first: before the first load the empty list
|
|
47194
47721
|
* is not a fact, and a failed load leaves whatever was already mirrored.
|
|
47195
47722
|
*/
|
|
47723
|
+
/**
|
|
47724
|
+
* The rows the last SUCCESSFUL load could not parse — `null` until one has
|
|
47725
|
+
* succeeded, so "not read yet" never reads as "nothing broken" (D315).
|
|
47726
|
+
*/
|
|
47727
|
+
skippedRows() {
|
|
47728
|
+
return this.skipped;
|
|
47729
|
+
}
|
|
47196
47730
|
isLoaded() {
|
|
47197
47731
|
return this.loaded;
|
|
47198
47732
|
}
|
|
@@ -47222,7 +47756,7 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47222
47756
|
/** Create a new rule. `createdBy` is the SERVER-injected caller userId. */
|
|
47223
47757
|
async create(input, createdBy) {
|
|
47224
47758
|
const now = this.now();
|
|
47225
|
-
const rule =
|
|
47759
|
+
const rule = normalizeStoredRule({
|
|
47226
47760
|
...input,
|
|
47227
47761
|
id: this.newId(),
|
|
47228
47762
|
createdBy,
|
|
@@ -47267,7 +47801,7 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47267
47801
|
createdAt: existing.createdAt,
|
|
47268
47802
|
updatedAt: this.now()
|
|
47269
47803
|
};
|
|
47270
|
-
const updated =
|
|
47804
|
+
const updated = normalizeStoredRule(NcRuleSchema.parse(candidate));
|
|
47271
47805
|
await this.write(updated);
|
|
47272
47806
|
return updated;
|
|
47273
47807
|
}
|
|
@@ -47307,7 +47841,7 @@ var NcRuleStore = class NcRuleStore {
|
|
|
47307
47841
|
id: ruleId
|
|
47308
47842
|
} : candidate);
|
|
47309
47843
|
if (!parsed.success) return existing;
|
|
47310
|
-
const next =
|
|
47844
|
+
const next = normalizeStoredRule(parsed.data);
|
|
47311
47845
|
const { id: _omit, ...data } = next;
|
|
47312
47846
|
await this.kind.put({
|
|
47313
47847
|
id: ruleId,
|
|
@@ -53336,6 +53870,104 @@ var NcZoneOwnershipCache = class {
|
|
|
53336
53870
|
}
|
|
53337
53871
|
};
|
|
53338
53872
|
//#endregion
|
|
53873
|
+
//#region src/notification-center/rule-health.ts
|
|
53874
|
+
var EMPTY_SNAPSHOT = {
|
|
53875
|
+
timelapse: [],
|
|
53876
|
+
summary: [],
|
|
53877
|
+
alarm: null,
|
|
53878
|
+
mutedDeviceIds: []
|
|
53879
|
+
};
|
|
53880
|
+
function fingerprintOf(reasons) {
|
|
53881
|
+
return reasons.map((r) => `${r.code}:${[...r.ids].sort().join(",")}`).join("|");
|
|
53882
|
+
}
|
|
53883
|
+
/** The deleted subjects a rule still names, from the prune's `kept*` records. */
|
|
53884
|
+
function deletedReasons(rule, prunes) {
|
|
53885
|
+
const snapshot = {
|
|
53886
|
+
...EMPTY_SNAPSHOT,
|
|
53887
|
+
rules: [rule]
|
|
53888
|
+
};
|
|
53889
|
+
const devices = referencedDeviceIds(snapshot);
|
|
53890
|
+
const targets = referencedTargetIds(snapshot);
|
|
53891
|
+
const deadDevices = /* @__PURE__ */ new Set();
|
|
53892
|
+
const deadTargets = /* @__PURE__ */ new Set();
|
|
53893
|
+
for (const p of prunes) {
|
|
53894
|
+
if (p.kind !== "rule" || p.ruleId !== rule.id) continue;
|
|
53895
|
+
if (p.outcome !== "kept" && p.outcome !== "kept-disabled") continue;
|
|
53896
|
+
if (p.subjectKind === "device" && devices.has(Number(p.subject))) deadDevices.add(p.subject);
|
|
53897
|
+
if (p.subjectKind === "target" && targets.has(p.subject)) deadTargets.add(p.subject);
|
|
53898
|
+
}
|
|
53899
|
+
const reasons = [];
|
|
53900
|
+
if (deadDevices.size > 0) reasons.push({
|
|
53901
|
+
code: "device-deleted",
|
|
53902
|
+
ids: [...deadDevices].sort()
|
|
53903
|
+
});
|
|
53904
|
+
if (deadTargets.size > 0) reasons.push({
|
|
53905
|
+
code: "target-deleted",
|
|
53906
|
+
ids: [...deadTargets].sort()
|
|
53907
|
+
});
|
|
53908
|
+
return reasons;
|
|
53909
|
+
}
|
|
53910
|
+
var NcRuleHealthMonitor = class {
|
|
53911
|
+
verdicts = /* @__PURE__ */ new Map();
|
|
53912
|
+
/** Judge every rule once; return the edges to announce. */
|
|
53913
|
+
evaluate(inputs) {
|
|
53914
|
+
const next = /* @__PURE__ */ new Map();
|
|
53915
|
+
const seen = /* @__PURE__ */ new Set();
|
|
53916
|
+
if (inputs.skipped !== null) for (const row of inputs.skipped) {
|
|
53917
|
+
seen.add(row.ruleId);
|
|
53918
|
+
const unparseable = {
|
|
53919
|
+
code: "unparseable",
|
|
53920
|
+
ids: [],
|
|
53921
|
+
detail: row.reason
|
|
53922
|
+
};
|
|
53923
|
+
next.set(row.ruleId, this.verdict(row.name, unparseable, []));
|
|
53924
|
+
}
|
|
53925
|
+
if (inputs.rules !== null) for (const rule of inputs.rules) {
|
|
53926
|
+
seen.add(rule.id);
|
|
53927
|
+
const previous = this.verdicts.get(rule.id);
|
|
53928
|
+
const deleted = inputs.prunes !== null ? deletedReasons(rule, inputs.prunes) : previous?.deleted ?? [];
|
|
53929
|
+
const unparseable = inputs.skipped !== null ? null : previous?.unparseable ?? null;
|
|
53930
|
+
next.set(rule.id, this.verdict(rule.name, unparseable, deleted));
|
|
53931
|
+
}
|
|
53932
|
+
const edges = [];
|
|
53933
|
+
const answeredAll = inputs.rules !== null && inputs.skipped !== null;
|
|
53934
|
+
for (const [ruleId, previous] of this.verdicts) {
|
|
53935
|
+
if (seen.has(ruleId)) continue;
|
|
53936
|
+
if (!answeredAll) next.set(ruleId, previous);
|
|
53937
|
+
}
|
|
53938
|
+
for (const [ruleId, verdict] of next) {
|
|
53939
|
+
const previous = this.verdicts.get(ruleId);
|
|
53940
|
+
const reasons = reasonsOf(verdict);
|
|
53941
|
+
if (reasons.length > 0) {
|
|
53942
|
+
if (previous?.fingerprint !== verdict.fingerprint) edges.push({
|
|
53943
|
+
kind: "broken",
|
|
53944
|
+
ruleId,
|
|
53945
|
+
ruleName: verdict.name,
|
|
53946
|
+
reasons
|
|
53947
|
+
});
|
|
53948
|
+
} else if (previous !== void 0 && reasonsOf(previous).length > 0) edges.push({
|
|
53949
|
+
kind: "repaired",
|
|
53950
|
+
ruleId,
|
|
53951
|
+
ruleName: verdict.name
|
|
53952
|
+
});
|
|
53953
|
+
}
|
|
53954
|
+
this.verdicts.clear();
|
|
53955
|
+
for (const [ruleId, verdict] of next) if (reasonsOf(verdict).length > 0) this.verdicts.set(ruleId, verdict);
|
|
53956
|
+
return edges;
|
|
53957
|
+
}
|
|
53958
|
+
verdict(name, unparseable, deleted) {
|
|
53959
|
+
return {
|
|
53960
|
+
name,
|
|
53961
|
+
unparseable,
|
|
53962
|
+
deleted,
|
|
53963
|
+
fingerprint: fingerprintOf([...unparseable !== null ? [unparseable] : [], ...deleted])
|
|
53964
|
+
};
|
|
53965
|
+
}
|
|
53966
|
+
};
|
|
53967
|
+
function reasonsOf(verdict) {
|
|
53968
|
+
return [...verdict.unparseable !== null ? [verdict.unparseable] : [], ...verdict.deleted];
|
|
53969
|
+
}
|
|
53970
|
+
//#endregion
|
|
53339
53971
|
//#region src/notification-center/index.ts
|
|
53340
53972
|
/**
|
|
53341
53973
|
* NotificationCenter — the P1 core of the Notification Center (spec
|
|
@@ -53402,7 +54034,7 @@ var NC_ALARM_ANNOUNCE_RULE_ID = "nc-alarm-announce";
|
|
|
53402
54034
|
* media (the SOURCE rule of a combined alarm row), and a zone overlay is
|
|
53403
54035
|
* frozen only when the rule asked for one AND fired on a zone.
|
|
53404
54036
|
*/
|
|
53405
|
-
function mediaPolicyFields(mediaRule, cropZoneIds) {
|
|
54037
|
+
function mediaPolicyFields(mediaRule, cropZoneIds, matchedZoneIds) {
|
|
53406
54038
|
const media = effectiveMediaPolicyForRule(mediaRule);
|
|
53407
54039
|
const image = media.image ?? "crop";
|
|
53408
54040
|
return {
|
|
@@ -53411,6 +54043,7 @@ function mediaPolicyFields(mediaRule, cropZoneIds) {
|
|
|
53411
54043
|
...media.showTriggerZone === true && cropZoneIds !== void 0 && cropZoneIds.length > 0 ? { mediaShowTriggerZone: true } : {},
|
|
53412
54044
|
...media.showSubjectPath === true && image !== "none" ? { mediaShowSubjectPath: true } : {},
|
|
53413
54045
|
...cropZoneIds !== void 0 ? { triggerZoneIds: [...cropZoneIds] } : {},
|
|
54046
|
+
...matchedZoneIds !== void 0 ? { matchedZoneIds: [...matchedZoneIds] } : {},
|
|
53414
54047
|
...media.profile !== void 0 ? { mediaProfile: media.profile } : {},
|
|
53415
54048
|
...media.gif === true ? { mediaGif: true } : {},
|
|
53416
54049
|
...media.clip === true ? { mediaClip: true } : {},
|
|
@@ -53916,6 +54549,21 @@ function groupOrderInput(fate, triggerZoneIds) {
|
|
|
53916
54549
|
* condition's ids, else an occupancy rule's one zone. A grouped member freezes the same
|
|
53917
54550
|
* ids a plain row of the rule would.
|
|
53918
54551
|
*/
|
|
54552
|
+
/**
|
|
54553
|
+
* The zones of `cropZoneIds` this subject actually triggered — what the zone
|
|
54554
|
+
* condition saw (membership plus the crossed zone, `zonesVisitedBy`), on the
|
|
54555
|
+
* rule whose media the row carries. On a combined alarm row the subject is the
|
|
54556
|
+
* alarm, so the source match's frozen zones answer. An occupancy rule's one
|
|
54557
|
+
* zone is the zone the count edge is ABOUT, so it is its own trigger.
|
|
54558
|
+
*/
|
|
54559
|
+
function matchedTriggerZoneIds(mediaRule, cropZoneIds, subject) {
|
|
54560
|
+
if (cropZoneIds === void 0) return void 0;
|
|
54561
|
+
if (mediaRule.conditions.zones === void 0) return [...cropZoneIds];
|
|
54562
|
+
const alarmSource = subject.systemEvent?.alarmSource;
|
|
54563
|
+
const visited = new Set(alarmSource !== void 0 ? alarmSource.zones ?? [] : subject.zones);
|
|
54564
|
+
if (alarmSource === void 0 && subject.crossing !== void 0) visited.add(subject.crossing.zoneId);
|
|
54565
|
+
return cropZoneIds.filter((id) => visited.has(id));
|
|
54566
|
+
}
|
|
53919
54567
|
function ruleCropZoneIds(rule) {
|
|
53920
54568
|
if (rule.conditions.zones !== void 0) return rule.conditions.zones.ids;
|
|
53921
54569
|
const zoneId = rule.conditions.occupancy?.zoneId;
|
|
@@ -54069,6 +54717,8 @@ var NotificationCenter = class {
|
|
|
54069
54717
|
* evaluating one.
|
|
54070
54718
|
*/
|
|
54071
54719
|
pruneLedger;
|
|
54720
|
+
/** D905: which rules cannot work, announced once per distinct reason. */
|
|
54721
|
+
ruleHealth = new NcRuleHealthMonitor();
|
|
54072
54722
|
/** The prune itself — see `device-prune-runner.ts`. */
|
|
54073
54723
|
pruner;
|
|
54074
54724
|
/**
|
|
@@ -57498,7 +58148,7 @@ var NotificationCenter = class {
|
|
|
57498
58148
|
const subject = incoming.subject;
|
|
57499
58149
|
const alarmSource = subject.systemEvent?.alarmSource;
|
|
57500
58150
|
const sourceRule = alarmSource !== void 0 ? this.rules.get(alarmSource.ruleId) : null;
|
|
57501
|
-
const media = sourceRule !== null ? mediaPolicyFields(sourceRule, ruleCropZoneIds(sourceRule)) : { media: "none" };
|
|
58151
|
+
const media = sourceRule !== null ? mediaPolicyFields(sourceRule, ruleCropZoneIds(sourceRule), matchedTriggerZoneIds(sourceRule, ruleCropZoneIds(sourceRule), subject)) : { media: "none" };
|
|
57502
58152
|
const mediaDeviceId = alarmSource?.deviceId ?? panel.deviceId;
|
|
57503
58153
|
const entries = targets.map((targetId) => ({
|
|
57504
58154
|
ruleId: NC_ALARM_ANNOUNCE_RULE_ID,
|
|
@@ -58704,6 +59354,7 @@ var NotificationCenter = class {
|
|
|
58704
59354
|
const renderedZones = notificationZones(rule, subject);
|
|
58705
59355
|
const confirm = frozenConfirmPolicy(rule, isSystemEvent);
|
|
58706
59356
|
const cropZoneIds = ruleCropZoneIds(mediaRule);
|
|
59357
|
+
const matchedZoneIds = matchedTriggerZoneIds(mediaRule, cropZoneIds, subject);
|
|
58707
59358
|
const allTargets = [...rule.targets, ...userTargets.filter((t) => !direct.has(t.targetId))];
|
|
58708
59359
|
const linkedOwners = !noMedia && subject.linkedStills !== void 0 ? linkedMediaOwners(subject.linkedStills, linkedCameraMode(mediaRule.media)) : void 0;
|
|
58709
59360
|
const groupV2 = extras?.groupV2 ?? (subject.systemEvent !== void 0 && !noMedia ? alarmIncidentGroup(subject.systemEvent, subject.timestamp) : void 0);
|
|
@@ -58714,7 +59365,7 @@ var NotificationCenter = class {
|
|
|
58714
59365
|
...origin === "synthetic" ? { synthetic: true } : {},
|
|
58715
59366
|
priority: rule.priority,
|
|
58716
59367
|
...rule.template !== void 0 ? { template: rule.template } : {},
|
|
58717
|
-
...noMedia ? { media: "none" } : mediaPolicyFields(mediaRule, cropZoneIds),
|
|
59368
|
+
...noMedia ? { media: "none" } : mediaPolicyFields(mediaRule, cropZoneIds, matchedZoneIds),
|
|
58718
59369
|
...confirm !== void 0 ? {
|
|
58719
59370
|
confirm,
|
|
58720
59371
|
...cooldown !== void 0 ? { cooldown } : {},
|
|
@@ -59359,6 +60010,51 @@ var NotificationCenter = class {
|
|
|
59359
60010
|
if (!this.pruneLedger.isLoaded()) await this.pruneLedger.load();
|
|
59360
60011
|
await this.pruner.reconcileDevices();
|
|
59361
60012
|
await this.pruner.reconcileTargets();
|
|
60013
|
+
this.announceRuleHealth();
|
|
60014
|
+
}
|
|
60015
|
+
/**
|
|
60016
|
+
* D905 — say which rules cannot work: a row the load skipped, or a rule still
|
|
60017
|
+
* naming a device / target the prune confirmed deleted. One edge per rule per
|
|
60018
|
+
* distinct set of reasons; a reading that did not answer changes nothing.
|
|
60019
|
+
*/
|
|
60020
|
+
announceRuleHealth() {
|
|
60021
|
+
const edges = this.ruleHealth.evaluate({
|
|
60022
|
+
rules: this.rules.isLoaded() ? this.rules.list() : null,
|
|
60023
|
+
skipped: this.rules.skippedRows(),
|
|
60024
|
+
prunes: this.pruneLedger.isLoaded() ? this.pruner.listPrunes() : null
|
|
60025
|
+
});
|
|
60026
|
+
for (const edge of edges) {
|
|
60027
|
+
const source = {
|
|
60028
|
+
type: "addon",
|
|
60029
|
+
id: "notification-center"
|
|
60030
|
+
};
|
|
60031
|
+
if (edge.kind === "broken") {
|
|
60032
|
+
const [first] = edge.reasons;
|
|
60033
|
+
if (first === void 0) continue;
|
|
60034
|
+
const offendingIds = [...new Set(edge.reasons.flatMap((r) => r.ids))];
|
|
60035
|
+
this.logger.warn("notification rule cannot work — announced", { meta: {
|
|
60036
|
+
ruleId: edge.ruleId,
|
|
60037
|
+
rule: edge.ruleName,
|
|
60038
|
+
reasons: edge.reasons
|
|
60039
|
+
} });
|
|
60040
|
+
this.deps.emitBusEvent?.(createEvent(EventCategory.NotificationRuleBroken, source, {
|
|
60041
|
+
ruleId: edge.ruleId,
|
|
60042
|
+
ruleName: edge.ruleName,
|
|
60043
|
+
reason: first.code,
|
|
60044
|
+
reasons: edge.reasons,
|
|
60045
|
+
offendingIds
|
|
60046
|
+
}));
|
|
60047
|
+
} else {
|
|
60048
|
+
this.logger.info("notification rule works again — announced", { meta: {
|
|
60049
|
+
ruleId: edge.ruleId,
|
|
60050
|
+
rule: edge.ruleName
|
|
60051
|
+
} });
|
|
60052
|
+
this.deps.emitBusEvent?.(createEvent(EventCategory.NotificationRuleRepaired, source, {
|
|
60053
|
+
ruleId: edge.ruleId,
|
|
60054
|
+
ruleName: edge.ruleName
|
|
60055
|
+
}));
|
|
60056
|
+
}
|
|
60057
|
+
}
|
|
59362
60058
|
}
|
|
59363
60059
|
/**
|
|
59364
60060
|
* `DeviceUnregistered` — a device was DELETED (only `removeDevice` emits it,
|
|
@@ -87156,7 +87852,12 @@ var RetrainCollectionService = class {
|
|
|
87156
87852
|
reason: "unknown-collection"
|
|
87157
87853
|
};
|
|
87158
87854
|
}
|
|
87159
|
-
const
|
|
87855
|
+
const membershipOp = await this.deps.collections.addFrameOp({
|
|
87856
|
+
collectionId: input.collectionId,
|
|
87857
|
+
frameId: recordingFrameId(input.deviceId, input.epochMs),
|
|
87858
|
+
deviceId: input.deviceId
|
|
87859
|
+
});
|
|
87860
|
+
const stored = await this.storeStill(input.deviceId, input.epochMs, membershipOp === null ? [] : [membershipOp]);
|
|
87160
87861
|
if (stored.kind === "refused") {
|
|
87161
87862
|
this.deps.logger.info("retrain collection add refused", {
|
|
87162
87863
|
tags,
|
|
@@ -87168,7 +87869,7 @@ var RetrainCollectionService = class {
|
|
|
87168
87869
|
});
|
|
87169
87870
|
return stored;
|
|
87170
87871
|
}
|
|
87171
|
-
await this.deps.collections.addFrame({
|
|
87872
|
+
if (stored.reused) await this.deps.collections.addFrame({
|
|
87172
87873
|
collectionId: input.collectionId,
|
|
87173
87874
|
frameId: stored.frame.frameId,
|
|
87174
87875
|
deviceId: input.deviceId
|
|
@@ -87233,7 +87934,7 @@ var RetrainCollectionService = class {
|
|
|
87233
87934
|
const ids = await this.deps.collections.listFrameIds(input.collectionId);
|
|
87234
87935
|
return this.deps.retrain.toWireFrames(await this.deps.frames.getMany(ids));
|
|
87235
87936
|
}
|
|
87236
|
-
async storeStill(deviceId, epochMs) {
|
|
87937
|
+
async storeStill(deviceId, epochMs, withRow) {
|
|
87237
87938
|
const existing = await this.deps.frames.get(recordingFrameId(deviceId, epochMs));
|
|
87238
87939
|
if (existing !== null) return {
|
|
87239
87940
|
kind: "stored",
|
|
@@ -87253,7 +87954,7 @@ var RetrainCollectionService = class {
|
|
|
87253
87954
|
deviceId,
|
|
87254
87955
|
epochMs,
|
|
87255
87956
|
bytes: still.jpeg
|
|
87256
|
-
});
|
|
87957
|
+
}, withRow);
|
|
87257
87958
|
return stored.kind === "copied" ? {
|
|
87258
87959
|
kind: "stored",
|
|
87259
87960
|
frame: stored.frame,
|
|
@@ -89576,9 +90277,9 @@ var RetrainService = class {
|
|
|
89576
90277
|
...this.frameUrls(updated.frameId)
|
|
89577
90278
|
};
|
|
89578
90279
|
}
|
|
89579
|
-
/** Un-select:
|
|
89580
|
-
* an annotation whose frame is gone is unopenable
|
|
89581
|
-
*
|
|
90280
|
+
/** Un-select: the frame row, its annotations and its collection memberships
|
|
90281
|
+
* leave in one transaction (an annotation whose frame is gone is unopenable
|
|
90282
|
+
* and unfixable, so they never part); the blob is deleted after the row. */
|
|
89582
90283
|
async deselectFrame(input) {
|
|
89583
90284
|
const frame = await this.deps.frames.get(input.frameId);
|
|
89584
90285
|
if (frame === null) {
|
|
@@ -89597,11 +90298,16 @@ var RetrainService = class {
|
|
|
89597
90298
|
removedAnnotations: await this.removeFrame(frame)
|
|
89598
90299
|
};
|
|
89599
90300
|
}
|
|
89600
|
-
/**
|
|
90301
|
+
/**
|
|
90302
|
+
* Remove a frame from the dataset. The frame row, its annotations and its
|
|
90303
|
+
* collection memberships leave in ONE transaction (a failure removes nothing
|
|
90304
|
+
* and throws); the blob is deleted after the row, never before.
|
|
90305
|
+
*/
|
|
89601
90306
|
async removeFrame(frame) {
|
|
89602
|
-
const
|
|
89603
|
-
await this.deps.memberships?.
|
|
89604
|
-
await this.deps.frames.deleteFrame(frame);
|
|
90307
|
+
const annotationOps = await this.deps.annotations.deleteOpsForFrame(frame.frameId);
|
|
90308
|
+
const membershipOps = await this.deps.memberships?.forgetFrameOps(frame.frameId) ?? [];
|
|
90309
|
+
await this.deps.frames.deleteFrame(frame, [...annotationOps, ...membershipOps]);
|
|
90310
|
+
const removedAnnotations = annotationOps.length;
|
|
89605
90311
|
this.deps.logger.info("retrain frame removed from the dataset", {
|
|
89606
90312
|
tags: { deviceId: frame.deviceId },
|
|
89607
90313
|
meta: {
|
|
@@ -89795,7 +90501,8 @@ var RetrainService = class {
|
|
|
89795
90501
|
* The guarded replace. A caller that names the revision it saw gets exactly
|
|
89796
90502
|
* one attempt: a different current revision is a conflict, never a retry —
|
|
89797
90503
|
* the point is that it must look again. A caller that names none asked for
|
|
89798
|
-
* last-write-wins, so a race lost between our read and the
|
|
90504
|
+
* last-write-wins, so a race lost between our revision read and the records
|
|
90505
|
+
* `transact` that commits the save (a per-frame lock serialises the two) is
|
|
89799
90506
|
* retried on the fresh revision, a bounded number of times.
|
|
89800
90507
|
*/
|
|
89801
90508
|
async replaceGuarded(frame, input, drafts, dismissed) {
|
|
@@ -103422,12 +104129,16 @@ var NC_SYSTEM_EVENT_CATEGORIES = [
|
|
|
103422
104129
|
EventCategory.WorkerOffline,
|
|
103423
104130
|
EventCategory.PipelineNodeInferenceUnavailable,
|
|
103424
104131
|
EventCategory.PipelineDetectionBlind,
|
|
104132
|
+
EventCategory.PipelineMotionNoisy,
|
|
104133
|
+
EventCategory.PipelineMotionCalm,
|
|
103425
104134
|
EventCategory.RecordingHealthStalled,
|
|
103426
104135
|
EventCategory.RecordingHealthOk,
|
|
103427
104136
|
EventCategory.RecordingHealthDegraded,
|
|
103428
104137
|
EventCategory.StorageLocationLow,
|
|
103429
104138
|
EventCategory.StorageLocationCritical,
|
|
103430
104139
|
EventCategory.StorageLocationOk,
|
|
104140
|
+
EventCategory.NotificationRuleBroken,
|
|
104141
|
+
EventCategory.NotificationRuleRepaired,
|
|
103431
104142
|
EventCategory.AddonRunnerFailed,
|
|
103432
104143
|
EventCategory.AddonStarted,
|
|
103433
104144
|
EventCategory.AddonRestarted,
|
|
@@ -106438,6 +107149,18 @@ Object.freeze({
|
|
|
106438
107149
|
"empty"
|
|
106439
107150
|
]
|
|
106440
107151
|
},
|
|
107152
|
+
{
|
|
107153
|
+
"path": "filterCleaningDue",
|
|
107154
|
+
"valueType": "boolean"
|
|
107155
|
+
},
|
|
107156
|
+
{
|
|
107157
|
+
"path": "machineCareDue",
|
|
107158
|
+
"valueType": "boolean"
|
|
107159
|
+
},
|
|
107160
|
+
{
|
|
107161
|
+
"path": "machineCareRemainingRuns",
|
|
107162
|
+
"valueType": "number"
|
|
107163
|
+
},
|
|
106441
107164
|
{
|
|
106442
107165
|
"path": "halfLoad",
|
|
106443
107166
|
"valueType": "boolean"
|
|
@@ -115458,6 +116181,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
115458
116181
|
nodeAddons: (nodeId) => api.nodes.getNodeAddons.query({ nodeId })
|
|
115459
116182
|
},
|
|
115460
116183
|
readAlerts: () => api.alerts.list.query({ limit: 5e3 }),
|
|
116184
|
+
emitBusEvent: (event) => this.ctx.eventBus.emit(event),
|
|
115461
116185
|
getSnapshot: (deviceId) => api.snapshot.getSnapshot.query({ deviceId }),
|
|
115462
116186
|
recognitionOutlook: ({ deviceId, trackId, className, labelKind, now }) => recognitionOutlook({
|
|
115463
116187
|
className,
|
|
@@ -115731,7 +116455,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
115731
116455
|
since: timestamp - toleranceMs,
|
|
115732
116456
|
limit: NC_TRACKS_AT_INSTANT_LIMIT
|
|
115733
116457
|
});
|
|
115734
|
-
return tracksAtInstant([...live, ...persisted.tracks], timestamp, toleranceMs, this.lastFrameDimsByDevice.get(deviceId))
|
|
116458
|
+
return tracksAtInstant([...live, ...persisted.tracks], timestamp, toleranceMs, this.lastFrameDimsByDevice.get(deviceId), (drop) => this.ctx.logger.info("scene-boxes: other track not drawn", {
|
|
116459
|
+
tags: { deviceId },
|
|
116460
|
+
meta: {
|
|
116461
|
+
trackId: drop.trackId,
|
|
116462
|
+
className: drop.className,
|
|
116463
|
+
reason: drop.reason,
|
|
116464
|
+
instant: timestamp,
|
|
116465
|
+
...drop.nearestDtMs !== void 0 ? { nearestDtMs: drop.nearestDtMs } : {}
|
|
116466
|
+
}
|
|
116467
|
+
}));
|
|
115735
116468
|
},
|
|
115736
116469
|
getDeviceName,
|
|
115737
116470
|
getDeviceLocation,
|