@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
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-C2w3cl2p.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -1579,6 +1579,401 @@ var NC_ALARM_DECLARATION = {
|
|
|
1579
1579
|
DeviceClass: NcAlarmPanelDevice
|
|
1580
1580
|
};
|
|
1581
1581
|
//#endregion
|
|
1582
|
+
//#region src/notification-center/artifact-index.ts
|
|
1583
|
+
var NC_ARTIFACTS_COLLECTION = "notification-center:artifacts";
|
|
1584
|
+
var NcArtifactKindSchema = require_dist._enum([
|
|
1585
|
+
"timelapse",
|
|
1586
|
+
"timelapse-preview",
|
|
1587
|
+
"summary-mosaic"
|
|
1588
|
+
]);
|
|
1589
|
+
/**
|
|
1590
|
+
* How the viewer should DRAW it. Derived from the mime once, here, so no client
|
|
1591
|
+
* has to carry a mime table — and so a GIF that degraded to a JPEG is drawn as
|
|
1592
|
+
* the JPEG it actually is.
|
|
1593
|
+
*/
|
|
1594
|
+
var NcArtifactMediaTypeSchema = require_dist._enum([
|
|
1595
|
+
"image",
|
|
1596
|
+
"gif",
|
|
1597
|
+
"video"
|
|
1598
|
+
]);
|
|
1599
|
+
/** The relative hub path of the authenticated bytes route for an artefact id. */
|
|
1600
|
+
var NC_ARTIFACT_READ_PREFIX = "nc-artifact-read";
|
|
1601
|
+
var NcArtifactRecordSchema = require_dist.object({
|
|
1602
|
+
/** The `NcArtifactStore` id — the same id the bytes route resolves. */
|
|
1603
|
+
id: require_dist.string().min(1),
|
|
1604
|
+
kind: NcArtifactKindSchema,
|
|
1605
|
+
mediaType: NcArtifactMediaTypeSchema,
|
|
1606
|
+
mime: require_dist.string().min(1),
|
|
1607
|
+
sizeBytes: require_dist.number().int().nonnegative(),
|
|
1608
|
+
/** When the artefact was PRODUCED. */
|
|
1609
|
+
createdAt: require_dist.number().int(),
|
|
1610
|
+
/** What the content is ABOUT (window end / event time). The sort key: a
|
|
1611
|
+
* timelapse rendered at 06:05 is about the night before, and a list ordered
|
|
1612
|
+
* by production time puts it in the wrong morning. */
|
|
1613
|
+
capturedAt: require_dist.number().int(),
|
|
1614
|
+
/** After this the bytes are gone. Derived from the retained shelf's own age
|
|
1615
|
+
* bound — never a second retention policy. */
|
|
1616
|
+
expiresAt: require_dist.number().int(),
|
|
1617
|
+
/** The camera the artefact is FILED under. Null only if it belongs to none. */
|
|
1618
|
+
deviceId: require_dist.number().int().nullable(),
|
|
1619
|
+
/** Every camera the artefact covers, for display. */
|
|
1620
|
+
deviceIds: require_dist.array(require_dist.number().int()),
|
|
1621
|
+
ruleId: require_dist.string().optional(),
|
|
1622
|
+
ruleName: require_dist.string().optional(),
|
|
1623
|
+
title: require_dist.string().optional(),
|
|
1624
|
+
/** A companion still for a video artefact (a timelapse's preview frame) —
|
|
1625
|
+
* what a list renders as the thumbnail. */
|
|
1626
|
+
posterArtifactId: require_dist.string().optional(),
|
|
1627
|
+
/** RELATIVE hub path of the authenticated bytes route. Relative on purpose:
|
|
1628
|
+
* the viewer already holds the hub origin it is authenticated against, and
|
|
1629
|
+
* only the SIGNED plane needs to guess which host a stranger can reach. */
|
|
1630
|
+
downloadPath: require_dist.string().min(1)
|
|
1631
|
+
});
|
|
1632
|
+
var NcArtifactQuerySchema = require_dist.object({
|
|
1633
|
+
kinds: require_dist.array(NcArtifactKindSchema).optional(),
|
|
1634
|
+
deviceIds: require_dist.array(require_dist.number().int()).optional(),
|
|
1635
|
+
ruleId: require_dist.string().optional(),
|
|
1636
|
+
/** Inclusive `capturedAt` window. */
|
|
1637
|
+
fromMs: require_dist.number().int().optional(),
|
|
1638
|
+
toMs: require_dist.number().int().optional(),
|
|
1639
|
+
limit: require_dist.number().int().min(1).max(200).default(50),
|
|
1640
|
+
offset: require_dist.number().int().min(0).default(0)
|
|
1641
|
+
});
|
|
1642
|
+
var NC_ARTIFACTS_COLUMNS = [
|
|
1643
|
+
{
|
|
1644
|
+
name: "id",
|
|
1645
|
+
type: "TEXT",
|
|
1646
|
+
primaryKey: true,
|
|
1647
|
+
notNull: true
|
|
1648
|
+
},
|
|
1649
|
+
{
|
|
1650
|
+
name: "kind",
|
|
1651
|
+
type: "TEXT",
|
|
1652
|
+
notNull: true
|
|
1653
|
+
},
|
|
1654
|
+
{
|
|
1655
|
+
name: "deviceId",
|
|
1656
|
+
type: "INTEGER"
|
|
1657
|
+
},
|
|
1658
|
+
{
|
|
1659
|
+
name: "ruleId",
|
|
1660
|
+
type: "TEXT"
|
|
1661
|
+
},
|
|
1662
|
+
(
|
|
1663
|
+
/** `''` for a global artefact — see the header note on visibility. */
|
|
1664
|
+
{
|
|
1665
|
+
name: "ownerUserId",
|
|
1666
|
+
type: "TEXT",
|
|
1667
|
+
notNull: true
|
|
1668
|
+
}),
|
|
1669
|
+
{
|
|
1670
|
+
name: "capturedAt",
|
|
1671
|
+
type: "INTEGER",
|
|
1672
|
+
notNull: true
|
|
1673
|
+
},
|
|
1674
|
+
{
|
|
1675
|
+
name: "expiresAt",
|
|
1676
|
+
type: "INTEGER",
|
|
1677
|
+
notNull: true
|
|
1678
|
+
},
|
|
1679
|
+
(
|
|
1680
|
+
/** The FULL record (Zod-validated on read) — the scalars above are indexed
|
|
1681
|
+
* projections only. */
|
|
1682
|
+
{
|
|
1683
|
+
name: "record",
|
|
1684
|
+
type: "JSON",
|
|
1685
|
+
notNull: true
|
|
1686
|
+
})
|
|
1687
|
+
];
|
|
1688
|
+
var NC_ARTIFACTS_INDEXES = [
|
|
1689
|
+
{
|
|
1690
|
+
name: "idx_nc_artifacts_captured",
|
|
1691
|
+
columns: ["capturedAt"]
|
|
1692
|
+
},
|
|
1693
|
+
{
|
|
1694
|
+
name: "idx_nc_artifacts_device",
|
|
1695
|
+
columns: ["deviceId", "capturedAt"]
|
|
1696
|
+
},
|
|
1697
|
+
{
|
|
1698
|
+
name: "idx_nc_artifacts_kind",
|
|
1699
|
+
columns: ["kind", "capturedAt"]
|
|
1700
|
+
},
|
|
1701
|
+
{
|
|
1702
|
+
name: "idx_nc_artifacts_expires",
|
|
1703
|
+
columns: ["expiresAt"]
|
|
1704
|
+
}
|
|
1705
|
+
];
|
|
1706
|
+
/** Upper bound for a half-open `whereBetween` — the store has no `>=`. */
|
|
1707
|
+
var TIME_MAX = 864e13;
|
|
1708
|
+
var TIME_MIN = -864e13;
|
|
1709
|
+
/** Prune scan ceiling — the index is bounded by the shelf it mirrors (2000
|
|
1710
|
+
* entries by default), so this only has to be comfortably above it. */
|
|
1711
|
+
var NC_ARTIFACT_PRUNE_SCAN_LIMIT = 2e4;
|
|
1712
|
+
/** `image/gif` → `gif`. Keyed on what the bytes ARE, never on what was asked
|
|
1713
|
+
* for: a GIF that degraded to a still must be drawn as a still. */
|
|
1714
|
+
function mediaTypeForMime(mime) {
|
|
1715
|
+
if (mime === "image/gif") return "gif";
|
|
1716
|
+
if (mime.startsWith("video/")) return "video";
|
|
1717
|
+
return "image";
|
|
1718
|
+
}
|
|
1719
|
+
/** The relative hub path an authenticated client fetches the bytes from. */
|
|
1720
|
+
function artifactDownloadPath(addonId, artifactId) {
|
|
1721
|
+
return `/addon/${addonId}/${NC_ARTIFACT_READ_PREFIX}/${artifactId}`;
|
|
1722
|
+
}
|
|
1723
|
+
var NcArtifactIndex = class {
|
|
1724
|
+
store;
|
|
1725
|
+
logger;
|
|
1726
|
+
addonId;
|
|
1727
|
+
retentionMs;
|
|
1728
|
+
now;
|
|
1729
|
+
constructor(deps) {
|
|
1730
|
+
this.store = deps.store;
|
|
1731
|
+
this.logger = deps.logger;
|
|
1732
|
+
this.addonId = deps.addonId;
|
|
1733
|
+
this.retentionMs = deps.retentionMs;
|
|
1734
|
+
this.now = deps.now ?? (() => Date.now());
|
|
1735
|
+
}
|
|
1736
|
+
static async declare(store) {
|
|
1737
|
+
await store.declareCollection.mutate({
|
|
1738
|
+
collection: NC_ARTIFACTS_COLLECTION,
|
|
1739
|
+
columns: [...NC_ARTIFACTS_COLUMNS],
|
|
1740
|
+
indexes: [...NC_ARTIFACTS_INDEXES]
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* Record one retained artefact. Returns the row as the viewer will see it, or
|
|
1745
|
+
* null when the write failed — and a failure is LOGGED, per camera: an
|
|
1746
|
+
* artefact silently absent from the index reads to an operator as an artefact
|
|
1747
|
+
* that was never produced.
|
|
1748
|
+
*/
|
|
1749
|
+
async record(input) {
|
|
1750
|
+
const createdAt = this.now();
|
|
1751
|
+
const record = NcArtifactRecordSchema.parse({
|
|
1752
|
+
id: input.id,
|
|
1753
|
+
kind: input.kind,
|
|
1754
|
+
mediaType: mediaTypeForMime(input.mime),
|
|
1755
|
+
mime: input.mime,
|
|
1756
|
+
sizeBytes: input.sizeBytes,
|
|
1757
|
+
createdAt,
|
|
1758
|
+
capturedAt: input.capturedAt,
|
|
1759
|
+
expiresAt: createdAt + this.retentionMs,
|
|
1760
|
+
deviceId: input.deviceId,
|
|
1761
|
+
deviceIds: [...input.deviceIds],
|
|
1762
|
+
...input.ruleId !== void 0 ? { ruleId: input.ruleId } : {},
|
|
1763
|
+
...input.ruleName !== void 0 ? { ruleName: input.ruleName } : {},
|
|
1764
|
+
...input.title !== void 0 ? { title: input.title } : {},
|
|
1765
|
+
...input.posterArtifactId !== void 0 ? { posterArtifactId: input.posterArtifactId } : {},
|
|
1766
|
+
downloadPath: artifactDownloadPath(this.addonId, input.id)
|
|
1767
|
+
});
|
|
1768
|
+
try {
|
|
1769
|
+
await this.store.set.mutate({
|
|
1770
|
+
collection: NC_ARTIFACTS_COLLECTION,
|
|
1771
|
+
key: record.id,
|
|
1772
|
+
value: {
|
|
1773
|
+
kind: record.kind,
|
|
1774
|
+
deviceId: record.deviceId,
|
|
1775
|
+
ruleId: record.ruleId ?? "",
|
|
1776
|
+
ownerUserId: input.ownerUserId ?? "",
|
|
1777
|
+
capturedAt: record.capturedAt,
|
|
1778
|
+
expiresAt: record.expiresAt,
|
|
1779
|
+
record
|
|
1780
|
+
}
|
|
1781
|
+
});
|
|
1782
|
+
return record;
|
|
1783
|
+
} catch (err) {
|
|
1784
|
+
this.logger.warn("artifact not indexed — it will not appear in the viewer", {
|
|
1785
|
+
...record.deviceId !== null ? { tags: { deviceId: record.deviceId } } : {},
|
|
1786
|
+
meta: {
|
|
1787
|
+
artifactId: record.id,
|
|
1788
|
+
kind: record.kind,
|
|
1789
|
+
error: String(err)
|
|
1790
|
+
}
|
|
1791
|
+
});
|
|
1792
|
+
return null;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
/**
|
|
1796
|
+
* One page, newest `capturedAt` first.
|
|
1797
|
+
*
|
|
1798
|
+
* Expiry is a PREDICATE, not a post-filter: a row whose bytes the shelf has
|
|
1799
|
+
* already swept must not consume a slot of the page the caller asked for.
|
|
1800
|
+
*/
|
|
1801
|
+
async list(query, viewer) {
|
|
1802
|
+
const filter = this.buildFilter(query, viewer);
|
|
1803
|
+
try {
|
|
1804
|
+
const [rows, total] = await Promise.all([this.store.query.query({
|
|
1805
|
+
collection: NC_ARTIFACTS_COLLECTION,
|
|
1806
|
+
filter: {
|
|
1807
|
+
...filter,
|
|
1808
|
+
orderBy: {
|
|
1809
|
+
field: "capturedAt",
|
|
1810
|
+
direction: "desc"
|
|
1811
|
+
},
|
|
1812
|
+
limit: query.limit,
|
|
1813
|
+
offset: query.offset
|
|
1814
|
+
}
|
|
1815
|
+
}), this.store.count.query({
|
|
1816
|
+
collection: NC_ARTIFACTS_COLLECTION,
|
|
1817
|
+
filter
|
|
1818
|
+
})]);
|
|
1819
|
+
const artifacts = this.parseRows(rows);
|
|
1820
|
+
return {
|
|
1821
|
+
artifacts,
|
|
1822
|
+
total,
|
|
1823
|
+
hasMore: query.offset + artifacts.length < total
|
|
1824
|
+
};
|
|
1825
|
+
} catch (err) {
|
|
1826
|
+
this.logger.warn("artifact index query failed", { meta: { error: String(err) } });
|
|
1827
|
+
return {
|
|
1828
|
+
artifacts: [],
|
|
1829
|
+
total: 0,
|
|
1830
|
+
hasMore: false
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
/**
|
|
1835
|
+
* One artefact by id, or null when it does not exist, has expired, or belongs
|
|
1836
|
+
* to another user. All three answer the same way ON PURPOSE — a surface that
|
|
1837
|
+
* distinguishes "not yours" from "not there" tells a caller what other people
|
|
1838
|
+
* own.
|
|
1839
|
+
*/
|
|
1840
|
+
async get(artifactId, viewer) {
|
|
1841
|
+
const row = await this.readRow(artifactId);
|
|
1842
|
+
if (row === null) return null;
|
|
1843
|
+
if (row.record.expiresAt <= this.now()) return null;
|
|
1844
|
+
if (!viewer.isAdmin && row.ownerUserId !== "" && row.ownerUserId !== viewer.userId) return null;
|
|
1845
|
+
return row.record;
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Resolve an id for the BYTES route — visibility-free by design.
|
|
1849
|
+
*
|
|
1850
|
+
* The route is served `authenticated`, so the hub has already established a
|
|
1851
|
+
* principal; what this answers is only "is this id an indexed artefact, and is
|
|
1852
|
+
* it still meant to exist". Per-user artefact scoping on the byte route would
|
|
1853
|
+
* be a second authorization model layered on an unguessable id, and this repo
|
|
1854
|
+
* has one of those already (`data-plane-access.ts`): a plane serving ONE item
|
|
1855
|
+
* the caller had to be told about is `authenticated`.
|
|
1856
|
+
*/
|
|
1857
|
+
async resolve(artifactId) {
|
|
1858
|
+
return (await this.readRow(artifactId))?.record ?? null;
|
|
1859
|
+
}
|
|
1860
|
+
/** One row by primary key, parsed. Null when absent or no longer valid. */
|
|
1861
|
+
async readRow(artifactId) {
|
|
1862
|
+
let raw;
|
|
1863
|
+
try {
|
|
1864
|
+
raw = await this.store.get.query({
|
|
1865
|
+
collection: NC_ARTIFACTS_COLLECTION,
|
|
1866
|
+
key: artifactId
|
|
1867
|
+
});
|
|
1868
|
+
} catch (err) {
|
|
1869
|
+
this.logger.warn("artifact index lookup failed", { meta: {
|
|
1870
|
+
artifactId,
|
|
1871
|
+
error: String(err)
|
|
1872
|
+
} });
|
|
1873
|
+
return null;
|
|
1874
|
+
}
|
|
1875
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
|
1876
|
+
const row = { ...raw };
|
|
1877
|
+
const parsed = NcArtifactRecordSchema.safeParse(row["record"]);
|
|
1878
|
+
if (!parsed.success) return null;
|
|
1879
|
+
const owner = row["ownerUserId"];
|
|
1880
|
+
return {
|
|
1881
|
+
record: parsed.data,
|
|
1882
|
+
ownerUserId: typeof owner === "string" ? owner : ""
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Drop rows whose bytes are gone, in ONE `deleteWhere` rather than an N+1
|
|
1887
|
+
* drain.
|
|
1888
|
+
*
|
|
1889
|
+
* TWO halves, and only one of them is safe everywhere:
|
|
1890
|
+
*
|
|
1891
|
+
* - **Expired rows** are time-based and node-independent. Always swept.
|
|
1892
|
+
* - **Orphans** — rows whose file a byte/count bound took before the age bound
|
|
1893
|
+
* did — are found by reconciling against the shelf. That reconcile is only
|
|
1894
|
+
* valid for a caller whose shelf holds the bytes for the WHOLE index, and
|
|
1895
|
+
* the index is CENTRAL while the shelf is local: a second node running this
|
|
1896
|
+
* with its own (empty) shelf would read every row as an orphan and delete
|
|
1897
|
+
* the entire library. Hence `keptIds: null` — a caller that is not the
|
|
1898
|
+
* producing node states so, and the destructive half becomes unreachable
|
|
1899
|
+
* rather than merely un-called.
|
|
1900
|
+
*
|
|
1901
|
+
* @param keptIds the ids the caller's shelf holds, or `null` when this node is
|
|
1902
|
+
* not the one that produces artefacts.
|
|
1903
|
+
*/
|
|
1904
|
+
async prune(keptIds) {
|
|
1905
|
+
let removed = 0;
|
|
1906
|
+
try {
|
|
1907
|
+
const expired = await this.store.deleteWhere.mutate({
|
|
1908
|
+
collection: NC_ARTIFACTS_COLLECTION,
|
|
1909
|
+
filter: { whereBetween: { expiresAt: [TIME_MIN, this.now() - 1] } }
|
|
1910
|
+
});
|
|
1911
|
+
removed += expired.deleted;
|
|
1912
|
+
if (keptIds === null) {
|
|
1913
|
+
if (removed > 0) this.logger.debug("artifact index pruned (expired only)", { meta: { removed } });
|
|
1914
|
+
return removed;
|
|
1915
|
+
}
|
|
1916
|
+
const alive = new Set(keptIds);
|
|
1917
|
+
const orphans = (await this.store.query.query({
|
|
1918
|
+
collection: NC_ARTIFACTS_COLLECTION,
|
|
1919
|
+
filter: { limit: NC_ARTIFACT_PRUNE_SCAN_LIMIT }
|
|
1920
|
+
})).map((r) => r.id).filter((id) => !alive.has(id));
|
|
1921
|
+
for (const id of orphans) {
|
|
1922
|
+
await this.store.delete.mutate({
|
|
1923
|
+
collection: NC_ARTIFACTS_COLLECTION,
|
|
1924
|
+
key: id
|
|
1925
|
+
});
|
|
1926
|
+
removed += 1;
|
|
1927
|
+
}
|
|
1928
|
+
if (removed > 0) this.logger.debug("artifact index pruned", { meta: {
|
|
1929
|
+
removed,
|
|
1930
|
+
expired: expired.deleted,
|
|
1931
|
+
orphaned: orphans.length
|
|
1932
|
+
} });
|
|
1933
|
+
return removed;
|
|
1934
|
+
} catch (err) {
|
|
1935
|
+
this.logger.warn("artifact index prune failed", { meta: { error: String(err) } });
|
|
1936
|
+
return removed;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
/** The visibility predicate: a caller sees global artefacts plus his own; an
|
|
1940
|
+
* admin sees every one. */
|
|
1941
|
+
visibility(viewer) {
|
|
1942
|
+
if (viewer.isAdmin) return {};
|
|
1943
|
+
return { whereIn: { ownerUserId: ["", viewer.userId] } };
|
|
1944
|
+
}
|
|
1945
|
+
buildFilter(query, viewer) {
|
|
1946
|
+
const whereIn = { ...this.visibility(viewer).whereIn };
|
|
1947
|
+
if (query.kinds !== void 0 && query.kinds.length > 0) whereIn["kind"] = [...query.kinds];
|
|
1948
|
+
if (query.deviceIds !== void 0 && query.deviceIds.length > 0) whereIn["deviceId"] = [...query.deviceIds];
|
|
1949
|
+
const whereBetween = {
|
|
1950
|
+
capturedAt: [query.fromMs ?? TIME_MIN, query.toMs ?? TIME_MAX],
|
|
1951
|
+
expiresAt: [this.now(), TIME_MAX]
|
|
1952
|
+
};
|
|
1953
|
+
return {
|
|
1954
|
+
...query.ruleId !== void 0 ? { where: { ruleId: query.ruleId } } : {},
|
|
1955
|
+
...Object.keys(whereIn).length > 0 ? { whereIn } : {},
|
|
1956
|
+
whereBetween
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
/** Parse the JSON column, skipping rows that no longer validate. A degraded
|
|
1960
|
+
* row must never cost the whole page. */
|
|
1961
|
+
parseRows(rows) {
|
|
1962
|
+
const out = [];
|
|
1963
|
+
let skipped = 0;
|
|
1964
|
+
for (const row of rows) {
|
|
1965
|
+
const parsed = NcArtifactRecordSchema.safeParse(row.data["record"]);
|
|
1966
|
+
if (!parsed.success) {
|
|
1967
|
+
skipped += 1;
|
|
1968
|
+
continue;
|
|
1969
|
+
}
|
|
1970
|
+
out.push(parsed.data);
|
|
1971
|
+
}
|
|
1972
|
+
if (skipped > 0) this.logger.warn("artifact index rows skipped as invalid", { meta: { skipped } });
|
|
1973
|
+
return out;
|
|
1974
|
+
}
|
|
1975
|
+
};
|
|
1976
|
+
//#endregion
|
|
1582
1977
|
//#region ../types/dist/node.mjs
|
|
1583
1978
|
var node_exports = /* @__PURE__ */ require_dist.__exportAll({
|
|
1584
1979
|
FilesystemStorageProvider: () => FilesystemStorageProvider,
|
|
@@ -1969,6 +2364,61 @@ var NcArtifactPlane = class {
|
|
|
1969
2364
|
}
|
|
1970
2365
|
}
|
|
1971
2366
|
/**
|
|
2367
|
+
* Publish bytes AND file them in the viewer's browsable index (task #36).
|
|
2368
|
+
*
|
|
2369
|
+
* Two differences from {@link publish}, both deliberate:
|
|
2370
|
+
*
|
|
2371
|
+
* 1. **The bytes are stored FIRST, and stored even when no public base URL
|
|
2372
|
+
* exists.** `publish` returns early on a loopback-only install because a
|
|
2373
|
+
* signed link nobody can open is worth nothing — but the VIEWER reaches
|
|
2374
|
+
* the hub directly over its own session, so an install with no tunnel
|
|
2375
|
+
* still gets a full artifact library. Making the index depend on a
|
|
2376
|
+
* reachable public address would have made the feature absent exactly on
|
|
2377
|
+
* the installs that are LAN-only.
|
|
2378
|
+
* 2. **It answers the artefact id**, so a caller can link one artefact to
|
|
2379
|
+
* another — a timelapse's poster is its preview frame, and the video row
|
|
2380
|
+
* cannot name it without knowing the id the preview got.
|
|
2381
|
+
*
|
|
2382
|
+
* Never throws, for the same reason `publish` does not: an artefact library
|
|
2383
|
+
* is an enhancement and must never cost a notification.
|
|
2384
|
+
*/
|
|
2385
|
+
async publishIndexed(bytes, mime, meta) {
|
|
2386
|
+
let artifactId = null;
|
|
2387
|
+
try {
|
|
2388
|
+
const index = this.deps.index;
|
|
2389
|
+
const stored = await this.deps.store.put(bytes, mime, { retain: index !== void 0 });
|
|
2390
|
+
if (index !== void 0) artifactId = (await index.record({
|
|
2391
|
+
...meta,
|
|
2392
|
+
id: stored.id,
|
|
2393
|
+
mime,
|
|
2394
|
+
sizeBytes: stored.bytes
|
|
2395
|
+
}))?.id ?? null;
|
|
2396
|
+
const baseUrl = await this.resolveBaseUrl();
|
|
2397
|
+
return {
|
|
2398
|
+
url: baseUrl === null ? null : buildArtifactUrl({
|
|
2399
|
+
baseUrl,
|
|
2400
|
+
routePrefix: this.deps.routePrefix,
|
|
2401
|
+
id: stored.id,
|
|
2402
|
+
secret: this.deps.secret,
|
|
2403
|
+
expMs: this.now() + (this.deps.ttlMs ?? DEFAULT_TTL_MS)
|
|
2404
|
+
}),
|
|
2405
|
+
artifactId
|
|
2406
|
+
};
|
|
2407
|
+
} catch (err) {
|
|
2408
|
+
this.deps.logger.warn("indexed artifact publish failed — shipping bytes only", {
|
|
2409
|
+
...meta.deviceId !== null ? { tags: { deviceId: meta.deviceId } } : {},
|
|
2410
|
+
meta: {
|
|
2411
|
+
kind: meta.kind,
|
|
2412
|
+
error: String(err)
|
|
2413
|
+
}
|
|
2414
|
+
});
|
|
2415
|
+
return {
|
|
2416
|
+
url: null,
|
|
2417
|
+
artifactId
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
/**
|
|
1972
2422
|
* Absolutize a path against the SAME base URL an artifact link would use.
|
|
1973
2423
|
*
|
|
1974
2424
|
* For links this plane does not host but a RECIPIENT still has to open — a
|
|
@@ -2034,6 +2484,124 @@ var NcArtifactPlane = class {
|
|
|
2034
2484
|
}
|
|
2035
2485
|
};
|
|
2036
2486
|
//#endregion
|
|
2487
|
+
//#region src/notification-center/artifact-read-plane.ts
|
|
2488
|
+
/**
|
|
2489
|
+
* Parse a single-range `Range` header against a known length.
|
|
2490
|
+
*
|
|
2491
|
+
* Returns `null` when there is nothing to honour (absent, malformed, multi-range
|
|
2492
|
+
* — all of which mean "send the whole thing"), and `'unsatisfiable'` when the
|
|
2493
|
+
* client asked for a range that cannot exist, which is a 416 and not a 200.
|
|
2494
|
+
*/
|
|
2495
|
+
function parseByteRange(header, length) {
|
|
2496
|
+
if (header === void 0) return null;
|
|
2497
|
+
const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
|
|
2498
|
+
if (match === null) return null;
|
|
2499
|
+
const rawStart = match[1] ?? "";
|
|
2500
|
+
const rawEnd = match[2] ?? "";
|
|
2501
|
+
if (rawStart === "" && rawEnd === "") return null;
|
|
2502
|
+
if (length === 0) return "unsatisfiable";
|
|
2503
|
+
if (rawStart === "") {
|
|
2504
|
+
const suffix = Number(rawEnd);
|
|
2505
|
+
if (suffix === 0) return "unsatisfiable";
|
|
2506
|
+
return {
|
|
2507
|
+
start: Math.max(0, length - suffix),
|
|
2508
|
+
end: length - 1
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
const start = Number(rawStart);
|
|
2512
|
+
if (start >= length) return "unsatisfiable";
|
|
2513
|
+
const end = rawEnd === "" ? length - 1 : Math.min(Number(rawEnd), length - 1);
|
|
2514
|
+
if (end < start) return "unsatisfiable";
|
|
2515
|
+
return {
|
|
2516
|
+
start,
|
|
2517
|
+
end
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
/** The last path segment, percent-decoded. */
|
|
2521
|
+
function artifactIdFrom(req) {
|
|
2522
|
+
const url = new URL(req.url ?? "/", "http://placeholder");
|
|
2523
|
+
try {
|
|
2524
|
+
return decodeURIComponent(url.pathname.split("/").filter(Boolean).pop() ?? "");
|
|
2525
|
+
} catch {
|
|
2526
|
+
return "";
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
var NcArtifactReadPlane = class {
|
|
2530
|
+
deps;
|
|
2531
|
+
constructor(deps) {
|
|
2532
|
+
this.deps = deps;
|
|
2533
|
+
}
|
|
2534
|
+
handler = async (req, res) => {
|
|
2535
|
+
const artifactId = artifactIdFrom(req);
|
|
2536
|
+
const record = await this.deps.index.resolve(artifactId);
|
|
2537
|
+
if (record === null) {
|
|
2538
|
+
this.refuse(res, 404, "not found");
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
const found = await this.deps.store.read(artifactId);
|
|
2542
|
+
if (found === null) {
|
|
2543
|
+
this.deps.logger.warn("artifact bytes gone — index row outlived the shelf", {
|
|
2544
|
+
...record.deviceId !== null ? { tags: { deviceId: record.deviceId } } : {},
|
|
2545
|
+
meta: {
|
|
2546
|
+
artifactId,
|
|
2547
|
+
kind: record.kind,
|
|
2548
|
+
expiresAt: record.expiresAt
|
|
2549
|
+
}
|
|
2550
|
+
});
|
|
2551
|
+
this.refuse(res, 404, "not found");
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
if (req.method === "HEAD") {
|
|
2555
|
+
res.writeHead(200, this.baseHeaders(record, found.bytes.byteLength, found.mime));
|
|
2556
|
+
res.end();
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2559
|
+
const range = parseByteRange(headerOf(req, "range"), found.bytes.byteLength);
|
|
2560
|
+
if (range === "unsatisfiable") {
|
|
2561
|
+
res.writeHead(416, {
|
|
2562
|
+
"content-range": `bytes */${found.bytes.byteLength}`,
|
|
2563
|
+
"content-type": "text/plain"
|
|
2564
|
+
});
|
|
2565
|
+
res.end("range not satisfiable");
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
if (range === null) {
|
|
2569
|
+
res.writeHead(200, this.baseHeaders(record, found.bytes.byteLength, found.mime));
|
|
2570
|
+
res.end(found.bytes);
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
const slice = found.bytes.subarray(range.start, range.end + 1);
|
|
2574
|
+
res.writeHead(206, {
|
|
2575
|
+
...this.baseHeaders(record, slice.byteLength, found.mime),
|
|
2576
|
+
"content-range": `bytes ${range.start}-${range.end}/${found.bytes.byteLength}`
|
|
2577
|
+
});
|
|
2578
|
+
res.end(slice);
|
|
2579
|
+
};
|
|
2580
|
+
baseHeaders(record, length, mime) {
|
|
2581
|
+
return {
|
|
2582
|
+
"content-type": mime,
|
|
2583
|
+
"content-length": String(length),
|
|
2584
|
+
"accept-ranges": "bytes",
|
|
2585
|
+
"cache-control": "private, max-age=3600",
|
|
2586
|
+
"content-disposition": `inline; filename="${filenameFor(record)}"`
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
refuse(res, status, body) {
|
|
2590
|
+
res.writeHead(status, { "content-type": "text/plain" });
|
|
2591
|
+
res.end(body);
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
/** A name a client will accept, from what the artefact IS. */
|
|
2595
|
+
function filenameFor(record) {
|
|
2596
|
+
const ext = record.mime === "video/mp4" ? "mp4" : record.mime === "image/gif" ? "gif" : record.mime === "image/png" ? "png" : "jpg";
|
|
2597
|
+
return `${record.kind}-${record.capturedAt}.${ext}`;
|
|
2598
|
+
}
|
|
2599
|
+
function headerOf(req, name) {
|
|
2600
|
+
const value = req.headers[name];
|
|
2601
|
+
if (Array.isArray(value)) return value[0];
|
|
2602
|
+
return value;
|
|
2603
|
+
}
|
|
2604
|
+
//#endregion
|
|
2037
2605
|
//#region src/notification-center/artifact-store.ts
|
|
2038
2606
|
/**
|
|
2039
2607
|
* NcArtifactStore — the bytes behind a signed notification-artifact URL.
|
|
@@ -2048,9 +2616,25 @@ var NcArtifactPlane = class {
|
|
|
2048
2616
|
* bites first, the oldest artifacts go. A sweep on boot clears whatever a crash
|
|
2049
2617
|
* left behind — the directory is disposable by construction, so a lost file is
|
|
2050
2618
|
* a 404 on one link, never a corrupt state.
|
|
2619
|
+
*
|
|
2620
|
+
* ## The retained shelf (`kept/`)
|
|
2621
|
+
*
|
|
2622
|
+
* Task #36 gave the viewer a browsable artifact index, and an index whose bytes
|
|
2623
|
+
* evaporate in six hours is an index of 404s. So a `put` may ask to be RETAINED:
|
|
2624
|
+
* the bytes then land on a second shelf inside the same directory with its OWN
|
|
2625
|
+
* three bounds (age, count, and — because a timelapse mp4 is megabytes — total
|
|
2626
|
+
* BYTES). It is still a scratch, just a longer one: the index row carries an
|
|
2627
|
+
* `expiresAt` derived from the same age bound, so "gone" is a state the viewer
|
|
2628
|
+
* is told about rather than a surprise 404.
|
|
2629
|
+
*
|
|
2630
|
+
* The shelf is a subdirectory ON PURPOSE. The scratch's `list()` skips
|
|
2631
|
+
* non-files, so the two shelves cannot sweep each other, and neither bound had
|
|
2632
|
+
* to learn about the other's entries.
|
|
2051
2633
|
*/
|
|
2052
2634
|
var DEFAULT_MAX_AGE_MS = 6 * 36e5;
|
|
2053
2635
|
var DEFAULT_MAX_ENTRIES = 500;
|
|
2636
|
+
/** The retained shelf's subdirectory name, inside the scratch directory. */
|
|
2637
|
+
var KEPT_SUBDIR = "kept";
|
|
2054
2638
|
/** `image/jpeg` → `jpg`, so a fetched link has a name a client will accept. */
|
|
2055
2639
|
function extensionFor(mime) {
|
|
2056
2640
|
if (mime === "image/jpeg") return "jpg";
|
|
@@ -2064,6 +2648,10 @@ var NcArtifactStore = class {
|
|
|
2064
2648
|
now;
|
|
2065
2649
|
maxAgeMs;
|
|
2066
2650
|
maxEntries;
|
|
2651
|
+
keptMaxAgeMs;
|
|
2652
|
+
keptMaxEntries;
|
|
2653
|
+
keptMaxBytes;
|
|
2654
|
+
keptDir;
|
|
2067
2655
|
/** id → mime, so the route can answer with the right content-type without
|
|
2068
2656
|
* re-deriving it from the extension on every request. */
|
|
2069
2657
|
mimeById = /* @__PURE__ */ new Map();
|
|
@@ -2072,17 +2660,32 @@ var NcArtifactStore = class {
|
|
|
2072
2660
|
this.now = options.now ?? (() => Date.now());
|
|
2073
2661
|
this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
2074
2662
|
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
2663
|
+
this.keptMaxAgeMs = options.keptMaxAgeMs ?? 12096e5;
|
|
2664
|
+
this.keptMaxEntries = options.keptMaxEntries ?? 2e3;
|
|
2665
|
+
this.keptMaxBytes = options.keptMaxBytes ?? 2147483648;
|
|
2666
|
+
this.keptDir = node_path.default.join(options.dir, KEPT_SUBDIR);
|
|
2667
|
+
}
|
|
2668
|
+
/** How long a retained artifact stays fetchable — the index's `expiresAt`
|
|
2669
|
+
* is derived from this, so the two can never disagree. */
|
|
2670
|
+
get retentionMs() {
|
|
2671
|
+
return this.keptMaxAgeMs;
|
|
2075
2672
|
}
|
|
2076
2673
|
/** Create the directory and sweep whatever a previous run left behind. */
|
|
2077
2674
|
async start() {
|
|
2078
2675
|
await node_fs.promises.mkdir(this.options.dir, { recursive: true });
|
|
2079
2676
|
await this.sweep();
|
|
2080
2677
|
}
|
|
2081
|
-
/**
|
|
2082
|
-
|
|
2678
|
+
/**
|
|
2679
|
+
* Persist bytes and return the artifact handle (never throws on sweep).
|
|
2680
|
+
*
|
|
2681
|
+
* The retained shelf is created LAZILY: an install that never produces a
|
|
2682
|
+
* browsable artifact keeps a directory that is empty when it is empty.
|
|
2683
|
+
*/
|
|
2684
|
+
async put(bytes, mime, options = {}) {
|
|
2083
2685
|
const id = `${this.now()}-${(0, node_crypto.randomUUID)()}`;
|
|
2084
|
-
const
|
|
2085
|
-
|
|
2686
|
+
const dir = options.retain === true ? this.keptDir : this.options.dir;
|
|
2687
|
+
const file = node_path.default.join(dir, `${id}.${extensionFor(mime)}`);
|
|
2688
|
+
await node_fs.promises.mkdir(dir, { recursive: true });
|
|
2086
2689
|
await node_fs.promises.writeFile(file, bytes);
|
|
2087
2690
|
this.mimeById.set(id, mime);
|
|
2088
2691
|
await this.sweep();
|
|
@@ -2095,7 +2698,7 @@ var NcArtifactStore = class {
|
|
|
2095
2698
|
/** Read an artifact's bytes + mime, or null when it is gone (expired/swept). */
|
|
2096
2699
|
async read(id) {
|
|
2097
2700
|
if (!/^[0-9]+-[0-9a-f-]{36}$/i.test(id)) return null;
|
|
2098
|
-
const found = (await this.list()
|
|
2701
|
+
const found = [...await this.list(this.options.dir), ...await this.list(this.keptDir)].find((e) => e.id === id);
|
|
2099
2702
|
if (!found) return null;
|
|
2100
2703
|
try {
|
|
2101
2704
|
return {
|
|
@@ -2106,6 +2709,10 @@ var NcArtifactStore = class {
|
|
|
2106
2709
|
return null;
|
|
2107
2710
|
}
|
|
2108
2711
|
}
|
|
2712
|
+
/** The ids still on the retained shelf — what the index reconciles against. */
|
|
2713
|
+
async keptIds() {
|
|
2714
|
+
return (await this.list(this.keptDir)).map((e) => e.id);
|
|
2715
|
+
}
|
|
2109
2716
|
/** Delete every artifact (operator cleanup / shutdown). */
|
|
2110
2717
|
async clear() {
|
|
2111
2718
|
await node_fs.promises.rm(this.options.dir, {
|
|
@@ -2114,13 +2721,39 @@ var NcArtifactStore = class {
|
|
|
2114
2721
|
}).catch(() => {});
|
|
2115
2722
|
this.mimeById.clear();
|
|
2116
2723
|
}
|
|
2117
|
-
/**
|
|
2724
|
+
/** Prune both shelves under their own bounds. Best-effort: a failed unlink is
|
|
2725
|
+
* logged once and the rest of the sweep still runs. */
|
|
2118
2726
|
async sweep() {
|
|
2119
|
-
|
|
2120
|
-
|
|
2727
|
+
await this.sweepShelf(await this.list(this.options.dir), {
|
|
2728
|
+
maxAgeMs: this.maxAgeMs,
|
|
2729
|
+
maxEntries: this.maxEntries
|
|
2730
|
+
});
|
|
2731
|
+
await this.sweepShelf(await this.list(this.keptDir), {
|
|
2732
|
+
maxAgeMs: this.keptMaxAgeMs,
|
|
2733
|
+
maxEntries: this.keptMaxEntries,
|
|
2734
|
+
maxBytes: this.keptMaxBytes
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
/**
|
|
2738
|
+
* Age first, then count, then total bytes — newest always survives.
|
|
2739
|
+
*
|
|
2740
|
+
* The newest entry is never swept by the byte bound even when it alone
|
|
2741
|
+
* exceeds it: a shelf that deletes what it has just written can serve
|
|
2742
|
+
* nothing, and the caller has already paid for the render.
|
|
2743
|
+
*/
|
|
2744
|
+
async sweepShelf(entries, bounds) {
|
|
2745
|
+
const cutoff = this.now() - bounds.maxAgeMs;
|
|
2121
2746
|
const doomed = entries.filter((e) => e.mtimeMs < cutoff);
|
|
2122
|
-
const
|
|
2123
|
-
doomed.push(...
|
|
2747
|
+
const fresh = entries.filter((e) => e.mtimeMs >= cutoff).toSorted((a, b) => b.mtimeMs - a.mtimeMs);
|
|
2748
|
+
doomed.push(...fresh.slice(bounds.maxEntries));
|
|
2749
|
+
const maxBytes = bounds.maxBytes;
|
|
2750
|
+
if (maxBytes !== void 0) {
|
|
2751
|
+
let running = 0;
|
|
2752
|
+
for (const [i, e] of fresh.slice(0, bounds.maxEntries).entries()) {
|
|
2753
|
+
running += e.sizeBytes;
|
|
2754
|
+
if (running > maxBytes && i > 0) doomed.push(e);
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2124
2757
|
for (const e of doomed) try {
|
|
2125
2758
|
await node_fs.promises.rm(e.file, { force: true });
|
|
2126
2759
|
this.mimeById.delete(e.id);
|
|
@@ -2131,23 +2764,24 @@ var NcArtifactStore = class {
|
|
|
2131
2764
|
} });
|
|
2132
2765
|
}
|
|
2133
2766
|
}
|
|
2134
|
-
async list() {
|
|
2767
|
+
async list(dir) {
|
|
2135
2768
|
let names;
|
|
2136
2769
|
try {
|
|
2137
|
-
names = await node_fs.promises.readdir(
|
|
2770
|
+
names = await node_fs.promises.readdir(dir);
|
|
2138
2771
|
} catch {
|
|
2139
2772
|
return [];
|
|
2140
2773
|
}
|
|
2141
2774
|
const out = [];
|
|
2142
2775
|
for (const name of names) {
|
|
2143
|
-
const file = node_path.default.join(
|
|
2776
|
+
const file = node_path.default.join(dir, name);
|
|
2144
2777
|
try {
|
|
2145
2778
|
const st = await node_fs.promises.stat(file);
|
|
2146
2779
|
if (!st.isFile()) continue;
|
|
2147
2780
|
out.push({
|
|
2148
2781
|
id: name.slice(0, name.lastIndexOf(".")),
|
|
2149
2782
|
file,
|
|
2150
|
-
mtimeMs: st.mtimeMs
|
|
2783
|
+
mtimeMs: st.mtimeMs,
|
|
2784
|
+
sizeBytes: st.size
|
|
2151
2785
|
});
|
|
2152
2786
|
} catch {}
|
|
2153
2787
|
}
|
|
@@ -10011,7 +10645,15 @@ var NcSummaryProducer = class {
|
|
|
10011
10645
|
}
|
|
10012
10646
|
const bytes = new Uint8Array(rendered.jpeg.byteLength);
|
|
10013
10647
|
bytes.set(rendered.jpeg);
|
|
10014
|
-
const url = await this.deps.publish(bytes, "image/jpeg"
|
|
10648
|
+
const { url } = await this.deps.publish(bytes, "image/jpeg", {
|
|
10649
|
+
kind: "summary-mosaic",
|
|
10650
|
+
deviceId: rule.deviceIds[0] ?? null,
|
|
10651
|
+
deviceIds: [...new Set(tiles.map((t) => t.candidate.deviceId))],
|
|
10652
|
+
capturedAt: window.endMs,
|
|
10653
|
+
ruleId: rule.id,
|
|
10654
|
+
ruleName: rule.name,
|
|
10655
|
+
...rule.ownerUserId !== void 0 ? { ownerUserId: rule.ownerUserId } : {}
|
|
10656
|
+
});
|
|
10015
10657
|
if (url === null) {
|
|
10016
10658
|
this.deps.logger.warn("summary mosaic not published: no reachable public endpoint", { meta: { ruleId: rule.id } });
|
|
10017
10659
|
return {
|
|
@@ -11013,7 +11655,7 @@ function unknownNcTextKeys(keys, bundles = NC_TEXT_BUNDLES) {
|
|
|
11013
11655
|
* durable document stays the sparse map its schema describes.
|
|
11014
11656
|
*/
|
|
11015
11657
|
function applyNcTextOverridePatch(current, language, patch) {
|
|
11016
|
-
const next = { ...current[language]
|
|
11658
|
+
const next = { ...current[language] };
|
|
11017
11659
|
for (const [key, value] of Object.entries(patch)) {
|
|
11018
11660
|
if (value === null || value.trim().length === 0) {
|
|
11019
11661
|
delete next[key];
|
|
@@ -11581,7 +12223,7 @@ var TimelapseRenderer = class {
|
|
|
11581
12223
|
}
|
|
11582
12224
|
});
|
|
11583
12225
|
const finished = await this.awaitReady(queued, tags);
|
|
11584
|
-
const published = await this.publish(finished.id,
|
|
12226
|
+
const published = await this.publish(finished.id, request, tags);
|
|
11585
12227
|
const downloadUrl = await this.ports.resolveDownloadUrl?.({ exportId: finished.id }).catch(() => null);
|
|
11586
12228
|
return {
|
|
11587
12229
|
exportId: finished.id,
|
|
@@ -11633,7 +12275,9 @@ var TimelapseRenderer = class {
|
|
|
11633
12275
|
* notification, not a failed window — and failing here would leave the pair
|
|
11634
12276
|
* unstamped and re-render the same eight hours on the next tick.
|
|
11635
12277
|
*/
|
|
11636
|
-
async publish(exportId,
|
|
12278
|
+
async publish(exportId, request, tags) {
|
|
12279
|
+
const { deviceId, previewText } = request;
|
|
12280
|
+
const previewMode = request.previewMode ?? "image";
|
|
11637
12281
|
const publish = this.ports.publishArtifact;
|
|
11638
12282
|
const none = {
|
|
11639
12283
|
video: null,
|
|
@@ -11653,7 +12297,11 @@ var TimelapseRenderer = class {
|
|
|
11653
12297
|
...previewText !== void 0 ? {
|
|
11654
12298
|
previewText,
|
|
11655
12299
|
previewMode
|
|
11656
|
-
} : {}
|
|
12300
|
+
} : {},
|
|
12301
|
+
capturedAt: request.window.endMs,
|
|
12302
|
+
ruleId: request.ruleId,
|
|
12303
|
+
ruleName: request.ruleName,
|
|
12304
|
+
...request.ownerUserId !== void 0 ? { ownerUserId: request.ownerUserId } : {}
|
|
11657
12305
|
});
|
|
11658
12306
|
if (set.video === null) this.ports.logger.warn("timelapse artifact could not be published — link-less delivery", {
|
|
11659
12307
|
tags,
|
|
@@ -12009,7 +12657,8 @@ var TimelapseScheduler = class {
|
|
|
12009
12657
|
...rule.previewText !== void 0 ? { text: rule.previewText } : {}
|
|
12010
12658
|
}),
|
|
12011
12659
|
previewMode: rule.previewMode,
|
|
12012
|
-
...rule.reportClasses !== void 0 ? { reportClasses: rule.reportClasses } : {}
|
|
12660
|
+
...rule.reportClasses !== void 0 ? { reportClasses: rule.reportClasses } : {},
|
|
12661
|
+
...rule.ownerUserId !== void 0 ? { ownerUserId: rule.ownerUserId } : {}
|
|
12013
12662
|
});
|
|
12014
12663
|
const rows = buildTimelapseOutboxInputs({
|
|
12015
12664
|
texts: this.texts,
|
|
@@ -13325,6 +13974,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
13325
13974
|
await TimelapseStore.declare(store);
|
|
13326
13975
|
await NcSummaryStore.declare(store);
|
|
13327
13976
|
await NcTextCatalogStore.declare(store);
|
|
13977
|
+
await NcArtifactIndex.declare(store);
|
|
13328
13978
|
}
|
|
13329
13979
|
/**
|
|
13330
13980
|
* Load rules (every node — the cap provider serves CRUD from any node).
|
|
@@ -15382,7 +16032,23 @@ var ncActions = require_dist.defineCustomActions({
|
|
|
15382
16032
|
}), NcTextCatalogViewSchema, {
|
|
15383
16033
|
kind: "mutation",
|
|
15384
16034
|
auth: "admin"
|
|
15385
|
-
})
|
|
16035
|
+
}),
|
|
16036
|
+
/**
|
|
16037
|
+
* One page of artefacts, newest `capturedAt` first.
|
|
16038
|
+
*
|
|
16039
|
+
* `deviceIds` filters on the camera an artefact is FILED under. A digest
|
|
16040
|
+
* spans cameras and is filed under its rule's first one (the same camera its
|
|
16041
|
+
* outbox row uses); to ask "everything this digest produced" use `ruleId`.
|
|
16042
|
+
*/
|
|
16043
|
+
"nc.listArtifacts": require_dist.customAction(NcArtifactQuerySchema, require_dist.object({
|
|
16044
|
+
artifacts: require_dist.array(NcArtifactRecordSchema),
|
|
16045
|
+
/** Rows matching the filter, not rows on this page. */
|
|
16046
|
+
total: require_dist.number().int().nonnegative(),
|
|
16047
|
+
hasMore: require_dist.boolean()
|
|
16048
|
+
}), { caller: "required" }),
|
|
16049
|
+
/** One artefact's metadata. `null` for unknown, expired AND not-yours alike —
|
|
16050
|
+
* a surface that distinguishes them reports what other people own. */
|
|
16051
|
+
"nc.getArtifact": require_dist.customAction(require_dist.object({ artifactId: require_dist.string().min(1) }), require_dist.object({ artifact: NcArtifactRecordSchema.nullable() }), { caller: "required" })
|
|
15386
16052
|
});
|
|
15387
16053
|
/** Fail-closed caller resolution — an absent forwarded caller is NEVER admin. */
|
|
15388
16054
|
function requireCaller(caller) {
|
|
@@ -15432,6 +16098,12 @@ function makeNcActionHandlers(deps) {
|
|
|
15432
16098
|
if (sum === void 0) throw new Error("this node does not host the summary producer — use the hub");
|
|
15433
16099
|
return sum;
|
|
15434
16100
|
};
|
|
16101
|
+
/** The artefact index, or an honest refusal. */
|
|
16102
|
+
const requireArtifacts = () => {
|
|
16103
|
+
const index = deps.artifacts;
|
|
16104
|
+
if (index === void 0) throw new Error("this node does not host the notification artifact index — use the hub");
|
|
16105
|
+
return index;
|
|
16106
|
+
};
|
|
15435
16107
|
/** The text catalog editor, or an honest refusal. */
|
|
15436
16108
|
const requireTexts = () => {
|
|
15437
16109
|
const editor = deps.texts;
|
|
@@ -15743,7 +16415,26 @@ function makeNcActionHandlers(deps) {
|
|
|
15743
16415
|
return input.language === void 0 ? texts.view() : texts.view(input.language);
|
|
15744
16416
|
},
|
|
15745
16417
|
"nc.setLanguage": async (input) => requireTexts().setLanguage(input.language),
|
|
15746
|
-
"nc.setTextOverrides": async (input) => requireTexts().setOverrides(input.language, input.overrides)
|
|
16418
|
+
"nc.setTextOverrides": async (input) => requireTexts().setOverrides(input.language, input.overrides),
|
|
16419
|
+
"nc.listArtifacts": async (input, caller) => {
|
|
16420
|
+
const c = requireCaller(caller);
|
|
16421
|
+
const page = await requireArtifacts().list(input, {
|
|
16422
|
+
userId: c.userId,
|
|
16423
|
+
isAdmin: c.isAdmin
|
|
16424
|
+
});
|
|
16425
|
+
return {
|
|
16426
|
+
artifacts: [...page.artifacts],
|
|
16427
|
+
total: page.total,
|
|
16428
|
+
hasMore: page.hasMore
|
|
16429
|
+
};
|
|
16430
|
+
},
|
|
16431
|
+
"nc.getArtifact": async (input, caller) => {
|
|
16432
|
+
const c = requireCaller(caller);
|
|
16433
|
+
return { artifact: await requireArtifacts().get(input.artifactId, {
|
|
16434
|
+
userId: c.userId,
|
|
16435
|
+
isAdmin: c.isAdmin
|
|
16436
|
+
}) };
|
|
16437
|
+
}
|
|
15747
16438
|
};
|
|
15748
16439
|
}
|
|
15749
16440
|
//#endregion
|
|
@@ -15797,7 +16488,8 @@ var PREVIEW_FALLBACK_NAME = "timelapse-preview.jpg";
|
|
|
15797
16488
|
* member has a logged reason. It never rejects.
|
|
15798
16489
|
*/
|
|
15799
16490
|
function buildTimelapseArtifactPublisher(ports) {
|
|
15800
|
-
return async (
|
|
16491
|
+
return async (request) => {
|
|
16492
|
+
const { exportId, deviceId, previewText, previewMode } = request;
|
|
15801
16493
|
const tags = { deviceId };
|
|
15802
16494
|
const none = {
|
|
15803
16495
|
video: null,
|
|
@@ -15828,15 +16520,32 @@ function buildTimelapseArtifactPublisher(ports) {
|
|
|
15828
16520
|
return none;
|
|
15829
16521
|
}
|
|
15830
16522
|
const mp4 = node_buffer.Buffer.from(read.base64, "base64");
|
|
16523
|
+
const capturedAt = request.capturedAt ?? Date.now();
|
|
16524
|
+
const common = {
|
|
16525
|
+
deviceId,
|
|
16526
|
+
deviceIds: [deviceId],
|
|
16527
|
+
capturedAt,
|
|
16528
|
+
...request.ruleId !== void 0 ? { ruleId: request.ruleId } : {},
|
|
16529
|
+
...request.ruleName !== void 0 ? { ruleName: request.ruleName } : {},
|
|
16530
|
+
...request.ownerUserId !== void 0 ? { ownerUserId: request.ownerUserId } : {}
|
|
16531
|
+
};
|
|
15831
16532
|
const preview = await publishPreview(ports, {
|
|
15832
16533
|
mp4,
|
|
15833
16534
|
deviceId,
|
|
15834
16535
|
tags,
|
|
15835
16536
|
previewText,
|
|
15836
|
-
previewMode: previewMode ?? "image"
|
|
16537
|
+
previewMode: previewMode ?? "image",
|
|
16538
|
+
meta: {
|
|
16539
|
+
...common,
|
|
16540
|
+
kind: "timelapse-preview"
|
|
16541
|
+
}
|
|
15837
16542
|
});
|
|
15838
16543
|
try {
|
|
15839
|
-
const url = await ports.publish(mp4, read.contentType
|
|
16544
|
+
const { url } = await ports.publish(mp4, read.contentType, {
|
|
16545
|
+
...common,
|
|
16546
|
+
kind: "timelapse",
|
|
16547
|
+
...preview.artifactId !== null ? { posterArtifactId: preview.artifactId } : {}
|
|
16548
|
+
});
|
|
15840
16549
|
if (url === null) {
|
|
15841
16550
|
ports.logger.warn("timelapse artifact not published: no reachable public endpoint", {
|
|
15842
16551
|
tags,
|
|
@@ -15844,7 +16553,7 @@ function buildTimelapseArtifactPublisher(ports) {
|
|
|
15844
16553
|
});
|
|
15845
16554
|
return {
|
|
15846
16555
|
video: null,
|
|
15847
|
-
preview
|
|
16556
|
+
preview: preview.ref
|
|
15848
16557
|
};
|
|
15849
16558
|
}
|
|
15850
16559
|
ports.logger.info("timelapse artifact published", {
|
|
@@ -15853,7 +16562,7 @@ function buildTimelapseArtifactPublisher(ports) {
|
|
|
15853
16562
|
exportId,
|
|
15854
16563
|
bytes: read.bytes,
|
|
15855
16564
|
name: read.name,
|
|
15856
|
-
preview: preview !== null
|
|
16565
|
+
preview: preview.ref !== null
|
|
15857
16566
|
}
|
|
15858
16567
|
});
|
|
15859
16568
|
return {
|
|
@@ -15862,7 +16571,7 @@ function buildTimelapseArtifactPublisher(ports) {
|
|
|
15862
16571
|
mime: read.contentType,
|
|
15863
16572
|
name: read.name
|
|
15864
16573
|
},
|
|
15865
|
-
preview
|
|
16574
|
+
preview: preview.ref
|
|
15866
16575
|
};
|
|
15867
16576
|
} catch (err) {
|
|
15868
16577
|
ports.logger.warn("timelapse artifact not published: artifact plane refused the bytes", {
|
|
@@ -15874,18 +16583,22 @@ function buildTimelapseArtifactPublisher(ports) {
|
|
|
15874
16583
|
});
|
|
15875
16584
|
return {
|
|
15876
16585
|
video: null,
|
|
15877
|
-
preview
|
|
16586
|
+
preview: preview.ref
|
|
15878
16587
|
};
|
|
15879
16588
|
}
|
|
15880
16589
|
};
|
|
15881
16590
|
}
|
|
16591
|
+
var NO_PREVIEW = {
|
|
16592
|
+
ref: null,
|
|
16593
|
+
artifactId: null
|
|
16594
|
+
};
|
|
15882
16595
|
/**
|
|
15883
16596
|
* The preview half. Best-effort at every step and never throws: the picture is
|
|
15884
16597
|
* an enhancement, the video is the notification.
|
|
15885
16598
|
*/
|
|
15886
16599
|
async function publishPreview(ports, input) {
|
|
15887
16600
|
const render = ports.renderPreview;
|
|
15888
|
-
if (render === void 0 || input.previewText === void 0) return
|
|
16601
|
+
if (render === void 0 || input.previewText === void 0) return NO_PREVIEW;
|
|
15889
16602
|
try {
|
|
15890
16603
|
const rendered = await render({
|
|
15891
16604
|
mp4: input.mp4,
|
|
@@ -15893,23 +16606,29 @@ async function publishPreview(ports, input) {
|
|
|
15893
16606
|
deviceId: input.deviceId,
|
|
15894
16607
|
mode: input.previewMode
|
|
15895
16608
|
});
|
|
15896
|
-
if (rendered === null) return
|
|
15897
|
-
const url = await ports.publish(rendered.bytes, rendered.mime);
|
|
16609
|
+
if (rendered === null) return NO_PREVIEW;
|
|
16610
|
+
const { url, artifactId } = await ports.publish(rendered.bytes, rendered.mime, input.meta);
|
|
15898
16611
|
if (url === null) {
|
|
15899
16612
|
ports.logger.warn("timelapse preview not published: no reachable public endpoint", { tags: input.tags });
|
|
15900
|
-
return
|
|
16613
|
+
return {
|
|
16614
|
+
ref: null,
|
|
16615
|
+
artifactId
|
|
16616
|
+
};
|
|
15901
16617
|
}
|
|
15902
16618
|
return {
|
|
15903
|
-
|
|
15904
|
-
|
|
15905
|
-
|
|
16619
|
+
ref: {
|
|
16620
|
+
url,
|
|
16621
|
+
mime: rendered.mime,
|
|
16622
|
+
name: PREVIEW_NAME_BY_MIME[rendered.mime] ?? PREVIEW_FALLBACK_NAME
|
|
16623
|
+
},
|
|
16624
|
+
artifactId
|
|
15906
16625
|
};
|
|
15907
16626
|
} catch (err) {
|
|
15908
16627
|
ports.logger.warn("timelapse preview not published: publish threw", {
|
|
15909
16628
|
tags: input.tags,
|
|
15910
16629
|
meta: { error: String(err) }
|
|
15911
16630
|
});
|
|
15912
|
-
return
|
|
16631
|
+
return NO_PREVIEW;
|
|
15913
16632
|
}
|
|
15914
16633
|
}
|
|
15915
16634
|
/** Title size as a fraction of the frame WIDTH — so it reads the same on a
|
|
@@ -16396,6 +17115,10 @@ function startAlarmPanelConvergence(ports) {
|
|
|
16396
17115
|
* `data-plane-access.spec.ts` asserts they still match the constants their
|
|
16397
17116
|
* handlers register with — a mirror nothing checks is a lie waiting to happen.
|
|
16398
17117
|
*/
|
|
17118
|
+
/** The artefact read route's prefix, imported rather than mirrored: it is
|
|
17119
|
+
* ALSO the prefix `artifactDownloadPath` writes into every index row, and two
|
|
17120
|
+
* copies of that string is a 404 nobody would find. */
|
|
17121
|
+
var NC_ARTIFACT_READ_PREFIX_REF = NC_ARTIFACT_READ_PREFIX;
|
|
16399
17122
|
var TRAINING_EXPORT_PREFIX_REF = "training-export";
|
|
16400
17123
|
var RETRAIN_EXPORT_PREFIX_REF = "retrain-export";
|
|
16401
17124
|
var DEBUG_EXPORT_PREFIX_REF = "debug-export";
|
|
@@ -16411,6 +17134,14 @@ var PIPELINE_ANALYTICS_PLANE_ACCESS = {
|
|
|
16411
17134
|
"nc-action": "public",
|
|
16412
17135
|
/** Self-authenticating single-use artifact token (D47). */
|
|
16413
17136
|
"nc-artifact": "public",
|
|
17137
|
+
/**
|
|
17138
|
+
* The VIEWER's read of the same bytes (task #36). One indexed artefact by an
|
|
17139
|
+
* unguessable id the caller was handed by `nc.listArtifacts` — the rule above,
|
|
17140
|
+
* applied unchanged. It is deliberately NOT the `public` signed plane: the
|
|
17141
|
+
* viewer holds a session, and pre-minting a 24 h signed link for every
|
|
17142
|
+
* artefact a user might scroll to is a link-leak surface for no gain.
|
|
17143
|
+
*/
|
|
17144
|
+
[NC_ARTIFACT_READ_PREFIX_REF]: "authenticated",
|
|
16414
17145
|
/** Every frame behind a marked selection — a corpus, not an item. */
|
|
16415
17146
|
[TRAINING_EXPORT_PREFIX_REF]: "admin",
|
|
16416
17147
|
[RETRAIN_EXPORT_PREFIX_REF]: "admin",
|
|
@@ -41353,6 +42084,15 @@ var REBUILD_FALLBACK_NODE_ID = "hub";
|
|
|
41353
42084
|
*/
|
|
41354
42085
|
var TIMELAPSE_TRACK_QUERY_LIMIT = 2e3;
|
|
41355
42086
|
var PIPELINE_ANALYTICS_ADDON_ID = "pipeline-analytics";
|
|
42087
|
+
/** Artefact index ⇄ shelf reconcile cadence. Hourly: the two only drift on the
|
|
42088
|
+
* bounds' own timescale (14 days / 2000 entries / 2 GiB). */
|
|
42089
|
+
var NC_ARTIFACT_PRUNE_INTERVAL_MS = 36e5;
|
|
42090
|
+
/** What a publish answers on a node whose artifact plane never came up: no
|
|
42091
|
+
* link, no index row — and, exactly as before, bytes-only delivery. */
|
|
42092
|
+
var NO_PUBLICATION = {
|
|
42093
|
+
url: null,
|
|
42094
|
+
artifactId: null
|
|
42095
|
+
};
|
|
41356
42096
|
/**
|
|
41357
42097
|
* The FULL action catalog for this addon.
|
|
41358
42098
|
*
|
|
@@ -41634,6 +42374,12 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
41634
42374
|
* data-plane handle. Null until served / when the facility is absent. */
|
|
41635
42375
|
ncArtifactPlane = null;
|
|
41636
42376
|
ncArtifactDataPlane = null;
|
|
42377
|
+
/** The viewer's artefact library (task #36): the index, its authenticated
|
|
42378
|
+
* bytes route, and the store the two share. Null until served. */
|
|
42379
|
+
ncArtifactIndex = null;
|
|
42380
|
+
ncArtifactStore = null;
|
|
42381
|
+
ncArtifactReadDataPlane = null;
|
|
42382
|
+
ncArtifactPruneTimer = null;
|
|
41637
42383
|
/**
|
|
41638
42384
|
* Roadmap 4b: ONE cut of an event, in every container the rule asked for.
|
|
41639
42385
|
*
|
|
@@ -41820,6 +42566,31 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
41820
42566
|
* Best-effort at every step: no facility, no secret or no reachable base URL
|
|
41821
42567
|
* ⇒ no plane, and the dispatcher keeps shipping bytes exactly as before.
|
|
41822
42568
|
*/
|
|
42569
|
+
/**
|
|
42570
|
+
* Reconcile the artefact index against the shelf that holds the bytes.
|
|
42571
|
+
*
|
|
42572
|
+
* Hourly, because the two drift only on the bounds' timescale: a row expires
|
|
42573
|
+
* on the shelf's own age bound (14 d) or its file is evicted by the count/byte
|
|
42574
|
+
* bound. Without this, a list eventually serves rows whose bytes 404 — and a
|
|
42575
|
+
* broken tile is the one failure an operator DOES report.
|
|
42576
|
+
*/
|
|
42577
|
+
startNcArtifactPrune() {
|
|
42578
|
+
if (this.ncArtifactPruneTimer !== null) return;
|
|
42579
|
+
const run = async () => {
|
|
42580
|
+
const index = this.ncArtifactIndex;
|
|
42581
|
+
const store = this.ncArtifactStore;
|
|
42582
|
+
if (index === null || store === null) return;
|
|
42583
|
+
try {
|
|
42584
|
+
await store.sweep();
|
|
42585
|
+
await index.prune(this.isPostProcessingNode ? await store.keptIds() : null);
|
|
42586
|
+
} catch (err) {
|
|
42587
|
+
this.ctx.logger.warn("nc artifact prune failed", { meta: { error: require_dist.errMsg(err) } });
|
|
42588
|
+
}
|
|
42589
|
+
};
|
|
42590
|
+
run();
|
|
42591
|
+
this.ncArtifactPruneTimer = setInterval(() => void run(), NC_ARTIFACT_PRUNE_INTERVAL_MS);
|
|
42592
|
+
this.ncArtifactPruneTimer.unref?.();
|
|
42593
|
+
}
|
|
41823
42594
|
async serveNcArtifactPlane() {
|
|
41824
42595
|
try {
|
|
41825
42596
|
const secretState = this.state("ncArtifactSecret", NcArtifactSecretSchema, "");
|
|
@@ -41833,9 +42604,18 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
41833
42604
|
logger: this.ctx.logger.child("nc-artifacts")
|
|
41834
42605
|
});
|
|
41835
42606
|
await store.start();
|
|
42607
|
+
this.ncArtifactStore = store;
|
|
42608
|
+
const index = new NcArtifactIndex({
|
|
42609
|
+
store: this.ctx.api.settingsStore,
|
|
42610
|
+
logger: this.ctx.logger.child("nc-artifacts"),
|
|
42611
|
+
addonId: this.ctx.id,
|
|
42612
|
+
retentionMs: store.retentionMs
|
|
42613
|
+
});
|
|
42614
|
+
this.ncArtifactIndex = index;
|
|
41836
42615
|
const plane = new NcArtifactPlane({
|
|
41837
42616
|
store,
|
|
41838
42617
|
secret,
|
|
42618
|
+
index,
|
|
41839
42619
|
logger: this.ctx.logger.child("nc-artifacts"),
|
|
41840
42620
|
routePrefix: `/addon/${this.ctx.id}/nc-artifact`,
|
|
41841
42621
|
listEndpoints: () => this.ncEndpoints()
|
|
@@ -41846,7 +42626,23 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
41846
42626
|
handler: plane.handler
|
|
41847
42627
|
}) ?? null;
|
|
41848
42628
|
this.ncArtifactPlane = this.ncArtifactDataPlane !== null ? plane : null;
|
|
41849
|
-
|
|
42629
|
+
const readPlane = new NcArtifactReadPlane({
|
|
42630
|
+
index,
|
|
42631
|
+
store,
|
|
42632
|
+
logger: this.ctx.logger.child("nc-artifacts")
|
|
42633
|
+
});
|
|
42634
|
+
this.ncArtifactReadDataPlane = await this.ctx.dataPlane?.serve({
|
|
42635
|
+
prefix: NC_ARTIFACT_READ_PREFIX_REF,
|
|
42636
|
+
access: planeAccess(NC_ARTIFACT_READ_PREFIX_REF),
|
|
42637
|
+
handler: readPlane.handler
|
|
42638
|
+
}) ?? null;
|
|
42639
|
+
this.startNcArtifactPrune();
|
|
42640
|
+
this.ctx.logger.info("nc-artifact data-plane served", { meta: {
|
|
42641
|
+
served: this.ncArtifactPlane !== null,
|
|
42642
|
+
libraryServed: this.ncArtifactReadDataPlane !== null,
|
|
42643
|
+
libraryAccess: planeAccess(NC_ARTIFACT_READ_PREFIX_REF),
|
|
42644
|
+
retentionDays: Math.round(store.retentionMs / 864e5)
|
|
42645
|
+
} });
|
|
41850
42646
|
} catch (err) {
|
|
41851
42647
|
this.ctx.logger.warn("nc-artifact data-plane failed to serve", { meta: { error: require_dist.errMsg(err) } });
|
|
41852
42648
|
}
|
|
@@ -42294,7 +43090,8 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
42294
43090
|
store: center.summaryStore,
|
|
42295
43091
|
runNow: (input) => center.runSummaryNow(input)
|
|
42296
43092
|
},
|
|
42297
|
-
texts: center.textCatalogEditor
|
|
43093
|
+
texts: center.textCatalogEditor,
|
|
43094
|
+
...this.ncArtifactIndex !== null ? { artifacts: this.ncArtifactIndex } : {}
|
|
42298
43095
|
});
|
|
42299
43096
|
}
|
|
42300
43097
|
/** Declare typed collections up-front so the first insert doesn't race with
|
|
@@ -43083,7 +43880,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
43083
43880
|
timestamp,
|
|
43084
43881
|
data: jpeg
|
|
43085
43882
|
}),
|
|
43086
|
-
publish: async (bytes, mime) => await this.ncArtifactPlane?.
|
|
43883
|
+
publish: async (bytes, mime, meta) => await this.ncArtifactPlane?.publishIndexed(bytes, mime, meta) ?? NO_PUBLICATION
|
|
43087
43884
|
};
|
|
43088
43885
|
}
|
|
43089
43886
|
/**
|
|
@@ -43156,7 +43953,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
43156
43953
|
publishArtifact: buildTimelapseArtifactPublisher({
|
|
43157
43954
|
logger: this.ctx.logger.child("Timelapse"),
|
|
43158
43955
|
readExportBytes: ({ exportId }) => api.recordingExport.readExportBytes.query({ exportId }),
|
|
43159
|
-
publish: async (bytes, mime) => await this.ncArtifactPlane?.
|
|
43956
|
+
publish: async (bytes, mime, meta) => await this.ncArtifactPlane?.publishIndexed(bytes, mime, meta) ?? NO_PUBLICATION,
|
|
43160
43957
|
renderPreview: async ({ mode, ...input }) => {
|
|
43161
43958
|
const previewPorts = {
|
|
43162
43959
|
logger: this.ctx.logger.child("Timelapse"),
|
|
@@ -43823,6 +44620,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
43823
44620
|
this.retrainExportDataPlane = null;
|
|
43824
44621
|
await this.debugExportDataPlane?.dispose();
|
|
43825
44622
|
this.debugExportDataPlane = null;
|
|
44623
|
+
if (this.ncArtifactPruneTimer !== null) {
|
|
44624
|
+
clearInterval(this.ncArtifactPruneTimer);
|
|
44625
|
+
this.ncArtifactPruneTimer = null;
|
|
44626
|
+
}
|
|
44627
|
+
await this.ncArtifactReadDataPlane?.dispose();
|
|
44628
|
+
this.ncArtifactReadDataPlane = null;
|
|
44629
|
+
this.ncArtifactIndex = null;
|
|
44630
|
+
this.ncArtifactStore = null;
|
|
43826
44631
|
}
|
|
43827
44632
|
async handleInferenceResult(data) {
|
|
43828
44633
|
if (this.shuttingDown) return;
|