@camstack/system 1.2.437 → 1.2.438
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/addon-runner.js +64 -12
- package/dist/addon-runner.mjs +65 -13
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +7 -1
- package/dist/builtins/alerts/alerts.addon.mjs +7 -1
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +9 -1
- package/dist/builtins/camera-grid/index.mjs +9 -1
- package/dist/builtins/composer/composer.addon.js +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- package/dist/builtins/console-logging/index.js +12 -5
- package/dist/builtins/console-logging/index.mjs +12 -5
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-manager/device-manager.addon.d.ts +5 -0
- package/dist/builtins/device-manager/device-manager.addon.js +702 -28
- package/dist/builtins/device-manager/device-manager.addon.mjs +702 -28
- package/dist/builtins/device-manager/device-meta-types.d.ts +4 -0
- package/dist/builtins/device-manager/device-provider-context.d.ts +9 -0
- package/dist/builtins/device-manager/device-removal-hooks.d.ts +103 -0
- package/dist/builtins/device-manager/device-row-store.d.ts +1 -0
- package/dist/builtins/device-manager/device-tombstone-store.d.ts +58 -0
- package/dist/builtins/device-manager/removal-cleanup.d.ts +50 -0
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +37 -4
- package/dist/builtins/keychain/index.mjs +37 -4
- package/dist/builtins/keychain/keychain-service.d.ts +14 -0
- package/dist/builtins/keychain/keychain-store.d.ts +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +12 -12
- package/dist/builtins/local-auth/local-auth.addon.mjs +12 -12
- package/dist/builtins/local-network/local-network.addon.js +10 -1
- package/dist/builtins/local-network/local-network.addon.mjs +10 -1
- package/dist/builtins/loki-logging/index.js +28 -5
- package/dist/builtins/loki-logging/index.mjs +28 -5
- package/dist/builtins/native-metrics/native-metrics.addon.js +11 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +11 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +11 -1
- package/dist/builtins/snapshot/index.mjs +11 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +4 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +4 -1
- package/dist/builtins/sqlite-storage/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +3 -3
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +3 -3
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +20 -2
- package/dist/builtins/system-config/system-config.addon.mjs +20 -2
- package/dist/builtins/winston-logging/index.js +14 -5
- package/dist/builtins/winston-logging/index.mjs +14 -5
- package/dist/{composition-sources-BIT8IUKm.js → composition-sources-DI3Z83vG.js} +1 -1
- package/dist/{composition-sources-tpYR0f1x.mjs → composition-sources-DpHGCjdL.mjs} +1 -1
- package/dist/{constants-Bc6trhoW.mjs → constants-DQK705Sb.mjs} +1 -1
- package/dist/{constants-CwRY32Nx.js → constants-wkcn5Ij3.js} +1 -1
- package/dist/{device-ca-DImD8IzR.js → device-ca-BSvkXqyI.js} +1 -1
- package/dist/{device-ca-BhdBByGD.mjs → device-ca-DvIZjXIl.mjs} +1 -1
- package/dist/{dist-m5JkT8UN.mjs → dist-Dc8GKC5H.mjs} +272 -7
- package/dist/{dist-CEAyXW3Q.js → dist-GgG-7amw.js} +307 -6
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/i18n/catalog-registry.d.ts +35 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +663 -20
- package/dist/index.mjs +659 -22
- package/dist/kernel/gc-census.d.ts +18 -1
- package/dist/kernel/settings-view-types.d.ts +2 -0
- package/dist/kernel/transport/local-transport.d.ts +3 -0
- package/dist/kernel/transport/peer-answered-error.d.ts +5 -1
- package/dist/{restore-archive-C75srVbG.js → restore-archive-CTRz8v9o.js} +1 -1
- package/dist/{restore-archive-DNxAStrJ.mjs → restore-archive-DPg9duVY.mjs} +1 -1
- package/dist/{retired-settings-keys-DNfV28R_.js → retired-settings-keys-Dr_4lfKu.js} +1 -1
- package/dist/{retired-settings-keys-C5qP9QIf.mjs → retired-settings-keys-Is0zO8gX.mjs} +1 -1
- package/dist/{wal-maintenance-DvnVO5IK.mjs → wal-maintenance-Bx3lcVA7.mjs} +0 -0
- package/dist/{wal-maintenance-Ch79gy9S.js → wal-maintenance-mjB2i-VR.js} +0 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-
|
|
1
|
+
import { Bt as enumerateItemArrayFields, C as DAY_NAMES, Dn as parseStreamParamsFormPatch, Dr as errMsg, Er as DeviceType, Et as createDeviceExtensionProvider, Ft as deviceExtensionCapability, Ir as SECRET_CLEAR, It as deviceManagerCapability, Lr as WELL_KNOWN_TAB_MAP, Lt as deviceStateCapability, N as FieldClaimModeSchema, Or as BaseAddon, P as FieldClaimSchema, Qr as isDeviceConfigCap, Rt as deviceStatusCapability, Tr as DeviceRole, Un as runtimeStatePolicyFor, Ur as collectZodSecretPaths, V as MINUTES_PER_DAY, Vt as enumerateSchemaFields, Wt as expandContainer, Xt as formatMinutes, Yn as siteZoneKey, _i as transformSecretWrite, c as CAMERA_SWITCH_ORDER, di as resolveSecretMarkers, ei as isKeychainStoredField, gi as toSecretFieldView, gt as canonicalIconKey, j as DeviceStatusSchema, jt as defineExtensionSlotProvider, ln as legacyAutoLinkIds, mi as sleep$1, ni as isSecretMarker, pi as secretTreeFromPaths, pn as locationIconKey, pt as buildStreamParamsConfigSchema, q as STREAM_PROFILE_META, r as ALL_CAPABILITY_DEFINITIONS, ri as isSecretMarkerLike, ti as isSecretClear, u as CAP_NAMES_WITH_STATUS, vi as userError, wn as normalizeUnit, wr as DeviceFeature, yi as EventCategory } from "../../dist-Dc8GKC5H.mjs";
|
|
2
|
+
import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-Is0zO8gX.mjs";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { canonicalDeviceFingerprint } from "@camstack/types/node";
|
|
@@ -964,7 +964,9 @@ function toWireShape(input) {
|
|
|
964
964
|
const out = { sections: input.sections.map((s) => ({
|
|
965
965
|
id: s.id,
|
|
966
966
|
title: s.title,
|
|
967
|
+
...s.titleKey !== void 0 ? { titleKey: s.titleKey } : {},
|
|
967
968
|
...s.description !== void 0 ? { description: s.description } : {},
|
|
969
|
+
...s.descriptionKey !== void 0 ? { descriptionKey: s.descriptionKey } : {},
|
|
968
970
|
...s.style !== void 0 ? { style: s.style } : {},
|
|
969
971
|
...s.defaultCollapsed !== void 0 ? { defaultCollapsed: s.defaultCollapsed } : {},
|
|
970
972
|
...s.columns !== void 0 ? { columns: s.columns } : {},
|
|
@@ -1775,6 +1777,7 @@ function buildDayNightConfigSchema(options, status) {
|
|
|
1775
1777
|
type: "select",
|
|
1776
1778
|
key: "dayNight_mode",
|
|
1777
1779
|
label: "Mode",
|
|
1780
|
+
labelKey: "server:dayNightConfig.dayNight_mode.label",
|
|
1778
1781
|
options: options.modes.map((m) => ({
|
|
1779
1782
|
value: m,
|
|
1780
1783
|
label: MODE_LABELS[m]
|
|
@@ -1785,6 +1788,7 @@ function buildDayNightConfigSchema(options, status) {
|
|
|
1785
1788
|
type: "slider",
|
|
1786
1789
|
key: "dayNight_sensitivity",
|
|
1787
1790
|
label: "IR-cut sensitivity",
|
|
1791
|
+
labelKey: "server:dayNightConfig.dayNight_sensitivity.label",
|
|
1788
1792
|
min: options.sensitivity.min,
|
|
1789
1793
|
max: options.sensitivity.max,
|
|
1790
1794
|
step: options.sensitivity.step,
|
|
@@ -1795,6 +1799,7 @@ function buildDayNightConfigSchema(options, status) {
|
|
|
1795
1799
|
type: "number",
|
|
1796
1800
|
key: "dayNight_switchDelaySec",
|
|
1797
1801
|
label: "Switch delay",
|
|
1802
|
+
labelKey: "server:dayNightConfig.dayNight_switchDelaySec.label",
|
|
1798
1803
|
unit: "s",
|
|
1799
1804
|
min: options.switchDelaySec.min,
|
|
1800
1805
|
max: options.switchDelaySec.max,
|
|
@@ -1806,7 +1811,9 @@ function buildDayNightConfigSchema(options, status) {
|
|
|
1806
1811
|
id: "day-night",
|
|
1807
1812
|
tab: "image",
|
|
1808
1813
|
title: "Day / Night",
|
|
1814
|
+
titleKey: "server:section.s_day-night.title",
|
|
1809
1815
|
description: "IR-cut switching mode and the photocell knobs that gate it.",
|
|
1816
|
+
descriptionKey: "server:section.s_day-night.description",
|
|
1810
1817
|
columns: 2,
|
|
1811
1818
|
fields
|
|
1812
1819
|
}] };
|
|
@@ -1899,6 +1906,7 @@ function buildImageSettingsConfigSchema(options, status) {
|
|
|
1899
1906
|
type: "slider",
|
|
1900
1907
|
key: "imageSettings_brightness",
|
|
1901
1908
|
label: "Brightness",
|
|
1909
|
+
labelKey: "server:imageSettingsConfig.imageSettings_brightness.label",
|
|
1902
1910
|
min: options.brightness.min,
|
|
1903
1911
|
max: options.brightness.max,
|
|
1904
1912
|
step: options.brightness.step,
|
|
@@ -1909,6 +1917,7 @@ function buildImageSettingsConfigSchema(options, status) {
|
|
|
1909
1917
|
type: "slider",
|
|
1910
1918
|
key: "imageSettings_contrast",
|
|
1911
1919
|
label: "Contrast",
|
|
1920
|
+
labelKey: "server:imageSettingsConfig.imageSettings_contrast.label",
|
|
1912
1921
|
min: options.contrast.min,
|
|
1913
1922
|
max: options.contrast.max,
|
|
1914
1923
|
step: options.contrast.step,
|
|
@@ -1919,6 +1928,7 @@ function buildImageSettingsConfigSchema(options, status) {
|
|
|
1919
1928
|
type: "slider",
|
|
1920
1929
|
key: "imageSettings_saturation",
|
|
1921
1930
|
label: "Saturation",
|
|
1931
|
+
labelKey: "server:imageSettingsConfig.imageSettings_saturation.label",
|
|
1922
1932
|
min: options.saturation.min,
|
|
1923
1933
|
max: options.saturation.max,
|
|
1924
1934
|
step: options.saturation.step,
|
|
@@ -1929,6 +1939,7 @@ function buildImageSettingsConfigSchema(options, status) {
|
|
|
1929
1939
|
type: "slider",
|
|
1930
1940
|
key: "imageSettings_sharpness",
|
|
1931
1941
|
label: "Sharpness",
|
|
1942
|
+
labelKey: "server:imageSettingsConfig.imageSettings_sharpness.label",
|
|
1932
1943
|
min: options.sharpness.min,
|
|
1933
1944
|
max: options.sharpness.max,
|
|
1934
1945
|
step: options.sharpness.step,
|
|
@@ -1939,6 +1950,7 @@ function buildImageSettingsConfigSchema(options, status) {
|
|
|
1939
1950
|
type: "boolean",
|
|
1940
1951
|
key: "imageSettings_mirror",
|
|
1941
1952
|
label: "Mirror",
|
|
1953
|
+
labelKey: "server:imageSettingsConfig.imageSettings_mirror.label",
|
|
1942
1954
|
style: "switch",
|
|
1943
1955
|
default: status?.mirror ?? false
|
|
1944
1956
|
});
|
|
@@ -1946,6 +1958,7 @@ function buildImageSettingsConfigSchema(options, status) {
|
|
|
1946
1958
|
type: "boolean",
|
|
1947
1959
|
key: "imageSettings_flip",
|
|
1948
1960
|
label: "Flip",
|
|
1961
|
+
labelKey: "server:imageSettingsConfig.imageSettings_flip.label",
|
|
1949
1962
|
style: "switch",
|
|
1950
1963
|
default: status?.flip ?? false
|
|
1951
1964
|
});
|
|
@@ -1953,6 +1966,7 @@ function buildImageSettingsConfigSchema(options, status) {
|
|
|
1953
1966
|
type: "select",
|
|
1954
1967
|
key: "imageSettings_rotate",
|
|
1955
1968
|
label: "Rotation",
|
|
1969
|
+
labelKey: "server:imageSettingsConfig.imageSettings_rotate.label",
|
|
1956
1970
|
options: options.rotateOptions.map((r) => ({
|
|
1957
1971
|
value: r,
|
|
1958
1972
|
label: ROTATE_LABELS[r]
|
|
@@ -1963,6 +1977,7 @@ function buildImageSettingsConfigSchema(options, status) {
|
|
|
1963
1977
|
type: "select",
|
|
1964
1978
|
key: "imageSettings_whiteBalance",
|
|
1965
1979
|
label: "White balance",
|
|
1980
|
+
labelKey: "server:imageSettingsConfig.imageSettings_whiteBalance.label",
|
|
1966
1981
|
options: options.whiteBalanceModes.map((m) => ({
|
|
1967
1982
|
value: m,
|
|
1968
1983
|
label: WHITE_BALANCE_LABELS[m]
|
|
@@ -1973,6 +1988,7 @@ function buildImageSettingsConfigSchema(options, status) {
|
|
|
1973
1988
|
type: "slider",
|
|
1974
1989
|
key: "imageSettings_warmth",
|
|
1975
1990
|
label: "Warmth",
|
|
1991
|
+
labelKey: "server:imageSettingsConfig.imageSettings_warmth.label",
|
|
1976
1992
|
min: options.warmth.min,
|
|
1977
1993
|
max: options.warmth.max,
|
|
1978
1994
|
step: options.warmth.step,
|
|
@@ -1987,6 +2003,7 @@ function buildImageSettingsConfigSchema(options, status) {
|
|
|
1987
2003
|
type: "select",
|
|
1988
2004
|
key: "imageSettings_exposureMode",
|
|
1989
2005
|
label: "Exposure mode",
|
|
2006
|
+
labelKey: "server:imageSettingsConfig.imageSettings_exposureMode.label",
|
|
1990
2007
|
options: options.exposureModes.map((m) => ({
|
|
1991
2008
|
value: m,
|
|
1992
2009
|
label: EXPOSURE_LABELS[m]
|
|
@@ -1997,6 +2014,7 @@ function buildImageSettingsConfigSchema(options, status) {
|
|
|
1997
2014
|
type: "select",
|
|
1998
2015
|
key: "imageSettings_backlightMode",
|
|
1999
2016
|
label: "Backlight compensation",
|
|
2017
|
+
labelKey: "server:imageSettingsConfig.imageSettings_backlightMode.label",
|
|
2000
2018
|
options: options.backlightModes.map((m) => ({
|
|
2001
2019
|
value: m,
|
|
2002
2020
|
label: BACKLIGHT_LABELS[m]
|
|
@@ -2008,7 +2026,9 @@ function buildImageSettingsConfigSchema(options, status) {
|
|
|
2008
2026
|
id: "image-settings",
|
|
2009
2027
|
tab: "image",
|
|
2010
2028
|
title: "Image adjustment",
|
|
2029
|
+
titleKey: "server:section.s_image-settings.title",
|
|
2011
2030
|
description: "Picture sliders, orientation and exposure. Option lists are read live from the camera — fields the firmware doesn't expose are hidden.",
|
|
2031
|
+
descriptionKey: "server:section.s_image-settings.description",
|
|
2012
2032
|
columns: 2,
|
|
2013
2033
|
fields
|
|
2014
2034
|
}] };
|
|
@@ -2142,26 +2162,31 @@ function volumesField(status) {
|
|
|
2142
2162
|
{
|
|
2143
2163
|
key: "id",
|
|
2144
2164
|
label: "Id",
|
|
2165
|
+
labelKey: "server:volumesField.id.label",
|
|
2145
2166
|
kind: "monospace",
|
|
2146
2167
|
width: "70px"
|
|
2147
2168
|
},
|
|
2148
2169
|
{
|
|
2149
2170
|
key: "label",
|
|
2150
2171
|
label: "Name",
|
|
2172
|
+
labelKey: "server:volumesField.label.label",
|
|
2151
2173
|
kind: "monospace"
|
|
2152
2174
|
},
|
|
2153
2175
|
{
|
|
2154
2176
|
key: "status",
|
|
2155
2177
|
label: "Status",
|
|
2178
|
+
labelKey: "server:volumesField.status.label",
|
|
2156
2179
|
kind: "status"
|
|
2157
2180
|
},
|
|
2158
2181
|
{
|
|
2159
2182
|
key: "capacity",
|
|
2160
|
-
label: "Capacity"
|
|
2183
|
+
label: "Capacity",
|
|
2184
|
+
labelKey: "server:volumesField.capacity.label"
|
|
2161
2185
|
},
|
|
2162
2186
|
{
|
|
2163
2187
|
key: "free",
|
|
2164
|
-
label: "Free"
|
|
2188
|
+
label: "Free",
|
|
2189
|
+
labelKey: "server:volumesField.free.label"
|
|
2165
2190
|
}
|
|
2166
2191
|
],
|
|
2167
2192
|
value: rows,
|
|
@@ -2185,27 +2210,32 @@ function tracksField(status) {
|
|
|
2185
2210
|
{
|
|
2186
2211
|
key: "id",
|
|
2187
2212
|
label: "Track",
|
|
2213
|
+
labelKey: "server:tracksField.id.label",
|
|
2188
2214
|
kind: "monospace",
|
|
2189
2215
|
width: "80px"
|
|
2190
2216
|
},
|
|
2191
2217
|
{
|
|
2192
2218
|
key: "primary",
|
|
2193
2219
|
label: "Primary",
|
|
2220
|
+
labelKey: "server:tracksField.primary.label",
|
|
2194
2221
|
width: "80px"
|
|
2195
2222
|
},
|
|
2196
2223
|
{
|
|
2197
2224
|
key: "enabled",
|
|
2198
2225
|
label: "Enabled",
|
|
2226
|
+
labelKey: "server:tracksField.enabled.label",
|
|
2199
2227
|
kind: "status",
|
|
2200
2228
|
width: "90px"
|
|
2201
2229
|
},
|
|
2202
2230
|
{
|
|
2203
2231
|
key: "codec",
|
|
2204
|
-
label: "Codec"
|
|
2232
|
+
label: "Codec",
|
|
2233
|
+
labelKey: "server:tracksField.codec.label"
|
|
2205
2234
|
},
|
|
2206
2235
|
{
|
|
2207
2236
|
key: "resolution",
|
|
2208
|
-
label: "Resolution"
|
|
2237
|
+
label: "Resolution",
|
|
2238
|
+
labelKey: "server:tracksField.resolution.label"
|
|
2209
2239
|
}
|
|
2210
2240
|
],
|
|
2211
2241
|
value: rows,
|
|
@@ -2407,7 +2437,9 @@ function buildRecordingOnboardConfigSchema(options, status) {
|
|
|
2407
2437
|
id: "recording-onboard-storage",
|
|
2408
2438
|
tab: "recording",
|
|
2409
2439
|
title: "Camera storage",
|
|
2440
|
+
titleKey: "server:storageSection.s_recording-onboard-storage.title",
|
|
2410
2441
|
description: "The card in the camera, as the camera itself describes it.",
|
|
2442
|
+
descriptionKey: "server:storageSection.s_recording-onboard-storage.description",
|
|
2411
2443
|
columns: 1,
|
|
2412
2444
|
fields: [
|
|
2413
2445
|
storageBanner(status),
|
|
@@ -2418,7 +2450,9 @@ function buildRecordingOnboardConfigSchema(options, status) {
|
|
|
2418
2450
|
id: "recording-onboard-recording",
|
|
2419
2451
|
tab: "recording",
|
|
2420
2452
|
title: "Camera recording",
|
|
2453
|
+
titleKey: "server:recordingSection.s_recording-onboard-recording.title",
|
|
2421
2454
|
description: "What the camera records to its own card. This writes the camera, not CamStack's recorder.",
|
|
2455
|
+
descriptionKey: "server:recordingSection.s_recording-onboard-recording.description",
|
|
2422
2456
|
columns: 2,
|
|
2423
2457
|
fields: [
|
|
2424
2458
|
booleanField(RECORDING_ONBOARD_KEYS.enabled, "Record to card", "The camera-side master switch.", options.enabled, status?.enabled ?? null),
|
|
@@ -3027,7 +3061,7 @@ async function getStreamSources(pctx, input) {
|
|
|
3027
3061
|
}));
|
|
3028
3062
|
}
|
|
3029
3063
|
}
|
|
3030
|
-
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw
|
|
3064
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw userError("device.not-found", { id: deviceId }, `[device-manager] Device with id ${deviceId} not found`);
|
|
3031
3065
|
return (await pctx.requireDeviceOps(deviceId).getStreamSources({ deviceId })).map((s) => ({ ...s }));
|
|
3032
3066
|
}
|
|
3033
3067
|
async function getConfigSchema(pctx, input) {
|
|
@@ -3040,7 +3074,7 @@ async function getConfigSchema(pctx, input) {
|
|
|
3040
3074
|
...entry.description !== void 0 ? { description: entry.description } : {}
|
|
3041
3075
|
}));
|
|
3042
3076
|
}
|
|
3043
|
-
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw
|
|
3077
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw userError("device.not-found", { id: deviceId }, `[device-manager] Device with id ${deviceId} not found`);
|
|
3044
3078
|
return (await pctx.requireDeviceOps(deviceId).getConfigEntries({ deviceId })).map((e) => ({ ...e }));
|
|
3045
3079
|
}
|
|
3046
3080
|
async function getSettingsSchema(pctx, input) {
|
|
@@ -3061,7 +3095,7 @@ async function updateConfig(pctx, input) {
|
|
|
3061
3095
|
return { success: true };
|
|
3062
3096
|
}
|
|
3063
3097
|
}
|
|
3064
|
-
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw
|
|
3098
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw userError("device.not-found", { id: deviceId }, `[device-manager] Device with id ${deviceId} not found`);
|
|
3065
3099
|
await pctx.requireDeviceOps(deviceId).setConfig({
|
|
3066
3100
|
deviceId,
|
|
3067
3101
|
values: input.values
|
|
@@ -3161,7 +3195,7 @@ async function setStreamProfileMap(pctx, input) {
|
|
|
3161
3195
|
return { success: true };
|
|
3162
3196
|
}
|
|
3163
3197
|
}
|
|
3164
|
-
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw
|
|
3198
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw userError("device.not-found", { id: deviceId }, `[device-manager] Device with id ${deviceId} not found`);
|
|
3165
3199
|
await pctx.requireDeviceOps(deviceId).setConfig({
|
|
3166
3200
|
deviceId,
|
|
3167
3201
|
values: { _profileMap: input.profileMap }
|
|
@@ -3198,7 +3232,7 @@ async function probeStreams(pctx, input) {
|
|
|
3198
3232
|
}
|
|
3199
3233
|
async function discoverDevices(pctx, input) {
|
|
3200
3234
|
const dp = await pctx.host.requireDeviceProvider(input.addonId);
|
|
3201
|
-
if (!await dp.supportsDiscovery({})) throw
|
|
3235
|
+
if (!await dp.supportsDiscovery({})) throw userError("system.addon-no-discovery", { addonId: String(input.addonId) }, `Addon "${input.addonId}" does not support device discovery`);
|
|
3202
3236
|
return (await dp.discoverDevices({})).map((d) => ({
|
|
3203
3237
|
stableId: d.stableId,
|
|
3204
3238
|
type: d.type,
|
|
@@ -3211,7 +3245,7 @@ async function discoverDevices(pctx, input) {
|
|
|
3211
3245
|
}
|
|
3212
3246
|
async function adoptDevice(pctx, input) {
|
|
3213
3247
|
const dp = await pctx.host.requireDeviceProvider(input.addonId);
|
|
3214
|
-
if (!await dp.supportsDiscovery({})) throw
|
|
3248
|
+
if (!await dp.supportsDiscovery({})) throw userError("system.addon-no-adoption", { addonId: String(input.addonId) }, `Addon "${input.addonId}" does not support device adoption`);
|
|
3215
3249
|
const summary = await pctx.removalFence.runUnfenced(() => dp.adoptDiscoveredDevice({ candidate: input.candidate }));
|
|
3216
3250
|
if (input.integrationId !== void 0) try {
|
|
3217
3251
|
await pctx.stampIntegrationId(summary.id, input.integrationId);
|
|
@@ -3233,7 +3267,7 @@ async function getCreationSchema(pctx, input) {
|
|
|
3233
3267
|
}
|
|
3234
3268
|
async function createDevice(pctx, input) {
|
|
3235
3269
|
const dp = await pctx.host.requireDeviceProvider(input.addonId);
|
|
3236
|
-
if (!await dp.supportsManualCreation({})) throw
|
|
3270
|
+
if (!await dp.supportsManualCreation({})) throw userError("system.addon-no-manual-create", { addonId: String(input.addonId) }, `Addon "${input.addonId}" does not support manual device creation`);
|
|
3237
3271
|
const summary = await pctx.removalFence.runUnfenced(() => dp.createDevice({
|
|
3238
3272
|
type: input.type,
|
|
3239
3273
|
config: input.config
|
|
@@ -3326,7 +3360,7 @@ async function testField(pctx, input) {
|
|
|
3326
3360
|
let owningAddonId = null;
|
|
3327
3361
|
if (pctx.registry) owningAddonId = pctx.registry.getAddonId(deviceId);
|
|
3328
3362
|
if (!owningAddonId) owningAddonId = (await pctx.metaStore.resolvePersistedById(deviceId))?.addonId ?? null;
|
|
3329
|
-
if (!owningAddonId) throw
|
|
3363
|
+
if (!owningAddonId) throw userError("device.not-found", { id: deviceId }, `Device with id ${deviceId} not found`);
|
|
3330
3364
|
const dp = await pctx.host.waitDeviceProvider(owningAddonId);
|
|
3331
3365
|
if (!dp) return {
|
|
3332
3366
|
status: "ok",
|
|
@@ -3835,12 +3869,14 @@ async function buildBaseDeviceSection(deps, deviceId) {
|
|
|
3835
3869
|
} else if (driverResult.status === "unavailable") sections.push({
|
|
3836
3870
|
id: "driver-settings-unavailable",
|
|
3837
3871
|
title: "Driver Settings",
|
|
3872
|
+
titleKey: "server:baseDeviceSection.s_driver-settings-unavailable.title",
|
|
3838
3873
|
tab: "general",
|
|
3839
3874
|
order: 1,
|
|
3840
3875
|
fields: [{
|
|
3841
3876
|
type: "info",
|
|
3842
3877
|
key: "driver-settings-unavailable-notice",
|
|
3843
3878
|
label: "Temporarily unavailable",
|
|
3879
|
+
labelKey: "server:baseDeviceSection.s_driver-settings-unavailable.driver-settings-unavailable-notice.label",
|
|
3844
3880
|
content: "Driver settings are temporarily unavailable — the addon may be restarting. Refresh the page in a moment.",
|
|
3845
3881
|
variant: "warning"
|
|
3846
3882
|
}]
|
|
@@ -5541,6 +5577,250 @@ async function runLocationBackfillAtBoot(pctx, bootRows) {
|
|
|
5541
5577
|
});
|
|
5542
5578
|
}
|
|
5543
5579
|
//#endregion
|
|
5580
|
+
//#region src/builtins/device-manager/device-removal-hooks.ts
|
|
5581
|
+
/**
|
|
5582
|
+
* device-manager's side of `device-removal-hook` (D865): tell every implementer
|
|
5583
|
+
* a device was deleted, and answer the admin question "which stored data belongs
|
|
5584
|
+
* to devices that no longer exist?".
|
|
5585
|
+
*
|
|
5586
|
+
* Two entry points, one rule: **an implementer is told to delete only on a
|
|
5587
|
+
* recorded delete (a tombstone), or on an explicit operator confirm.** Nothing
|
|
5588
|
+
* here infers a deletion from a device being absent from a single read.
|
|
5589
|
+
*/
|
|
5590
|
+
/** The capability name implementers register under. */
|
|
5591
|
+
var DEVICE_REMOVAL_HOOK_CAP = "device-removal-hook";
|
|
5592
|
+
async function hookProviders(api) {
|
|
5593
|
+
return (await api.addons.listCapabilityProviders.query({ capName: DEVICE_REMOVAL_HOOK_CAP })).map((p) => p.addonId);
|
|
5594
|
+
}
|
|
5595
|
+
/**
|
|
5596
|
+
* The removal step that calls every implementer. Each implementer is its own
|
|
5597
|
+
* nested step, so a failing one is retried by the bounded reconcile without
|
|
5598
|
+
* re-calling the others. Failing to ENUMERATE the implementers throws, and the
|
|
5599
|
+
* outer cleanup retries the whole step.
|
|
5600
|
+
*/
|
|
5601
|
+
function removalHookStep(deps, input) {
|
|
5602
|
+
return {
|
|
5603
|
+
store: "removal-hooks",
|
|
5604
|
+
run: async () => {
|
|
5605
|
+
const api = deps.api;
|
|
5606
|
+
if (api === void 0) {
|
|
5607
|
+
deps.logger.warn("removal hooks not called — no api on this context", { tags: { deviceId: input.deviceId } });
|
|
5608
|
+
return 0;
|
|
5609
|
+
}
|
|
5610
|
+
const addonIds = await hookProviders(api);
|
|
5611
|
+
if (addonIds.length === 0) return 0;
|
|
5612
|
+
const steps = addonIds.map((addonId) => ({
|
|
5613
|
+
store: `hook:${addonId}`,
|
|
5614
|
+
run: async () => {
|
|
5615
|
+
const result = await api.deviceRemovalHook.onDeviceRemoved.mutate({
|
|
5616
|
+
...input,
|
|
5617
|
+
addonId
|
|
5618
|
+
});
|
|
5619
|
+
return Object.values(result.counts).reduce((sum, n) => sum + n, 0);
|
|
5620
|
+
}
|
|
5621
|
+
}));
|
|
5622
|
+
const report = await deps.cleanup.run(input.deviceId, steps);
|
|
5623
|
+
return Object.values(report.counts).reduce((sum, n) => sum + n, 0);
|
|
5624
|
+
}
|
|
5625
|
+
};
|
|
5626
|
+
}
|
|
5627
|
+
/**
|
|
5628
|
+
* The ids device-manager knows, from TWO consistent reads (D49). Refuses when a
|
|
5629
|
+
* read throws, answers empty (an empty fleet and a failed read look the same),
|
|
5630
|
+
* or returns fewer ids than the ledger has rows (a malformed row is skipped by
|
|
5631
|
+
* the decoder, and a device skipped is a device whose data would be deleted).
|
|
5632
|
+
* The known set is the UNION of both reads: an id is absent only when BOTH
|
|
5633
|
+
* reads lack it.
|
|
5634
|
+
*/
|
|
5635
|
+
async function checkFleetTwice(reads) {
|
|
5636
|
+
const known = /* @__PURE__ */ new Set();
|
|
5637
|
+
for (const attempt of [1, 2]) {
|
|
5638
|
+
let read;
|
|
5639
|
+
try {
|
|
5640
|
+
read = await reads.readFleet();
|
|
5641
|
+
} catch (err) {
|
|
5642
|
+
return {
|
|
5643
|
+
ok: false,
|
|
5644
|
+
reason: `device list read ${attempt} failed: ${errMsg(err)}`
|
|
5645
|
+
};
|
|
5646
|
+
}
|
|
5647
|
+
if (read.ids.length === 0) return {
|
|
5648
|
+
ok: false,
|
|
5649
|
+
reason: `device list read ${attempt} answered empty`
|
|
5650
|
+
};
|
|
5651
|
+
if (read.ids.length !== read.rowCount) return {
|
|
5652
|
+
ok: false,
|
|
5653
|
+
reason: `device list read ${attempt} returned ${read.ids.length} of ${read.rowCount} rows`
|
|
5654
|
+
};
|
|
5655
|
+
for (const id of read.ids) known.add(id);
|
|
5656
|
+
}
|
|
5657
|
+
return {
|
|
5658
|
+
ok: true,
|
|
5659
|
+
known
|
|
5660
|
+
};
|
|
5661
|
+
}
|
|
5662
|
+
/** Stores holding data for devices that are not in `known`, grouped per device. */
|
|
5663
|
+
function orphanDevices(held, known) {
|
|
5664
|
+
const byDevice = /* @__PURE__ */ new Map();
|
|
5665
|
+
const holders = /* @__PURE__ */ new Set();
|
|
5666
|
+
let overlap = 0;
|
|
5667
|
+
for (const [, entry] of held) if (!holders.has(entry.deviceId)) {
|
|
5668
|
+
holders.add(entry.deviceId);
|
|
5669
|
+
if (known.has(entry.deviceId)) overlap += 1;
|
|
5670
|
+
}
|
|
5671
|
+
for (const [addonId, entry] of held) {
|
|
5672
|
+
if (known.has(entry.deviceId) || entry.count === 0) continue;
|
|
5673
|
+
const stores = byDevice.get(entry.deviceId) ?? {};
|
|
5674
|
+
stores[`${addonId}:${entry.store}`] = (stores[`${addonId}:${entry.store}`] ?? 0) + entry.count;
|
|
5675
|
+
byDevice.set(entry.deviceId, stores);
|
|
5676
|
+
}
|
|
5677
|
+
const idSpaceMismatch = holders.size >= 5 && overlap === 0;
|
|
5678
|
+
return {
|
|
5679
|
+
devices: [...byDevice.entries()].toSorted(([a], [b]) => a - b).map(([deviceId, stores]) => ({
|
|
5680
|
+
deviceId,
|
|
5681
|
+
stores
|
|
5682
|
+
})),
|
|
5683
|
+
idSpaceMismatch
|
|
5684
|
+
};
|
|
5685
|
+
}
|
|
5686
|
+
/** The admin "Clean data of deleted devices" action: dry run unless `confirm`. */
|
|
5687
|
+
async function cleanupDeletedDevicesData(deps, input) {
|
|
5688
|
+
const skipped = (reason, unreachable = []) => {
|
|
5689
|
+
deps.logger.warn("cleanup of deleted devices data skipped — nothing listed or deleted", { meta: { reason } });
|
|
5690
|
+
return {
|
|
5691
|
+
status: "skipped",
|
|
5692
|
+
reason,
|
|
5693
|
+
devices: [],
|
|
5694
|
+
unreachable: [...unreachable]
|
|
5695
|
+
};
|
|
5696
|
+
};
|
|
5697
|
+
const api = deps.api;
|
|
5698
|
+
if (api === void 0) return skipped("no api on this context");
|
|
5699
|
+
const fleet = await checkFleetTwice(deps.reads);
|
|
5700
|
+
if (!fleet.ok) return skipped(fleet.reason);
|
|
5701
|
+
let addonIds;
|
|
5702
|
+
try {
|
|
5703
|
+
addonIds = await hookProviders(api);
|
|
5704
|
+
} catch (err) {
|
|
5705
|
+
return skipped(`implementers could not be listed: ${errMsg(err)}`);
|
|
5706
|
+
}
|
|
5707
|
+
const unreachable = /* @__PURE__ */ new Set();
|
|
5708
|
+
const held = [];
|
|
5709
|
+
for (const addonId of addonIds) try {
|
|
5710
|
+
for (const entry of await api.deviceRemovalHook.listDeviceData.query({ addonId })) held.push([addonId, entry]);
|
|
5711
|
+
} catch (err) {
|
|
5712
|
+
unreachable.add(addonId);
|
|
5713
|
+
deps.logger.warn("cleanup of deleted devices data: an implementer did not answer", { meta: {
|
|
5714
|
+
addonId,
|
|
5715
|
+
error: errMsg(err)
|
|
5716
|
+
} });
|
|
5717
|
+
}
|
|
5718
|
+
if (deps.settings !== void 0) try {
|
|
5719
|
+
for (const scope of await deps.settings.list()) held.push(["settings", {
|
|
5720
|
+
store: scope.addonId,
|
|
5721
|
+
deviceId: scope.deviceId,
|
|
5722
|
+
count: 1
|
|
5723
|
+
}]);
|
|
5724
|
+
} catch (err) {
|
|
5725
|
+
return skipped(`device settings could not be listed: ${errMsg(err)}`, [...unreachable]);
|
|
5726
|
+
}
|
|
5727
|
+
const { devices, idSpaceMismatch } = orphanDevices(held, fleet.known);
|
|
5728
|
+
if (idSpaceMismatch) return skipped("none of the device ids holding data is a known device — the id space looks wrong, refusing", [...unreachable]);
|
|
5729
|
+
if (!input.confirm) {
|
|
5730
|
+
deps.logger.info("cleanup of deleted devices data: dry run", { meta: {
|
|
5731
|
+
devices: devices.length,
|
|
5732
|
+
unreachable: [...unreachable]
|
|
5733
|
+
} });
|
|
5734
|
+
return {
|
|
5735
|
+
status: "dry-run",
|
|
5736
|
+
reason: null,
|
|
5737
|
+
devices: [...devices],
|
|
5738
|
+
unreachable: [...unreachable]
|
|
5739
|
+
};
|
|
5740
|
+
}
|
|
5741
|
+
const now = (deps.now ?? (() => Date.now()))();
|
|
5742
|
+
const cleaned = [];
|
|
5743
|
+
for (const device of devices) {
|
|
5744
|
+
const deleted = {};
|
|
5745
|
+
for (const addonId of addonIds) try {
|
|
5746
|
+
const result = await api.deviceRemovalHook.onDeviceRemoved.mutate({
|
|
5747
|
+
addonId,
|
|
5748
|
+
deviceId: device.deviceId,
|
|
5749
|
+
deletedAt: now,
|
|
5750
|
+
providerAddonId: "unknown"
|
|
5751
|
+
});
|
|
5752
|
+
for (const [store, count] of Object.entries(result.counts)) deleted[`${addonId}:${store}`] = count;
|
|
5753
|
+
} catch (err) {
|
|
5754
|
+
deps.logger.warn("cleanup of deleted devices data: an implementer failed", {
|
|
5755
|
+
tags: { deviceId: device.deviceId },
|
|
5756
|
+
meta: {
|
|
5757
|
+
addonId,
|
|
5758
|
+
error: errMsg(err)
|
|
5759
|
+
}
|
|
5760
|
+
});
|
|
5761
|
+
}
|
|
5762
|
+
if (deps.settings !== void 0 && Object.keys(device.stores).some((k) => k.startsWith("settings:"))) try {
|
|
5763
|
+
deleted["settings"] = await deps.settings.clear(device.deviceId);
|
|
5764
|
+
} catch (err) {
|
|
5765
|
+
deps.logger.warn("cleanup of deleted devices data: settings not cleared", {
|
|
5766
|
+
tags: { deviceId: device.deviceId },
|
|
5767
|
+
meta: { error: errMsg(err) }
|
|
5768
|
+
});
|
|
5769
|
+
}
|
|
5770
|
+
deps.logger.info("cleaned data of a device deleted before tombstones", {
|
|
5771
|
+
tags: { deviceId: device.deviceId },
|
|
5772
|
+
meta: { counts: deleted }
|
|
5773
|
+
});
|
|
5774
|
+
cleaned.push({
|
|
5775
|
+
deviceId: device.deviceId,
|
|
5776
|
+
stores: deleted
|
|
5777
|
+
});
|
|
5778
|
+
}
|
|
5779
|
+
return {
|
|
5780
|
+
status: "cleaned",
|
|
5781
|
+
reason: null,
|
|
5782
|
+
devices: cleaned,
|
|
5783
|
+
unreachable: [...unreachable]
|
|
5784
|
+
};
|
|
5785
|
+
}
|
|
5786
|
+
/** The settings-store collection every addon's per-device settings share. */
|
|
5787
|
+
var ADDON_DEVICE_SETTINGS_COLLECTION = "addon-device-settings";
|
|
5788
|
+
/** The engine's hard ceiling on one read; a result this long may be a truncated one. */
|
|
5789
|
+
var SCOPE_READ_LIMIT = 2e4;
|
|
5790
|
+
/** Scope access over the settings store; undefined when the store is unavailable. */
|
|
5791
|
+
function settingsScopes(store) {
|
|
5792
|
+
if (store === void 0) return void 0;
|
|
5793
|
+
return {
|
|
5794
|
+
list: async () => {
|
|
5795
|
+
const rows = await store.query.query({
|
|
5796
|
+
collection: ADDON_DEVICE_SETTINGS_COLLECTION,
|
|
5797
|
+
filter: { limit: SCOPE_READ_LIMIT }
|
|
5798
|
+
});
|
|
5799
|
+
if (rows.length >= SCOPE_READ_LIMIT) throw new Error(`the settings read returned ${rows.length} rows — possibly truncated`);
|
|
5800
|
+
const seen = /* @__PURE__ */ new Map();
|
|
5801
|
+
for (const row of rows) {
|
|
5802
|
+
const addonId = row.data["addonId"];
|
|
5803
|
+
const raw = row.data["deviceId"];
|
|
5804
|
+
if (typeof addonId !== "string" || typeof raw !== "string") continue;
|
|
5805
|
+
const deviceId = Number(raw);
|
|
5806
|
+
if (!Number.isInteger(deviceId) || deviceId <= 0) continue;
|
|
5807
|
+
seen.set(`${addonId}:${deviceId}`, {
|
|
5808
|
+
deviceId,
|
|
5809
|
+
addonId
|
|
5810
|
+
});
|
|
5811
|
+
}
|
|
5812
|
+
return [...seen.values()];
|
|
5813
|
+
},
|
|
5814
|
+
clear: async (deviceId) => {
|
|
5815
|
+
const { deleted } = await store.deleteWhere.mutate({
|
|
5816
|
+
collection: ADDON_DEVICE_SETTINGS_COLLECTION,
|
|
5817
|
+
filter: { where: { deviceId: String(deviceId) } }
|
|
5818
|
+
});
|
|
5819
|
+
return deleted;
|
|
5820
|
+
}
|
|
5821
|
+
};
|
|
5822
|
+
}
|
|
5823
|
+
//#endregion
|
|
5544
5824
|
//#region src/builtins/device-manager/device-meta-actions.ts
|
|
5545
5825
|
/**
|
|
5546
5826
|
* Device meta-mutation + persistence actions for the device-manager addon —
|
|
@@ -5939,6 +6219,91 @@ async function registerDevice(pctx, input) {
|
|
|
5939
6219
|
}
|
|
5940
6220
|
});
|
|
5941
6221
|
}
|
|
6222
|
+
/**
|
|
6223
|
+
* The stores device-manager (or the keychain, for secrets) keeps for a device.
|
|
6224
|
+
* Another addon's own per-device data is cleaned by that addon, off
|
|
6225
|
+
* `DeviceUnregistered` — the kernel never reaches into its tables (D864).
|
|
6226
|
+
*/
|
|
6227
|
+
function removalSteps(pctx, deviceId, providerAddonId, deletedAt) {
|
|
6228
|
+
const secrets = pctx.host.ctx.secrets;
|
|
6229
|
+
return [
|
|
6230
|
+
{
|
|
6231
|
+
store: "tombstone",
|
|
6232
|
+
run: async () => {
|
|
6233
|
+
await pctx.tombstones.record({
|
|
6234
|
+
deviceId,
|
|
6235
|
+
deletedAt,
|
|
6236
|
+
providerAddonId
|
|
6237
|
+
});
|
|
6238
|
+
return 1;
|
|
6239
|
+
}
|
|
6240
|
+
},
|
|
6241
|
+
{
|
|
6242
|
+
store: "keychain-secrets",
|
|
6243
|
+
run: async () => {
|
|
6244
|
+
const purge = secrets?.deleteDeviceScope;
|
|
6245
|
+
if (purge === void 0) {
|
|
6246
|
+
pctx.host.ctx.logger.warn("device secrets not purged — no keychain door", { tags: { deviceId } });
|
|
6247
|
+
return 0;
|
|
6248
|
+
}
|
|
6249
|
+
return (await purge(deviceId)).length;
|
|
6250
|
+
}
|
|
6251
|
+
},
|
|
6252
|
+
{
|
|
6253
|
+
store: "device-config",
|
|
6254
|
+
run: async () => {
|
|
6255
|
+
const stored = await pctx.settings.readDeviceStore(deviceId);
|
|
6256
|
+
await pctx.settings.clearDeviceStore(deviceId);
|
|
6257
|
+
return Object.keys(stored).length;
|
|
6258
|
+
}
|
|
6259
|
+
},
|
|
6260
|
+
{
|
|
6261
|
+
store: "addon-device-settings",
|
|
6262
|
+
run: async () => {
|
|
6263
|
+
const api = pctx.host.ctx.api?.settingsStore;
|
|
6264
|
+
if (api === void 0) throw new Error("settings-store api not available");
|
|
6265
|
+
const { deleted } = await api.deleteWhere.mutate({
|
|
6266
|
+
collection: "addon-device-settings",
|
|
6267
|
+
filter: { where: { deviceId: String(deviceId) } }
|
|
6268
|
+
});
|
|
6269
|
+
return deleted;
|
|
6270
|
+
}
|
|
6271
|
+
},
|
|
6272
|
+
{
|
|
6273
|
+
store: "runtime-state",
|
|
6274
|
+
run: async () => {
|
|
6275
|
+
await pctx.host.forgetMirrorDevice(deviceId);
|
|
6276
|
+
const stored = await pctx.settings.readDeviceRuntimeState(deviceId);
|
|
6277
|
+
await pctx.settings.clearDeviceRuntimeState(deviceId);
|
|
6278
|
+
return Object.keys(stored).length;
|
|
6279
|
+
}
|
|
6280
|
+
},
|
|
6281
|
+
{
|
|
6282
|
+
store: "field-claims",
|
|
6283
|
+
run: async () => {
|
|
6284
|
+
await pctx.host.fieldClaims.pruneDevice(deviceId, pctx.settings);
|
|
6285
|
+
return 0;
|
|
6286
|
+
}
|
|
6287
|
+
},
|
|
6288
|
+
{
|
|
6289
|
+
store: "bindings",
|
|
6290
|
+
run: () => pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
6291
|
+
const key = String(deviceId);
|
|
6292
|
+
const bindingsStore = await readBindingsStore(pctx.bindingsDeps);
|
|
6293
|
+
const slice = bindingsStore.deviceBindings[key];
|
|
6294
|
+
if (!slice) return 0;
|
|
6295
|
+
const { [key]: _removed, ...rest } = bindingsStore.deviceBindings;
|
|
6296
|
+
await writeBindingsStore(pctx.bindingsDeps, { deviceBindings: rest });
|
|
6297
|
+
return Object.keys(slice).length;
|
|
6298
|
+
})
|
|
6299
|
+
},
|
|
6300
|
+
removalHookStep(pctx.removalHooks, {
|
|
6301
|
+
deviceId,
|
|
6302
|
+
deletedAt,
|
|
6303
|
+
providerAddonId
|
|
6304
|
+
})
|
|
6305
|
+
];
|
|
6306
|
+
}
|
|
5942
6307
|
async function removeDevice(pctx, input) {
|
|
5943
6308
|
const { deviceId } = input;
|
|
5944
6309
|
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
@@ -5954,17 +6319,7 @@ async function removeDevice(pctx, input) {
|
|
|
5954
6319
|
});
|
|
5955
6320
|
});
|
|
5956
6321
|
pctx.secretConfigPaths.delete(deviceId);
|
|
5957
|
-
await pctx.
|
|
5958
|
-
await pctx.host.forgetMirrorDevice(deviceId);
|
|
5959
|
-
await pctx.settings.clearDeviceRuntimeState(deviceId);
|
|
5960
|
-
await pctx.host.fieldClaims.pruneDevice(deviceId, pctx.settings);
|
|
5961
|
-
const bindingKey = String(deviceId);
|
|
5962
|
-
await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
5963
|
-
const bindingsStore = await readBindingsStore(pctx.bindingsDeps);
|
|
5964
|
-
if (!bindingsStore.deviceBindings[bindingKey]) return;
|
|
5965
|
-
const { [bindingKey]: _removedBindings, ...restBindings } = bindingsStore.deviceBindings;
|
|
5966
|
-
await writeBindingsStore(pctx.bindingsDeps, { deviceBindings: restBindings });
|
|
5967
|
-
});
|
|
6322
|
+
await pctx.removalCleanup.run(deviceId, removalSteps(pctx, deviceId, addonId, Date.now()));
|
|
5968
6323
|
pctx.host.remoteNativeCaps.delete(deviceId);
|
|
5969
6324
|
pctx.host.capabilityRegistry?.unregisterAllNativeForDevice(deviceId);
|
|
5970
6325
|
pctx.metaStore.idToAddonId.delete(deviceId);
|
|
@@ -6661,7 +7016,7 @@ async function listLocations(pctx) {
|
|
|
6661
7016
|
*/
|
|
6662
7017
|
async function addLocation(pctx, input) {
|
|
6663
7018
|
const trimmed = input.name.trim();
|
|
6664
|
-
if (trimmed.length === 0) throw
|
|
7019
|
+
if (trimmed.length === 0) throw userError("system.location-name-required", void 0, "[device-manager] addLocation: name must be non-empty");
|
|
6665
7020
|
await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
6666
7021
|
const current = (await pctx.metaStore.readStore()).locations ?? [];
|
|
6667
7022
|
if (current.some((l) => l.toLowerCase() === trimmed.toLowerCase())) return;
|
|
@@ -6707,7 +7062,7 @@ async function listLocationIcons(pctx) {
|
|
|
6707
7062
|
*/
|
|
6708
7063
|
async function setLocationIcon(pctx, input) {
|
|
6709
7064
|
const trimmed = input.location.trim();
|
|
6710
|
-
if (trimmed.length === 0) throw
|
|
7065
|
+
if (trimmed.length === 0) throw userError("system.location-required", void 0, "[device-manager] setLocationIcon: location must be non-empty");
|
|
6711
7066
|
const key = locationIconKey(trimmed);
|
|
6712
7067
|
await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
6713
7068
|
const icons = (await pctx.metaStore.readStore()).locationIcons ?? {};
|
|
@@ -6807,7 +7162,7 @@ async function listSiteZones(pctx) {
|
|
|
6807
7162
|
*/
|
|
6808
7163
|
async function setSiteZone(pctx, input) {
|
|
6809
7164
|
const label = input.name.trim();
|
|
6810
|
-
if (label.length === 0) throw
|
|
7165
|
+
if (label.length === 0) throw userError("system.site-zone-name-required", void 0, "[device-manager] setSiteZone: name must be non-empty");
|
|
6811
7166
|
const key = siteZoneKey(label);
|
|
6812
7167
|
await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
|
|
6813
7168
|
const zones = (await pctx.metaStore.readStore()).siteZones ?? {};
|
|
@@ -8294,6 +8649,171 @@ var DeviceRowStore = class {
|
|
|
8294
8649
|
}
|
|
8295
8650
|
};
|
|
8296
8651
|
//#endregion
|
|
8652
|
+
//#region src/builtins/device-manager/device-tombstone-store.ts
|
|
8653
|
+
/**
|
|
8654
|
+
* The tombstones of deleted devices (D865).
|
|
8655
|
+
*
|
|
8656
|
+
* A device DELETE is a fact other addons must be able to learn reliably, long
|
|
8657
|
+
* after the `DeviceUnregistered` event (telemetry, droppable — D8/D11) is gone.
|
|
8658
|
+
* device-manager is the only authority on whether a device exists, so the record
|
|
8659
|
+
* of a deletion lives here, in its own collection, beside the device ledger.
|
|
8660
|
+
*
|
|
8661
|
+
* ## What a tombstone is NOT
|
|
8662
|
+
*
|
|
8663
|
+
* It is not "the device is unregistered / offline / its provider is restarting".
|
|
8664
|
+
* Those are states of a device that still exists; only {@link record}'s one
|
|
8665
|
+
* caller — `removeDevice`, the single door every delete converges on — writes a
|
|
8666
|
+
* tombstone. Anything that reconciles from tombstones therefore can never
|
|
8667
|
+
* destroy data of a device that merely went quiet.
|
|
8668
|
+
*
|
|
8669
|
+
* ## Expiry
|
|
8670
|
+
*
|
|
8671
|
+
* {@link DEVICE_TOMBSTONE_RETENTION_MS} (90 days). A tombstone older than that
|
|
8672
|
+
* is deleted the next time one is written or read. Ninety days is generous on
|
|
8673
|
+
* purpose: an addon that stayed disabled for a quarter still learns of the
|
|
8674
|
+
* deletions on its return. Data of a device deleted longer ago than that is
|
|
8675
|
+
* reachable only by the admin "clean data of deleted devices" action.
|
|
8676
|
+
*/
|
|
8677
|
+
/**
|
|
8678
|
+
* @durable class=ledger owner=device-manager
|
|
8679
|
+
* write="`removeDevice` records one row per deleted device (key = device id) after the
|
|
8680
|
+
* device row is gone"
|
|
8681
|
+
* retention="90 days (`DEVICE_TOMBSTONE_RETENTION_MS`), pruned whenever a tombstone is
|
|
8682
|
+
* written or the list is read. Bounded by the deletions an operator makes in a quarter."
|
|
8683
|
+
*/
|
|
8684
|
+
var DEVICE_TOMBSTONES_COLLECTION = "device-manager:tombstones";
|
|
8685
|
+
var DEVICE_TOMBSTONE_COLUMNS = [
|
|
8686
|
+
{
|
|
8687
|
+
name: "id",
|
|
8688
|
+
type: "TEXT",
|
|
8689
|
+
primaryKey: true,
|
|
8690
|
+
notNull: true
|
|
8691
|
+
},
|
|
8692
|
+
{
|
|
8693
|
+
name: "deviceId",
|
|
8694
|
+
type: "INTEGER",
|
|
8695
|
+
notNull: true
|
|
8696
|
+
},
|
|
8697
|
+
{
|
|
8698
|
+
name: "deletedAt",
|
|
8699
|
+
type: "INTEGER",
|
|
8700
|
+
notNull: true
|
|
8701
|
+
},
|
|
8702
|
+
{
|
|
8703
|
+
name: "providerAddonId",
|
|
8704
|
+
type: "TEXT",
|
|
8705
|
+
notNull: true
|
|
8706
|
+
}
|
|
8707
|
+
];
|
|
8708
|
+
var DEVICE_TOMBSTONE_INDEXES = [{
|
|
8709
|
+
name: "idx_dm_tombstones_deleted_at",
|
|
8710
|
+
columns: ["deletedAt"]
|
|
8711
|
+
}];
|
|
8712
|
+
/** More than the engine's default row cap, so a read is never silently truncated. */
|
|
8713
|
+
var TOMBSTONE_READ_LIMIT = 2e4;
|
|
8714
|
+
function decodeTombstone(raw) {
|
|
8715
|
+
const deviceId = raw["deviceId"];
|
|
8716
|
+
const deletedAt = raw["deletedAt"];
|
|
8717
|
+
const providerAddonId = raw["providerAddonId"];
|
|
8718
|
+
if (typeof deviceId !== "number" || !Number.isInteger(deviceId) || deviceId <= 0) return null;
|
|
8719
|
+
if (typeof deletedAt !== "number" || typeof providerAddonId !== "string") return null;
|
|
8720
|
+
return {
|
|
8721
|
+
deviceId,
|
|
8722
|
+
deletedAt,
|
|
8723
|
+
providerAddonId
|
|
8724
|
+
};
|
|
8725
|
+
}
|
|
8726
|
+
var DeviceTombstoneStore = class {
|
|
8727
|
+
backend;
|
|
8728
|
+
logger;
|
|
8729
|
+
now;
|
|
8730
|
+
retentionMs;
|
|
8731
|
+
declared = null;
|
|
8732
|
+
constructor(opts) {
|
|
8733
|
+
this.backend = opts.backend;
|
|
8734
|
+
this.logger = opts.logger;
|
|
8735
|
+
this.now = opts.now ?? (() => Date.now());
|
|
8736
|
+
this.retentionMs = opts.retentionMs ?? 7776e6;
|
|
8737
|
+
}
|
|
8738
|
+
/** Lazy, idempotent declaration; a rejection is not latched. */
|
|
8739
|
+
declare() {
|
|
8740
|
+
if (this.declared !== null) return this.declared;
|
|
8741
|
+
const run = this.backend.declareCollection({
|
|
8742
|
+
collection: DEVICE_TOMBSTONES_COLLECTION,
|
|
8743
|
+
columns: DEVICE_TOMBSTONE_COLUMNS,
|
|
8744
|
+
indexes: DEVICE_TOMBSTONE_INDEXES
|
|
8745
|
+
}).catch((err) => {
|
|
8746
|
+
this.declared = null;
|
|
8747
|
+
throw err;
|
|
8748
|
+
});
|
|
8749
|
+
this.declared = run;
|
|
8750
|
+
return run;
|
|
8751
|
+
}
|
|
8752
|
+
/** Record a deletion. Re-recording the same id keeps the EARLIER `deletedAt`. */
|
|
8753
|
+
async record(tombstone) {
|
|
8754
|
+
await this.declare();
|
|
8755
|
+
const key = String(tombstone.deviceId);
|
|
8756
|
+
const existing = await this.backend.get({
|
|
8757
|
+
collection: DEVICE_TOMBSTONES_COLLECTION,
|
|
8758
|
+
key
|
|
8759
|
+
});
|
|
8760
|
+
if ((isPlainObject(existing) ? decodeTombstone(existing) : null) === null) await this.backend.set({
|
|
8761
|
+
collection: DEVICE_TOMBSTONES_COLLECTION,
|
|
8762
|
+
key,
|
|
8763
|
+
value: {
|
|
8764
|
+
deviceId: tombstone.deviceId,
|
|
8765
|
+
deletedAt: tombstone.deletedAt,
|
|
8766
|
+
providerAddonId: tombstone.providerAddonId
|
|
8767
|
+
}
|
|
8768
|
+
});
|
|
8769
|
+
await this.pruneExpired();
|
|
8770
|
+
}
|
|
8771
|
+
/** Tombstones with `deletedAt >= sinceMs`, oldest first. Expired ones are pruned first. */
|
|
8772
|
+
async listSince(sinceMs) {
|
|
8773
|
+
await this.declare();
|
|
8774
|
+
await this.pruneExpired();
|
|
8775
|
+
const floor = Math.max(sinceMs, this.now() - this.retentionMs);
|
|
8776
|
+
const rows = await this.backend.query({
|
|
8777
|
+
collection: DEVICE_TOMBSTONES_COLLECTION,
|
|
8778
|
+
filter: {
|
|
8779
|
+
whereBetween: { deletedAt: [floor, Number.MAX_SAFE_INTEGER] },
|
|
8780
|
+
orderBy: {
|
|
8781
|
+
field: "deletedAt",
|
|
8782
|
+
direction: "asc"
|
|
8783
|
+
},
|
|
8784
|
+
limit: TOMBSTONE_READ_LIMIT
|
|
8785
|
+
}
|
|
8786
|
+
});
|
|
8787
|
+
const out = [];
|
|
8788
|
+
for (const row of rows) {
|
|
8789
|
+
const decoded = decodeTombstone(row.data);
|
|
8790
|
+
if (decoded !== null) out.push(decoded);
|
|
8791
|
+
}
|
|
8792
|
+
return out;
|
|
8793
|
+
}
|
|
8794
|
+
/** Delete tombstones older than the retention window. Best-effort: a failure is logged. */
|
|
8795
|
+
async pruneExpired() {
|
|
8796
|
+
try {
|
|
8797
|
+
const cutoff = this.now() - this.retentionMs;
|
|
8798
|
+
const stale = await this.backend.query({
|
|
8799
|
+
collection: DEVICE_TOMBSTONES_COLLECTION,
|
|
8800
|
+
filter: {
|
|
8801
|
+
whereBetween: { deletedAt: [0, cutoff - 1] },
|
|
8802
|
+
limit: TOMBSTONE_READ_LIMIT
|
|
8803
|
+
}
|
|
8804
|
+
});
|
|
8805
|
+
for (const row of stale) await this.backend.delete({
|
|
8806
|
+
collection: DEVICE_TOMBSTONES_COLLECTION,
|
|
8807
|
+
key: row.id
|
|
8808
|
+
});
|
|
8809
|
+
return stale.length;
|
|
8810
|
+
} catch (err) {
|
|
8811
|
+
this.logger.warn("device tombstone prune failed — retried on the next write", { meta: { error: errMsg(err) } });
|
|
8812
|
+
return 0;
|
|
8813
|
+
}
|
|
8814
|
+
}
|
|
8815
|
+
};
|
|
8816
|
+
//#endregion
|
|
8297
8817
|
//#region src/builtins/device-manager/device-state-claims.ts
|
|
8298
8818
|
function createDeviceStateClaimMethods(deps) {
|
|
8299
8819
|
const { mirror, index, settings, logger } = deps;
|
|
@@ -9760,6 +10280,119 @@ function createMigrationGuard(now = Date.now) {
|
|
|
9760
10280
|
}
|
|
9761
10281
|
};
|
|
9762
10282
|
}
|
|
10283
|
+
//#endregion
|
|
10284
|
+
//#region src/builtins/device-manager/removal-cleanup.ts
|
|
10285
|
+
/**
|
|
10286
|
+
* What a device removal leaves behind is cleaned AT REMOVAL, by steps that
|
|
10287
|
+
* cannot block one another (D864).
|
|
10288
|
+
*
|
|
10289
|
+
* `removeDevice` used to run its clean-up as one straight line: the first step
|
|
10290
|
+
* that threw skipped every later one, and the keychain was not in the line at
|
|
10291
|
+
* all — a device-scoped secret (`secret://provider-dreame/device/4483/…`)
|
|
10292
|
+
* outlived its device for good. Each store is a {@link RemovalStep} now: run
|
|
10293
|
+
* independently, counted, logged ONCE per device with `tags: { deviceId }`,
|
|
10294
|
+
* and a step that failed is retried by a BOUNDED in-memory reconcile
|
|
10295
|
+
* (a few attempts, growing delays, then it gives up loudly).
|
|
10296
|
+
*
|
|
10297
|
+
* There is deliberately no boot-time sweep (the operator forbids purges on
|
|
10298
|
+
* boot): a retry that dies with the process is picked up by the keychain
|
|
10299
|
+
* admin's orphan listing (System -> Secrets), which names a device-scoped
|
|
10300
|
+
* secret whose device no longer points at it.
|
|
10301
|
+
*/
|
|
10302
|
+
/** Retry delays after a failed step; the list length bounds the attempts. */
|
|
10303
|
+
var REMOVAL_RETRY_DELAYS_MS = [
|
|
10304
|
+
3e4,
|
|
10305
|
+
12e4,
|
|
10306
|
+
6e5
|
|
10307
|
+
];
|
|
10308
|
+
async function attempt(steps) {
|
|
10309
|
+
const counts = {};
|
|
10310
|
+
const failed = [];
|
|
10311
|
+
const errors = {};
|
|
10312
|
+
for (const step of steps) try {
|
|
10313
|
+
counts[step.store] = await step.run();
|
|
10314
|
+
} catch (err) {
|
|
10315
|
+
failed.push(step);
|
|
10316
|
+
errors[step.store] = errMsg(err);
|
|
10317
|
+
}
|
|
10318
|
+
return {
|
|
10319
|
+
counts,
|
|
10320
|
+
failed,
|
|
10321
|
+
errors
|
|
10322
|
+
};
|
|
10323
|
+
}
|
|
10324
|
+
function createRemovalCleanup(opts) {
|
|
10325
|
+
const label = opts.label ?? "device removal cleanup";
|
|
10326
|
+
const delays = opts.retryDelaysMs ?? REMOVAL_RETRY_DELAYS_MS;
|
|
10327
|
+
const setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
10328
|
+
const clearTimer = opts.clearTimer ?? ((handle) => clearTimeout(handle));
|
|
10329
|
+
const pending = /* @__PURE__ */ new Set();
|
|
10330
|
+
let disposed = false;
|
|
10331
|
+
const schedule = (deviceId, steps, retryIndex, first) => {
|
|
10332
|
+
if (disposed) return;
|
|
10333
|
+
const delay = delays[retryIndex];
|
|
10334
|
+
if (delay === void 0) {
|
|
10335
|
+
opts.logger.error(`${label} gave up — the data stays until cleaned by hand`, {
|
|
10336
|
+
tags: { deviceId },
|
|
10337
|
+
meta: {
|
|
10338
|
+
stores: steps.map((s) => s.store),
|
|
10339
|
+
attempts: retryIndex + (first ? 1 : 0)
|
|
10340
|
+
}
|
|
10341
|
+
});
|
|
10342
|
+
return;
|
|
10343
|
+
}
|
|
10344
|
+
const handle = setTimer(() => {
|
|
10345
|
+
pending.delete(handle);
|
|
10346
|
+
retry(deviceId, steps, retryIndex);
|
|
10347
|
+
}, delay);
|
|
10348
|
+
handle.unref?.();
|
|
10349
|
+
pending.add(handle);
|
|
10350
|
+
};
|
|
10351
|
+
const retry = async (deviceId, steps, retryIndex) => {
|
|
10352
|
+
const result = await attempt(steps);
|
|
10353
|
+
opts.logger.info(`${label} retried`, {
|
|
10354
|
+
tags: { deviceId },
|
|
10355
|
+
meta: {
|
|
10356
|
+
counts: result.counts,
|
|
10357
|
+
failed: result.failed.map((s) => s.store),
|
|
10358
|
+
retryIndex
|
|
10359
|
+
}
|
|
10360
|
+
});
|
|
10361
|
+
if (result.failed.length > 0) schedule(deviceId, result.failed, retryIndex + 1, false);
|
|
10362
|
+
};
|
|
10363
|
+
return {
|
|
10364
|
+
async run(deviceId, steps) {
|
|
10365
|
+
const result = await attempt(steps);
|
|
10366
|
+
const failedStores = result.failed.map((s) => s.store);
|
|
10367
|
+
const meta = {
|
|
10368
|
+
counts: result.counts,
|
|
10369
|
+
failed: failedStores,
|
|
10370
|
+
errors: result.errors
|
|
10371
|
+
};
|
|
10372
|
+
if (failedStores.length === 0) opts.logger.info(label, {
|
|
10373
|
+
tags: { deviceId },
|
|
10374
|
+
meta
|
|
10375
|
+
});
|
|
10376
|
+
else {
|
|
10377
|
+
opts.logger.warn(`${label} incomplete — failed steps are retried`, {
|
|
10378
|
+
tags: { deviceId },
|
|
10379
|
+
meta
|
|
10380
|
+
});
|
|
10381
|
+
schedule(deviceId, result.failed, 0, true);
|
|
10382
|
+
}
|
|
10383
|
+
return {
|
|
10384
|
+
deviceId,
|
|
10385
|
+
counts: result.counts,
|
|
10386
|
+
failed: failedStores
|
|
10387
|
+
};
|
|
10388
|
+
},
|
|
10389
|
+
dispose() {
|
|
10390
|
+
disposed = true;
|
|
10391
|
+
for (const handle of pending) clearTimer(handle);
|
|
10392
|
+
pending.clear();
|
|
10393
|
+
}
|
|
10394
|
+
};
|
|
10395
|
+
}
|
|
9763
10396
|
function pairKey(addonId, stableId) {
|
|
9764
10397
|
return `${addonId}${stableId}`;
|
|
9765
10398
|
}
|
|
@@ -9882,6 +10515,9 @@ var DeviceManagerAddon = class extends BaseAddon {
|
|
|
9882
10515
|
* `allocateDeviceId` never consults.
|
|
9883
10516
|
*/
|
|
9884
10517
|
removalFence = createRemovalFence();
|
|
10518
|
+
/** Store-by-store clean-up of a removed device, with a bounded retry (D864). */
|
|
10519
|
+
removalCleanupImpl = null;
|
|
10520
|
+
removalHooksCleanupImpl = null;
|
|
9885
10521
|
/** Declared secret config paths per device, as last registered (D783). */
|
|
9886
10522
|
secretConfigPaths = createSecretConfigPathRegistry();
|
|
9887
10523
|
/** The keychain side of driver device forms (D789), wired in onInitialize. */
|
|
@@ -10093,6 +10729,10 @@ var DeviceManagerAddon = class extends BaseAddon {
|
|
|
10093
10729
|
const deviceRows = new DeviceRowStore(deviceRowBackendOf(settingsStoreApi), this.ctx.logger.child("rows"));
|
|
10094
10730
|
await deviceRows.declare();
|
|
10095
10731
|
this.deviceRows = deviceRows;
|
|
10732
|
+
const tombstones = new DeviceTombstoneStore({
|
|
10733
|
+
backend: deviceRowBackendOf(settingsStoreApi),
|
|
10734
|
+
logger: this.ctx.logger.child("tombstones")
|
|
10735
|
+
});
|
|
10096
10736
|
this.staleWrapperActivations.checkOnce(() => readBindingsStore(this.bindingsDeps));
|
|
10097
10737
|
try {
|
|
10098
10738
|
await purgeRetiredSettingsRows(deviceRows.retiredRowStore(), this.ctx.logger.child("RetiredRows"));
|
|
@@ -10128,6 +10768,13 @@ var DeviceManagerAddon = class extends BaseAddon {
|
|
|
10128
10768
|
bindingsDeps: this.bindingsDeps,
|
|
10129
10769
|
migrationGuard: createMigrationGuard(),
|
|
10130
10770
|
removalFence: this.removalFence,
|
|
10771
|
+
removalCleanup: this.removalCleanup,
|
|
10772
|
+
tombstones,
|
|
10773
|
+
removalHooks: {
|
|
10774
|
+
api: this.ctx.api,
|
|
10775
|
+
logger: this.ctx.logger,
|
|
10776
|
+
cleanup: this.removalHooksCleanup
|
|
10777
|
+
},
|
|
10131
10778
|
secretConfigPaths: this.secretConfigPaths,
|
|
10132
10779
|
requireDeviceOps,
|
|
10133
10780
|
stampIntegrationId: stampIntegrationId$1,
|
|
@@ -10161,6 +10808,18 @@ var DeviceManagerAddon = class extends BaseAddon {
|
|
|
10161
10808
|
migrateDevice: (input) => migrateDevice(pctx, input),
|
|
10162
10809
|
registerDevice: (input) => registerDevice(pctx, input),
|
|
10163
10810
|
removeDevice: (input) => removeDevice(pctx, input),
|
|
10811
|
+
listDeletedSince: ({ sinceMs }) => tombstones.listSince(sinceMs),
|
|
10812
|
+
cleanupDeletedDevicesData: (input) => cleanupDeletedDevicesData({
|
|
10813
|
+
api: this.ctx.api,
|
|
10814
|
+
logger: this.ctx.logger,
|
|
10815
|
+
settings: settingsScopes(this.ctx.api?.settingsStore),
|
|
10816
|
+
reads: { readFleet: async () => {
|
|
10817
|
+
return {
|
|
10818
|
+
ids: (await deviceRows.listAll()).map((r) => r.meta.id),
|
|
10819
|
+
rowCount: await deviceRows.count()
|
|
10820
|
+
};
|
|
10821
|
+
} }
|
|
10822
|
+
}, input),
|
|
10164
10823
|
persistConfig: (input) => persistConfig(pctx, input),
|
|
10165
10824
|
loadConfig: (input) => loadConfig(pctx, input),
|
|
10166
10825
|
loadMeta: (input) => loadMeta(pctx, input),
|
|
@@ -10384,7 +11043,22 @@ var DeviceManagerAddon = class extends BaseAddon {
|
|
|
10384
11043
|
}
|
|
10385
11044
|
];
|
|
10386
11045
|
}
|
|
11046
|
+
get removalCleanup() {
|
|
11047
|
+
if (this.removalCleanupImpl === null) this.removalCleanupImpl = createRemovalCleanup({ logger: this.ctx.logger });
|
|
11048
|
+
return this.removalCleanupImpl;
|
|
11049
|
+
}
|
|
11050
|
+
get removalHooksCleanup() {
|
|
11051
|
+
if (this.removalHooksCleanupImpl === null) this.removalHooksCleanupImpl = createRemovalCleanup({
|
|
11052
|
+
logger: this.ctx.logger,
|
|
11053
|
+
label: "device removal hooks"
|
|
11054
|
+
});
|
|
11055
|
+
return this.removalHooksCleanupImpl;
|
|
11056
|
+
}
|
|
10387
11057
|
async onShutdown() {
|
|
11058
|
+
this.removalCleanupImpl?.dispose();
|
|
11059
|
+
this.removalCleanupImpl = null;
|
|
11060
|
+
this.removalHooksCleanupImpl?.dispose();
|
|
11061
|
+
this.removalHooksCleanupImpl = null;
|
|
10388
11062
|
this.propagator?.stop();
|
|
10389
11063
|
this.propagator = null;
|
|
10390
11064
|
this.uninstallStatusOverlay?.();
|