@camstack/addon-post-analysis 1.2.71 → 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-B--dE3Ry.mjs → dist-6te1200B.mjs} +1906 -64
- package/dist/{dist-DjPaVd2L.js → dist-C2w3cl2p.js} +1906 -64
- 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-3nHxWRIp.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-_DSzSo5N.mjs → hostInit-DpWdIydx.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +847 -42
- package/dist/pipeline-analytics/index.mjs +847 -42
- 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";
|
|
@@ -1573,6 +1573,401 @@ var NC_ALARM_DECLARATION = {
|
|
|
1573
1573
|
DeviceClass: NcAlarmPanelDevice
|
|
1574
1574
|
};
|
|
1575
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
|
|
1576
1971
|
//#region ../types/dist/node.mjs
|
|
1577
1972
|
var node_exports = /* @__PURE__ */ __exportAll({
|
|
1578
1973
|
FilesystemStorageProvider: () => FilesystemStorageProvider,
|
|
@@ -1963,6 +2358,61 @@ var NcArtifactPlane = class {
|
|
|
1963
2358
|
}
|
|
1964
2359
|
}
|
|
1965
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
|
+
/**
|
|
1966
2416
|
* Absolutize a path against the SAME base URL an artifact link would use.
|
|
1967
2417
|
*
|
|
1968
2418
|
* For links this plane does not host but a RECIPIENT still has to open — a
|
|
@@ -2028,6 +2478,124 @@ var NcArtifactPlane = class {
|
|
|
2028
2478
|
}
|
|
2029
2479
|
};
|
|
2030
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
|
|
2031
2599
|
//#region src/notification-center/artifact-store.ts
|
|
2032
2600
|
/**
|
|
2033
2601
|
* NcArtifactStore — the bytes behind a signed notification-artifact URL.
|
|
@@ -2042,9 +2610,25 @@ var NcArtifactPlane = class {
|
|
|
2042
2610
|
* bites first, the oldest artifacts go. A sweep on boot clears whatever a crash
|
|
2043
2611
|
* left behind — the directory is disposable by construction, so a lost file is
|
|
2044
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.
|
|
2045
2627
|
*/
|
|
2046
2628
|
var DEFAULT_MAX_AGE_MS = 6 * 36e5;
|
|
2047
2629
|
var DEFAULT_MAX_ENTRIES = 500;
|
|
2630
|
+
/** The retained shelf's subdirectory name, inside the scratch directory. */
|
|
2631
|
+
var KEPT_SUBDIR = "kept";
|
|
2048
2632
|
/** `image/jpeg` → `jpg`, so a fetched link has a name a client will accept. */
|
|
2049
2633
|
function extensionFor(mime) {
|
|
2050
2634
|
if (mime === "image/jpeg") return "jpg";
|
|
@@ -2058,6 +2642,10 @@ var NcArtifactStore = class {
|
|
|
2058
2642
|
now;
|
|
2059
2643
|
maxAgeMs;
|
|
2060
2644
|
maxEntries;
|
|
2645
|
+
keptMaxAgeMs;
|
|
2646
|
+
keptMaxEntries;
|
|
2647
|
+
keptMaxBytes;
|
|
2648
|
+
keptDir;
|
|
2061
2649
|
/** id → mime, so the route can answer with the right content-type without
|
|
2062
2650
|
* re-deriving it from the extension on every request. */
|
|
2063
2651
|
mimeById = /* @__PURE__ */ new Map();
|
|
@@ -2066,17 +2654,32 @@ var NcArtifactStore = class {
|
|
|
2066
2654
|
this.now = options.now ?? (() => Date.now());
|
|
2067
2655
|
this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
2068
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;
|
|
2069
2666
|
}
|
|
2070
2667
|
/** Create the directory and sweep whatever a previous run left behind. */
|
|
2071
2668
|
async start() {
|
|
2072
2669
|
await promises.mkdir(this.options.dir, { recursive: true });
|
|
2073
2670
|
await this.sweep();
|
|
2074
2671
|
}
|
|
2075
|
-
/**
|
|
2076
|
-
|
|
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 = {}) {
|
|
2077
2679
|
const id = `${this.now()}-${randomUUID()}`;
|
|
2078
|
-
const
|
|
2079
|
-
|
|
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 });
|
|
2080
2683
|
await promises.writeFile(file, bytes);
|
|
2081
2684
|
this.mimeById.set(id, mime);
|
|
2082
2685
|
await this.sweep();
|
|
@@ -2089,7 +2692,7 @@ var NcArtifactStore = class {
|
|
|
2089
2692
|
/** Read an artifact's bytes + mime, or null when it is gone (expired/swept). */
|
|
2090
2693
|
async read(id) {
|
|
2091
2694
|
if (!/^[0-9]+-[0-9a-f-]{36}$/i.test(id)) return null;
|
|
2092
|
-
const found = (await this.list()
|
|
2695
|
+
const found = [...await this.list(this.options.dir), ...await this.list(this.keptDir)].find((e) => e.id === id);
|
|
2093
2696
|
if (!found) return null;
|
|
2094
2697
|
try {
|
|
2095
2698
|
return {
|
|
@@ -2100,6 +2703,10 @@ var NcArtifactStore = class {
|
|
|
2100
2703
|
return null;
|
|
2101
2704
|
}
|
|
2102
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
|
+
}
|
|
2103
2710
|
/** Delete every artifact (operator cleanup / shutdown). */
|
|
2104
2711
|
async clear() {
|
|
2105
2712
|
await promises.rm(this.options.dir, {
|
|
@@ -2108,13 +2715,39 @@ var NcArtifactStore = class {
|
|
|
2108
2715
|
}).catch(() => {});
|
|
2109
2716
|
this.mimeById.clear();
|
|
2110
2717
|
}
|
|
2111
|
-
/**
|
|
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. */
|
|
2112
2720
|
async sweep() {
|
|
2113
|
-
|
|
2114
|
-
|
|
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;
|
|
2115
2740
|
const doomed = entries.filter((e) => e.mtimeMs < cutoff);
|
|
2116
|
-
const
|
|
2117
|
-
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
|
+
}
|
|
2118
2751
|
for (const e of doomed) try {
|
|
2119
2752
|
await promises.rm(e.file, { force: true });
|
|
2120
2753
|
this.mimeById.delete(e.id);
|
|
@@ -2125,23 +2758,24 @@ var NcArtifactStore = class {
|
|
|
2125
2758
|
} });
|
|
2126
2759
|
}
|
|
2127
2760
|
}
|
|
2128
|
-
async list() {
|
|
2761
|
+
async list(dir) {
|
|
2129
2762
|
let names;
|
|
2130
2763
|
try {
|
|
2131
|
-
names = await promises.readdir(
|
|
2764
|
+
names = await promises.readdir(dir);
|
|
2132
2765
|
} catch {
|
|
2133
2766
|
return [];
|
|
2134
2767
|
}
|
|
2135
2768
|
const out = [];
|
|
2136
2769
|
for (const name of names) {
|
|
2137
|
-
const file = path.join(
|
|
2770
|
+
const file = path.join(dir, name);
|
|
2138
2771
|
try {
|
|
2139
2772
|
const st = await promises.stat(file);
|
|
2140
2773
|
if (!st.isFile()) continue;
|
|
2141
2774
|
out.push({
|
|
2142
2775
|
id: name.slice(0, name.lastIndexOf(".")),
|
|
2143
2776
|
file,
|
|
2144
|
-
mtimeMs: st.mtimeMs
|
|
2777
|
+
mtimeMs: st.mtimeMs,
|
|
2778
|
+
sizeBytes: st.size
|
|
2145
2779
|
});
|
|
2146
2780
|
} catch {}
|
|
2147
2781
|
}
|
|
@@ -10005,7 +10639,15 @@ var NcSummaryProducer = class {
|
|
|
10005
10639
|
}
|
|
10006
10640
|
const bytes = new Uint8Array(rendered.jpeg.byteLength);
|
|
10007
10641
|
bytes.set(rendered.jpeg);
|
|
10008
|
-
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
|
+
});
|
|
10009
10651
|
if (url === null) {
|
|
10010
10652
|
this.deps.logger.warn("summary mosaic not published: no reachable public endpoint", { meta: { ruleId: rule.id } });
|
|
10011
10653
|
return {
|
|
@@ -10999,7 +11641,7 @@ function unknownNcTextKeys(keys, bundles = NC_TEXT_BUNDLES) {
|
|
|
10999
11641
|
* durable document stays the sparse map its schema describes.
|
|
11000
11642
|
*/
|
|
11001
11643
|
function applyNcTextOverridePatch(current, language, patch) {
|
|
11002
|
-
const next = { ...current[language]
|
|
11644
|
+
const next = { ...current[language] };
|
|
11003
11645
|
for (const [key, value] of Object.entries(patch)) {
|
|
11004
11646
|
if (value === null || value.trim().length === 0) {
|
|
11005
11647
|
delete next[key];
|
|
@@ -11567,7 +12209,7 @@ var TimelapseRenderer = class {
|
|
|
11567
12209
|
}
|
|
11568
12210
|
});
|
|
11569
12211
|
const finished = await this.awaitReady(queued, tags);
|
|
11570
|
-
const published = await this.publish(finished.id,
|
|
12212
|
+
const published = await this.publish(finished.id, request, tags);
|
|
11571
12213
|
const downloadUrl = await this.ports.resolveDownloadUrl?.({ exportId: finished.id }).catch(() => null);
|
|
11572
12214
|
return {
|
|
11573
12215
|
exportId: finished.id,
|
|
@@ -11619,7 +12261,9 @@ var TimelapseRenderer = class {
|
|
|
11619
12261
|
* notification, not a failed window — and failing here would leave the pair
|
|
11620
12262
|
* unstamped and re-render the same eight hours on the next tick.
|
|
11621
12263
|
*/
|
|
11622
|
-
async publish(exportId,
|
|
12264
|
+
async publish(exportId, request, tags) {
|
|
12265
|
+
const { deviceId, previewText } = request;
|
|
12266
|
+
const previewMode = request.previewMode ?? "image";
|
|
11623
12267
|
const publish = this.ports.publishArtifact;
|
|
11624
12268
|
const none = {
|
|
11625
12269
|
video: null,
|
|
@@ -11639,7 +12283,11 @@ var TimelapseRenderer = class {
|
|
|
11639
12283
|
...previewText !== void 0 ? {
|
|
11640
12284
|
previewText,
|
|
11641
12285
|
previewMode
|
|
11642
|
-
} : {}
|
|
12286
|
+
} : {},
|
|
12287
|
+
capturedAt: request.window.endMs,
|
|
12288
|
+
ruleId: request.ruleId,
|
|
12289
|
+
ruleName: request.ruleName,
|
|
12290
|
+
...request.ownerUserId !== void 0 ? { ownerUserId: request.ownerUserId } : {}
|
|
11643
12291
|
});
|
|
11644
12292
|
if (set.video === null) this.ports.logger.warn("timelapse artifact could not be published — link-less delivery", {
|
|
11645
12293
|
tags,
|
|
@@ -11995,7 +12643,8 @@ var TimelapseScheduler = class {
|
|
|
11995
12643
|
...rule.previewText !== void 0 ? { text: rule.previewText } : {}
|
|
11996
12644
|
}),
|
|
11997
12645
|
previewMode: rule.previewMode,
|
|
11998
|
-
...rule.reportClasses !== void 0 ? { reportClasses: rule.reportClasses } : {}
|
|
12646
|
+
...rule.reportClasses !== void 0 ? { reportClasses: rule.reportClasses } : {},
|
|
12647
|
+
...rule.ownerUserId !== void 0 ? { ownerUserId: rule.ownerUserId } : {}
|
|
11999
12648
|
});
|
|
12000
12649
|
const rows = buildTimelapseOutboxInputs({
|
|
12001
12650
|
texts: this.texts,
|
|
@@ -13311,6 +13960,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
13311
13960
|
await TimelapseStore.declare(store);
|
|
13312
13961
|
await NcSummaryStore.declare(store);
|
|
13313
13962
|
await NcTextCatalogStore.declare(store);
|
|
13963
|
+
await NcArtifactIndex.declare(store);
|
|
13314
13964
|
}
|
|
13315
13965
|
/**
|
|
13316
13966
|
* Load rules (every node — the cap provider serves CRUD from any node).
|
|
@@ -15368,7 +16018,23 @@ var ncActions = defineCustomActions({
|
|
|
15368
16018
|
}), NcTextCatalogViewSchema, {
|
|
15369
16019
|
kind: "mutation",
|
|
15370
16020
|
auth: "admin"
|
|
15371
|
-
})
|
|
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" })
|
|
15372
16038
|
});
|
|
15373
16039
|
/** Fail-closed caller resolution — an absent forwarded caller is NEVER admin. */
|
|
15374
16040
|
function requireCaller(caller) {
|
|
@@ -15418,6 +16084,12 @@ function makeNcActionHandlers(deps) {
|
|
|
15418
16084
|
if (sum === void 0) throw new Error("this node does not host the summary producer — use the hub");
|
|
15419
16085
|
return sum;
|
|
15420
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
|
+
};
|
|
15421
16093
|
/** The text catalog editor, or an honest refusal. */
|
|
15422
16094
|
const requireTexts = () => {
|
|
15423
16095
|
const editor = deps.texts;
|
|
@@ -15729,7 +16401,26 @@ function makeNcActionHandlers(deps) {
|
|
|
15729
16401
|
return input.language === void 0 ? texts.view() : texts.view(input.language);
|
|
15730
16402
|
},
|
|
15731
16403
|
"nc.setLanguage": async (input) => requireTexts().setLanguage(input.language),
|
|
15732
|
-
"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
|
+
}
|
|
15733
16424
|
};
|
|
15734
16425
|
}
|
|
15735
16426
|
//#endregion
|
|
@@ -15783,7 +16474,8 @@ var PREVIEW_FALLBACK_NAME = "timelapse-preview.jpg";
|
|
|
15783
16474
|
* member has a logged reason. It never rejects.
|
|
15784
16475
|
*/
|
|
15785
16476
|
function buildTimelapseArtifactPublisher(ports) {
|
|
15786
|
-
return async (
|
|
16477
|
+
return async (request) => {
|
|
16478
|
+
const { exportId, deviceId, previewText, previewMode } = request;
|
|
15787
16479
|
const tags = { deviceId };
|
|
15788
16480
|
const none = {
|
|
15789
16481
|
video: null,
|
|
@@ -15814,15 +16506,32 @@ function buildTimelapseArtifactPublisher(ports) {
|
|
|
15814
16506
|
return none;
|
|
15815
16507
|
}
|
|
15816
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
|
+
};
|
|
15817
16518
|
const preview = await publishPreview(ports, {
|
|
15818
16519
|
mp4,
|
|
15819
16520
|
deviceId,
|
|
15820
16521
|
tags,
|
|
15821
16522
|
previewText,
|
|
15822
|
-
previewMode: previewMode ?? "image"
|
|
16523
|
+
previewMode: previewMode ?? "image",
|
|
16524
|
+
meta: {
|
|
16525
|
+
...common,
|
|
16526
|
+
kind: "timelapse-preview"
|
|
16527
|
+
}
|
|
15823
16528
|
});
|
|
15824
16529
|
try {
|
|
15825
|
-
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
|
+
});
|
|
15826
16535
|
if (url === null) {
|
|
15827
16536
|
ports.logger.warn("timelapse artifact not published: no reachable public endpoint", {
|
|
15828
16537
|
tags,
|
|
@@ -15830,7 +16539,7 @@ function buildTimelapseArtifactPublisher(ports) {
|
|
|
15830
16539
|
});
|
|
15831
16540
|
return {
|
|
15832
16541
|
video: null,
|
|
15833
|
-
preview
|
|
16542
|
+
preview: preview.ref
|
|
15834
16543
|
};
|
|
15835
16544
|
}
|
|
15836
16545
|
ports.logger.info("timelapse artifact published", {
|
|
@@ -15839,7 +16548,7 @@ function buildTimelapseArtifactPublisher(ports) {
|
|
|
15839
16548
|
exportId,
|
|
15840
16549
|
bytes: read.bytes,
|
|
15841
16550
|
name: read.name,
|
|
15842
|
-
preview: preview !== null
|
|
16551
|
+
preview: preview.ref !== null
|
|
15843
16552
|
}
|
|
15844
16553
|
});
|
|
15845
16554
|
return {
|
|
@@ -15848,7 +16557,7 @@ function buildTimelapseArtifactPublisher(ports) {
|
|
|
15848
16557
|
mime: read.contentType,
|
|
15849
16558
|
name: read.name
|
|
15850
16559
|
},
|
|
15851
|
-
preview
|
|
16560
|
+
preview: preview.ref
|
|
15852
16561
|
};
|
|
15853
16562
|
} catch (err) {
|
|
15854
16563
|
ports.logger.warn("timelapse artifact not published: artifact plane refused the bytes", {
|
|
@@ -15860,18 +16569,22 @@ function buildTimelapseArtifactPublisher(ports) {
|
|
|
15860
16569
|
});
|
|
15861
16570
|
return {
|
|
15862
16571
|
video: null,
|
|
15863
|
-
preview
|
|
16572
|
+
preview: preview.ref
|
|
15864
16573
|
};
|
|
15865
16574
|
}
|
|
15866
16575
|
};
|
|
15867
16576
|
}
|
|
16577
|
+
var NO_PREVIEW = {
|
|
16578
|
+
ref: null,
|
|
16579
|
+
artifactId: null
|
|
16580
|
+
};
|
|
15868
16581
|
/**
|
|
15869
16582
|
* The preview half. Best-effort at every step and never throws: the picture is
|
|
15870
16583
|
* an enhancement, the video is the notification.
|
|
15871
16584
|
*/
|
|
15872
16585
|
async function publishPreview(ports, input) {
|
|
15873
16586
|
const render = ports.renderPreview;
|
|
15874
|
-
if (render === void 0 || input.previewText === void 0) return
|
|
16587
|
+
if (render === void 0 || input.previewText === void 0) return NO_PREVIEW;
|
|
15875
16588
|
try {
|
|
15876
16589
|
const rendered = await render({
|
|
15877
16590
|
mp4: input.mp4,
|
|
@@ -15879,23 +16592,29 @@ async function publishPreview(ports, input) {
|
|
|
15879
16592
|
deviceId: input.deviceId,
|
|
15880
16593
|
mode: input.previewMode
|
|
15881
16594
|
});
|
|
15882
|
-
if (rendered === null) return
|
|
15883
|
-
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);
|
|
15884
16597
|
if (url === null) {
|
|
15885
16598
|
ports.logger.warn("timelapse preview not published: no reachable public endpoint", { tags: input.tags });
|
|
15886
|
-
return
|
|
16599
|
+
return {
|
|
16600
|
+
ref: null,
|
|
16601
|
+
artifactId
|
|
16602
|
+
};
|
|
15887
16603
|
}
|
|
15888
16604
|
return {
|
|
15889
|
-
|
|
15890
|
-
|
|
15891
|
-
|
|
16605
|
+
ref: {
|
|
16606
|
+
url,
|
|
16607
|
+
mime: rendered.mime,
|
|
16608
|
+
name: PREVIEW_NAME_BY_MIME[rendered.mime] ?? PREVIEW_FALLBACK_NAME
|
|
16609
|
+
},
|
|
16610
|
+
artifactId
|
|
15892
16611
|
};
|
|
15893
16612
|
} catch (err) {
|
|
15894
16613
|
ports.logger.warn("timelapse preview not published: publish threw", {
|
|
15895
16614
|
tags: input.tags,
|
|
15896
16615
|
meta: { error: String(err) }
|
|
15897
16616
|
});
|
|
15898
|
-
return
|
|
16617
|
+
return NO_PREVIEW;
|
|
15899
16618
|
}
|
|
15900
16619
|
}
|
|
15901
16620
|
/** Title size as a fraction of the frame WIDTH — so it reads the same on a
|
|
@@ -16382,6 +17101,10 @@ function startAlarmPanelConvergence(ports) {
|
|
|
16382
17101
|
* `data-plane-access.spec.ts` asserts they still match the constants their
|
|
16383
17102
|
* handlers register with — a mirror nothing checks is a lie waiting to happen.
|
|
16384
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;
|
|
16385
17108
|
var TRAINING_EXPORT_PREFIX_REF = "training-export";
|
|
16386
17109
|
var RETRAIN_EXPORT_PREFIX_REF = "retrain-export";
|
|
16387
17110
|
var DEBUG_EXPORT_PREFIX_REF = "debug-export";
|
|
@@ -16397,6 +17120,14 @@ var PIPELINE_ANALYTICS_PLANE_ACCESS = {
|
|
|
16397
17120
|
"nc-action": "public",
|
|
16398
17121
|
/** Self-authenticating single-use artifact token (D47). */
|
|
16399
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",
|
|
16400
17131
|
/** Every frame behind a marked selection — a corpus, not an item. */
|
|
16401
17132
|
[TRAINING_EXPORT_PREFIX_REF]: "admin",
|
|
16402
17133
|
[RETRAIN_EXPORT_PREFIX_REF]: "admin",
|
|
@@ -41339,6 +42070,15 @@ var REBUILD_FALLBACK_NODE_ID = "hub";
|
|
|
41339
42070
|
*/
|
|
41340
42071
|
var TIMELAPSE_TRACK_QUERY_LIMIT = 2e3;
|
|
41341
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
|
+
};
|
|
41342
42082
|
/**
|
|
41343
42083
|
* The FULL action catalog for this addon.
|
|
41344
42084
|
*
|
|
@@ -41620,6 +42360,12 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
41620
42360
|
* data-plane handle. Null until served / when the facility is absent. */
|
|
41621
42361
|
ncArtifactPlane = null;
|
|
41622
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;
|
|
41623
42369
|
/**
|
|
41624
42370
|
* Roadmap 4b: ONE cut of an event, in every container the rule asked for.
|
|
41625
42371
|
*
|
|
@@ -41806,6 +42552,31 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
41806
42552
|
* Best-effort at every step: no facility, no secret or no reachable base URL
|
|
41807
42553
|
* ⇒ no plane, and the dispatcher keeps shipping bytes exactly as before.
|
|
41808
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
|
+
}
|
|
41809
42580
|
async serveNcArtifactPlane() {
|
|
41810
42581
|
try {
|
|
41811
42582
|
const secretState = this.state("ncArtifactSecret", NcArtifactSecretSchema, "");
|
|
@@ -41819,9 +42590,18 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
41819
42590
|
logger: this.ctx.logger.child("nc-artifacts")
|
|
41820
42591
|
});
|
|
41821
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;
|
|
41822
42601
|
const plane = new NcArtifactPlane({
|
|
41823
42602
|
store,
|
|
41824
42603
|
secret,
|
|
42604
|
+
index,
|
|
41825
42605
|
logger: this.ctx.logger.child("nc-artifacts"),
|
|
41826
42606
|
routePrefix: `/addon/${this.ctx.id}/nc-artifact`,
|
|
41827
42607
|
listEndpoints: () => this.ncEndpoints()
|
|
@@ -41832,7 +42612,23 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
41832
42612
|
handler: plane.handler
|
|
41833
42613
|
}) ?? null;
|
|
41834
42614
|
this.ncArtifactPlane = this.ncArtifactDataPlane !== null ? plane : null;
|
|
41835
|
-
|
|
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
|
+
} });
|
|
41836
42632
|
} catch (err) {
|
|
41837
42633
|
this.ctx.logger.warn("nc-artifact data-plane failed to serve", { meta: { error: errMsg(err) } });
|
|
41838
42634
|
}
|
|
@@ -42280,7 +43076,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
42280
43076
|
store: center.summaryStore,
|
|
42281
43077
|
runNow: (input) => center.runSummaryNow(input)
|
|
42282
43078
|
},
|
|
42283
|
-
texts: center.textCatalogEditor
|
|
43079
|
+
texts: center.textCatalogEditor,
|
|
43080
|
+
...this.ncArtifactIndex !== null ? { artifacts: this.ncArtifactIndex } : {}
|
|
42284
43081
|
});
|
|
42285
43082
|
}
|
|
42286
43083
|
/** Declare typed collections up-front so the first insert doesn't race with
|
|
@@ -43069,7 +43866,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
43069
43866
|
timestamp,
|
|
43070
43867
|
data: jpeg
|
|
43071
43868
|
}),
|
|
43072
|
-
publish: async (bytes, mime) => await this.ncArtifactPlane?.
|
|
43869
|
+
publish: async (bytes, mime, meta) => await this.ncArtifactPlane?.publishIndexed(bytes, mime, meta) ?? NO_PUBLICATION
|
|
43073
43870
|
};
|
|
43074
43871
|
}
|
|
43075
43872
|
/**
|
|
@@ -43142,7 +43939,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
43142
43939
|
publishArtifact: buildTimelapseArtifactPublisher({
|
|
43143
43940
|
logger: this.ctx.logger.child("Timelapse"),
|
|
43144
43941
|
readExportBytes: ({ exportId }) => api.recordingExport.readExportBytes.query({ exportId }),
|
|
43145
|
-
publish: async (bytes, mime) => await this.ncArtifactPlane?.
|
|
43942
|
+
publish: async (bytes, mime, meta) => await this.ncArtifactPlane?.publishIndexed(bytes, mime, meta) ?? NO_PUBLICATION,
|
|
43146
43943
|
renderPreview: async ({ mode, ...input }) => {
|
|
43147
43944
|
const previewPorts = {
|
|
43148
43945
|
logger: this.ctx.logger.child("Timelapse"),
|
|
@@ -43809,6 +44606,14 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
43809
44606
|
this.retrainExportDataPlane = null;
|
|
43810
44607
|
await this.debugExportDataPlane?.dispose();
|
|
43811
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;
|
|
43812
44617
|
}
|
|
43813
44618
|
async handleInferenceResult(data) {
|
|
43814
44619
|
if (this.shuttingDown) return;
|