@camstack/addon-post-analysis 1.2.70 → 1.2.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{dist-6T-F0YJO.mjs → dist-6te1200B.mjs} +1907 -65
- package/dist/{dist-DnpVHUcH.js → dist-C2w3cl2p.js} +1907 -65
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Cxzt8LHT.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Brh9RYTk.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BfeJzFhL.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-DW1qt-In.mjs → hostInit-DpWdIydx.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +1136 -182
- package/dist/pipeline-analytics/index.mjs +1136 -182
- 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_ui_mf_2_library__loadShare__.js-B57Jy4_o.mjs +0 -26
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as readDeviceStateFrom, A as TimelapseRuleInputSchema, B as customAction, C as NcSnoozeSchema, Ct as unknown, D as RECORDING_EXPORT_MAX_READ_BYTES, E as OpsLogEntrySchema, F as alarmPanelCapability, G as faceGalleryCapability, H as deriveRecordingMode, I as assertTimelapseCadences, J as isScheduleActive, L as audioMetricsCapability, M as TimelapseRuleSchema, N as TrackSourceSchema, O as RetrainStatusSchema, P as addonWidgetsSourceCapability, Q as plateGalleryCapability, R as buildEventKindDescriptor, S as NcSnoozeInputSchema, St as string, T as NcTaxonomySchema, V as defineCustomActions, W as encodeVectorBase64, X as notificationRulesCapability, Y as kebabToCamel, Z as pipelineAnalyticsCapability, _ as NcRuleInputSchema, _t as literal, at as errMsg, b as NcRuleTargetSchema, bt as partialRecord, c as EVENT_PAD_MS, ct as createEvent, d as NC_ALARM_SYSTEM_EVENT_KINDS, dt as nodePin, et as readTimelapseGeneratedAt, f as NC_CONDITION_CATALOG, ft as sleep, g as NcConditionDescriptorSchema, gt as discriminatedUnion, h as NC_TAXONOMY, ht as boolean, i as DETECTION_MACRO_CLASSES, it as zoneAnalyticsCapability, j as TimelapseRulePatchSchema, k as TIMELAPSE_DENSE_FLOOR_SEC, l as LabelAttributionSchema, lt as hydrateSchema, mt as array, n as DEFAULT_EVENT_COLOR, nt as vectorDimFromBase64, o as DeclaredDevices, ot as BaseAddon, p as NC_DEFAULT_SNOOZE_MINUTES, pt as _enum, q as isDetectionMacroClass, rt as videoclipsCapability, s as EVENT_KIND_BY_CAP, st as DeviceType, t as BaseDevice, tt as subKindsOf, u as MACRO_LABELS, ut as isDeviceScopedCap, v as NcRulePatchSchema, vt as number, w as NcSnoozeSuppressedSchema, wt as EventCategory, x as NcScheduleSchema, xt as record, y as NcRuleSchema, yt as object, z as cosineSimilarity } from "../dist-
|
|
1
|
+
import { $ as readDeviceStateFrom, A as TimelapseRuleInputSchema, B as customAction, C as NcSnoozeSchema, Ct as unknown, D as RECORDING_EXPORT_MAX_READ_BYTES, E as OpsLogEntrySchema, F as alarmPanelCapability, G as faceGalleryCapability, H as deriveRecordingMode, I as assertTimelapseCadences, J as isScheduleActive, L as audioMetricsCapability, M as TimelapseRuleSchema, N as TrackSourceSchema, O as RetrainStatusSchema, P as addonWidgetsSourceCapability, Q as plateGalleryCapability, R as buildEventKindDescriptor, S as NcSnoozeInputSchema, St as string, T as NcTaxonomySchema, V as defineCustomActions, W as encodeVectorBase64, X as notificationRulesCapability, Y as kebabToCamel, Z as pipelineAnalyticsCapability, _ as NcRuleInputSchema, _t as literal, at as errMsg, b as NcRuleTargetSchema, bt as partialRecord, c as EVENT_PAD_MS, ct as createEvent, d as NC_ALARM_SYSTEM_EVENT_KINDS, dt as nodePin, et as readTimelapseGeneratedAt, f as NC_CONDITION_CATALOG, ft as sleep, g as NcConditionDescriptorSchema, gt as discriminatedUnion, h as NC_TAXONOMY, ht as boolean, i as DETECTION_MACRO_CLASSES, it as zoneAnalyticsCapability, j as TimelapseRulePatchSchema, k as TIMELAPSE_DENSE_FLOOR_SEC, l as LabelAttributionSchema, lt as hydrateSchema, mt as array, n as DEFAULT_EVENT_COLOR, nt as vectorDimFromBase64, o as DeclaredDevices, ot as BaseAddon, p as NC_DEFAULT_SNOOZE_MINUTES, pt as _enum, q as isDetectionMacroClass, rt as videoclipsCapability, s as EVENT_KIND_BY_CAP, st as DeviceType, t as BaseDevice, tt as subKindsOf, u as MACRO_LABELS, ut as isDeviceScopedCap, v as NcRulePatchSchema, vt as number, w as NcSnoozeSuppressedSchema, wt as EventCategory, x as NcScheduleSchema, xt as record, y as NcRuleSchema, yt as object, z as cosineSimilarity } from "../dist-6te1200B.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { promises } from "node:fs";
|
|
@@ -549,7 +549,14 @@ function alarmCoverageGap(coverage) {
|
|
|
549
549
|
return "all-skipped";
|
|
550
550
|
}
|
|
551
551
|
/**
|
|
552
|
-
* "
|
|
552
|
+
* "Attivi: Cancello, Garage — Esclusi: Porta retro."
|
|
553
|
+
*
|
|
554
|
+
* A LIST, not a sentence. The mode is in the title and the body is one
|
|
555
|
+
* `Label: names` line per device group, because this is read on a lock screen
|
|
556
|
+
* and the question is always "which one" — the prose version ("3 dispositivi
|
|
557
|
+
* attivi: …. Esclusi: ….") made the operator parse a paragraph to find a
|
|
558
|
+
* window. Each variant still owns its own catalog key, so the Texts tab can
|
|
559
|
+
* re-word "nothing is armed" without touching "these are armed".
|
|
553
560
|
*
|
|
554
561
|
* Names, not ids: this is read by a person standing at a door. An id that has
|
|
555
562
|
* no name falls back to a NAMED, translated placeholder rather than being
|
|
@@ -628,7 +635,7 @@ function buildArmAnnouncement(coverage, deviceName, bypassed = []) {
|
|
|
628
635
|
};
|
|
629
636
|
}
|
|
630
637
|
/**
|
|
631
|
-
* "
|
|
638
|
+
* "Attivi: 2 — Bypassati: Finestra veranda."
|
|
632
639
|
*
|
|
633
640
|
* The bypassed sensors are named and the armed ones are COUNTED, not listed:
|
|
634
641
|
* this sentence's subject is the exception, and burying it under thirteen
|
|
@@ -651,13 +658,13 @@ function armedWithBypass(coverage, bypassed, deviceName) {
|
|
|
651
658
|
};
|
|
652
659
|
}
|
|
653
660
|
/**
|
|
654
|
-
* "
|
|
661
|
+
* "Aperti: Finestra veranda, Porta retro" + how to arm anyway.
|
|
655
662
|
*
|
|
656
663
|
* The one alarm sentence about something that did not happen, so it has to
|
|
657
664
|
* carry both halves an operator needs to act: WHICH sensors are open, and that
|
|
658
665
|
* pressing arm again arms without them. Naming them is the whole message — a
|
|
659
666
|
* refusal that says only "cannot arm" is a panel the operator will conclude is
|
|
660
|
-
* broken.
|
|
667
|
+
* broken — so the names are the FIRST line and the retry hint is the second.
|
|
661
668
|
*/
|
|
662
669
|
function buildArmRefusedAnnouncement(mode, names, retrySeconds) {
|
|
663
670
|
return {
|
|
@@ -672,7 +679,7 @@ function buildArmRefusedAnnouncement(mode, names, retrySeconds) {
|
|
|
672
679
|
};
|
|
673
680
|
}
|
|
674
681
|
/**
|
|
675
|
-
* The same coverage, said 30 seconds earlier
|
|
682
|
+
* The EXCEPTIONS to the same coverage, said 30 seconds earlier.
|
|
676
683
|
*
|
|
677
684
|
* The exit-delay sentence is deliberately SHORTER than the arm one — it is read
|
|
678
685
|
* on the way out of a door — and it carries exactly one thing the arm sentence
|
|
@@ -680,8 +687,15 @@ function buildArmRefusedAnnouncement(mode, names, retrySeconds) {
|
|
|
680
687
|
* A mode that arms nothing is worth interrupting somebody for at second 0 and
|
|
681
688
|
* merely worth recording at second 30.
|
|
682
689
|
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
690
|
+
* **It never repeats the device list.** The countdown lives in the TITLE
|
|
691
|
+
* (`system.alarm-arming.title.<mode>` renders `{{seconds}}`), and the armed
|
|
692
|
+
* list arrives thirty seconds later in the arm announcement — printing it
|
|
693
|
+
* twice is the verbosity that made the operator stop reading both. So the body
|
|
694
|
+
* is what is WRONG: what was bypassed, what is excluded, or that nothing is.
|
|
695
|
+
*
|
|
696
|
+
* `seconds` stays in the params because the title asks for it, and `textCount`
|
|
697
|
+
* stays with it: a row enqueued before this deploy still pluralises on the
|
|
698
|
+
* countdown it was written for.
|
|
685
699
|
*/
|
|
686
700
|
function buildArmingAnnouncement(coverage, seconds, deviceName, bypassed = []) {
|
|
687
701
|
const textParams = {
|
|
@@ -696,6 +710,7 @@ function buildArmingAnnouncement(coverage, seconds, deviceName, bypassed = []) {
|
|
|
696
710
|
},
|
|
697
711
|
textCount: seconds
|
|
698
712
|
};
|
|
713
|
+
const skippedNames = coverage.skippedDevices.map((s) => deviceName(s.deviceId)).join(", ");
|
|
699
714
|
const gap = alarmCoverageGap(coverage);
|
|
700
715
|
if (gap === "uncovered") return {
|
|
701
716
|
bodyKey: "alarm.arming.body.uncovered",
|
|
@@ -706,10 +721,18 @@ function buildArmingAnnouncement(coverage, seconds, deviceName, bypassed = []) {
|
|
|
706
721
|
bodyKey: "alarm.arming.body.allSkipped",
|
|
707
722
|
textParams: {
|
|
708
723
|
...textParams,
|
|
709
|
-
names:
|
|
724
|
+
names: skippedNames
|
|
710
725
|
},
|
|
711
726
|
textCount: seconds
|
|
712
727
|
};
|
|
728
|
+
if (coverage.skippedDevices.length > 0) return {
|
|
729
|
+
bodyKey: "alarm.arming.body.skipped",
|
|
730
|
+
textParams: {
|
|
731
|
+
...textParams,
|
|
732
|
+
names: skippedNames
|
|
733
|
+
},
|
|
734
|
+
textCount: coverage.skippedDevices.length
|
|
735
|
+
};
|
|
713
736
|
return {
|
|
714
737
|
bodyKey: "alarm.arming.body",
|
|
715
738
|
textParams,
|
|
@@ -1550,6 +1573,401 @@ var NC_ALARM_DECLARATION = {
|
|
|
1550
1573
|
DeviceClass: NcAlarmPanelDevice
|
|
1551
1574
|
};
|
|
1552
1575
|
//#endregion
|
|
1576
|
+
//#region src/notification-center/artifact-index.ts
|
|
1577
|
+
var NC_ARTIFACTS_COLLECTION = "notification-center:artifacts";
|
|
1578
|
+
var NcArtifactKindSchema = _enum([
|
|
1579
|
+
"timelapse",
|
|
1580
|
+
"timelapse-preview",
|
|
1581
|
+
"summary-mosaic"
|
|
1582
|
+
]);
|
|
1583
|
+
/**
|
|
1584
|
+
* How the viewer should DRAW it. Derived from the mime once, here, so no client
|
|
1585
|
+
* has to carry a mime table — and so a GIF that degraded to a JPEG is drawn as
|
|
1586
|
+
* the JPEG it actually is.
|
|
1587
|
+
*/
|
|
1588
|
+
var NcArtifactMediaTypeSchema = _enum([
|
|
1589
|
+
"image",
|
|
1590
|
+
"gif",
|
|
1591
|
+
"video"
|
|
1592
|
+
]);
|
|
1593
|
+
/** The relative hub path of the authenticated bytes route for an artefact id. */
|
|
1594
|
+
var NC_ARTIFACT_READ_PREFIX = "nc-artifact-read";
|
|
1595
|
+
var NcArtifactRecordSchema = object({
|
|
1596
|
+
/** The `NcArtifactStore` id — the same id the bytes route resolves. */
|
|
1597
|
+
id: string().min(1),
|
|
1598
|
+
kind: NcArtifactKindSchema,
|
|
1599
|
+
mediaType: NcArtifactMediaTypeSchema,
|
|
1600
|
+
mime: string().min(1),
|
|
1601
|
+
sizeBytes: number().int().nonnegative(),
|
|
1602
|
+
/** When the artefact was PRODUCED. */
|
|
1603
|
+
createdAt: number().int(),
|
|
1604
|
+
/** What the content is ABOUT (window end / event time). The sort key: a
|
|
1605
|
+
* timelapse rendered at 06:05 is about the night before, and a list ordered
|
|
1606
|
+
* by production time puts it in the wrong morning. */
|
|
1607
|
+
capturedAt: number().int(),
|
|
1608
|
+
/** After this the bytes are gone. Derived from the retained shelf's own age
|
|
1609
|
+
* bound — never a second retention policy. */
|
|
1610
|
+
expiresAt: number().int(),
|
|
1611
|
+
/** The camera the artefact is FILED under. Null only if it belongs to none. */
|
|
1612
|
+
deviceId: number().int().nullable(),
|
|
1613
|
+
/** Every camera the artefact covers, for display. */
|
|
1614
|
+
deviceIds: array(number().int()),
|
|
1615
|
+
ruleId: string().optional(),
|
|
1616
|
+
ruleName: string().optional(),
|
|
1617
|
+
title: string().optional(),
|
|
1618
|
+
/** A companion still for a video artefact (a timelapse's preview frame) —
|
|
1619
|
+
* what a list renders as the thumbnail. */
|
|
1620
|
+
posterArtifactId: string().optional(),
|
|
1621
|
+
/** RELATIVE hub path of the authenticated bytes route. Relative on purpose:
|
|
1622
|
+
* the viewer already holds the hub origin it is authenticated against, and
|
|
1623
|
+
* only the SIGNED plane needs to guess which host a stranger can reach. */
|
|
1624
|
+
downloadPath: string().min(1)
|
|
1625
|
+
});
|
|
1626
|
+
var NcArtifactQuerySchema = object({
|
|
1627
|
+
kinds: array(NcArtifactKindSchema).optional(),
|
|
1628
|
+
deviceIds: array(number().int()).optional(),
|
|
1629
|
+
ruleId: string().optional(),
|
|
1630
|
+
/** Inclusive `capturedAt` window. */
|
|
1631
|
+
fromMs: number().int().optional(),
|
|
1632
|
+
toMs: number().int().optional(),
|
|
1633
|
+
limit: number().int().min(1).max(200).default(50),
|
|
1634
|
+
offset: number().int().min(0).default(0)
|
|
1635
|
+
});
|
|
1636
|
+
var NC_ARTIFACTS_COLUMNS = [
|
|
1637
|
+
{
|
|
1638
|
+
name: "id",
|
|
1639
|
+
type: "TEXT",
|
|
1640
|
+
primaryKey: true,
|
|
1641
|
+
notNull: true
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
name: "kind",
|
|
1645
|
+
type: "TEXT",
|
|
1646
|
+
notNull: true
|
|
1647
|
+
},
|
|
1648
|
+
{
|
|
1649
|
+
name: "deviceId",
|
|
1650
|
+
type: "INTEGER"
|
|
1651
|
+
},
|
|
1652
|
+
{
|
|
1653
|
+
name: "ruleId",
|
|
1654
|
+
type: "TEXT"
|
|
1655
|
+
},
|
|
1656
|
+
(
|
|
1657
|
+
/** `''` for a global artefact — see the header note on visibility. */
|
|
1658
|
+
{
|
|
1659
|
+
name: "ownerUserId",
|
|
1660
|
+
type: "TEXT",
|
|
1661
|
+
notNull: true
|
|
1662
|
+
}),
|
|
1663
|
+
{
|
|
1664
|
+
name: "capturedAt",
|
|
1665
|
+
type: "INTEGER",
|
|
1666
|
+
notNull: true
|
|
1667
|
+
},
|
|
1668
|
+
{
|
|
1669
|
+
name: "expiresAt",
|
|
1670
|
+
type: "INTEGER",
|
|
1671
|
+
notNull: true
|
|
1672
|
+
},
|
|
1673
|
+
(
|
|
1674
|
+
/** The FULL record (Zod-validated on read) — the scalars above are indexed
|
|
1675
|
+
* projections only. */
|
|
1676
|
+
{
|
|
1677
|
+
name: "record",
|
|
1678
|
+
type: "JSON",
|
|
1679
|
+
notNull: true
|
|
1680
|
+
})
|
|
1681
|
+
];
|
|
1682
|
+
var NC_ARTIFACTS_INDEXES = [
|
|
1683
|
+
{
|
|
1684
|
+
name: "idx_nc_artifacts_captured",
|
|
1685
|
+
columns: ["capturedAt"]
|
|
1686
|
+
},
|
|
1687
|
+
{
|
|
1688
|
+
name: "idx_nc_artifacts_device",
|
|
1689
|
+
columns: ["deviceId", "capturedAt"]
|
|
1690
|
+
},
|
|
1691
|
+
{
|
|
1692
|
+
name: "idx_nc_artifacts_kind",
|
|
1693
|
+
columns: ["kind", "capturedAt"]
|
|
1694
|
+
},
|
|
1695
|
+
{
|
|
1696
|
+
name: "idx_nc_artifacts_expires",
|
|
1697
|
+
columns: ["expiresAt"]
|
|
1698
|
+
}
|
|
1699
|
+
];
|
|
1700
|
+
/** Upper bound for a half-open `whereBetween` — the store has no `>=`. */
|
|
1701
|
+
var TIME_MAX = 864e13;
|
|
1702
|
+
var TIME_MIN = -864e13;
|
|
1703
|
+
/** Prune scan ceiling — the index is bounded by the shelf it mirrors (2000
|
|
1704
|
+
* entries by default), so this only has to be comfortably above it. */
|
|
1705
|
+
var NC_ARTIFACT_PRUNE_SCAN_LIMIT = 2e4;
|
|
1706
|
+
/** `image/gif` → `gif`. Keyed on what the bytes ARE, never on what was asked
|
|
1707
|
+
* for: a GIF that degraded to a still must be drawn as a still. */
|
|
1708
|
+
function mediaTypeForMime(mime) {
|
|
1709
|
+
if (mime === "image/gif") return "gif";
|
|
1710
|
+
if (mime.startsWith("video/")) return "video";
|
|
1711
|
+
return "image";
|
|
1712
|
+
}
|
|
1713
|
+
/** The relative hub path an authenticated client fetches the bytes from. */
|
|
1714
|
+
function artifactDownloadPath(addonId, artifactId) {
|
|
1715
|
+
return `/addon/${addonId}/${NC_ARTIFACT_READ_PREFIX}/${artifactId}`;
|
|
1716
|
+
}
|
|
1717
|
+
var NcArtifactIndex = class {
|
|
1718
|
+
store;
|
|
1719
|
+
logger;
|
|
1720
|
+
addonId;
|
|
1721
|
+
retentionMs;
|
|
1722
|
+
now;
|
|
1723
|
+
constructor(deps) {
|
|
1724
|
+
this.store = deps.store;
|
|
1725
|
+
this.logger = deps.logger;
|
|
1726
|
+
this.addonId = deps.addonId;
|
|
1727
|
+
this.retentionMs = deps.retentionMs;
|
|
1728
|
+
this.now = deps.now ?? (() => Date.now());
|
|
1729
|
+
}
|
|
1730
|
+
static async declare(store) {
|
|
1731
|
+
await store.declareCollection.mutate({
|
|
1732
|
+
collection: NC_ARTIFACTS_COLLECTION,
|
|
1733
|
+
columns: [...NC_ARTIFACTS_COLUMNS],
|
|
1734
|
+
indexes: [...NC_ARTIFACTS_INDEXES]
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Record one retained artefact. Returns the row as the viewer will see it, or
|
|
1739
|
+
* null when the write failed — and a failure is LOGGED, per camera: an
|
|
1740
|
+
* artefact silently absent from the index reads to an operator as an artefact
|
|
1741
|
+
* that was never produced.
|
|
1742
|
+
*/
|
|
1743
|
+
async record(input) {
|
|
1744
|
+
const createdAt = this.now();
|
|
1745
|
+
const record = NcArtifactRecordSchema.parse({
|
|
1746
|
+
id: input.id,
|
|
1747
|
+
kind: input.kind,
|
|
1748
|
+
mediaType: mediaTypeForMime(input.mime),
|
|
1749
|
+
mime: input.mime,
|
|
1750
|
+
sizeBytes: input.sizeBytes,
|
|
1751
|
+
createdAt,
|
|
1752
|
+
capturedAt: input.capturedAt,
|
|
1753
|
+
expiresAt: createdAt + this.retentionMs,
|
|
1754
|
+
deviceId: input.deviceId,
|
|
1755
|
+
deviceIds: [...input.deviceIds],
|
|
1756
|
+
...input.ruleId !== void 0 ? { ruleId: input.ruleId } : {},
|
|
1757
|
+
...input.ruleName !== void 0 ? { ruleName: input.ruleName } : {},
|
|
1758
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
1759
|
+
...input.posterArtifactId !== void 0 ? { posterArtifactId: input.posterArtifactId } : {},
|
|
1760
|
+
downloadPath: artifactDownloadPath(this.addonId, input.id)
|
|
1761
|
+
});
|
|
1762
|
+
try {
|
|
1763
|
+
await this.store.set.mutate({
|
|
1764
|
+
collection: NC_ARTIFACTS_COLLECTION,
|
|
1765
|
+
key: record.id,
|
|
1766
|
+
value: {
|
|
1767
|
+
kind: record.kind,
|
|
1768
|
+
deviceId: record.deviceId,
|
|
1769
|
+
ruleId: record.ruleId ?? "",
|
|
1770
|
+
ownerUserId: input.ownerUserId ?? "",
|
|
1771
|
+
capturedAt: record.capturedAt,
|
|
1772
|
+
expiresAt: record.expiresAt,
|
|
1773
|
+
record
|
|
1774
|
+
}
|
|
1775
|
+
});
|
|
1776
|
+
return record;
|
|
1777
|
+
} catch (err) {
|
|
1778
|
+
this.logger.warn("artifact not indexed — it will not appear in the viewer", {
|
|
1779
|
+
...record.deviceId !== null ? { tags: { deviceId: record.deviceId } } : {},
|
|
1780
|
+
meta: {
|
|
1781
|
+
artifactId: record.id,
|
|
1782
|
+
kind: record.kind,
|
|
1783
|
+
error: String(err)
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
return null;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* One page, newest `capturedAt` first.
|
|
1791
|
+
*
|
|
1792
|
+
* Expiry is a PREDICATE, not a post-filter: a row whose bytes the shelf has
|
|
1793
|
+
* already swept must not consume a slot of the page the caller asked for.
|
|
1794
|
+
*/
|
|
1795
|
+
async list(query, viewer) {
|
|
1796
|
+
const filter = this.buildFilter(query, viewer);
|
|
1797
|
+
try {
|
|
1798
|
+
const [rows, total] = await Promise.all([this.store.query.query({
|
|
1799
|
+
collection: NC_ARTIFACTS_COLLECTION,
|
|
1800
|
+
filter: {
|
|
1801
|
+
...filter,
|
|
1802
|
+
orderBy: {
|
|
1803
|
+
field: "capturedAt",
|
|
1804
|
+
direction: "desc"
|
|
1805
|
+
},
|
|
1806
|
+
limit: query.limit,
|
|
1807
|
+
offset: query.offset
|
|
1808
|
+
}
|
|
1809
|
+
}), this.store.count.query({
|
|
1810
|
+
collection: NC_ARTIFACTS_COLLECTION,
|
|
1811
|
+
filter
|
|
1812
|
+
})]);
|
|
1813
|
+
const artifacts = this.parseRows(rows);
|
|
1814
|
+
return {
|
|
1815
|
+
artifacts,
|
|
1816
|
+
total,
|
|
1817
|
+
hasMore: query.offset + artifacts.length < total
|
|
1818
|
+
};
|
|
1819
|
+
} catch (err) {
|
|
1820
|
+
this.logger.warn("artifact index query failed", { meta: { error: String(err) } });
|
|
1821
|
+
return {
|
|
1822
|
+
artifacts: [],
|
|
1823
|
+
total: 0,
|
|
1824
|
+
hasMore: false
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* One artefact by id, or null when it does not exist, has expired, or belongs
|
|
1830
|
+
* to another user. All three answer the same way ON PURPOSE — a surface that
|
|
1831
|
+
* distinguishes "not yours" from "not there" tells a caller what other people
|
|
1832
|
+
* own.
|
|
1833
|
+
*/
|
|
1834
|
+
async get(artifactId, viewer) {
|
|
1835
|
+
const row = await this.readRow(artifactId);
|
|
1836
|
+
if (row === null) return null;
|
|
1837
|
+
if (row.record.expiresAt <= this.now()) return null;
|
|
1838
|
+
if (!viewer.isAdmin && row.ownerUserId !== "" && row.ownerUserId !== viewer.userId) return null;
|
|
1839
|
+
return row.record;
|
|
1840
|
+
}
|
|
1841
|
+
/**
|
|
1842
|
+
* Resolve an id for the BYTES route — visibility-free by design.
|
|
1843
|
+
*
|
|
1844
|
+
* The route is served `authenticated`, so the hub has already established a
|
|
1845
|
+
* principal; what this answers is only "is this id an indexed artefact, and is
|
|
1846
|
+
* it still meant to exist". Per-user artefact scoping on the byte route would
|
|
1847
|
+
* be a second authorization model layered on an unguessable id, and this repo
|
|
1848
|
+
* has one of those already (`data-plane-access.ts`): a plane serving ONE item
|
|
1849
|
+
* the caller had to be told about is `authenticated`.
|
|
1850
|
+
*/
|
|
1851
|
+
async resolve(artifactId) {
|
|
1852
|
+
return (await this.readRow(artifactId))?.record ?? null;
|
|
1853
|
+
}
|
|
1854
|
+
/** One row by primary key, parsed. Null when absent or no longer valid. */
|
|
1855
|
+
async readRow(artifactId) {
|
|
1856
|
+
let raw;
|
|
1857
|
+
try {
|
|
1858
|
+
raw = await this.store.get.query({
|
|
1859
|
+
collection: NC_ARTIFACTS_COLLECTION,
|
|
1860
|
+
key: artifactId
|
|
1861
|
+
});
|
|
1862
|
+
} catch (err) {
|
|
1863
|
+
this.logger.warn("artifact index lookup failed", { meta: {
|
|
1864
|
+
artifactId,
|
|
1865
|
+
error: String(err)
|
|
1866
|
+
} });
|
|
1867
|
+
return null;
|
|
1868
|
+
}
|
|
1869
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
|
1870
|
+
const row = { ...raw };
|
|
1871
|
+
const parsed = NcArtifactRecordSchema.safeParse(row["record"]);
|
|
1872
|
+
if (!parsed.success) return null;
|
|
1873
|
+
const owner = row["ownerUserId"];
|
|
1874
|
+
return {
|
|
1875
|
+
record: parsed.data,
|
|
1876
|
+
ownerUserId: typeof owner === "string" ? owner : ""
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1880
|
+
* Drop rows whose bytes are gone, in ONE `deleteWhere` rather than an N+1
|
|
1881
|
+
* drain.
|
|
1882
|
+
*
|
|
1883
|
+
* TWO halves, and only one of them is safe everywhere:
|
|
1884
|
+
*
|
|
1885
|
+
* - **Expired rows** are time-based and node-independent. Always swept.
|
|
1886
|
+
* - **Orphans** — rows whose file a byte/count bound took before the age bound
|
|
1887
|
+
* did — are found by reconciling against the shelf. That reconcile is only
|
|
1888
|
+
* valid for a caller whose shelf holds the bytes for the WHOLE index, and
|
|
1889
|
+
* the index is CENTRAL while the shelf is local: a second node running this
|
|
1890
|
+
* with its own (empty) shelf would read every row as an orphan and delete
|
|
1891
|
+
* the entire library. Hence `keptIds: null` — a caller that is not the
|
|
1892
|
+
* producing node states so, and the destructive half becomes unreachable
|
|
1893
|
+
* rather than merely un-called.
|
|
1894
|
+
*
|
|
1895
|
+
* @param keptIds the ids the caller's shelf holds, or `null` when this node is
|
|
1896
|
+
* not the one that produces artefacts.
|
|
1897
|
+
*/
|
|
1898
|
+
async prune(keptIds) {
|
|
1899
|
+
let removed = 0;
|
|
1900
|
+
try {
|
|
1901
|
+
const expired = await this.store.deleteWhere.mutate({
|
|
1902
|
+
collection: NC_ARTIFACTS_COLLECTION,
|
|
1903
|
+
filter: { whereBetween: { expiresAt: [TIME_MIN, this.now() - 1] } }
|
|
1904
|
+
});
|
|
1905
|
+
removed += expired.deleted;
|
|
1906
|
+
if (keptIds === null) {
|
|
1907
|
+
if (removed > 0) this.logger.debug("artifact index pruned (expired only)", { meta: { removed } });
|
|
1908
|
+
return removed;
|
|
1909
|
+
}
|
|
1910
|
+
const alive = new Set(keptIds);
|
|
1911
|
+
const orphans = (await this.store.query.query({
|
|
1912
|
+
collection: NC_ARTIFACTS_COLLECTION,
|
|
1913
|
+
filter: { limit: NC_ARTIFACT_PRUNE_SCAN_LIMIT }
|
|
1914
|
+
})).map((r) => r.id).filter((id) => !alive.has(id));
|
|
1915
|
+
for (const id of orphans) {
|
|
1916
|
+
await this.store.delete.mutate({
|
|
1917
|
+
collection: NC_ARTIFACTS_COLLECTION,
|
|
1918
|
+
key: id
|
|
1919
|
+
});
|
|
1920
|
+
removed += 1;
|
|
1921
|
+
}
|
|
1922
|
+
if (removed > 0) this.logger.debug("artifact index pruned", { meta: {
|
|
1923
|
+
removed,
|
|
1924
|
+
expired: expired.deleted,
|
|
1925
|
+
orphaned: orphans.length
|
|
1926
|
+
} });
|
|
1927
|
+
return removed;
|
|
1928
|
+
} catch (err) {
|
|
1929
|
+
this.logger.warn("artifact index prune failed", { meta: { error: String(err) } });
|
|
1930
|
+
return removed;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
/** The visibility predicate: a caller sees global artefacts plus his own; an
|
|
1934
|
+
* admin sees every one. */
|
|
1935
|
+
visibility(viewer) {
|
|
1936
|
+
if (viewer.isAdmin) return {};
|
|
1937
|
+
return { whereIn: { ownerUserId: ["", viewer.userId] } };
|
|
1938
|
+
}
|
|
1939
|
+
buildFilter(query, viewer) {
|
|
1940
|
+
const whereIn = { ...this.visibility(viewer).whereIn };
|
|
1941
|
+
if (query.kinds !== void 0 && query.kinds.length > 0) whereIn["kind"] = [...query.kinds];
|
|
1942
|
+
if (query.deviceIds !== void 0 && query.deviceIds.length > 0) whereIn["deviceId"] = [...query.deviceIds];
|
|
1943
|
+
const whereBetween = {
|
|
1944
|
+
capturedAt: [query.fromMs ?? TIME_MIN, query.toMs ?? TIME_MAX],
|
|
1945
|
+
expiresAt: [this.now(), TIME_MAX]
|
|
1946
|
+
};
|
|
1947
|
+
return {
|
|
1948
|
+
...query.ruleId !== void 0 ? { where: { ruleId: query.ruleId } } : {},
|
|
1949
|
+
...Object.keys(whereIn).length > 0 ? { whereIn } : {},
|
|
1950
|
+
whereBetween
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
/** Parse the JSON column, skipping rows that no longer validate. A degraded
|
|
1954
|
+
* row must never cost the whole page. */
|
|
1955
|
+
parseRows(rows) {
|
|
1956
|
+
const out = [];
|
|
1957
|
+
let skipped = 0;
|
|
1958
|
+
for (const row of rows) {
|
|
1959
|
+
const parsed = NcArtifactRecordSchema.safeParse(row.data["record"]);
|
|
1960
|
+
if (!parsed.success) {
|
|
1961
|
+
skipped += 1;
|
|
1962
|
+
continue;
|
|
1963
|
+
}
|
|
1964
|
+
out.push(parsed.data);
|
|
1965
|
+
}
|
|
1966
|
+
if (skipped > 0) this.logger.warn("artifact index rows skipped as invalid", { meta: { skipped } });
|
|
1967
|
+
return out;
|
|
1968
|
+
}
|
|
1969
|
+
};
|
|
1970
|
+
//#endregion
|
|
1553
1971
|
//#region ../types/dist/node.mjs
|
|
1554
1972
|
var node_exports = /* @__PURE__ */ __exportAll({
|
|
1555
1973
|
FilesystemStorageProvider: () => FilesystemStorageProvider,
|
|
@@ -1940,6 +2358,61 @@ var NcArtifactPlane = class {
|
|
|
1940
2358
|
}
|
|
1941
2359
|
}
|
|
1942
2360
|
/**
|
|
2361
|
+
* Publish bytes AND file them in the viewer's browsable index (task #36).
|
|
2362
|
+
*
|
|
2363
|
+
* Two differences from {@link publish}, both deliberate:
|
|
2364
|
+
*
|
|
2365
|
+
* 1. **The bytes are stored FIRST, and stored even when no public base URL
|
|
2366
|
+
* exists.** `publish` returns early on a loopback-only install because a
|
|
2367
|
+
* signed link nobody can open is worth nothing — but the VIEWER reaches
|
|
2368
|
+
* the hub directly over its own session, so an install with no tunnel
|
|
2369
|
+
* still gets a full artifact library. Making the index depend on a
|
|
2370
|
+
* reachable public address would have made the feature absent exactly on
|
|
2371
|
+
* the installs that are LAN-only.
|
|
2372
|
+
* 2. **It answers the artefact id**, so a caller can link one artefact to
|
|
2373
|
+
* another — a timelapse's poster is its preview frame, and the video row
|
|
2374
|
+
* cannot name it without knowing the id the preview got.
|
|
2375
|
+
*
|
|
2376
|
+
* Never throws, for the same reason `publish` does not: an artefact library
|
|
2377
|
+
* is an enhancement and must never cost a notification.
|
|
2378
|
+
*/
|
|
2379
|
+
async publishIndexed(bytes, mime, meta) {
|
|
2380
|
+
let artifactId = null;
|
|
2381
|
+
try {
|
|
2382
|
+
const index = this.deps.index;
|
|
2383
|
+
const stored = await this.deps.store.put(bytes, mime, { retain: index !== void 0 });
|
|
2384
|
+
if (index !== void 0) artifactId = (await index.record({
|
|
2385
|
+
...meta,
|
|
2386
|
+
id: stored.id,
|
|
2387
|
+
mime,
|
|
2388
|
+
sizeBytes: stored.bytes
|
|
2389
|
+
}))?.id ?? null;
|
|
2390
|
+
const baseUrl = await this.resolveBaseUrl();
|
|
2391
|
+
return {
|
|
2392
|
+
url: baseUrl === null ? null : buildArtifactUrl({
|
|
2393
|
+
baseUrl,
|
|
2394
|
+
routePrefix: this.deps.routePrefix,
|
|
2395
|
+
id: stored.id,
|
|
2396
|
+
secret: this.deps.secret,
|
|
2397
|
+
expMs: this.now() + (this.deps.ttlMs ?? DEFAULT_TTL_MS)
|
|
2398
|
+
}),
|
|
2399
|
+
artifactId
|
|
2400
|
+
};
|
|
2401
|
+
} catch (err) {
|
|
2402
|
+
this.deps.logger.warn("indexed artifact publish failed — shipping bytes only", {
|
|
2403
|
+
...meta.deviceId !== null ? { tags: { deviceId: meta.deviceId } } : {},
|
|
2404
|
+
meta: {
|
|
2405
|
+
kind: meta.kind,
|
|
2406
|
+
error: String(err)
|
|
2407
|
+
}
|
|
2408
|
+
});
|
|
2409
|
+
return {
|
|
2410
|
+
url: null,
|
|
2411
|
+
artifactId
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
/**
|
|
1943
2416
|
* Absolutize a path against the SAME base URL an artifact link would use.
|
|
1944
2417
|
*
|
|
1945
2418
|
* For links this plane does not host but a RECIPIENT still has to open — a
|
|
@@ -2005,6 +2478,124 @@ var NcArtifactPlane = class {
|
|
|
2005
2478
|
}
|
|
2006
2479
|
};
|
|
2007
2480
|
//#endregion
|
|
2481
|
+
//#region src/notification-center/artifact-read-plane.ts
|
|
2482
|
+
/**
|
|
2483
|
+
* Parse a single-range `Range` header against a known length.
|
|
2484
|
+
*
|
|
2485
|
+
* Returns `null` when there is nothing to honour (absent, malformed, multi-range
|
|
2486
|
+
* — all of which mean "send the whole thing"), and `'unsatisfiable'` when the
|
|
2487
|
+
* client asked for a range that cannot exist, which is a 416 and not a 200.
|
|
2488
|
+
*/
|
|
2489
|
+
function parseByteRange(header, length) {
|
|
2490
|
+
if (header === void 0) return null;
|
|
2491
|
+
const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
|
|
2492
|
+
if (match === null) return null;
|
|
2493
|
+
const rawStart = match[1] ?? "";
|
|
2494
|
+
const rawEnd = match[2] ?? "";
|
|
2495
|
+
if (rawStart === "" && rawEnd === "") return null;
|
|
2496
|
+
if (length === 0) return "unsatisfiable";
|
|
2497
|
+
if (rawStart === "") {
|
|
2498
|
+
const suffix = Number(rawEnd);
|
|
2499
|
+
if (suffix === 0) return "unsatisfiable";
|
|
2500
|
+
return {
|
|
2501
|
+
start: Math.max(0, length - suffix),
|
|
2502
|
+
end: length - 1
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
const start = Number(rawStart);
|
|
2506
|
+
if (start >= length) return "unsatisfiable";
|
|
2507
|
+
const end = rawEnd === "" ? length - 1 : Math.min(Number(rawEnd), length - 1);
|
|
2508
|
+
if (end < start) return "unsatisfiable";
|
|
2509
|
+
return {
|
|
2510
|
+
start,
|
|
2511
|
+
end
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
/** The last path segment, percent-decoded. */
|
|
2515
|
+
function artifactIdFrom(req) {
|
|
2516
|
+
const url = new URL(req.url ?? "/", "http://placeholder");
|
|
2517
|
+
try {
|
|
2518
|
+
return decodeURIComponent(url.pathname.split("/").filter(Boolean).pop() ?? "");
|
|
2519
|
+
} catch {
|
|
2520
|
+
return "";
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
var NcArtifactReadPlane = class {
|
|
2524
|
+
deps;
|
|
2525
|
+
constructor(deps) {
|
|
2526
|
+
this.deps = deps;
|
|
2527
|
+
}
|
|
2528
|
+
handler = async (req, res) => {
|
|
2529
|
+
const artifactId = artifactIdFrom(req);
|
|
2530
|
+
const record = await this.deps.index.resolve(artifactId);
|
|
2531
|
+
if (record === null) {
|
|
2532
|
+
this.refuse(res, 404, "not found");
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2535
|
+
const found = await this.deps.store.read(artifactId);
|
|
2536
|
+
if (found === null) {
|
|
2537
|
+
this.deps.logger.warn("artifact bytes gone — index row outlived the shelf", {
|
|
2538
|
+
...record.deviceId !== null ? { tags: { deviceId: record.deviceId } } : {},
|
|
2539
|
+
meta: {
|
|
2540
|
+
artifactId,
|
|
2541
|
+
kind: record.kind,
|
|
2542
|
+
expiresAt: record.expiresAt
|
|
2543
|
+
}
|
|
2544
|
+
});
|
|
2545
|
+
this.refuse(res, 404, "not found");
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
if (req.method === "HEAD") {
|
|
2549
|
+
res.writeHead(200, this.baseHeaders(record, found.bytes.byteLength, found.mime));
|
|
2550
|
+
res.end();
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
const range = parseByteRange(headerOf(req, "range"), found.bytes.byteLength);
|
|
2554
|
+
if (range === "unsatisfiable") {
|
|
2555
|
+
res.writeHead(416, {
|
|
2556
|
+
"content-range": `bytes */${found.bytes.byteLength}`,
|
|
2557
|
+
"content-type": "text/plain"
|
|
2558
|
+
});
|
|
2559
|
+
res.end("range not satisfiable");
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
if (range === null) {
|
|
2563
|
+
res.writeHead(200, this.baseHeaders(record, found.bytes.byteLength, found.mime));
|
|
2564
|
+
res.end(found.bytes);
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
const slice = found.bytes.subarray(range.start, range.end + 1);
|
|
2568
|
+
res.writeHead(206, {
|
|
2569
|
+
...this.baseHeaders(record, slice.byteLength, found.mime),
|
|
2570
|
+
"content-range": `bytes ${range.start}-${range.end}/${found.bytes.byteLength}`
|
|
2571
|
+
});
|
|
2572
|
+
res.end(slice);
|
|
2573
|
+
};
|
|
2574
|
+
baseHeaders(record, length, mime) {
|
|
2575
|
+
return {
|
|
2576
|
+
"content-type": mime,
|
|
2577
|
+
"content-length": String(length),
|
|
2578
|
+
"accept-ranges": "bytes",
|
|
2579
|
+
"cache-control": "private, max-age=3600",
|
|
2580
|
+
"content-disposition": `inline; filename="${filenameFor(record)}"`
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
refuse(res, status, body) {
|
|
2584
|
+
res.writeHead(status, { "content-type": "text/plain" });
|
|
2585
|
+
res.end(body);
|
|
2586
|
+
}
|
|
2587
|
+
};
|
|
2588
|
+
/** A name a client will accept, from what the artefact IS. */
|
|
2589
|
+
function filenameFor(record) {
|
|
2590
|
+
const ext = record.mime === "video/mp4" ? "mp4" : record.mime === "image/gif" ? "gif" : record.mime === "image/png" ? "png" : "jpg";
|
|
2591
|
+
return `${record.kind}-${record.capturedAt}.${ext}`;
|
|
2592
|
+
}
|
|
2593
|
+
function headerOf(req, name) {
|
|
2594
|
+
const value = req.headers[name];
|
|
2595
|
+
if (Array.isArray(value)) return value[0];
|
|
2596
|
+
return value;
|
|
2597
|
+
}
|
|
2598
|
+
//#endregion
|
|
2008
2599
|
//#region src/notification-center/artifact-store.ts
|
|
2009
2600
|
/**
|
|
2010
2601
|
* NcArtifactStore — the bytes behind a signed notification-artifact URL.
|
|
@@ -2019,9 +2610,25 @@ var NcArtifactPlane = class {
|
|
|
2019
2610
|
* bites first, the oldest artifacts go. A sweep on boot clears whatever a crash
|
|
2020
2611
|
* left behind — the directory is disposable by construction, so a lost file is
|
|
2021
2612
|
* a 404 on one link, never a corrupt state.
|
|
2613
|
+
*
|
|
2614
|
+
* ## The retained shelf (`kept/`)
|
|
2615
|
+
*
|
|
2616
|
+
* Task #36 gave the viewer a browsable artifact index, and an index whose bytes
|
|
2617
|
+
* evaporate in six hours is an index of 404s. So a `put` may ask to be RETAINED:
|
|
2618
|
+
* the bytes then land on a second shelf inside the same directory with its OWN
|
|
2619
|
+
* three bounds (age, count, and — because a timelapse mp4 is megabytes — total
|
|
2620
|
+
* BYTES). It is still a scratch, just a longer one: the index row carries an
|
|
2621
|
+
* `expiresAt` derived from the same age bound, so "gone" is a state the viewer
|
|
2622
|
+
* is told about rather than a surprise 404.
|
|
2623
|
+
*
|
|
2624
|
+
* The shelf is a subdirectory ON PURPOSE. The scratch's `list()` skips
|
|
2625
|
+
* non-files, so the two shelves cannot sweep each other, and neither bound had
|
|
2626
|
+
* to learn about the other's entries.
|
|
2022
2627
|
*/
|
|
2023
2628
|
var DEFAULT_MAX_AGE_MS = 6 * 36e5;
|
|
2024
2629
|
var DEFAULT_MAX_ENTRIES = 500;
|
|
2630
|
+
/** The retained shelf's subdirectory name, inside the scratch directory. */
|
|
2631
|
+
var KEPT_SUBDIR = "kept";
|
|
2025
2632
|
/** `image/jpeg` → `jpg`, so a fetched link has a name a client will accept. */
|
|
2026
2633
|
function extensionFor(mime) {
|
|
2027
2634
|
if (mime === "image/jpeg") return "jpg";
|
|
@@ -2035,6 +2642,10 @@ var NcArtifactStore = class {
|
|
|
2035
2642
|
now;
|
|
2036
2643
|
maxAgeMs;
|
|
2037
2644
|
maxEntries;
|
|
2645
|
+
keptMaxAgeMs;
|
|
2646
|
+
keptMaxEntries;
|
|
2647
|
+
keptMaxBytes;
|
|
2648
|
+
keptDir;
|
|
2038
2649
|
/** id → mime, so the route can answer with the right content-type without
|
|
2039
2650
|
* re-deriving it from the extension on every request. */
|
|
2040
2651
|
mimeById = /* @__PURE__ */ new Map();
|
|
@@ -2043,17 +2654,32 @@ var NcArtifactStore = class {
|
|
|
2043
2654
|
this.now = options.now ?? (() => Date.now());
|
|
2044
2655
|
this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
2045
2656
|
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
2657
|
+
this.keptMaxAgeMs = options.keptMaxAgeMs ?? 12096e5;
|
|
2658
|
+
this.keptMaxEntries = options.keptMaxEntries ?? 2e3;
|
|
2659
|
+
this.keptMaxBytes = options.keptMaxBytes ?? 2147483648;
|
|
2660
|
+
this.keptDir = path.join(options.dir, KEPT_SUBDIR);
|
|
2661
|
+
}
|
|
2662
|
+
/** How long a retained artifact stays fetchable — the index's `expiresAt`
|
|
2663
|
+
* is derived from this, so the two can never disagree. */
|
|
2664
|
+
get retentionMs() {
|
|
2665
|
+
return this.keptMaxAgeMs;
|
|
2046
2666
|
}
|
|
2047
2667
|
/** Create the directory and sweep whatever a previous run left behind. */
|
|
2048
2668
|
async start() {
|
|
2049
2669
|
await promises.mkdir(this.options.dir, { recursive: true });
|
|
2050
2670
|
await this.sweep();
|
|
2051
2671
|
}
|
|
2052
|
-
/**
|
|
2053
|
-
|
|
2672
|
+
/**
|
|
2673
|
+
* Persist bytes and return the artifact handle (never throws on sweep).
|
|
2674
|
+
*
|
|
2675
|
+
* The retained shelf is created LAZILY: an install that never produces a
|
|
2676
|
+
* browsable artifact keeps a directory that is empty when it is empty.
|
|
2677
|
+
*/
|
|
2678
|
+
async put(bytes, mime, options = {}) {
|
|
2054
2679
|
const id = `${this.now()}-${randomUUID()}`;
|
|
2055
|
-
const
|
|
2056
|
-
|
|
2680
|
+
const dir = options.retain === true ? this.keptDir : this.options.dir;
|
|
2681
|
+
const file = path.join(dir, `${id}.${extensionFor(mime)}`);
|
|
2682
|
+
await promises.mkdir(dir, { recursive: true });
|
|
2057
2683
|
await promises.writeFile(file, bytes);
|
|
2058
2684
|
this.mimeById.set(id, mime);
|
|
2059
2685
|
await this.sweep();
|
|
@@ -2066,7 +2692,7 @@ var NcArtifactStore = class {
|
|
|
2066
2692
|
/** Read an artifact's bytes + mime, or null when it is gone (expired/swept). */
|
|
2067
2693
|
async read(id) {
|
|
2068
2694
|
if (!/^[0-9]+-[0-9a-f-]{36}$/i.test(id)) return null;
|
|
2069
|
-
const found = (await this.list()
|
|
2695
|
+
const found = [...await this.list(this.options.dir), ...await this.list(this.keptDir)].find((e) => e.id === id);
|
|
2070
2696
|
if (!found) return null;
|
|
2071
2697
|
try {
|
|
2072
2698
|
return {
|
|
@@ -2077,6 +2703,10 @@ var NcArtifactStore = class {
|
|
|
2077
2703
|
return null;
|
|
2078
2704
|
}
|
|
2079
2705
|
}
|
|
2706
|
+
/** The ids still on the retained shelf — what the index reconciles against. */
|
|
2707
|
+
async keptIds() {
|
|
2708
|
+
return (await this.list(this.keptDir)).map((e) => e.id);
|
|
2709
|
+
}
|
|
2080
2710
|
/** Delete every artifact (operator cleanup / shutdown). */
|
|
2081
2711
|
async clear() {
|
|
2082
2712
|
await promises.rm(this.options.dir, {
|
|
@@ -2085,13 +2715,39 @@ var NcArtifactStore = class {
|
|
|
2085
2715
|
}).catch(() => {});
|
|
2086
2716
|
this.mimeById.clear();
|
|
2087
2717
|
}
|
|
2088
|
-
/**
|
|
2718
|
+
/** Prune both shelves under their own bounds. Best-effort: a failed unlink is
|
|
2719
|
+
* logged once and the rest of the sweep still runs. */
|
|
2089
2720
|
async sweep() {
|
|
2090
|
-
|
|
2091
|
-
|
|
2721
|
+
await this.sweepShelf(await this.list(this.options.dir), {
|
|
2722
|
+
maxAgeMs: this.maxAgeMs,
|
|
2723
|
+
maxEntries: this.maxEntries
|
|
2724
|
+
});
|
|
2725
|
+
await this.sweepShelf(await this.list(this.keptDir), {
|
|
2726
|
+
maxAgeMs: this.keptMaxAgeMs,
|
|
2727
|
+
maxEntries: this.keptMaxEntries,
|
|
2728
|
+
maxBytes: this.keptMaxBytes
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2731
|
+
/**
|
|
2732
|
+
* Age first, then count, then total bytes — newest always survives.
|
|
2733
|
+
*
|
|
2734
|
+
* The newest entry is never swept by the byte bound even when it alone
|
|
2735
|
+
* exceeds it: a shelf that deletes what it has just written can serve
|
|
2736
|
+
* nothing, and the caller has already paid for the render.
|
|
2737
|
+
*/
|
|
2738
|
+
async sweepShelf(entries, bounds) {
|
|
2739
|
+
const cutoff = this.now() - bounds.maxAgeMs;
|
|
2092
2740
|
const doomed = entries.filter((e) => e.mtimeMs < cutoff);
|
|
2093
|
-
const
|
|
2094
|
-
doomed.push(...
|
|
2741
|
+
const fresh = entries.filter((e) => e.mtimeMs >= cutoff).toSorted((a, b) => b.mtimeMs - a.mtimeMs);
|
|
2742
|
+
doomed.push(...fresh.slice(bounds.maxEntries));
|
|
2743
|
+
const maxBytes = bounds.maxBytes;
|
|
2744
|
+
if (maxBytes !== void 0) {
|
|
2745
|
+
let running = 0;
|
|
2746
|
+
for (const [i, e] of fresh.slice(0, bounds.maxEntries).entries()) {
|
|
2747
|
+
running += e.sizeBytes;
|
|
2748
|
+
if (running > maxBytes && i > 0) doomed.push(e);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2095
2751
|
for (const e of doomed) try {
|
|
2096
2752
|
await promises.rm(e.file, { force: true });
|
|
2097
2753
|
this.mimeById.delete(e.id);
|
|
@@ -2102,23 +2758,24 @@ var NcArtifactStore = class {
|
|
|
2102
2758
|
} });
|
|
2103
2759
|
}
|
|
2104
2760
|
}
|
|
2105
|
-
async list() {
|
|
2761
|
+
async list(dir) {
|
|
2106
2762
|
let names;
|
|
2107
2763
|
try {
|
|
2108
|
-
names = await promises.readdir(
|
|
2764
|
+
names = await promises.readdir(dir);
|
|
2109
2765
|
} catch {
|
|
2110
2766
|
return [];
|
|
2111
2767
|
}
|
|
2112
2768
|
const out = [];
|
|
2113
2769
|
for (const name of names) {
|
|
2114
|
-
const file = path.join(
|
|
2770
|
+
const file = path.join(dir, name);
|
|
2115
2771
|
try {
|
|
2116
2772
|
const st = await promises.stat(file);
|
|
2117
2773
|
if (!st.isFile()) continue;
|
|
2118
2774
|
out.push({
|
|
2119
2775
|
id: name.slice(0, name.lastIndexOf(".")),
|
|
2120
2776
|
file,
|
|
2121
|
-
mtimeMs: st.mtimeMs
|
|
2777
|
+
mtimeMs: st.mtimeMs,
|
|
2778
|
+
sizeBytes: st.size
|
|
2122
2779
|
});
|
|
2123
2780
|
} catch {}
|
|
2124
2781
|
}
|
|
@@ -4336,69 +4993,38 @@ var en_default = {
|
|
|
4336
4993
|
"snooze.scope.anyDevice": "A camera",
|
|
4337
4994
|
"snooze.scope.anyRule": "A rule",
|
|
4338
4995
|
"snooze.scope.rule": "“{{rule}}”",
|
|
4339
|
-
"alarm.arm.body.allDevices":
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
},
|
|
4343
|
-
"alarm.arm.body.
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
"alarm.
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
},
|
|
4351
|
-
"alarm.
|
|
4352
|
-
"one": "{{devices}} device armed: {{names}}. Excluded: {{skippedNames}}.",
|
|
4353
|
-
"other": "{{devices}} devices armed: {{names}}. Excluded: {{skippedNames}}."
|
|
4354
|
-
},
|
|
4355
|
-
"alarm.arm.body.uncovered": "Nothing is armed — no enabled rule is gated on this mode.",
|
|
4356
|
-
"alarm.arm.body.allSkipped": {
|
|
4357
|
-
"one": "Nothing is armed: the {{devices}} device this mode covers is excluded ({{names}}).",
|
|
4358
|
-
"other": "Nothing is armed: all {{devices}} devices this mode covers are excluded ({{names}})."
|
|
4359
|
-
},
|
|
4360
|
-
"alarm.arming.body": {
|
|
4361
|
-
"one": "Armed in {{seconds}} second.",
|
|
4362
|
-
"other": "Armed in {{seconds}} seconds."
|
|
4363
|
-
},
|
|
4364
|
-
"alarm.arming.body.uncovered": {
|
|
4365
|
-
"one": "Armed in {{seconds}} second — but no enabled rule is gated on this mode, so it will protect nothing.",
|
|
4366
|
-
"other": "Armed in {{seconds}} seconds — but no enabled rule is gated on this mode, so it will protect nothing."
|
|
4367
|
-
},
|
|
4368
|
-
"alarm.arming.body.allSkipped": {
|
|
4369
|
-
"one": "Armed in {{seconds}} second — but every device this mode covers is excluded ({{names}}), so it will protect nothing.",
|
|
4370
|
-
"other": "Armed in {{seconds}} seconds — but every device this mode covers is excluded ({{names}}), so it will protect nothing."
|
|
4371
|
-
},
|
|
4372
|
-
"alarm.arm.body.bypassed": {
|
|
4373
|
-
"one": "{{devices}} device armed — open and excluded: {{bypassedNames}}.",
|
|
4374
|
-
"other": "{{devices}} devices armed — open and excluded: {{bypassedNames}}."
|
|
4375
|
-
},
|
|
4376
|
-
"alarm.arming.body.bypassed": {
|
|
4377
|
-
"one": "Armed in {{seconds}} second, excluding what is open: {{bypassedNames}}.",
|
|
4378
|
-
"other": "Armed in {{seconds}} seconds, excluding what is open: {{bypassedNames}}."
|
|
4379
|
-
},
|
|
4380
|
-
"alarm.armRefused.body": {
|
|
4381
|
-
"one": "{{names}} is open. Press arm again within {{seconds}}s to arm without it.",
|
|
4382
|
-
"other": "These are open: {{names}}. Press arm again within {{seconds}}s to arm without them."
|
|
4383
|
-
},
|
|
4996
|
+
"alarm.arm.body.allDevices": "Active: all devices",
|
|
4997
|
+
"alarm.arm.body.noDevices": "Active: no device restriction",
|
|
4998
|
+
"alarm.arm.body.devices": "Active: {{names}}",
|
|
4999
|
+
"alarm.arm.body.devicesSkipped": "Active: {{names}}\nExcluded: {{skippedNames}}",
|
|
5000
|
+
"alarm.arm.body.uncovered": "Active: none — no rule uses this mode",
|
|
5001
|
+
"alarm.arm.body.allSkipped": "Active: none\nExcluded: {{names}}",
|
|
5002
|
+
"alarm.arming.body": "No exclusions",
|
|
5003
|
+
"alarm.arming.body.uncovered": "Active: none — no rule uses this mode",
|
|
5004
|
+
"alarm.arming.body.allSkipped": "Active: none\nExcluded: {{names}}",
|
|
5005
|
+
"alarm.arming.body.skipped": "Excluded: {{names}}",
|
|
5006
|
+
"alarm.arm.body.bypassed": "Active: {{devices}}\nBypassed: {{bypassedNames}}",
|
|
5007
|
+
"alarm.arming.body.bypassed": "Bypassed: {{bypassedNames}}",
|
|
5008
|
+
"alarm.armRefused.body": "Open: {{names}}\nRepeat within {{seconds}}s to arm with bypass.",
|
|
4384
5009
|
"alarm.device.unnamed": "Device {{id}}",
|
|
4385
|
-
"
|
|
4386
|
-
"system.alarm-arm-refused.title.
|
|
4387
|
-
"system.alarm-arm-refused.title.
|
|
4388
|
-
"system.alarm-arm-refused.
|
|
4389
|
-
"system.alarm-
|
|
4390
|
-
"system.alarm-arming.title.
|
|
4391
|
-
"system.alarm-arming.title.
|
|
4392
|
-
"system.alarm-arming.
|
|
4393
|
-
"system.alarm-
|
|
4394
|
-
"system.alarm-armed.title.
|
|
4395
|
-
"system.alarm-armed.title.
|
|
4396
|
-
"system.alarm-armed.
|
|
4397
|
-
"system.alarm-
|
|
4398
|
-
"system.alarm-disarmed.
|
|
5010
|
+
"alarm.combined.title": "Alarm: {{deviceName}}",
|
|
5011
|
+
"system.alarm-arm-refused.title.home": "Arming home refused",
|
|
5012
|
+
"system.alarm-arm-refused.title.away": "Arming away refused",
|
|
5013
|
+
"system.alarm-arm-refused.title.night": "Arming night refused",
|
|
5014
|
+
"system.alarm-arm-refused.body": "Arming refused",
|
|
5015
|
+
"system.alarm-arming.title.home": "Arming home — {{seconds}}s",
|
|
5016
|
+
"system.alarm-arming.title.away": "Arming away — {{seconds}}s",
|
|
5017
|
+
"system.alarm-arming.title.night": "Arming night — {{seconds}}s",
|
|
5018
|
+
"system.alarm-arming.body": "Arming",
|
|
5019
|
+
"system.alarm-armed.title.home": "Armed: home",
|
|
5020
|
+
"system.alarm-armed.title.away": "Armed: away",
|
|
5021
|
+
"system.alarm-armed.title.night": "Armed: night",
|
|
5022
|
+
"system.alarm-armed.body": "Alarm armed",
|
|
5023
|
+
"system.alarm-disarmed.title": "Disarmed",
|
|
5024
|
+
"system.alarm-disarmed.body": "Alarm disarmed",
|
|
4399
5025
|
"system.alarm-triggered.title": "ALARM",
|
|
4400
|
-
"system.alarm-triggered.body": "
|
|
4401
|
-
"system.alarm-triggered.body.rule": "
|
|
5026
|
+
"system.alarm-triggered.body": "Alarm triggered",
|
|
5027
|
+
"system.alarm-triggered.body.rule": "Triggered by {{rule}} on {{deviceName}}",
|
|
4402
5028
|
"button.snooze.minutes": "Snooze {{minutes}} min",
|
|
4403
5029
|
"button.snooze.hours": {
|
|
4404
5030
|
"one": "Snooze {{hours}} hour",
|
|
@@ -4530,69 +5156,38 @@ var it_default = {
|
|
|
4530
5156
|
"snooze.scope.anyDevice": "Una camera",
|
|
4531
5157
|
"snooze.scope.anyRule": "Una regola",
|
|
4532
5158
|
"snooze.scope.rule": "“{{rule}}”",
|
|
4533
|
-
"alarm.arm.body.allDevices":
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
},
|
|
4537
|
-
"alarm.arm.body.
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
"alarm.
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
},
|
|
4545
|
-
"alarm.
|
|
4546
|
-
"one": "{{devices}} dispositivo attivo: {{names}}. Esclusi: {{skippedNames}}.",
|
|
4547
|
-
"other": "{{devices}} dispositivi attivi: {{names}}. Esclusi: {{skippedNames}}."
|
|
4548
|
-
},
|
|
4549
|
-
"alarm.arm.body.uncovered": "Non è attivo nulla — nessuna regola abilitata è legata a questa modalità.",
|
|
4550
|
-
"alarm.arm.body.allSkipped": {
|
|
4551
|
-
"one": "Non è attivo nulla: l'unico dispositivo ({{devices}}) coperto da questa modalità è escluso ({{names}}).",
|
|
4552
|
-
"other": "Non è attivo nulla: tutti i {{devices}} dispositivi coperti da questa modalità sono esclusi ({{names}})."
|
|
4553
|
-
},
|
|
4554
|
-
"alarm.arming.body": {
|
|
4555
|
-
"one": "Attivo tra {{seconds}} secondo.",
|
|
4556
|
-
"other": "Attivo tra {{seconds}} secondi."
|
|
4557
|
-
},
|
|
4558
|
-
"alarm.arming.body.uncovered": {
|
|
4559
|
-
"one": "Attivo tra {{seconds}} secondo — ma nessuna regola abilitata è legata a questa modalità: non proteggerà nulla.",
|
|
4560
|
-
"other": "Attivo tra {{seconds}} secondi — ma nessuna regola abilitata è legata a questa modalità: non proteggerà nulla."
|
|
4561
|
-
},
|
|
4562
|
-
"alarm.arming.body.allSkipped": {
|
|
4563
|
-
"one": "Attivo tra {{seconds}} secondo — ma ogni dispositivo coperto da questa modalità è escluso ({{names}}): non proteggerà nulla.",
|
|
4564
|
-
"other": "Attivo tra {{seconds}} secondi — ma ogni dispositivo coperto da questa modalità è escluso ({{names}}): non proteggerà nulla."
|
|
4565
|
-
},
|
|
4566
|
-
"alarm.arm.body.bypassed": {
|
|
4567
|
-
"one": "{{devices}} dispositivo attivo — aperto ed escluso: {{bypassedNames}}.",
|
|
4568
|
-
"other": "{{devices}} dispositivi attivi — aperti ed esclusi: {{bypassedNames}}."
|
|
4569
|
-
},
|
|
4570
|
-
"alarm.arming.body.bypassed": {
|
|
4571
|
-
"one": "Attivo tra {{seconds}} secondo, escludendo ciò che è aperto: {{bypassedNames}}.",
|
|
4572
|
-
"other": "Attivo tra {{seconds}} secondi, escludendo ciò che è aperto: {{bypassedNames}}."
|
|
4573
|
-
},
|
|
4574
|
-
"alarm.armRefused.body": {
|
|
4575
|
-
"one": "{{names}} è aperto. Premi di nuovo entro {{seconds}}s per inserire senza di esso.",
|
|
4576
|
-
"other": "Sono aperti: {{names}}. Premi di nuovo entro {{seconds}}s per inserire senza di essi."
|
|
4577
|
-
},
|
|
5159
|
+
"alarm.arm.body.allDevices": "Attivi: tutti i dispositivi",
|
|
5160
|
+
"alarm.arm.body.noDevices": "Attivi: nessuna restrizione",
|
|
5161
|
+
"alarm.arm.body.devices": "Attivi: {{names}}",
|
|
5162
|
+
"alarm.arm.body.devicesSkipped": "Attivi: {{names}}\nEsclusi: {{skippedNames}}",
|
|
5163
|
+
"alarm.arm.body.uncovered": "Attivi: nessuno — nessuna regola usa questa modalità",
|
|
5164
|
+
"alarm.arm.body.allSkipped": "Attivi: nessuno\nEsclusi: {{names}}",
|
|
5165
|
+
"alarm.arming.body": "Nessuna esclusione",
|
|
5166
|
+
"alarm.arming.body.uncovered": "Attivi: nessuno — nessuna regola usa questa modalità",
|
|
5167
|
+
"alarm.arming.body.allSkipped": "Attivi: nessuno\nEsclusi: {{names}}",
|
|
5168
|
+
"alarm.arming.body.skipped": "Esclusi: {{names}}",
|
|
5169
|
+
"alarm.arm.body.bypassed": "Attivi: {{devices}}\nBypassati: {{bypassedNames}}",
|
|
5170
|
+
"alarm.arming.body.bypassed": "Bypassati: {{bypassedNames}}",
|
|
5171
|
+
"alarm.armRefused.body": "Aperti: {{names}}\nRipeti entro {{seconds}}s per inserire con bypass.",
|
|
4578
5172
|
"alarm.device.unnamed": "Dispositivo {{id}}",
|
|
5173
|
+
"alarm.combined.title": "Allarme: {{deviceName}}",
|
|
4579
5174
|
"system.alarm-arm-refused.title.home": "Inserimento Casa rifiutato",
|
|
4580
5175
|
"system.alarm-arm-refused.title.away": "Inserimento Fuori rifiutato",
|
|
4581
5176
|
"system.alarm-arm-refused.title.night": "Inserimento Notte rifiutato",
|
|
4582
|
-
"system.alarm-arm-refused.body": "
|
|
4583
|
-
"system.alarm-arming.title.home": "Inserimento Casa",
|
|
4584
|
-
"system.alarm-arming.title.away": "Inserimento Fuori",
|
|
4585
|
-
"system.alarm-arming.title.night": "Inserimento Notte",
|
|
4586
|
-
"system.alarm-arming.body": "
|
|
4587
|
-
"system.alarm-armed.title.home": "Casa
|
|
4588
|
-
"system.alarm-armed.title.away": "Fuori
|
|
4589
|
-
"system.alarm-armed.title.night": "Notte
|
|
4590
|
-
"system.alarm-armed.body": "
|
|
4591
|
-
"system.alarm-disarmed.title": "
|
|
4592
|
-
"system.alarm-disarmed.body": "
|
|
5177
|
+
"system.alarm-arm-refused.body": "Inserimento rifiutato",
|
|
5178
|
+
"system.alarm-arming.title.home": "Inserimento Casa — {{seconds}}s",
|
|
5179
|
+
"system.alarm-arming.title.away": "Inserimento Fuori — {{seconds}}s",
|
|
5180
|
+
"system.alarm-arming.title.night": "Inserimento Notte — {{seconds}}s",
|
|
5181
|
+
"system.alarm-arming.body": "Inserimento in corso",
|
|
5182
|
+
"system.alarm-armed.title.home": "Inserito: Casa",
|
|
5183
|
+
"system.alarm-armed.title.away": "Inserito: Fuori",
|
|
5184
|
+
"system.alarm-armed.title.night": "Inserito: Notte",
|
|
5185
|
+
"system.alarm-armed.body": "Allarme inserito",
|
|
5186
|
+
"system.alarm-disarmed.title": "Disinserito",
|
|
5187
|
+
"system.alarm-disarmed.body": "Allarme disinserito",
|
|
4593
5188
|
"system.alarm-triggered.title": "ALLARME",
|
|
4594
|
-
"system.alarm-triggered.body": "
|
|
4595
|
-
"system.alarm-triggered.body.rule": "
|
|
5189
|
+
"system.alarm-triggered.body": "Allarme scattato",
|
|
5190
|
+
"system.alarm-triggered.body.rule": "Scattato da {{rule}} su {{deviceName}}",
|
|
4596
5191
|
"button.snooze.minutes": "Silenzia {{minutes}} min",
|
|
4597
5192
|
"button.snooze.hours": {
|
|
4598
5193
|
"one": "Silenzia {{hours}} ora",
|
|
@@ -5311,7 +5906,8 @@ var NcDispatcher = class {
|
|
|
5311
5906
|
const systemEvent = subject.systemEvent;
|
|
5312
5907
|
const displayDeviceId = systemEvent?.deviceId ?? subject.deviceId;
|
|
5313
5908
|
const deviceName = systemEvent !== void 0 && systemEvent.deviceId === void 0 ? "system" : await this.deps.getDeviceName(displayDeviceId).catch(() => null) ?? `camera ${displayDeviceId}`;
|
|
5314
|
-
const
|
|
5909
|
+
const alarmSource = systemEvent?.alarmSource;
|
|
5910
|
+
const zoneLabels = systemEvent === void 0 ? await resolveZoneLabels(this.deps.getZoneNames, subject.deviceId, subject.zones) : alarmSource?.zones !== void 0 && alarmSource.zones.length > 0 ? await resolveZoneLabels(this.deps.getZoneNames, alarmSource.deviceId, alarmSource.zones) : [];
|
|
5315
5911
|
const vars = buildTemplateVars(this.texts, entry, deviceName, zoneLabels);
|
|
5316
5912
|
const systemCount = systemEvent?.textCount !== void 0 ? { count: systemEvent.textCount } : {};
|
|
5317
5913
|
const title = this.renderText({
|
|
@@ -5654,6 +6250,10 @@ async function resolveZoneLabels(getZoneNames, deviceId, zoneIds) {
|
|
|
5654
6250
|
return [...zoneIds];
|
|
5655
6251
|
}
|
|
5656
6252
|
}
|
|
6253
|
+
/** `{{confidence}}` as a whole percent, or empty when nothing measured one. */
|
|
6254
|
+
function percentOf(confidence) {
|
|
6255
|
+
return confidence !== void 0 ? `${Math.round(confidence * 100)}%` : "";
|
|
6256
|
+
}
|
|
5657
6257
|
/**
|
|
5658
6258
|
* The variable map ONE render pass sees: scalars, then the composites built
|
|
5659
6259
|
* from them, then the params a system-event row froze at intake.
|
|
@@ -5669,19 +6269,23 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels) {
|
|
|
5669
6269
|
const subject = entry.payload.subject;
|
|
5670
6270
|
const occupancy = subject.occupancy;
|
|
5671
6271
|
const systemEvent = subject.systemEvent;
|
|
6272
|
+
const detection = systemEvent?.alarmSource?.className !== void 0 ? systemEvent.alarmSource : void 0;
|
|
5672
6273
|
return {
|
|
5673
6274
|
camera: deviceName,
|
|
5674
|
-
class: subject.className,
|
|
5675
|
-
label: subject.label ?? "",
|
|
6275
|
+
class: detection?.className ?? subject.className,
|
|
6276
|
+
label: detection?.label ?? subject.label ?? "",
|
|
5676
6277
|
zones: zoneLabels.join(", "),
|
|
5677
|
-
subject:
|
|
6278
|
+
subject: detection?.className !== void 0 ? ncSubjectText(texts, {
|
|
6279
|
+
className: detection.className,
|
|
6280
|
+
...detection.label !== void 0 ? { label: detection.label } : {}
|
|
6281
|
+
}) : systemEvent?.subject ?? ncSubjectText(texts, {
|
|
5678
6282
|
className: subject.className,
|
|
5679
6283
|
...subject.label !== void 0 ? { label: subject.label } : {}
|
|
5680
6284
|
}),
|
|
5681
6285
|
inZones: ncInZonesText(texts, zoneLabels),
|
|
5682
6286
|
scope: occupancy?.zone ?? deviceName,
|
|
5683
6287
|
zone: occupancy?.zone ?? zoneLabels[0] ?? "",
|
|
5684
|
-
confidence:
|
|
6288
|
+
confidence: percentOf(detection?.confidence ?? subject.confidence),
|
|
5685
6289
|
time: new Date(subject.timestamp).toLocaleTimeString(),
|
|
5686
6290
|
rule: entry.payload.ruleName,
|
|
5687
6291
|
count: occupancy !== void 0 ? `${occupancy.count}` : "",
|
|
@@ -6221,6 +6825,35 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
6221
6825
|
}
|
|
6222
6826
|
}
|
|
6223
6827
|
/**
|
|
6828
|
+
* The COMBINED row's two keys, or null when this transition is not one.
|
|
6829
|
+
*
|
|
6830
|
+
* A rule that trips the alarm produces ONE notification, and the operator has
|
|
6831
|
+
* to be able to read it: the title is the severity marker over the camera
|
|
6832
|
+
* (`alarm.combined.title`, shipped as `Alarm: {{deviceName}}` and re-wordable
|
|
6833
|
+
* per rule or in the Texts tab), and the body is `detection.body` — the exact
|
|
6834
|
+
* sentence that rule's ordinary notification would have carried, with its own
|
|
6835
|
+
* class, identity and zones.
|
|
6836
|
+
*
|
|
6837
|
+
* The alarm used to write its own prose here ("The alarm was triggered by
|
|
6838
|
+
* Person at gate on Ingresso"), which made the one notification that matters
|
|
6839
|
+
* the only one in the system that did not look like the rule's others: no
|
|
6840
|
+
* identity, no zone, a different subject. The prefix carries the severity; the
|
|
6841
|
+
* familiar line carries the meaning.
|
|
6842
|
+
*
|
|
6843
|
+
* **Gated on the detection vocabulary, not on the source.** `detection.body`
|
|
6844
|
+
* renders `{{subject}}`, and a fire ref with no class composes it from the
|
|
6845
|
+
* system event's dedup slot — "alarm:42 su Ingresso". A source without a class
|
|
6846
|
+
* therefore keeps the alarm's own sentence, which is also what every row
|
|
6847
|
+
* enqueued before this deploy carries.
|
|
6848
|
+
*/
|
|
6849
|
+
function combinedAlarmText(source) {
|
|
6850
|
+
if (source?.className === void 0) return null;
|
|
6851
|
+
return {
|
|
6852
|
+
titleKey: "alarm.combined.title",
|
|
6853
|
+
bodyKey: "detection.body"
|
|
6854
|
+
};
|
|
6855
|
+
}
|
|
6856
|
+
/**
|
|
6224
6857
|
* The panel's own transition, as a system-event subject.
|
|
6225
6858
|
*
|
|
6226
6859
|
* IN-PROCESS, not off the bus. The alarm and the Notification Center live in
|
|
@@ -6243,6 +6876,7 @@ function incomingFromAlarmTransition(input) {
|
|
|
6243
6876
|
const deviceId = source?.deviceId ?? panelDeviceId;
|
|
6244
6877
|
const identity = input.lookup(deviceId);
|
|
6245
6878
|
const kind = transition.kind;
|
|
6879
|
+
const combined = combinedAlarmText(source);
|
|
6246
6880
|
return systemSubject({
|
|
6247
6881
|
facts: {
|
|
6248
6882
|
eventId: `alarm:${kind}:${String(transition.at)}`,
|
|
@@ -6258,12 +6892,13 @@ function incomingFromAlarmTransition(input) {
|
|
|
6258
6892
|
},
|
|
6259
6893
|
kind,
|
|
6260
6894
|
subject: `alarm:${String(panelDeviceId)}`,
|
|
6261
|
-
titleKey: transition.mode !== void 0 ? `system.${kind}.title.${transition.mode}` : `system.${kind}.title`,
|
|
6262
|
-
bodyKey: input.body?.bodyKey ?? (source !== void 0 ? `system.${kind}.body.rule` : `system.${kind}.body`),
|
|
6895
|
+
titleKey: combined !== null ? combined.titleKey : transition.mode !== void 0 ? `system.${kind}.title.${transition.mode}` : `system.${kind}.title`,
|
|
6896
|
+
bodyKey: combined?.bodyKey ?? input.body?.bodyKey ?? (source !== void 0 ? `system.${kind}.body.rule` : `system.${kind}.body`),
|
|
6263
6897
|
textParams: {
|
|
6264
6898
|
...deviceTextParams(deviceId, identity),
|
|
6265
6899
|
...transition.mode !== void 0 ? { mode: transition.mode } : {},
|
|
6266
6900
|
...source !== void 0 ? { rule: source.ruleName } : {},
|
|
6901
|
+
...kind === "alarm-arming" ? { seconds: `${transition.seconds ?? 0}` } : {},
|
|
6267
6902
|
...input.body?.textParams
|
|
6268
6903
|
},
|
|
6269
6904
|
deviceId,
|
|
@@ -10004,7 +10639,15 @@ var NcSummaryProducer = class {
|
|
|
10004
10639
|
}
|
|
10005
10640
|
const bytes = new Uint8Array(rendered.jpeg.byteLength);
|
|
10006
10641
|
bytes.set(rendered.jpeg);
|
|
10007
|
-
const url = await this.deps.publish(bytes, "image/jpeg"
|
|
10642
|
+
const { url } = await this.deps.publish(bytes, "image/jpeg", {
|
|
10643
|
+
kind: "summary-mosaic",
|
|
10644
|
+
deviceId: rule.deviceIds[0] ?? null,
|
|
10645
|
+
deviceIds: [...new Set(tiles.map((t) => t.candidate.deviceId))],
|
|
10646
|
+
capturedAt: window.endMs,
|
|
10647
|
+
ruleId: rule.id,
|
|
10648
|
+
ruleName: rule.name,
|
|
10649
|
+
...rule.ownerUserId !== void 0 ? { ownerUserId: rule.ownerUserId } : {}
|
|
10650
|
+
});
|
|
10008
10651
|
if (url === null) {
|
|
10009
10652
|
this.deps.logger.warn("summary mosaic not published: no reachable public endpoint", { meta: { ruleId: rule.id } });
|
|
10010
10653
|
return {
|
|
@@ -10998,7 +11641,7 @@ function unknownNcTextKeys(keys, bundles = NC_TEXT_BUNDLES) {
|
|
|
10998
11641
|
* durable document stays the sparse map its schema describes.
|
|
10999
11642
|
*/
|
|
11000
11643
|
function applyNcTextOverridePatch(current, language, patch) {
|
|
11001
|
-
const next = { ...current[language]
|
|
11644
|
+
const next = { ...current[language] };
|
|
11002
11645
|
for (const [key, value] of Object.entries(patch)) {
|
|
11003
11646
|
if (value === null || value.trim().length === 0) {
|
|
11004
11647
|
delete next[key];
|
|
@@ -11566,7 +12209,7 @@ var TimelapseRenderer = class {
|
|
|
11566
12209
|
}
|
|
11567
12210
|
});
|
|
11568
12211
|
const finished = await this.awaitReady(queued, tags);
|
|
11569
|
-
const published = await this.publish(finished.id,
|
|
12212
|
+
const published = await this.publish(finished.id, request, tags);
|
|
11570
12213
|
const downloadUrl = await this.ports.resolveDownloadUrl?.({ exportId: finished.id }).catch(() => null);
|
|
11571
12214
|
return {
|
|
11572
12215
|
exportId: finished.id,
|
|
@@ -11618,7 +12261,9 @@ var TimelapseRenderer = class {
|
|
|
11618
12261
|
* notification, not a failed window — and failing here would leave the pair
|
|
11619
12262
|
* unstamped and re-render the same eight hours on the next tick.
|
|
11620
12263
|
*/
|
|
11621
|
-
async publish(exportId,
|
|
12264
|
+
async publish(exportId, request, tags) {
|
|
12265
|
+
const { deviceId, previewText } = request;
|
|
12266
|
+
const previewMode = request.previewMode ?? "image";
|
|
11622
12267
|
const publish = this.ports.publishArtifact;
|
|
11623
12268
|
const none = {
|
|
11624
12269
|
video: null,
|
|
@@ -11638,7 +12283,11 @@ var TimelapseRenderer = class {
|
|
|
11638
12283
|
...previewText !== void 0 ? {
|
|
11639
12284
|
previewText,
|
|
11640
12285
|
previewMode
|
|
11641
|
-
} : {}
|
|
12286
|
+
} : {},
|
|
12287
|
+
capturedAt: request.window.endMs,
|
|
12288
|
+
ruleId: request.ruleId,
|
|
12289
|
+
ruleName: request.ruleName,
|
|
12290
|
+
...request.ownerUserId !== void 0 ? { ownerUserId: request.ownerUserId } : {}
|
|
11642
12291
|
});
|
|
11643
12292
|
if (set.video === null) this.ports.logger.warn("timelapse artifact could not be published — link-less delivery", {
|
|
11644
12293
|
tags,
|
|
@@ -11994,7 +12643,8 @@ var TimelapseScheduler = class {
|
|
|
11994
12643
|
...rule.previewText !== void 0 ? { text: rule.previewText } : {}
|
|
11995
12644
|
}),
|
|
11996
12645
|
previewMode: rule.previewMode,
|
|
11997
|
-
...rule.reportClasses !== void 0 ? { reportClasses: rule.reportClasses } : {}
|
|
12646
|
+
...rule.reportClasses !== void 0 ? { reportClasses: rule.reportClasses } : {},
|
|
12647
|
+
...rule.ownerUserId !== void 0 ? { ownerUserId: rule.ownerUserId } : {}
|
|
11998
12648
|
});
|
|
11999
12649
|
const rows = buildTimelapseOutboxInputs({
|
|
12000
12650
|
texts: this.texts,
|
|
@@ -12570,6 +13220,7 @@ function frozenOccupancy(rule, occ) {
|
|
|
12570
13220
|
*/
|
|
12571
13221
|
function fireRefFor(rule, subject, kind) {
|
|
12572
13222
|
const hasEventMedia = kind === "object-event" || kind === "package-event";
|
|
13223
|
+
const className = subject.classNames[0];
|
|
12573
13224
|
return {
|
|
12574
13225
|
ruleId: rule.id,
|
|
12575
13226
|
ruleName: rule.name,
|
|
@@ -12578,7 +13229,11 @@ function fireRefFor(rule, subject, kind) {
|
|
|
12578
13229
|
recordId: subject.recordId,
|
|
12579
13230
|
...subject.trackId !== void 0 ? { trackId: subject.trackId } : {},
|
|
12580
13231
|
...hasEventMedia ? { eventId: subject.recordId } : {},
|
|
12581
|
-
timestamp: subject.timestamp
|
|
13232
|
+
timestamp: subject.timestamp,
|
|
13233
|
+
...className !== void 0 ? { className } : {},
|
|
13234
|
+
...subject.label !== void 0 ? { label: subject.label } : {},
|
|
13235
|
+
zones: notificationZones(rule, subject),
|
|
13236
|
+
...subject.confidence !== void 0 ? { confidence: subject.confidence } : {}
|
|
12582
13237
|
};
|
|
12583
13238
|
}
|
|
12584
13239
|
function notificationZones(rule, subject) {
|
|
@@ -13305,6 +13960,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
13305
13960
|
await TimelapseStore.declare(store);
|
|
13306
13961
|
await NcSummaryStore.declare(store);
|
|
13307
13962
|
await NcTextCatalogStore.declare(store);
|
|
13963
|
+
await NcArtifactIndex.declare(store);
|
|
13308
13964
|
}
|
|
13309
13965
|
/**
|
|
13310
13966
|
* Load rules (every node — the cap provider serves CRUD from any node).
|
|
@@ -15362,7 +16018,23 @@ var ncActions = defineCustomActions({
|
|
|
15362
16018
|
}), NcTextCatalogViewSchema, {
|
|
15363
16019
|
kind: "mutation",
|
|
15364
16020
|
auth: "admin"
|
|
15365
|
-
})
|
|
16021
|
+
}),
|
|
16022
|
+
/**
|
|
16023
|
+
* One page of artefacts, newest `capturedAt` first.
|
|
16024
|
+
*
|
|
16025
|
+
* `deviceIds` filters on the camera an artefact is FILED under. A digest
|
|
16026
|
+
* spans cameras and is filed under its rule's first one (the same camera its
|
|
16027
|
+
* outbox row uses); to ask "everything this digest produced" use `ruleId`.
|
|
16028
|
+
*/
|
|
16029
|
+
"nc.listArtifacts": customAction(NcArtifactQuerySchema, object({
|
|
16030
|
+
artifacts: array(NcArtifactRecordSchema),
|
|
16031
|
+
/** Rows matching the filter, not rows on this page. */
|
|
16032
|
+
total: number().int().nonnegative(),
|
|
16033
|
+
hasMore: boolean()
|
|
16034
|
+
}), { caller: "required" }),
|
|
16035
|
+
/** One artefact's metadata. `null` for unknown, expired AND not-yours alike —
|
|
16036
|
+
* a surface that distinguishes them reports what other people own. */
|
|
16037
|
+
"nc.getArtifact": customAction(object({ artifactId: string().min(1) }), object({ artifact: NcArtifactRecordSchema.nullable() }), { caller: "required" })
|
|
15366
16038
|
});
|
|
15367
16039
|
/** Fail-closed caller resolution — an absent forwarded caller is NEVER admin. */
|
|
15368
16040
|
function requireCaller(caller) {
|
|
@@ -15412,6 +16084,12 @@ function makeNcActionHandlers(deps) {
|
|
|
15412
16084
|
if (sum === void 0) throw new Error("this node does not host the summary producer — use the hub");
|
|
15413
16085
|
return sum;
|
|
15414
16086
|
};
|
|
16087
|
+
/** The artefact index, or an honest refusal. */
|
|
16088
|
+
const requireArtifacts = () => {
|
|
16089
|
+
const index = deps.artifacts;
|
|
16090
|
+
if (index === void 0) throw new Error("this node does not host the notification artifact index — use the hub");
|
|
16091
|
+
return index;
|
|
16092
|
+
};
|
|
15415
16093
|
/** The text catalog editor, or an honest refusal. */
|
|
15416
16094
|
const requireTexts = () => {
|
|
15417
16095
|
const editor = deps.texts;
|
|
@@ -15723,7 +16401,26 @@ function makeNcActionHandlers(deps) {
|
|
|
15723
16401
|
return input.language === void 0 ? texts.view() : texts.view(input.language);
|
|
15724
16402
|
},
|
|
15725
16403
|
"nc.setLanguage": async (input) => requireTexts().setLanguage(input.language),
|
|
15726
|
-
"nc.setTextOverrides": async (input) => requireTexts().setOverrides(input.language, input.overrides)
|
|
16404
|
+
"nc.setTextOverrides": async (input) => requireTexts().setOverrides(input.language, input.overrides),
|
|
16405
|
+
"nc.listArtifacts": async (input, caller) => {
|
|
16406
|
+
const c = requireCaller(caller);
|
|
16407
|
+
const page = await requireArtifacts().list(input, {
|
|
16408
|
+
userId: c.userId,
|
|
16409
|
+
isAdmin: c.isAdmin
|
|
16410
|
+
});
|
|
16411
|
+
return {
|
|
16412
|
+
artifacts: [...page.artifacts],
|
|
16413
|
+
total: page.total,
|
|
16414
|
+
hasMore: page.hasMore
|
|
16415
|
+
};
|
|
16416
|
+
},
|
|
16417
|
+
"nc.getArtifact": async (input, caller) => {
|
|
16418
|
+
const c = requireCaller(caller);
|
|
16419
|
+
return { artifact: await requireArtifacts().get(input.artifactId, {
|
|
16420
|
+
userId: c.userId,
|
|
16421
|
+
isAdmin: c.isAdmin
|
|
16422
|
+
}) };
|
|
16423
|
+
}
|
|
15727
16424
|
};
|
|
15728
16425
|
}
|
|
15729
16426
|
//#endregion
|
|
@@ -15777,7 +16474,8 @@ var PREVIEW_FALLBACK_NAME = "timelapse-preview.jpg";
|
|
|
15777
16474
|
* member has a logged reason. It never rejects.
|
|
15778
16475
|
*/
|
|
15779
16476
|
function buildTimelapseArtifactPublisher(ports) {
|
|
15780
|
-
return async (
|
|
16477
|
+
return async (request) => {
|
|
16478
|
+
const { exportId, deviceId, previewText, previewMode } = request;
|
|
15781
16479
|
const tags = { deviceId };
|
|
15782
16480
|
const none = {
|
|
15783
16481
|
video: null,
|
|
@@ -15808,15 +16506,32 @@ function buildTimelapseArtifactPublisher(ports) {
|
|
|
15808
16506
|
return none;
|
|
15809
16507
|
}
|
|
15810
16508
|
const mp4 = Buffer$1.from(read.base64, "base64");
|
|
16509
|
+
const capturedAt = request.capturedAt ?? Date.now();
|
|
16510
|
+
const common = {
|
|
16511
|
+
deviceId,
|
|
16512
|
+
deviceIds: [deviceId],
|
|
16513
|
+
capturedAt,
|
|
16514
|
+
...request.ruleId !== void 0 ? { ruleId: request.ruleId } : {},
|
|
16515
|
+
...request.ruleName !== void 0 ? { ruleName: request.ruleName } : {},
|
|
16516
|
+
...request.ownerUserId !== void 0 ? { ownerUserId: request.ownerUserId } : {}
|
|
16517
|
+
};
|
|
15811
16518
|
const preview = await publishPreview(ports, {
|
|
15812
16519
|
mp4,
|
|
15813
16520
|
deviceId,
|
|
15814
16521
|
tags,
|
|
15815
16522
|
previewText,
|
|
15816
|
-
previewMode: previewMode ?? "image"
|
|
16523
|
+
previewMode: previewMode ?? "image",
|
|
16524
|
+
meta: {
|
|
16525
|
+
...common,
|
|
16526
|
+
kind: "timelapse-preview"
|
|
16527
|
+
}
|
|
15817
16528
|
});
|
|
15818
16529
|
try {
|
|
15819
|
-
const url = await ports.publish(mp4, read.contentType
|
|
16530
|
+
const { url } = await ports.publish(mp4, read.contentType, {
|
|
16531
|
+
...common,
|
|
16532
|
+
kind: "timelapse",
|
|
16533
|
+
...preview.artifactId !== null ? { posterArtifactId: preview.artifactId } : {}
|
|
16534
|
+
});
|
|
15820
16535
|
if (url === null) {
|
|
15821
16536
|
ports.logger.warn("timelapse artifact not published: no reachable public endpoint", {
|
|
15822
16537
|
tags,
|
|
@@ -15824,7 +16539,7 @@ function buildTimelapseArtifactPublisher(ports) {
|
|
|
15824
16539
|
});
|
|
15825
16540
|
return {
|
|
15826
16541
|
video: null,
|
|
15827
|
-
preview
|
|
16542
|
+
preview: preview.ref
|
|
15828
16543
|
};
|
|
15829
16544
|
}
|
|
15830
16545
|
ports.logger.info("timelapse artifact published", {
|
|
@@ -15833,7 +16548,7 @@ function buildTimelapseArtifactPublisher(ports) {
|
|
|
15833
16548
|
exportId,
|
|
15834
16549
|
bytes: read.bytes,
|
|
15835
16550
|
name: read.name,
|
|
15836
|
-
preview: preview !== null
|
|
16551
|
+
preview: preview.ref !== null
|
|
15837
16552
|
}
|
|
15838
16553
|
});
|
|
15839
16554
|
return {
|
|
@@ -15842,7 +16557,7 @@ function buildTimelapseArtifactPublisher(ports) {
|
|
|
15842
16557
|
mime: read.contentType,
|
|
15843
16558
|
name: read.name
|
|
15844
16559
|
},
|
|
15845
|
-
preview
|
|
16560
|
+
preview: preview.ref
|
|
15846
16561
|
};
|
|
15847
16562
|
} catch (err) {
|
|
15848
16563
|
ports.logger.warn("timelapse artifact not published: artifact plane refused the bytes", {
|
|
@@ -15854,18 +16569,22 @@ function buildTimelapseArtifactPublisher(ports) {
|
|
|
15854
16569
|
});
|
|
15855
16570
|
return {
|
|
15856
16571
|
video: null,
|
|
15857
|
-
preview
|
|
16572
|
+
preview: preview.ref
|
|
15858
16573
|
};
|
|
15859
16574
|
}
|
|
15860
16575
|
};
|
|
15861
16576
|
}
|
|
16577
|
+
var NO_PREVIEW = {
|
|
16578
|
+
ref: null,
|
|
16579
|
+
artifactId: null
|
|
16580
|
+
};
|
|
15862
16581
|
/**
|
|
15863
16582
|
* The preview half. Best-effort at every step and never throws: the picture is
|
|
15864
16583
|
* an enhancement, the video is the notification.
|
|
15865
16584
|
*/
|
|
15866
16585
|
async function publishPreview(ports, input) {
|
|
15867
16586
|
const render = ports.renderPreview;
|
|
15868
|
-
if (render === void 0 || input.previewText === void 0) return
|
|
16587
|
+
if (render === void 0 || input.previewText === void 0) return NO_PREVIEW;
|
|
15869
16588
|
try {
|
|
15870
16589
|
const rendered = await render({
|
|
15871
16590
|
mp4: input.mp4,
|
|
@@ -15873,23 +16592,29 @@ async function publishPreview(ports, input) {
|
|
|
15873
16592
|
deviceId: input.deviceId,
|
|
15874
16593
|
mode: input.previewMode
|
|
15875
16594
|
});
|
|
15876
|
-
if (rendered === null) return
|
|
15877
|
-
const url = await ports.publish(rendered.bytes, rendered.mime);
|
|
16595
|
+
if (rendered === null) return NO_PREVIEW;
|
|
16596
|
+
const { url, artifactId } = await ports.publish(rendered.bytes, rendered.mime, input.meta);
|
|
15878
16597
|
if (url === null) {
|
|
15879
16598
|
ports.logger.warn("timelapse preview not published: no reachable public endpoint", { tags: input.tags });
|
|
15880
|
-
return
|
|
16599
|
+
return {
|
|
16600
|
+
ref: null,
|
|
16601
|
+
artifactId
|
|
16602
|
+
};
|
|
15881
16603
|
}
|
|
15882
16604
|
return {
|
|
15883
|
-
|
|
15884
|
-
|
|
15885
|
-
|
|
16605
|
+
ref: {
|
|
16606
|
+
url,
|
|
16607
|
+
mime: rendered.mime,
|
|
16608
|
+
name: PREVIEW_NAME_BY_MIME[rendered.mime] ?? PREVIEW_FALLBACK_NAME
|
|
16609
|
+
},
|
|
16610
|
+
artifactId
|
|
15886
16611
|
};
|
|
15887
16612
|
} catch (err) {
|
|
15888
16613
|
ports.logger.warn("timelapse preview not published: publish threw", {
|
|
15889
16614
|
tags: input.tags,
|
|
15890
16615
|
meta: { error: String(err) }
|
|
15891
16616
|
});
|
|
15892
|
-
return
|
|
16617
|
+
return NO_PREVIEW;
|
|
15893
16618
|
}
|
|
15894
16619
|
}
|
|
15895
16620
|
/** Title size as a fraction of the frame WIDTH — so it reads the same on a
|
|
@@ -16376,6 +17101,10 @@ function startAlarmPanelConvergence(ports) {
|
|
|
16376
17101
|
* `data-plane-access.spec.ts` asserts they still match the constants their
|
|
16377
17102
|
* handlers register with — a mirror nothing checks is a lie waiting to happen.
|
|
16378
17103
|
*/
|
|
17104
|
+
/** The artefact read route's prefix, imported rather than mirrored: it is
|
|
17105
|
+
* ALSO the prefix `artifactDownloadPath` writes into every index row, and two
|
|
17106
|
+
* copies of that string is a 404 nobody would find. */
|
|
17107
|
+
var NC_ARTIFACT_READ_PREFIX_REF = NC_ARTIFACT_READ_PREFIX;
|
|
16379
17108
|
var TRAINING_EXPORT_PREFIX_REF = "training-export";
|
|
16380
17109
|
var RETRAIN_EXPORT_PREFIX_REF = "retrain-export";
|
|
16381
17110
|
var DEBUG_EXPORT_PREFIX_REF = "debug-export";
|
|
@@ -16391,6 +17120,14 @@ var PIPELINE_ANALYTICS_PLANE_ACCESS = {
|
|
|
16391
17120
|
"nc-action": "public",
|
|
16392
17121
|
/** Self-authenticating single-use artifact token (D47). */
|
|
16393
17122
|
"nc-artifact": "public",
|
|
17123
|
+
/**
|
|
17124
|
+
* The VIEWER's read of the same bytes (task #36). One indexed artefact by an
|
|
17125
|
+
* unguessable id the caller was handed by `nc.listArtifacts` — the rule above,
|
|
17126
|
+
* applied unchanged. It is deliberately NOT the `public` signed plane: the
|
|
17127
|
+
* viewer holds a session, and pre-minting a 24 h signed link for every
|
|
17128
|
+
* artefact a user might scroll to is a link-leak surface for no gain.
|
|
17129
|
+
*/
|
|
17130
|
+
[NC_ARTIFACT_READ_PREFIX_REF]: "authenticated",
|
|
16394
17131
|
/** Every frame behind a marked selection — a corpus, not an item. */
|
|
16395
17132
|
[TRAINING_EXPORT_PREFIX_REF]: "admin",
|
|
16396
17133
|
[RETRAIN_EXPORT_PREFIX_REF]: "admin",
|
|
@@ -36056,6 +36793,138 @@ var BindingCache = class {
|
|
|
36056
36793
|
}
|
|
36057
36794
|
};
|
|
36058
36795
|
//#endregion
|
|
36796
|
+
//#region src/pipeline-analytics/runtime/detection-coverage-cache.ts
|
|
36797
|
+
/**
|
|
36798
|
+
* DetectionCoverageCache — fleet-seeded, event-invalidated mirror of WHICH
|
|
36799
|
+
* devices carry a WRAPPED `detection-pipeline` binding. This is the alarm's
|
|
36800
|
+
* coverage authority ("Away armed, 12 cameras" is false if three of them are
|
|
36801
|
+
* offline or no longer wrapped), read by the notification centre's device
|
|
36802
|
+
* directory (`listDeviceIdentities`).
|
|
36803
|
+
*
|
|
36804
|
+
* WHY a mirror, not a per-tick sweep. The directory refreshes on the NC's 30 s
|
|
36805
|
+
* rule-reload tick. Answering "who still has detection?" with a fleet-wide
|
|
36806
|
+
* `getAllBindings` on EVERY tick cost 903 ms / 373 KB / ~505 `getBindings:
|
|
36807
|
+
* default wrappers skipped` log lines on the live hub (524 devices), forever —
|
|
36808
|
+
* ~3% of hub-main's main thread plus a permanent log-spam baseline. Same defect
|
|
36809
|
+
* and fix as `SystemMirror.refreshBinding` (639073e8): ONE fleet read to warm,
|
|
36810
|
+
* then per-device updates driven by `DeviceBindingsChanged` events. A wrapper
|
|
36811
|
+
* toggle emits that event carrying the device's id and `capName`
|
|
36812
|
+
* (`detection-pipeline`), so a single `getBindings(deviceId)` — 21 ms / 613 B —
|
|
36813
|
+
* keeps the mirror current without ever touching the fleet again.
|
|
36814
|
+
*
|
|
36815
|
+
* ── Destructive-direction discipline (D49) ──────────────────────────────────
|
|
36816
|
+
*
|
|
36817
|
+
* `detectionActive: false` reports a camera as UNCOVERED / switched-off (D62) —
|
|
36818
|
+
* the destructive answer, and the one an operator reads as "my alarm is not
|
|
36819
|
+
* watching that door". A transient `getBindings` blip must never flip a
|
|
36820
|
+
* known-covered camera to uncovered:
|
|
36821
|
+
* • a failed refresh KEEPS the previous value (a read that throws changes
|
|
36822
|
+
* nothing), and
|
|
36823
|
+
* • a single read that would flip a known `true → false` is confirmed by a
|
|
36824
|
+
* concordant SECOND read before it lands — an empty binding list is not
|
|
36825
|
+
* proof the wrapper is gone.
|
|
36826
|
+
* A COLD mirror (seed not yet landed, or a seed that failed) answers `undefined`
|
|
36827
|
+
* for every id — and unknown never reports a camera as excluded from coverage.
|
|
36828
|
+
*/
|
|
36829
|
+
var DetectionCoverageCache = class {
|
|
36830
|
+
api;
|
|
36831
|
+
logger;
|
|
36832
|
+
/** deviceId → "has a WRAPPED detection-pipeline binding". Absent = unknown. */
|
|
36833
|
+
state = /* @__PURE__ */ new Map();
|
|
36834
|
+
warm = false;
|
|
36835
|
+
seeding = null;
|
|
36836
|
+
constructor(deps) {
|
|
36837
|
+
this.api = deps.api;
|
|
36838
|
+
this.logger = deps.logger;
|
|
36839
|
+
}
|
|
36840
|
+
/** Has the ONE fleet seed ever landed? A cold mirror knows nothing. */
|
|
36841
|
+
isWarm() {
|
|
36842
|
+
return this.warm;
|
|
36843
|
+
}
|
|
36844
|
+
/** Do we have an answer for this id at all? (`false` ⇒ absent ⇒ unknown.) */
|
|
36845
|
+
has(deviceId) {
|
|
36846
|
+
return this.state.has(deviceId);
|
|
36847
|
+
}
|
|
36848
|
+
/** The coverage answer, or `undefined` when this id is unknown. No I/O. */
|
|
36849
|
+
get(deviceId) {
|
|
36850
|
+
return this.state.get(deviceId);
|
|
36851
|
+
}
|
|
36852
|
+
/**
|
|
36853
|
+
* Seed the whole fleet ONCE with a single `getAllBindings`. Idempotent: a
|
|
36854
|
+
* warm cache is a no-op, and concurrent callers coalesce onto one read. A
|
|
36855
|
+
* failed seed leaves the mirror COLD so the next tick retries — during that
|
|
36856
|
+
* window every id answers unknown, which never reports a camera as excluded.
|
|
36857
|
+
*/
|
|
36858
|
+
async ensureWarm() {
|
|
36859
|
+
if (this.warm) return;
|
|
36860
|
+
if (this.seeding) return this.seeding;
|
|
36861
|
+
this.seeding = this.seed();
|
|
36862
|
+
try {
|
|
36863
|
+
await this.seeding;
|
|
36864
|
+
} finally {
|
|
36865
|
+
this.seeding = null;
|
|
36866
|
+
}
|
|
36867
|
+
}
|
|
36868
|
+
async seed() {
|
|
36869
|
+
try {
|
|
36870
|
+
const rows = await this.api.deviceManager.getAllBindings.query({});
|
|
36871
|
+
for (const row of rows) {
|
|
36872
|
+
if (this.state.has(row.deviceId)) continue;
|
|
36873
|
+
this.state.set(row.deviceId, hasWrappedDetection(row.entries));
|
|
36874
|
+
}
|
|
36875
|
+
this.warm = true;
|
|
36876
|
+
this.logger.info("detection-coverage mirror warm", { meta: { devices: this.state.size } });
|
|
36877
|
+
} catch (err) {
|
|
36878
|
+
this.logger.warn("detection-coverage seed failed — coverage unknown until it lands", { meta: { error: String(err) } });
|
|
36879
|
+
}
|
|
36880
|
+
}
|
|
36881
|
+
/**
|
|
36882
|
+
* A `DeviceBindingsChanged` for the detection cap re-reads THAT ONE device
|
|
36883
|
+
* (never the fleet). Fire-and-forget from the synchronous event handler.
|
|
36884
|
+
*/
|
|
36885
|
+
onBindingsChanged(event) {
|
|
36886
|
+
if (event.capName !== "detection-pipeline") return Promise.resolve();
|
|
36887
|
+
return this.refreshDevice(event.deviceId);
|
|
36888
|
+
}
|
|
36889
|
+
/** Drop a device the fleet no longer has (`device.unregistered`). */
|
|
36890
|
+
forget(deviceId) {
|
|
36891
|
+
this.state.delete(deviceId);
|
|
36892
|
+
}
|
|
36893
|
+
/** Return to cold — used on addon shutdown. */
|
|
36894
|
+
clearAll() {
|
|
36895
|
+
this.state.clear();
|
|
36896
|
+
this.warm = false;
|
|
36897
|
+
this.seeding = null;
|
|
36898
|
+
}
|
|
36899
|
+
async refreshDevice(deviceId) {
|
|
36900
|
+
try {
|
|
36901
|
+
const active = await this.readWrappedDetection(deviceId);
|
|
36902
|
+
const prev = this.state.get(deviceId);
|
|
36903
|
+
if (!active && prev === true) {
|
|
36904
|
+
if (await this.readWrappedDetection(deviceId)) {
|
|
36905
|
+
this.state.set(deviceId, true);
|
|
36906
|
+
return;
|
|
36907
|
+
}
|
|
36908
|
+
}
|
|
36909
|
+
this.state.set(deviceId, active);
|
|
36910
|
+
} catch (err) {
|
|
36911
|
+
this.logger.debug("detection-coverage refresh failed — keeping the previous value", {
|
|
36912
|
+
tags: { deviceId },
|
|
36913
|
+
meta: { error: String(err) }
|
|
36914
|
+
});
|
|
36915
|
+
}
|
|
36916
|
+
}
|
|
36917
|
+
async readWrappedDetection(deviceId) {
|
|
36918
|
+
return hasWrappedDetection((await this.api.deviceManager.getBindings.query({ deviceId })).entries);
|
|
36919
|
+
}
|
|
36920
|
+
};
|
|
36921
|
+
/** The one authority: a WRAPPED `detection-pipeline` binding is present. A
|
|
36922
|
+
* native binding of the same cap is NOT coverage (D62: absence of the WRAPPED
|
|
36923
|
+
* binding is how a deactivated wrapper is represented). */
|
|
36924
|
+
function hasWrappedDetection(entries) {
|
|
36925
|
+
return entries.some((e) => e.capName === "detection-pipeline" && e.kind === "wrapped");
|
|
36926
|
+
}
|
|
36927
|
+
//#endregion
|
|
36059
36928
|
//#region src/pipeline-analytics/runtime/capture-scheduler.ts
|
|
36060
36929
|
/**
|
|
36061
36930
|
* Kind-derived default priorities (0 = highest). Explicit `priority` wins.
|
|
@@ -41201,6 +42070,15 @@ var REBUILD_FALLBACK_NODE_ID = "hub";
|
|
|
41201
42070
|
*/
|
|
41202
42071
|
var TIMELAPSE_TRACK_QUERY_LIMIT = 2e3;
|
|
41203
42072
|
var PIPELINE_ANALYTICS_ADDON_ID = "pipeline-analytics";
|
|
42073
|
+
/** Artefact index ⇄ shelf reconcile cadence. Hourly: the two only drift on the
|
|
42074
|
+
* bounds' own timescale (14 days / 2000 entries / 2 GiB). */
|
|
42075
|
+
var NC_ARTIFACT_PRUNE_INTERVAL_MS = 36e5;
|
|
42076
|
+
/** What a publish answers on a node whose artifact plane never came up: no
|
|
42077
|
+
* link, no index row — and, exactly as before, bytes-only delivery. */
|
|
42078
|
+
var NO_PUBLICATION = {
|
|
42079
|
+
url: null,
|
|
42080
|
+
artifactId: null
|
|
42081
|
+
};
|
|
41204
42082
|
/**
|
|
41205
42083
|
* The FULL action catalog for this addon.
|
|
41206
42084
|
*
|
|
@@ -41438,6 +42316,12 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
41438
42316
|
* path). Null on non-post-processing nodes or when disabled. */
|
|
41439
42317
|
embeddingDispatcher = null;
|
|
41440
42318
|
bindingCache = null;
|
|
42319
|
+
/** Alarm-coverage mirror the NC device directory reads — WHICH devices carry
|
|
42320
|
+
* a wrapped `detection-pipeline` binding. Fleet-seeded ONCE, then per-device
|
|
42321
|
+
* updates off `DeviceBindingsChanged`, so the 30 s rule-reload tick stops
|
|
42322
|
+
* sweeping `getAllBindings` over the whole fleet. See
|
|
42323
|
+
* `runtime/detection-coverage-cache.ts`. */
|
|
42324
|
+
detectionCoverage = null;
|
|
41441
42325
|
/** Per-device throttle for the inactive-binding frame-drop warn (5 min). */
|
|
41442
42326
|
inactiveBindingDropWarnAt = /* @__PURE__ */ new Map();
|
|
41443
42327
|
zoneAnalytics = null;
|
|
@@ -41476,6 +42360,12 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
41476
42360
|
* data-plane handle. Null until served / when the facility is absent. */
|
|
41477
42361
|
ncArtifactPlane = null;
|
|
41478
42362
|
ncArtifactDataPlane = null;
|
|
42363
|
+
/** The viewer's artefact library (task #36): the index, its authenticated
|
|
42364
|
+
* bytes route, and the store the two share. Null until served. */
|
|
42365
|
+
ncArtifactIndex = null;
|
|
42366
|
+
ncArtifactStore = null;
|
|
42367
|
+
ncArtifactReadDataPlane = null;
|
|
42368
|
+
ncArtifactPruneTimer = null;
|
|
41479
42369
|
/**
|
|
41480
42370
|
* Roadmap 4b: ONE cut of an event, in every container the rule asked for.
|
|
41481
42371
|
*
|
|
@@ -41662,6 +42552,31 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
41662
42552
|
* Best-effort at every step: no facility, no secret or no reachable base URL
|
|
41663
42553
|
* ⇒ no plane, and the dispatcher keeps shipping bytes exactly as before.
|
|
41664
42554
|
*/
|
|
42555
|
+
/**
|
|
42556
|
+
* Reconcile the artefact index against the shelf that holds the bytes.
|
|
42557
|
+
*
|
|
42558
|
+
* Hourly, because the two drift only on the bounds' timescale: a row expires
|
|
42559
|
+
* on the shelf's own age bound (14 d) or its file is evicted by the count/byte
|
|
42560
|
+
* bound. Without this, a list eventually serves rows whose bytes 404 — and a
|
|
42561
|
+
* broken tile is the one failure an operator DOES report.
|
|
42562
|
+
*/
|
|
42563
|
+
startNcArtifactPrune() {
|
|
42564
|
+
if (this.ncArtifactPruneTimer !== null) return;
|
|
42565
|
+
const run = async () => {
|
|
42566
|
+
const index = this.ncArtifactIndex;
|
|
42567
|
+
const store = this.ncArtifactStore;
|
|
42568
|
+
if (index === null || store === null) return;
|
|
42569
|
+
try {
|
|
42570
|
+
await store.sweep();
|
|
42571
|
+
await index.prune(this.isPostProcessingNode ? await store.keptIds() : null);
|
|
42572
|
+
} catch (err) {
|
|
42573
|
+
this.ctx.logger.warn("nc artifact prune failed", { meta: { error: errMsg(err) } });
|
|
42574
|
+
}
|
|
42575
|
+
};
|
|
42576
|
+
run();
|
|
42577
|
+
this.ncArtifactPruneTimer = setInterval(() => void run(), NC_ARTIFACT_PRUNE_INTERVAL_MS);
|
|
42578
|
+
this.ncArtifactPruneTimer.unref?.();
|
|
42579
|
+
}
|
|
41665
42580
|
async serveNcArtifactPlane() {
|
|
41666
42581
|
try {
|
|
41667
42582
|
const secretState = this.state("ncArtifactSecret", NcArtifactSecretSchema, "");
|
|
@@ -41675,9 +42590,18 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
41675
42590
|
logger: this.ctx.logger.child("nc-artifacts")
|
|
41676
42591
|
});
|
|
41677
42592
|
await store.start();
|
|
42593
|
+
this.ncArtifactStore = store;
|
|
42594
|
+
const index = new NcArtifactIndex({
|
|
42595
|
+
store: this.ctx.api.settingsStore,
|
|
42596
|
+
logger: this.ctx.logger.child("nc-artifacts"),
|
|
42597
|
+
addonId: this.ctx.id,
|
|
42598
|
+
retentionMs: store.retentionMs
|
|
42599
|
+
});
|
|
42600
|
+
this.ncArtifactIndex = index;
|
|
41678
42601
|
const plane = new NcArtifactPlane({
|
|
41679
42602
|
store,
|
|
41680
42603
|
secret,
|
|
42604
|
+
index,
|
|
41681
42605
|
logger: this.ctx.logger.child("nc-artifacts"),
|
|
41682
42606
|
routePrefix: `/addon/${this.ctx.id}/nc-artifact`,
|
|
41683
42607
|
listEndpoints: () => this.ncEndpoints()
|
|
@@ -41688,7 +42612,23 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
41688
42612
|
handler: plane.handler
|
|
41689
42613
|
}) ?? null;
|
|
41690
42614
|
this.ncArtifactPlane = this.ncArtifactDataPlane !== null ? plane : null;
|
|
41691
|
-
|
|
42615
|
+
const readPlane = new NcArtifactReadPlane({
|
|
42616
|
+
index,
|
|
42617
|
+
store,
|
|
42618
|
+
logger: this.ctx.logger.child("nc-artifacts")
|
|
42619
|
+
});
|
|
42620
|
+
this.ncArtifactReadDataPlane = await this.ctx.dataPlane?.serve({
|
|
42621
|
+
prefix: NC_ARTIFACT_READ_PREFIX_REF,
|
|
42622
|
+
access: planeAccess(NC_ARTIFACT_READ_PREFIX_REF),
|
|
42623
|
+
handler: readPlane.handler
|
|
42624
|
+
}) ?? null;
|
|
42625
|
+
this.startNcArtifactPrune();
|
|
42626
|
+
this.ctx.logger.info("nc-artifact data-plane served", { meta: {
|
|
42627
|
+
served: this.ncArtifactPlane !== null,
|
|
42628
|
+
libraryServed: this.ncArtifactReadDataPlane !== null,
|
|
42629
|
+
libraryAccess: planeAccess(NC_ARTIFACT_READ_PREFIX_REF),
|
|
42630
|
+
retentionDays: Math.round(store.retentionMs / 864e5)
|
|
42631
|
+
} });
|
|
41692
42632
|
} catch (err) {
|
|
41693
42633
|
this.ctx.logger.warn("nc-artifact data-plane failed to serve", { meta: { error: errMsg(err) } });
|
|
41694
42634
|
}
|
|
@@ -42136,7 +43076,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
42136
43076
|
store: center.summaryStore,
|
|
42137
43077
|
runNow: (input) => center.runSummaryNow(input)
|
|
42138
43078
|
},
|
|
42139
|
-
texts: center.textCatalogEditor
|
|
43079
|
+
texts: center.textCatalogEditor,
|
|
43080
|
+
...this.ncArtifactIndex !== null ? { artifacts: this.ncArtifactIndex } : {}
|
|
42140
43081
|
});
|
|
42141
43082
|
}
|
|
42142
43083
|
/** Declare typed collections up-front so the first insert doesn't race with
|
|
@@ -42621,6 +43562,10 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
42621
43562
|
api,
|
|
42622
43563
|
logger: logger.child("BindingCache")
|
|
42623
43564
|
});
|
|
43565
|
+
this.detectionCoverage = new DetectionCoverageCache({
|
|
43566
|
+
api,
|
|
43567
|
+
logger: logger.child("DetectionCoverage")
|
|
43568
|
+
});
|
|
42624
43569
|
const zoneAnalytics = new ZoneAnalyticsProvider({
|
|
42625
43570
|
logger: logger.child("ZoneAnalytics"),
|
|
42626
43571
|
onOccupancySample: (deviceId, count, at, zoneId) => this.lts?.note("occupancy", deviceId, count, at, zoneId ?? ""),
|
|
@@ -42702,17 +43647,15 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
42702
43647
|
},
|
|
42703
43648
|
listDeviceIdentities: async () => {
|
|
42704
43649
|
const all = await api.deviceManager.listAll.query({ projection: "slim" });
|
|
42705
|
-
const
|
|
42706
|
-
|
|
42707
|
-
return /* @__PURE__ */ new Map();
|
|
42708
|
-
});
|
|
43650
|
+
const coverage = this.detectionCoverage;
|
|
43651
|
+
if (coverage) await coverage.ensureWarm();
|
|
42709
43652
|
return all.map((d) => ({
|
|
42710
43653
|
deviceId: d.id,
|
|
42711
43654
|
type: d.type,
|
|
42712
43655
|
name: d.name,
|
|
42713
43656
|
isCamera: d.isCamera,
|
|
42714
43657
|
online: d.online && !d.disabled,
|
|
42715
|
-
...
|
|
43658
|
+
...coverage?.has(d.id) ? { detectionActive: coverage.get(d.id) === true } : {}
|
|
42716
43659
|
}));
|
|
42717
43660
|
},
|
|
42718
43661
|
readDeviceZoneIds: async (ids) => {
|
|
@@ -42923,7 +43866,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
42923
43866
|
timestamp,
|
|
42924
43867
|
data: jpeg
|
|
42925
43868
|
}),
|
|
42926
|
-
publish: async (bytes, mime) => await this.ncArtifactPlane?.
|
|
43869
|
+
publish: async (bytes, mime, meta) => await this.ncArtifactPlane?.publishIndexed(bytes, mime, meta) ?? NO_PUBLICATION
|
|
42927
43870
|
};
|
|
42928
43871
|
}
|
|
42929
43872
|
/**
|
|
@@ -42996,7 +43939,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
42996
43939
|
publishArtifact: buildTimelapseArtifactPublisher({
|
|
42997
43940
|
logger: this.ctx.logger.child("Timelapse"),
|
|
42998
43941
|
readExportBytes: ({ exportId }) => api.recordingExport.readExportBytes.query({ exportId }),
|
|
42999
|
-
publish: async (bytes, mime) => await this.ncArtifactPlane?.
|
|
43942
|
+
publish: async (bytes, mime, meta) => await this.ncArtifactPlane?.publishIndexed(bytes, mime, meta) ?? NO_PUBLICATION,
|
|
43000
43943
|
renderPreview: async ({ mode, ...input }) => {
|
|
43001
43944
|
const previewPorts = {
|
|
43002
43945
|
logger: this.ctx.logger.child("Timelapse"),
|
|
@@ -43420,6 +44363,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
43420
44363
|
* `runtime/wire-subscriptions.ts`. */
|
|
43421
44364
|
handleBindingsChangedEvent(data) {
|
|
43422
44365
|
this.bindingCache?.onBindingsChanged(data);
|
|
44366
|
+
this.detectionCoverage?.onBindingsChanged(data);
|
|
43423
44367
|
if (data.capName === "pipeline-analytics" && data.reason === "wrapper-deactivated") {
|
|
43424
44368
|
this.trackStore?.clearDevice(data.deviceId);
|
|
43425
44369
|
this.residents.closeDevice(data.deviceId);
|
|
@@ -43445,6 +44389,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
43445
44389
|
this.audioMarkers?.forgetDevice(deviceId);
|
|
43446
44390
|
this.settingsCaches.clearDevice(deviceId);
|
|
43447
44391
|
this.bindingCache?.invalidate(deviceId);
|
|
44392
|
+
this.detectionCoverage?.forget(deviceId);
|
|
43448
44393
|
this.zoneAnalytics?.forgetDevice(deviceId);
|
|
43449
44394
|
this.audioMetrics?.forgetDevice(deviceId);
|
|
43450
44395
|
this.releaseProxy(deviceId);
|
|
@@ -43650,6 +44595,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
43650
44595
|
this.trackStore?.clearAll();
|
|
43651
44596
|
this.stationaryRegistry = null;
|
|
43652
44597
|
this.bindingCache?.clearAll();
|
|
44598
|
+
this.detectionCoverage?.clearAll();
|
|
43653
44599
|
await this.eventMediaDataPlane?.dispose();
|
|
43654
44600
|
this.eventMediaDataPlane = null;
|
|
43655
44601
|
this.eventMediaBaseUrl = null;
|
|
@@ -43660,6 +44606,14 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
43660
44606
|
this.retrainExportDataPlane = null;
|
|
43661
44607
|
await this.debugExportDataPlane?.dispose();
|
|
43662
44608
|
this.debugExportDataPlane = null;
|
|
44609
|
+
if (this.ncArtifactPruneTimer !== null) {
|
|
44610
|
+
clearInterval(this.ncArtifactPruneTimer);
|
|
44611
|
+
this.ncArtifactPruneTimer = null;
|
|
44612
|
+
}
|
|
44613
|
+
await this.ncArtifactReadDataPlane?.dispose();
|
|
44614
|
+
this.ncArtifactReadDataPlane = null;
|
|
44615
|
+
this.ncArtifactIndex = null;
|
|
44616
|
+
this.ncArtifactStore = null;
|
|
43663
44617
|
}
|
|
43664
44618
|
async handleInferenceResult(data) {
|
|
43665
44619
|
if (this.shuttingDown) return;
|