@camstack/addon-admin-ui 1.2.38 → 1.2.40
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/assets/{AddLocationWizard-Dt7Vz9Le.js → AddLocationWizard-_UpnD2Rb.js} +1 -1
- package/dist/assets/AddonCollectionPage-DsqpERSx.js +1 -0
- package/dist/assets/Addons-Cbzvs2ne.js +1 -0
- package/dist/assets/{AdminTabs-Ye2mzpqs.js → AdminTabs-C1b21Umm.js} +1 -1
- package/dist/assets/{AiProfiles-DqPxH0J5.js → AiProfiles-CxmKPOGN.js} +1 -1
- package/dist/assets/{BrokerForm-D3fTTpv3.js → BrokerForm-BxVQ7-k2.js} +1 -1
- package/dist/assets/Build-eERcQhc5.js +32 -0
- package/dist/assets/{Capabilities-Ce97mPD2.js → Capabilities-CkTJoG8P.js} +1 -1
- package/dist/assets/{CapabilityBadges-DtkhlewS.js → CapabilityBadges-Pi0oFNyA.js} +1 -1
- package/dist/assets/{Cluster-BG2luAid.js → Cluster-D7MeMUz9.js} +1 -1
- package/dist/assets/{ClusterRecordingTab-Htot8l7Z.js → ClusterRecordingTab-g0pESRbc.js} +1 -1
- package/dist/assets/{Dashboard-mWuskBWk.js → Dashboard-BOdpB8Zi.js} +1 -1
- package/dist/assets/Data-D_lbeA81.js +1 -0
- package/dist/assets/DetectionIntelligence-Dsj8oMfR.js +1 -0
- package/dist/assets/{DeviceAggregateSection-bI7kL3uM.js → DeviceAggregateSection-UayHxH_7.js} +1 -1
- package/dist/assets/DeviceDetail-2vx05ko3.js +2 -0
- package/dist/assets/{Devices-jtpgjbMp.js → Devices-Ctgv5UD9.js} +1 -1
- package/dist/assets/{EmbedPlayerPage-DuZxiSdE.js → EmbedPlayerPage-BH6rYrQY.js} +1 -1
- package/dist/assets/Events-GU0DqH5U.js +1 -0
- package/dist/assets/{FormBuilder-BILjZtIe.js → FormBuilder-4y2vfIFX.js} +1 -1
- package/dist/assets/{Identity-Crk0vUgk.js → Identity-lIu4PKp0.js} +1 -1
- package/dist/assets/{IntegrationDetail-DJurXzOC.js → IntegrationDetail-C0Zcxbgi.js} +1 -1
- package/dist/assets/{Integrations-ySnIH1Sd.js → Integrations-BP2jk3NT.js} +1 -1
- package/dist/assets/Integrations-CtXQURhT.js +1 -0
- package/dist/assets/{Logs-Bb1TGrRD.js → Logs-CjKk3Pid.js} +1 -1
- package/dist/assets/{MotionZonesSettings-BWWbwGHM.js → MotionZonesSettings-Lydieqfe.js} +1 -1
- package/dist/assets/{MyAccess-C2PAPIAg.js → MyAccess-W28I5kd-.js} +1 -1
- package/dist/assets/Network-Cnq4bp10.js +1 -0
- package/dist/assets/{NodeAddonsSettingsPanel-JNSINmAv.js → NodeAddonsSettingsPanel-BI-P31wN.js} +1 -1
- package/dist/assets/{NodeInferenceDevices-Cht6qC4t.js → NodeInferenceDevices-BmhhZKpR.js} +1 -1
- package/dist/assets/Notifications-BGKlEsy-.js +1 -0
- package/dist/assets/Pipeline-6vY94dNS.js +1 -0
- package/dist/assets/{Plates-HrXlNj-Q.js → Plates-CXXsVO_i.js} +1 -1
- package/dist/assets/{PrivacyMaskSettings-DXEaUNBs.js → PrivacyMaskSettings-Cm6_Ayev.js} +1 -1
- package/dist/assets/{ProviderIcon-KgF3V3AC.js → ProviderIcon-NBKDrcT9.js} +1 -1
- package/dist/assets/{Recordings-BnrqoOrs.js → Recordings-fIC-_Plr.js} +1 -1
- package/dist/assets/{ScanNetworkModal-Dxx2BKAi.js → ScanNetworkModal-BEJE3rpa.js} +1 -1
- package/dist/assets/{Settings-ButH6GlA.js → Settings-2DUxyOI7.js} +1 -1
- package/dist/assets/{Showroom-DLHqovib.js → Showroom-DsDBDxPb.js} +1 -1
- package/dist/assets/{Terminal-CHE7uMQo.js → Terminal-DsDxn595.js} +1 -1
- package/dist/assets/{_virtual_mf-localSharedImportMap___mfe_internal__admin_ui_host-KB1NgTYv.js → _virtual_mf-localSharedImportMap___mfe_internal__admin_ui_host-DuoUM9go.js} +1 -1
- package/dist/assets/{boxes-DXbr65aS.js → boxes-byknuFCy.js} +1 -1
- package/dist/assets/{build-cluster-graph-C_OKGVaH.js → build-cluster-graph-gWLixa2m.js} +2 -2
- package/dist/assets/circle-check-big-D8Wjv0ct.js +1 -0
- package/dist/assets/clock-ISV-5mJ1.js +1 -0
- package/dist/assets/{copy-DRrD5vDH.js → copy-DtilJJvc.js} +1 -1
- package/dist/assets/{debounced-invalidate-D66PzHBd.js → debounced-invalidate-DHj6wyxg.js} +1 -1
- package/dist/assets/{dist-DJVIQtZJ.js → dist-CL9jy06y.js} +1 -1
- package/dist/assets/dist-DwLd0mpq.js +345 -0
- package/dist/assets/{download-D3-L-PWN.js → download-Dj8UGwXy.js} +1 -1
- package/dist/assets/{face-bbox-D2xVdmEv.js → face-bbox-BFrLPrmw.js} +1 -1
- package/dist/assets/{hostInit-DWq68ula.js → hostInit-dir45U0P.js} +1 -1
- package/dist/assets/index-BMoYnIQI.js +4 -0
- package/dist/assets/index-DW7B7QJD.css +2 -0
- package/dist/assets/{key-round-D5V99ce_.js → key-round-DmdC7g1C.js} +1 -1
- package/dist/assets/{mf-entry-bootstrap-0-93f87015.js → mf-entry-bootstrap-0-7417bcdb.js} +2 -2
- package/dist/assets/{pencil-DMAfyiaP.js → pencil-CM0OcKoQ.js} +1 -1
- package/dist/assets/play-CBwgjWMW.js +1 -0
- package/dist/assets/{player-overlays-DTb-cTgy.js → player-overlays-CM9K8NbB.js} +1 -1
- package/dist/assets/plus-CByuXPqh.js +1 -0
- package/dist/assets/{radio-CykuQXCK.js → radio-C1-CuToc.js} +1 -1
- package/dist/assets/{refresh-cw-B0NcvZHF.js → refresh-cw-BCQ0uqSo.js} +1 -1
- package/dist/assets/remoteEntry-BtIghGso.js +1 -0
- package/dist/assets/rotate-ccw-CiIETsd8.js +1 -0
- package/dist/assets/rotate-cw-Bo4UhVCj.js +1 -0
- package/dist/assets/search-m5N8S4EI.js +1 -0
- package/dist/assets/{send-CtqhPev6.js → send-85b3c2pF.js} +1 -1
- package/dist/assets/{server-BbRPd6PC.js → server-CqHxFx4j.js} +1 -1
- package/dist/assets/square-CwYcDkPi.js +1 -0
- package/dist/assets/{square-check-big-p7HpHDXe.js → square-check-big-BFcA-8wr.js} +1 -1
- package/dist/assets/src-BaYQ7lHC.js +345 -0
- package/dist/assets/src-s1tm1Ako.js +67 -0
- package/dist/assets/{undo-2-BWEA1U6J.js → undo-2-DDQAKjyU.js} +1 -1
- package/dist/assets/{upload-CmfKc09s.js → upload-D0e5f6HZ.js} +1 -1
- package/dist/assets/{useEventInvalidation-R75mI5og.js → useEventInvalidation-BnSiq41g.js} +1 -1
- package/dist/assets/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__admin_ui_host__remoteEntry-_hash_-CeDNF2vl.js → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__admin_ui_host__remoteEntry-_hash_-1o9kxso_.js} +2 -2
- package/dist/assets/{wifi-CTmFh8-0.js → wifi-CkMq-H2F.js} +1 -1
- package/dist/assets/{zap-Cf83BCyl.js → zap-C0CZz-JF.js} +1 -1
- package/dist/index.html +6 -6
- package/dist/server/addon.js +1918 -1719
- package/package.json +1 -1
- package/dist/assets/AddonCollectionPage-6Aw2YAGA.js +0 -1
- package/dist/assets/Addons-CjZHq6IY.js +0 -1
- package/dist/assets/Build-DwyWB5rK.js +0 -29
- package/dist/assets/Data-rCQoSzSV.js +0 -1
- package/dist/assets/DetectionIntelligence-DlGH85sl.js +0 -1
- package/dist/assets/DeviceDetail-CWLhoPV9.js +0 -2
- package/dist/assets/Events-DUxFB5Bn.js +0 -1
- package/dist/assets/Integrations-BHcc42O_.js +0 -1
- package/dist/assets/Network-Cy3RUcrg.js +0 -1
- package/dist/assets/Notifications-D27LJgTU.js +0 -1
- package/dist/assets/Pipeline-Cf3BtloJ.js +0 -1
- package/dist/assets/circle-check-big-CDvvzYtx.js +0 -1
- package/dist/assets/clock-MA7PvDC7.js +0 -1
- package/dist/assets/dist-CTF62Sfi.js +0 -7
- package/dist/assets/index-dqzu-f71.js +0 -4
- package/dist/assets/index-mdJi1UIz.css +0 -2
- package/dist/assets/plus-DBb9xrVk.js +0 -1
- package/dist/assets/remoteEntry-vNWEaSPf.js +0 -1
- package/dist/assets/rotate-ccw-CpixHLR5.js +0 -1
- package/dist/assets/rotate-cw-DiU5mosj.js +0 -1
- package/dist/assets/search-871hv572.js +0 -1
- package/dist/assets/square-DX2ha0JW.js +0 -1
- package/dist/assets/src-BxX2FkJ3.js +0 -67
- package/dist/assets/src-CV0JUB8F.js +0 -7
package/dist/server/addon.js
CHANGED
|
@@ -7913,1026 +7913,1800 @@ var ConvertResultSchema = object({
|
|
|
7913
7913
|
})).readonly()
|
|
7914
7914
|
});
|
|
7915
7915
|
/**
|
|
7916
|
-
*
|
|
7917
|
-
*
|
|
7916
|
+
* Error types for the safe expression engine. Two distinct classes so callers
|
|
7917
|
+
* can tell a compile-time (grammar) failure from a runtime (evaluation)
|
|
7918
|
+
* failure — both are non-fatal to the host: read paths degrade to "skip link".
|
|
7919
|
+
*/
|
|
7920
|
+
/** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
|
|
7921
|
+
* the failure is anchored to a character (author-facing inline feedback). */
|
|
7922
|
+
var ExpressionParseError = class extends Error {
|
|
7923
|
+
position;
|
|
7924
|
+
constructor(message, position) {
|
|
7925
|
+
super(message);
|
|
7926
|
+
this.name = "ExpressionParseError";
|
|
7927
|
+
this.position = position;
|
|
7928
|
+
}
|
|
7929
|
+
};
|
|
7930
|
+
/** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
|
|
7931
|
+
* result, unknown builtin, step-budget exceeded). */
|
|
7932
|
+
var ExpressionEvalError = class extends Error {
|
|
7933
|
+
constructor(message) {
|
|
7934
|
+
super(message);
|
|
7935
|
+
this.name = "ExpressionEvalError";
|
|
7936
|
+
}
|
|
7937
|
+
};
|
|
7938
|
+
/**
|
|
7939
|
+
* Frozen, null-prototype builtin function table for the expression engine
|
|
7940
|
+
* (spec §4 rule 4). The table is the SOLE surface of callable functions: the
|
|
7941
|
+
* parser rejects any callee not in it, and the evaluator gates each call on an
|
|
7942
|
+
* own-property check against it.
|
|
7918
7943
|
*
|
|
7919
|
-
*
|
|
7920
|
-
*
|
|
7921
|
-
*
|
|
7922
|
-
*
|
|
7923
|
-
*
|
|
7944
|
+
* Because the object has a NULL prototype AND is `Object.freeze`d:
|
|
7945
|
+
* - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
|
|
7946
|
+
* - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
|
|
7947
|
+
* (there is no `Object.prototype` in the chain), so those names are not
|
|
7948
|
+
* callable — they are simply "unknown function" at parse time.
|
|
7924
7949
|
*
|
|
7925
|
-
*
|
|
7926
|
-
*
|
|
7927
|
-
*
|
|
7928
|
-
*
|
|
7929
|
-
* `addon-pages.router.ts` wrapper anymore.
|
|
7950
|
+
* Every numeric argument is validated as a finite number and every numeric
|
|
7951
|
+
* RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
|
|
7952
|
+
* (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
|
|
7953
|
+
* closed rather than emitting a garbage value.
|
|
7930
7954
|
*/
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
});
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
}
|
|
7946
|
-
|
|
7955
|
+
function asFiniteNumber(value, name, index) {
|
|
7956
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
7957
|
+
return value;
|
|
7958
|
+
}
|
|
7959
|
+
function asString$1(value, name, index) {
|
|
7960
|
+
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
7961
|
+
return value;
|
|
7962
|
+
}
|
|
7963
|
+
function finiteResult(value, name) {
|
|
7964
|
+
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
7965
|
+
return value;
|
|
7966
|
+
}
|
|
7967
|
+
function allFiniteNumbers(args, name) {
|
|
7968
|
+
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
7969
|
+
}
|
|
7970
|
+
var INF = Number.POSITIVE_INFINITY;
|
|
7971
|
+
var table = {
|
|
7972
|
+
min: {
|
|
7973
|
+
minArgs: 1,
|
|
7974
|
+
maxArgs: INF,
|
|
7975
|
+
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
7976
|
+
},
|
|
7977
|
+
max: {
|
|
7978
|
+
minArgs: 1,
|
|
7979
|
+
maxArgs: INF,
|
|
7980
|
+
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
7981
|
+
},
|
|
7982
|
+
abs: {
|
|
7983
|
+
minArgs: 1,
|
|
7984
|
+
maxArgs: 1,
|
|
7985
|
+
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
7986
|
+
},
|
|
7987
|
+
floor: {
|
|
7988
|
+
minArgs: 1,
|
|
7989
|
+
maxArgs: 1,
|
|
7990
|
+
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
7991
|
+
},
|
|
7992
|
+
ceil: {
|
|
7993
|
+
minArgs: 1,
|
|
7994
|
+
maxArgs: 1,
|
|
7995
|
+
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
7996
|
+
},
|
|
7997
|
+
sqrt: {
|
|
7998
|
+
minArgs: 1,
|
|
7999
|
+
maxArgs: 1,
|
|
8000
|
+
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
8001
|
+
},
|
|
8002
|
+
round: {
|
|
8003
|
+
minArgs: 1,
|
|
8004
|
+
maxArgs: 2,
|
|
8005
|
+
apply: (args) => {
|
|
8006
|
+
const x = asFiniteNumber(args[0], "round", 0);
|
|
8007
|
+
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
8008
|
+
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
8009
|
+
const factor = 10 ** digits;
|
|
8010
|
+
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
8011
|
+
}
|
|
8012
|
+
},
|
|
8013
|
+
pow: {
|
|
8014
|
+
minArgs: 2,
|
|
8015
|
+
maxArgs: 2,
|
|
8016
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
8017
|
+
},
|
|
8018
|
+
clamp: {
|
|
8019
|
+
minArgs: 3,
|
|
8020
|
+
maxArgs: 3,
|
|
8021
|
+
apply: (args) => {
|
|
8022
|
+
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
8023
|
+
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
8024
|
+
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
8025
|
+
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
8026
|
+
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
8027
|
+
}
|
|
8028
|
+
},
|
|
8029
|
+
avg: {
|
|
8030
|
+
minArgs: 1,
|
|
8031
|
+
maxArgs: INF,
|
|
8032
|
+
apply: (args) => {
|
|
8033
|
+
const nums = allFiniteNumbers(args, "avg");
|
|
8034
|
+
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
8035
|
+
}
|
|
8036
|
+
},
|
|
8037
|
+
sum: {
|
|
8038
|
+
minArgs: 1,
|
|
8039
|
+
maxArgs: INF,
|
|
8040
|
+
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
8041
|
+
},
|
|
8042
|
+
coalesce: {
|
|
8043
|
+
minArgs: 1,
|
|
8044
|
+
maxArgs: INF,
|
|
8045
|
+
apply: (args) => {
|
|
8046
|
+
for (const a of args) if (a !== null) return a;
|
|
8047
|
+
return null;
|
|
8048
|
+
}
|
|
8049
|
+
},
|
|
8050
|
+
age: {
|
|
8051
|
+
minArgs: 2,
|
|
8052
|
+
maxArgs: 2,
|
|
8053
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
8054
|
+
},
|
|
8055
|
+
convert: {
|
|
8056
|
+
minArgs: 3,
|
|
8057
|
+
maxArgs: 3,
|
|
8058
|
+
apply: (args, hooks) => {
|
|
8059
|
+
const x = asFiniteNumber(args[0], "convert", 0);
|
|
8060
|
+
const from = asString$1(args[1], "convert", 1).trim();
|
|
8061
|
+
const to = asString$1(args[2], "convert", 2).trim();
|
|
8062
|
+
if (hooks.convert) {
|
|
8063
|
+
const out = hooks.convert(x, from, to);
|
|
8064
|
+
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
8065
|
+
return finiteResult(out, "convert");
|
|
8066
|
+
}
|
|
8067
|
+
if (from === to) return x;
|
|
8068
|
+
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
8069
|
+
}
|
|
8070
|
+
}
|
|
8071
|
+
};
|
|
8072
|
+
Object.freeze(Object.assign(Object.create(null), table));
|
|
8073
|
+
/** The set of valid builtin names — used by the parser to reject unknown
|
|
8074
|
+
* callees at parse time (immediate author feedback). */
|
|
8075
|
+
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
7947
8076
|
/**
|
|
7948
|
-
*
|
|
7949
|
-
* declarations. Every addon that contributes a UI page registers a
|
|
7950
|
-
* provider here. The hub-side singleton aggregator (`addon-pages` cap,
|
|
7951
|
-
* see `addon-pages.cap.ts`) walks this collection, stamps versioned
|
|
7952
|
-
* `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
|
|
7953
|
-
* that admin-ui consumes.
|
|
8077
|
+
* Resource-bound constants for the safe expression engine.
|
|
7954
8078
|
*
|
|
7955
|
-
*
|
|
7956
|
-
*
|
|
7957
|
-
*
|
|
8079
|
+
* Every bound is defense-in-depth: the grammar is non-Turing-complete (no
|
|
8080
|
+
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
8081
|
+
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
8082
|
+
* work a single author-supplied expression can request, so a hostile or
|
|
8083
|
+
* accidental pathological string can never spend unbounded CPU/memory.
|
|
7958
8084
|
*/
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
/**
|
|
7981
|
-
* Sidebar section this page docks into. Well-known ids: `'detection'`,
|
|
7982
|
-
* `'cluster'`, `'administration'` — the page renders inside that group.
|
|
7983
|
-
* Any OTHER string creates (or joins) a custom section rendered after
|
|
7984
|
-
* the built-in groups; its label comes from `sectionLabel` (first
|
|
7985
|
-
* declaration wins), falling back to the id. Absent → the legacy
|
|
7986
|
-
* "Addon Pages" group.
|
|
7987
|
-
*/
|
|
7988
|
-
section: string().optional(),
|
|
7989
|
-
/** Display label for a CUSTOM `section` id (ignored for well-known ids). */
|
|
7990
|
-
sectionLabel: string().optional()
|
|
7991
|
-
});
|
|
7992
|
-
method(_void(), array(AddonPageDeclarationSchema).readonly());
|
|
7993
|
-
var AddonHttpRouteSchema = object({
|
|
7994
|
-
method: _enum([
|
|
7995
|
-
"GET",
|
|
7996
|
-
"POST",
|
|
7997
|
-
"PUT",
|
|
7998
|
-
"DELETE",
|
|
7999
|
-
"PATCH"
|
|
8000
|
-
]),
|
|
8001
|
-
path: string(),
|
|
8002
|
-
access: _enum([
|
|
8003
|
-
"public",
|
|
8004
|
-
"authenticated",
|
|
8005
|
-
"admin"
|
|
8006
|
-
]).optional(),
|
|
8007
|
-
description: string().optional()
|
|
8008
|
-
});
|
|
8009
|
-
/**
|
|
8010
|
-
* Cross-process route invocation envelope. The hub captures the
|
|
8011
|
-
* request as plain data, ships it to the worker via Moleculer, and
|
|
8012
|
-
* the worker runs the local handler against a capturing reply. The
|
|
8013
|
-
* envelope returned describes what the handler intended (status,
|
|
8014
|
-
* headers, body, or a redirect) so the hub can translate it back to
|
|
8015
|
-
* the Fastify reply that's actually wired to the socket.
|
|
8085
|
+
/** Max source length (chars) — checked BEFORE tokenizing so a huge string is
|
|
8086
|
+
* rejected without allocation. */
|
|
8087
|
+
var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
|
|
8088
|
+
/** A legal binding / identifier name. */
|
|
8089
|
+
var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8090
|
+
/** Binding names an author may NOT use: `now` is auto-injected; the literal
|
|
8091
|
+
* keywords lex as values, not identifiers, so binding to them is meaningless. */
|
|
8092
|
+
var RESERVED_BINDING_NAMES = new Set([
|
|
8093
|
+
"now",
|
|
8094
|
+
"true",
|
|
8095
|
+
"false",
|
|
8096
|
+
"null"
|
|
8097
|
+
]);
|
|
8098
|
+
/**
|
|
8099
|
+
* Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
|
|
8100
|
+
* zero-dependency. The grammar is deliberately boring: decimal numbers,
|
|
8101
|
+
* single/double-quoted strings with a tiny escape set, identifiers, the three
|
|
8102
|
+
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
8103
|
+
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
8104
|
+
* is a parse error with a source position, so member access / assignment /
|
|
8105
|
+
* template literals are lexically impossible.
|
|
8016
8106
|
*/
|
|
8017
|
-
var
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
}
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
}
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
}
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
})
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8107
|
+
var KEYWORDS = new Set([
|
|
8108
|
+
"true",
|
|
8109
|
+
"false",
|
|
8110
|
+
"null"
|
|
8111
|
+
]);
|
|
8112
|
+
function isDigit(ch) {
|
|
8113
|
+
return ch >= "0" && ch <= "9";
|
|
8114
|
+
}
|
|
8115
|
+
function isIdentStart(ch) {
|
|
8116
|
+
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
8117
|
+
}
|
|
8118
|
+
function isIdentPart(ch) {
|
|
8119
|
+
return isIdentStart(ch) || isDigit(ch);
|
|
8120
|
+
}
|
|
8121
|
+
function isWhitespace(ch) {
|
|
8122
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
8123
|
+
}
|
|
8124
|
+
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
8125
|
+
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
8126
|
+
* string. */
|
|
8127
|
+
function tokenize(source) {
|
|
8128
|
+
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
8129
|
+
const tokens = [];
|
|
8130
|
+
let i = 0;
|
|
8131
|
+
const n = source.length;
|
|
8132
|
+
while (i < n) {
|
|
8133
|
+
const ch = source[i];
|
|
8134
|
+
if (isWhitespace(ch)) {
|
|
8135
|
+
i += 1;
|
|
8136
|
+
continue;
|
|
8137
|
+
}
|
|
8138
|
+
if (isDigit(ch)) {
|
|
8139
|
+
const start = i;
|
|
8140
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
8141
|
+
if (i < n && source[i] === ".") {
|
|
8142
|
+
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
8143
|
+
i += 1;
|
|
8144
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
8145
|
+
}
|
|
8146
|
+
const text = source.slice(start, i);
|
|
8147
|
+
const value = Number(text);
|
|
8148
|
+
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
8149
|
+
tokens.push({
|
|
8150
|
+
type: "number",
|
|
8151
|
+
value,
|
|
8152
|
+
pos: start
|
|
8153
|
+
});
|
|
8154
|
+
continue;
|
|
8155
|
+
}
|
|
8156
|
+
if (ch === "'" || ch === "\"") {
|
|
8157
|
+
const quote = ch;
|
|
8158
|
+
const start = i;
|
|
8159
|
+
i += 1;
|
|
8160
|
+
let out = "";
|
|
8161
|
+
let closed = false;
|
|
8162
|
+
while (i < n) {
|
|
8163
|
+
const c = source[i];
|
|
8164
|
+
if (c === "\\") {
|
|
8165
|
+
const next = i + 1 < n ? source[i + 1] : "";
|
|
8166
|
+
if (next === "\\" || next === "'" || next === "\"") {
|
|
8167
|
+
out += next;
|
|
8168
|
+
i += 2;
|
|
8169
|
+
continue;
|
|
8170
|
+
}
|
|
8171
|
+
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
8172
|
+
}
|
|
8173
|
+
if (c === quote) {
|
|
8174
|
+
closed = true;
|
|
8175
|
+
i += 1;
|
|
8176
|
+
break;
|
|
8177
|
+
}
|
|
8178
|
+
out += c;
|
|
8179
|
+
i += 1;
|
|
8180
|
+
}
|
|
8181
|
+
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
8182
|
+
tokens.push({
|
|
8183
|
+
type: "string",
|
|
8184
|
+
value: out,
|
|
8185
|
+
pos: start
|
|
8186
|
+
});
|
|
8187
|
+
continue;
|
|
8188
|
+
}
|
|
8189
|
+
if (isIdentStart(ch)) {
|
|
8190
|
+
const start = i;
|
|
8191
|
+
while (i < n && isIdentPart(source[i])) i += 1;
|
|
8192
|
+
const text = source.slice(start, i);
|
|
8193
|
+
if (KEYWORDS.has(text)) tokens.push({
|
|
8194
|
+
type: "keyword",
|
|
8195
|
+
keyword: keywordOf(text),
|
|
8196
|
+
pos: start
|
|
8197
|
+
});
|
|
8198
|
+
else tokens.push({
|
|
8199
|
+
type: "identifier",
|
|
8200
|
+
name: text,
|
|
8201
|
+
pos: start
|
|
8202
|
+
});
|
|
8203
|
+
continue;
|
|
8204
|
+
}
|
|
8205
|
+
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
8206
|
+
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
8207
|
+
tokens.push({
|
|
8208
|
+
type: "punct",
|
|
8209
|
+
punct: two,
|
|
8210
|
+
pos: i
|
|
8211
|
+
});
|
|
8212
|
+
i += 2;
|
|
8213
|
+
continue;
|
|
8214
|
+
}
|
|
8215
|
+
if (isSinglePunct(ch)) {
|
|
8216
|
+
tokens.push({
|
|
8217
|
+
type: "punct",
|
|
8218
|
+
punct: ch,
|
|
8219
|
+
pos: i
|
|
8220
|
+
});
|
|
8221
|
+
i += 1;
|
|
8222
|
+
continue;
|
|
8223
|
+
}
|
|
8224
|
+
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
8225
|
+
}
|
|
8226
|
+
tokens.push({
|
|
8227
|
+
type: "eof",
|
|
8228
|
+
pos: n
|
|
8229
|
+
});
|
|
8230
|
+
return tokens;
|
|
8231
|
+
}
|
|
8232
|
+
function keywordOf(text) {
|
|
8233
|
+
if (text === "true") return "true";
|
|
8234
|
+
if (text === "false") return "false";
|
|
8235
|
+
return "null";
|
|
8236
|
+
}
|
|
8237
|
+
function isSinglePunct(ch) {
|
|
8238
|
+
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
8239
|
+
}
|
|
8100
8240
|
/**
|
|
8101
|
-
*
|
|
8102
|
-
* declarations. Mirrors the addon-pages split: every addon shipping
|
|
8103
|
-
* widgets registers a provider on this collection cap; the hub-local
|
|
8104
|
-
* aggregator (`addon-widgets`, see `addon-widgets.cap.ts`) walks the
|
|
8105
|
-
* collection, stamps versioned `bundleUrl`s onto each declaration, and
|
|
8106
|
-
* exposes the public listing surface that admin-ui consumes.
|
|
8241
|
+
* Pratt (precedence-climbing) parser for the safe expression mini-language.
|
|
8107
8242
|
*
|
|
8108
|
-
*
|
|
8109
|
-
*
|
|
8110
|
-
*
|
|
8243
|
+
* Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
|
|
8244
|
+
* → relational → additive → multiplicative → unary `! -` → call / primary.
|
|
8245
|
+
* Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
|
|
8246
|
+
* string validated against the builtin table at parse time, so an unknown
|
|
8247
|
+
* function is rejected immediately (author feedback) and a persisted expression
|
|
8248
|
+
* that references a since-removed builtin degrades at read.
|
|
8111
8249
|
*
|
|
8112
|
-
*
|
|
8113
|
-
*
|
|
8114
|
-
* same `ContributionRenderer` / Module-Federation path as every other
|
|
8115
|
-
* contributed UI surface — no bespoke widget-rendering path. The widget-
|
|
8116
|
-
* only metadata (sizing hints, `requires`) lives as extra fields on the
|
|
8117
|
-
* descriptor; the `UiContribution` core (`tab` / `label` / `order` /
|
|
8118
|
-
* `kind` / `remote`) carries identity + placement + the MF remote.
|
|
8119
|
-
*/
|
|
8120
|
-
/** Where the widget makes sense to render — maps to a contribution `tab`. */
|
|
8121
|
-
var WidgetHostEnum = _enum([
|
|
8122
|
-
"device-tab",
|
|
8123
|
-
"dashboard",
|
|
8124
|
-
"integration-detail"
|
|
8125
|
-
]);
|
|
8126
|
-
var WidgetSizeEnum = _enum([
|
|
8127
|
-
"xs",
|
|
8128
|
-
"sm",
|
|
8129
|
-
"md",
|
|
8130
|
-
"lg",
|
|
8131
|
-
"xl"
|
|
8132
|
-
]);
|
|
8133
|
-
/**
|
|
8134
|
-
* MF remote descriptor — mirrors `UiContributionRemote` from
|
|
8135
|
-
* `capability-definition.ts`. Widget remotes expose a single
|
|
8136
|
-
* `'./widgets'` module whose default export is a
|
|
8137
|
-
* `Record<componentKey, Component>` map; `componentKey` (the widget
|
|
8138
|
-
* `stableId`) picks the entry the host mounts.
|
|
8250
|
+
* A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
|
|
8251
|
+
* arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
|
|
8139
8252
|
*/
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8253
|
+
/** Binary/logical operator precedence (higher binds tighter). */
|
|
8254
|
+
var BINARY_PRECEDENCE = {
|
|
8255
|
+
"||": 1,
|
|
8256
|
+
"&&": 2,
|
|
8257
|
+
"==": 3,
|
|
8258
|
+
"!=": 3,
|
|
8259
|
+
"<": 4,
|
|
8260
|
+
"<=": 4,
|
|
8261
|
+
">": 4,
|
|
8262
|
+
">=": 4,
|
|
8263
|
+
"+": 5,
|
|
8264
|
+
"-": 5,
|
|
8265
|
+
"*": 6,
|
|
8266
|
+
"/": 6,
|
|
8267
|
+
"%": 6
|
|
8268
|
+
};
|
|
8269
|
+
function isLogicalOp(op) {
|
|
8270
|
+
return op === "&&" || op === "||";
|
|
8271
|
+
}
|
|
8272
|
+
function isBinaryOp(op) {
|
|
8273
|
+
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
8274
|
+
}
|
|
8275
|
+
var Parser = class {
|
|
8276
|
+
tokens;
|
|
8277
|
+
pos = 0;
|
|
8278
|
+
nodeCount = 0;
|
|
8279
|
+
identifiers = /* @__PURE__ */ new Set();
|
|
8280
|
+
callees = /* @__PURE__ */ new Set();
|
|
8281
|
+
constructor(tokens) {
|
|
8282
|
+
this.tokens = tokens;
|
|
8283
|
+
}
|
|
8284
|
+
parse() {
|
|
8285
|
+
const ast = this.parseTernary();
|
|
8286
|
+
const tok = this.peek();
|
|
8287
|
+
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
8288
|
+
return {
|
|
8289
|
+
ast,
|
|
8290
|
+
identifiers: this.identifiers,
|
|
8291
|
+
callees: this.callees,
|
|
8292
|
+
nodeCount: this.nodeCount
|
|
8293
|
+
};
|
|
8294
|
+
}
|
|
8295
|
+
peek() {
|
|
8296
|
+
return this.tokens[this.pos];
|
|
8297
|
+
}
|
|
8298
|
+
next() {
|
|
8299
|
+
return this.tokens[this.pos++];
|
|
8300
|
+
}
|
|
8301
|
+
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
8302
|
+
expectPunct(punct) {
|
|
8303
|
+
const tok = this.peek();
|
|
8304
|
+
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
8305
|
+
this.pos += 1;
|
|
8306
|
+
}
|
|
8307
|
+
matchPunct(punct) {
|
|
8308
|
+
const tok = this.peek();
|
|
8309
|
+
if (tok.type === "punct" && tok.punct === punct) {
|
|
8310
|
+
this.pos += 1;
|
|
8311
|
+
return true;
|
|
8312
|
+
}
|
|
8313
|
+
return false;
|
|
8314
|
+
}
|
|
8315
|
+
countNode() {
|
|
8316
|
+
this.nodeCount += 1;
|
|
8317
|
+
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
8318
|
+
}
|
|
8319
|
+
parseTernary() {
|
|
8320
|
+
const test = this.parseBinary(1);
|
|
8321
|
+
if (this.matchPunct("?")) {
|
|
8322
|
+
const consequent = this.parseTernary();
|
|
8323
|
+
this.expectPunct(":");
|
|
8324
|
+
const alternate = this.parseTernary();
|
|
8325
|
+
this.countNode();
|
|
8326
|
+
return {
|
|
8327
|
+
kind: "conditional",
|
|
8328
|
+
test,
|
|
8329
|
+
consequent,
|
|
8330
|
+
alternate
|
|
8331
|
+
};
|
|
8332
|
+
}
|
|
8333
|
+
return test;
|
|
8334
|
+
}
|
|
8335
|
+
parseBinary(minPrec) {
|
|
8336
|
+
let left = this.parseUnary();
|
|
8337
|
+
for (;;) {
|
|
8338
|
+
const tok = this.peek();
|
|
8339
|
+
if (tok.type !== "punct") break;
|
|
8340
|
+
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
8341
|
+
if (prec === void 0 || prec < minPrec) break;
|
|
8342
|
+
const op = tok.punct;
|
|
8343
|
+
this.pos += 1;
|
|
8344
|
+
const right = this.parseBinary(prec + 1);
|
|
8345
|
+
this.countNode();
|
|
8346
|
+
if (isLogicalOp(op)) left = {
|
|
8347
|
+
kind: "logical",
|
|
8348
|
+
op,
|
|
8349
|
+
left,
|
|
8350
|
+
right
|
|
8351
|
+
};
|
|
8352
|
+
else if (isBinaryOp(op)) left = {
|
|
8353
|
+
kind: "binary",
|
|
8354
|
+
op,
|
|
8355
|
+
left,
|
|
8356
|
+
right
|
|
8357
|
+
};
|
|
8358
|
+
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
8359
|
+
}
|
|
8360
|
+
return left;
|
|
8361
|
+
}
|
|
8362
|
+
parseUnary() {
|
|
8363
|
+
const tok = this.peek();
|
|
8364
|
+
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
8365
|
+
const op = tok.punct;
|
|
8366
|
+
this.pos += 1;
|
|
8367
|
+
const operand = this.parseUnary();
|
|
8368
|
+
this.countNode();
|
|
8369
|
+
return {
|
|
8370
|
+
kind: "unary",
|
|
8371
|
+
op,
|
|
8372
|
+
operand
|
|
8373
|
+
};
|
|
8374
|
+
}
|
|
8375
|
+
return this.parsePrimary();
|
|
8376
|
+
}
|
|
8377
|
+
parsePrimary() {
|
|
8378
|
+
const tok = this.next();
|
|
8379
|
+
switch (tok.type) {
|
|
8380
|
+
case "number":
|
|
8381
|
+
this.countNode();
|
|
8382
|
+
return {
|
|
8383
|
+
kind: "literal",
|
|
8384
|
+
value: tok.value
|
|
8385
|
+
};
|
|
8386
|
+
case "string":
|
|
8387
|
+
this.countNode();
|
|
8388
|
+
return {
|
|
8389
|
+
kind: "literal",
|
|
8390
|
+
value: tok.value
|
|
8391
|
+
};
|
|
8392
|
+
case "keyword":
|
|
8393
|
+
this.countNode();
|
|
8394
|
+
return {
|
|
8395
|
+
kind: "literal",
|
|
8396
|
+
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
8397
|
+
};
|
|
8398
|
+
case "identifier": {
|
|
8399
|
+
const nextTok = this.peek();
|
|
8400
|
+
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
8401
|
+
this.identifiers.add(tok.name);
|
|
8402
|
+
this.countNode();
|
|
8403
|
+
return {
|
|
8404
|
+
kind: "identifier",
|
|
8405
|
+
name: tok.name
|
|
8406
|
+
};
|
|
8407
|
+
}
|
|
8408
|
+
case "punct":
|
|
8409
|
+
if (tok.punct === "(") {
|
|
8410
|
+
const inner = this.parseTernary();
|
|
8411
|
+
this.expectPunct(")");
|
|
8412
|
+
return inner;
|
|
8413
|
+
}
|
|
8414
|
+
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
8415
|
+
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
8416
|
+
}
|
|
8417
|
+
}
|
|
8418
|
+
parseCall(callee, pos) {
|
|
8419
|
+
if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
|
|
8420
|
+
this.expectPunct("(");
|
|
8421
|
+
const args = [];
|
|
8422
|
+
if (!this.matchPunct(")")) for (;;) {
|
|
8423
|
+
args.push(this.parseTernary());
|
|
8424
|
+
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
8425
|
+
if (this.matchPunct(",")) continue;
|
|
8426
|
+
this.expectPunct(")");
|
|
8427
|
+
break;
|
|
8428
|
+
}
|
|
8429
|
+
this.callees.add(callee);
|
|
8430
|
+
this.countNode();
|
|
8431
|
+
return {
|
|
8432
|
+
kind: "call",
|
|
8433
|
+
callee,
|
|
8434
|
+
args
|
|
8435
|
+
};
|
|
8436
|
+
}
|
|
8437
|
+
};
|
|
8438
|
+
/** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
|
|
8439
|
+
* `ExpressionParseError` on any lexical or grammatical failure. */
|
|
8440
|
+
function parseExpression(source) {
|
|
8441
|
+
return new Parser(tokenize(source)).parse();
|
|
8442
|
+
}
|
|
8145
8443
|
/**
|
|
8146
|
-
*
|
|
8147
|
-
*
|
|
8148
|
-
*
|
|
8149
|
-
*
|
|
8150
|
-
* dashboard declares `tab:'dashboard'`; a device-tab
|
|
8151
|
-
* widget declares the target device-detail tab id.
|
|
8152
|
-
* - `subTab` — optional sub-tab within `tab`.
|
|
8153
|
-
* - `label` — operator-facing label.
|
|
8154
|
-
* - `order` — ordering within `(tab, subTab)`.
|
|
8155
|
-
* - `kind` — always `'remote'` for widgets.
|
|
8156
|
-
* - `remote` — the MF remote `{ remoteName, exposedModule, componentKey }`.
|
|
8157
|
-
*
|
|
8158
|
-
* Widget-only fields retained alongside the contribution core:
|
|
8444
|
+
* LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
|
|
8445
|
+
* by expr"). The cache stores BOTH successes and failures (negative caching),
|
|
8446
|
+
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
8447
|
+
* one per read on a hot resolve path.
|
|
8159
8448
|
*
|
|
8160
|
-
*
|
|
8161
|
-
*
|
|
8162
|
-
*
|
|
8163
|
-
* - `description` / `icon` — picker metadata.
|
|
8164
|
-
* - `bundle` — entry filename inside the addon `dist/` dir; the
|
|
8165
|
-
* aggregator stamps a versioned `bundleUrl` from it.
|
|
8166
|
-
* - `hosts` — every host the widget supports (a widget can run
|
|
8167
|
-
* both on the dashboard and a device tab). `tab`
|
|
8168
|
-
* is the PRIMARY host; `hosts` is the full set the
|
|
8169
|
-
* picker filters on.
|
|
8170
|
-
* - `requires` — host-context requirements validated at mount.
|
|
8171
|
-
* - `defaultSize` / `allowedSizes` / `defaultColumns` / `defaultRows`
|
|
8172
|
-
* — dashboard placement hints.
|
|
8449
|
+
* The cache is a module-level singleton: entries are pure, content-addressed
|
|
8450
|
+
* ASTs keyed by the raw source string, so sharing one instance across all
|
|
8451
|
+
* callers is safe and maximises hit rate.
|
|
8173
8452
|
*/
|
|
8174
|
-
var
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8453
|
+
var cache = /* @__PURE__ */ new Map();
|
|
8454
|
+
function getCached(source) {
|
|
8455
|
+
const hit = cache.get(source);
|
|
8456
|
+
if (hit !== void 0) {
|
|
8457
|
+
cache.delete(source);
|
|
8458
|
+
cache.set(source, hit);
|
|
8459
|
+
return hit;
|
|
8460
|
+
}
|
|
8461
|
+
let result;
|
|
8462
|
+
try {
|
|
8463
|
+
result = {
|
|
8464
|
+
ok: true,
|
|
8465
|
+
parsed: parseExpression(source)
|
|
8466
|
+
};
|
|
8467
|
+
} catch (err) {
|
|
8468
|
+
result = {
|
|
8469
|
+
ok: false,
|
|
8470
|
+
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
8471
|
+
};
|
|
8472
|
+
}
|
|
8473
|
+
cache.set(source, result);
|
|
8474
|
+
if (cache.size > 256) {
|
|
8475
|
+
const oldest = cache.keys().next().value;
|
|
8476
|
+
if (oldest !== void 0) cache.delete(oldest);
|
|
8477
|
+
}
|
|
8478
|
+
return result;
|
|
8479
|
+
}
|
|
8480
|
+
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
8481
|
+
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
8482
|
+
function compileExpressionSafe(source) {
|
|
8483
|
+
return getCached(source);
|
|
8484
|
+
}
|
|
8485
|
+
Object.freeze({});
|
|
8486
|
+
/**
|
|
8487
|
+
* Author-time validation. Returns `null` when the source is valid, else a
|
|
8488
|
+
* human-readable error message. Checks: the expression compiles; binding count
|
|
8489
|
+
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
8490
|
+
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
8491
|
+
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
8492
|
+
*/
|
|
8493
|
+
function validateExpressionSource(src) {
|
|
8494
|
+
const names = Object.keys(src.bindings);
|
|
8495
|
+
if (names.length > 32) return `too many bindings (${names.length} > 32)`;
|
|
8496
|
+
for (const name of names) {
|
|
8497
|
+
if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
|
|
8498
|
+
if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
|
|
8499
|
+
if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
|
|
8500
|
+
}
|
|
8501
|
+
const compiled = compileExpressionSafe(src.expr);
|
|
8502
|
+
if (!compiled.ok) return compiled.error;
|
|
8503
|
+
const bound = new Set(names);
|
|
8504
|
+
for (const id of compiled.parsed.identifiers) {
|
|
8505
|
+
if (id === "now") continue;
|
|
8506
|
+
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
8507
|
+
}
|
|
8508
|
+
return null;
|
|
8509
|
+
}
|
|
8510
|
+
var ExpressionBindingSourceSchema = union([
|
|
8511
|
+
object({
|
|
8512
|
+
kind: literal("field").optional(),
|
|
8513
|
+
sourceKey: string(),
|
|
8514
|
+
cap: string(),
|
|
8515
|
+
fieldPath: string()
|
|
8205
8516
|
}),
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8517
|
+
object({
|
|
8518
|
+
kind: literal("literal"),
|
|
8519
|
+
value: union([
|
|
8520
|
+
string(),
|
|
8521
|
+
number(),
|
|
8522
|
+
boolean(),
|
|
8523
|
+
_null()
|
|
8524
|
+
])
|
|
8525
|
+
}),
|
|
8526
|
+
object({
|
|
8527
|
+
kind: literal("global"),
|
|
8528
|
+
sourceStableId: string(),
|
|
8529
|
+
cap: string(),
|
|
8530
|
+
fieldPath: string()
|
|
8531
|
+
})
|
|
8532
|
+
]);
|
|
8533
|
+
object({
|
|
8534
|
+
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
8535
|
+
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
8536
|
+
}).superRefine((src, ctx) => {
|
|
8537
|
+
const err = validateExpressionSource(src);
|
|
8538
|
+
if (err !== null) ctx.addIssue({
|
|
8539
|
+
code: "custom",
|
|
8540
|
+
message: err,
|
|
8541
|
+
path: ["expr"]
|
|
8542
|
+
});
|
|
8543
|
+
});
|
|
8544
|
+
/** How a leaf compares a device field to a value. Derived from the field's
|
|
8545
|
+
* `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
|
|
8546
|
+
var AutomationConditionOperatorSchema = _enum([
|
|
8547
|
+
"eq",
|
|
8548
|
+
"ne",
|
|
8549
|
+
"gt",
|
|
8550
|
+
"gte",
|
|
8551
|
+
"lt",
|
|
8552
|
+
"lte",
|
|
8553
|
+
"contains",
|
|
8554
|
+
"in"
|
|
8555
|
+
]);
|
|
8556
|
+
var AutomationConditionLeafSchema = object({
|
|
8557
|
+
kind: literal("condition"),
|
|
8558
|
+
deviceId: number().int().nonnegative(),
|
|
8559
|
+
cap: string().min(1),
|
|
8560
|
+
fieldPath: string().min(1),
|
|
8561
|
+
operator: AutomationConditionOperatorSchema,
|
|
8562
|
+
value: union([
|
|
8563
|
+
string(),
|
|
8564
|
+
number(),
|
|
8565
|
+
boolean(),
|
|
8566
|
+
array(union([string(), number()]))
|
|
8567
|
+
])
|
|
8226
8568
|
});
|
|
8227
|
-
method(_void(), array(WidgetMetadataSchema).readonly());
|
|
8228
8569
|
/**
|
|
8229
|
-
*
|
|
8230
|
-
*
|
|
8231
|
-
*
|
|
8232
|
-
*
|
|
8233
|
-
*
|
|
8234
|
-
* `bundleUrl` strings pointing at
|
|
8235
|
-
* `/api/addon-widgets/<addonId>/<bundle>?v=<mtime>`. The filesystem
|
|
8236
|
-
* `mtime` cache-buster lets the browser pick up addon rebuilds without
|
|
8237
|
-
* manual reload — same scheme used by `addon-pages`.
|
|
8238
|
-
*
|
|
8239
|
-
* The hub-local builtin `addon-widgets-aggregator` (see
|
|
8240
|
-
* `@camstack/system/builtins/addon-widgets-aggregator`) registers the
|
|
8241
|
-
* provider. Splitting the public aggregator from the raw collection
|
|
8242
|
-
* keeps both ends in codegen — there's no hand-written wrapper.
|
|
8570
|
+
* The expression leaf, declared as a plain object rather than an intersection
|
|
8571
|
+
* with {@link ExpressionSourceSchema}: a discriminated union has to be able to
|
|
8572
|
+
* read `kind` off each option, and an intersection hides it. The author-time
|
|
8573
|
+
* validation is the SAME function `ExpressionSourceSchema` runs, so the two
|
|
8574
|
+
* cannot drift — an expression that one accepts, the other accepts.
|
|
8243
8575
|
*/
|
|
8244
|
-
var
|
|
8245
|
-
|
|
8246
|
-
|
|
8576
|
+
var AutomationConditionExpressionSchema = object({
|
|
8577
|
+
kind: literal("expression"),
|
|
8578
|
+
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
8579
|
+
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
8580
|
+
}).superRefine((src, ctx) => {
|
|
8581
|
+
const err = validateExpressionSource(src);
|
|
8582
|
+
if (err !== null) ctx.addIssue({
|
|
8583
|
+
code: "custom",
|
|
8584
|
+
message: err,
|
|
8585
|
+
path: ["expr"]
|
|
8586
|
+
});
|
|
8247
8587
|
});
|
|
8248
|
-
|
|
8588
|
+
var AutomationConditionSchema = lazy(() => discriminatedUnion("kind", [
|
|
8589
|
+
object({
|
|
8590
|
+
kind: literal("all"),
|
|
8591
|
+
children: array(AutomationConditionSchema)
|
|
8592
|
+
}),
|
|
8593
|
+
object({
|
|
8594
|
+
kind: literal("any"),
|
|
8595
|
+
children: array(AutomationConditionSchema)
|
|
8596
|
+
}),
|
|
8597
|
+
object({
|
|
8598
|
+
kind: literal("not"),
|
|
8599
|
+
child: AutomationConditionSchema
|
|
8600
|
+
}),
|
|
8601
|
+
AutomationConditionLeafSchema,
|
|
8602
|
+
AutomationConditionExpressionSchema
|
|
8603
|
+
]));
|
|
8249
8604
|
/**
|
|
8250
|
-
*
|
|
8605
|
+
* What starts a run.
|
|
8251
8606
|
*
|
|
8252
|
-
*
|
|
8253
|
-
*
|
|
8254
|
-
*
|
|
8607
|
+
* D8 compliance, and it is the reason `device-state` is not merely an event
|
|
8608
|
+
* subscription: the trigger evaluates against the **state mirror**, which is
|
|
8609
|
+
* reconciled, and an event only WAKES the evaluation. A dropped event therefore
|
|
8610
|
+
* DELAYS a trigger; it does not lose it. `schedule` uses `croner` — the one
|
|
8611
|
+
* already in the repo — because `setInterval(24h)` drifts and "at 23:30" does
|
|
8612
|
+
* not.
|
|
8255
8613
|
*/
|
|
8256
|
-
var
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8614
|
+
var AutomationTriggerSchema = discriminatedUnion("kind", [
|
|
8615
|
+
object({
|
|
8616
|
+
kind: literal("device-state"),
|
|
8617
|
+
deviceId: number().int().nonnegative(),
|
|
8618
|
+
cap: string().min(1),
|
|
8619
|
+
fieldPath: string().min(1),
|
|
8620
|
+
/** Fire when the field takes this value. Omit to fire on any change. */
|
|
8621
|
+
becomes: union([
|
|
8622
|
+
string(),
|
|
8623
|
+
number(),
|
|
8624
|
+
boolean()
|
|
8625
|
+
]).optional(),
|
|
8626
|
+
/** Only on a CHANGE of value, not on every re-report. */
|
|
8627
|
+
edge: boolean().optional(),
|
|
8628
|
+
/** The condition must hold this long before the run starts. */
|
|
8629
|
+
forMs: number().int().min(0).max(864e5).optional(),
|
|
8630
|
+
/** Collapse a burst into one run. */
|
|
8631
|
+
debounceMs: number().int().min(0).max(6e5).optional()
|
|
8632
|
+
}),
|
|
8633
|
+
object({
|
|
8634
|
+
kind: literal("device-event"),
|
|
8635
|
+
/** An `EventCategory` value. */
|
|
8636
|
+
category: string().min(1),
|
|
8637
|
+
deviceId: number().int().nonnegative().optional()
|
|
8638
|
+
}),
|
|
8639
|
+
object({
|
|
8640
|
+
kind: literal("schedule"),
|
|
8641
|
+
cron: string().min(1).max(120)
|
|
8642
|
+
}),
|
|
8643
|
+
object({ kind: literal("manual") })
|
|
8261
8644
|
]);
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8645
|
+
/**
|
|
8646
|
+
* One action step.
|
|
8647
|
+
*
|
|
8648
|
+
* `wait` and `cap` are `NcRuleActionSchema`'s two members, kept structurally
|
|
8649
|
+
* identical so `NcRuleActionRunner` runs them unchanged — its device-scope
|
|
8650
|
+
* check, stop-at-first-failure and per-sequence throttle are the whole reason
|
|
8651
|
+
* to reuse it, and none of them are re-implemented here.
|
|
8652
|
+
*
|
|
8653
|
+
* **The one divergence, and it is forced.** `NcRuleActionSchema.cap.deviceId` is
|
|
8654
|
+
* a literal `z.number().int()`, and the NC runner's own `RunSequencesInput`
|
|
8655
|
+
* documents its subject device as *"for the log tag, never for routing"*. So an
|
|
8656
|
+
* NC action can never target the device that triggered it — which is fine for
|
|
8657
|
+
* the NC (its rules already scope to a device) and fatal for an automation
|
|
8658
|
+
* ("sound the siren of the camera that saw the person"). `deviceId` therefore
|
|
8659
|
+
* also accepts `{ $var }`, resolved from the run's `vars` bag BEFORE the runner
|
|
8660
|
+
* is called. The runner still receives a number and is untouched; the
|
|
8661
|
+
* resolution is the recipe's job, not the runner's.
|
|
8662
|
+
*/
|
|
8663
|
+
var AutomationActionSchema = discriminatedUnion("kind", [
|
|
8664
|
+
object({
|
|
8665
|
+
kind: literal("wait"),
|
|
8666
|
+
seconds: number().min(0).max(300)
|
|
8667
|
+
}),
|
|
8668
|
+
object({
|
|
8669
|
+
kind: literal("cap"),
|
|
8670
|
+
deviceId: union([number().int(), object({ $var: string().min(1) })]),
|
|
8671
|
+
cap: string().min(1),
|
|
8672
|
+
method: string().min(1),
|
|
8673
|
+
/** Values may carry `{{vars.x}}` slots, which SUBSTITUTE and do not
|
|
8674
|
+
* evaluate (§3.2.3). Anything beyond substitution is the expression leaf. */
|
|
8675
|
+
args: record(string(), unknown()).optional()
|
|
8676
|
+
}),
|
|
8677
|
+
object({
|
|
8678
|
+
kind: literal("code"),
|
|
8679
|
+
/** Compiled into the automation's OWN block by esbuild — not a third
|
|
8680
|
+
* runtime, not a `vm`, and not dynamically evaluated. */
|
|
8681
|
+
code: string().min(1).max(2e4)
|
|
8682
|
+
})
|
|
8268
8683
|
]);
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8684
|
+
object({
|
|
8685
|
+
triggers: array(AutomationTriggerSchema),
|
|
8686
|
+
conditions: AutomationConditionSchema.optional(),
|
|
8687
|
+
actions: array(AutomationActionSchema)
|
|
8272
8688
|
});
|
|
8273
|
-
|
|
8689
|
+
/**
|
|
8690
|
+
* `addon-pages` — system-scoped singleton aggregator cap. Public-facing
|
|
8691
|
+
* surface that admin-ui consumes through `useAddonPagesListPages()`.
|
|
8692
|
+
*
|
|
8693
|
+
* The provider iterates every `addon-pages-source` (collection) provider
|
|
8694
|
+
* and emits `AddonPageInfo[]` enriched with versioned `bundleUrl` strings
|
|
8695
|
+
* pointing at `/api/addon-pages/<addonId>/<bundle>?v=<mtime>`. The
|
|
8696
|
+
* filesystem `mtime` cache-buster lets the browser pick up addon
|
|
8697
|
+
* rebuilds without manual reload.
|
|
8698
|
+
*
|
|
8699
|
+
* The hub-local builtin `addon-pages-aggregator` (see
|
|
8700
|
+
* `@camstack/system/builtins/addon-pages-aggregator`) registers the
|
|
8701
|
+
* provider. Splitting the public aggregator from the raw collection
|
|
8702
|
+
* keeps both ends in codegen — there's no hand-written
|
|
8703
|
+
* `addon-pages.router.ts` wrapper anymore.
|
|
8704
|
+
*/
|
|
8705
|
+
var AddonPageDeclarationSchema$1 = object({
|
|
8274
8706
|
id: string(),
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
createdAt: number(),
|
|
8283
|
-
updatedAt: number(),
|
|
8284
|
-
source: AlertSourceSchema.optional(),
|
|
8285
|
-
metadata: record(string(), unknown()).optional()
|
|
8707
|
+
label: string(),
|
|
8708
|
+
icon: string(),
|
|
8709
|
+
path: string(),
|
|
8710
|
+
remoteName: string(),
|
|
8711
|
+
bundle: string(),
|
|
8712
|
+
section: string().optional(),
|
|
8713
|
+
sectionLabel: string().optional()
|
|
8286
8714
|
});
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
unreadOnly: boolean().optional(),
|
|
8292
|
-
limit: number().optional()
|
|
8293
|
-
}).optional(), array(AlertSchema).readonly()), method(_void(), number()), method(object({ alertId: string() }), _void(), { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(object({ alertId: string() }), _void(), { kind: "mutation" });
|
|
8294
|
-
DeviceType.Camera, method(object({ deviceId: number() }), custom()), object({
|
|
8295
|
-
deviceId: number(),
|
|
8296
|
-
rms: number(),
|
|
8297
|
-
dbfs: number()
|
|
8298
|
-
});
|
|
8299
|
-
/** Shared Zod schemas used across detection capabilities. */
|
|
8300
|
-
/**
|
|
8301
|
-
* Canonical frame-format enum mirrored on `FrameFormat` in
|
|
8302
|
-
* `packages/types/src/types/io.ts`. Kept inline (vs imported) so the
|
|
8303
|
-
* Zod runtime schema and TypeScript type stay in sync at the call site
|
|
8304
|
-
* — adding a new format requires changing both this enum and the
|
|
8305
|
-
* `FrameFormat` type alias together.
|
|
8306
|
-
*/
|
|
8307
|
-
var FrameFormatSchema = _enum([
|
|
8308
|
-
"jpeg",
|
|
8309
|
-
"rgb",
|
|
8310
|
-
"bgr",
|
|
8311
|
-
"yuv420",
|
|
8312
|
-
"gray"
|
|
8313
|
-
]);
|
|
8314
|
-
var FrameInputSchema = object({
|
|
8315
|
-
data: custom(),
|
|
8316
|
-
format: FrameFormatSchema,
|
|
8317
|
-
width: number(),
|
|
8318
|
-
height: number(),
|
|
8319
|
-
timestamp: number()
|
|
8320
|
-
});
|
|
8321
|
-
var BoundingBoxSchema = object({
|
|
8322
|
-
x: number(),
|
|
8323
|
-
y: number(),
|
|
8324
|
-
w: number(),
|
|
8325
|
-
h: number()
|
|
8326
|
-
});
|
|
8327
|
-
object({
|
|
8328
|
-
class: string(),
|
|
8329
|
-
originalClass: string(),
|
|
8330
|
-
score: number(),
|
|
8331
|
-
bbox: BoundingBoxSchema
|
|
8715
|
+
var AddonPageInfoSchema = object({
|
|
8716
|
+
addonId: string(),
|
|
8717
|
+
page: AddonPageDeclarationSchema$1,
|
|
8718
|
+
bundleUrl: string()
|
|
8332
8719
|
});
|
|
8720
|
+
method(_void(), array(AddonPageInfoSchema).readonly());
|
|
8333
8721
|
/**
|
|
8334
|
-
* `
|
|
8335
|
-
*
|
|
8336
|
-
*
|
|
8337
|
-
*
|
|
8338
|
-
*
|
|
8339
|
-
*
|
|
8340
|
-
* wire type causes receivers to read bytes as sample values, producing
|
|
8341
|
-
* wildly wrong RMS/dBFS results (~+43 dBFS instead of ≤0).
|
|
8722
|
+
* `addon-pages-source` — collection cap exposing per-provider raw page
|
|
8723
|
+
* declarations. Every addon that contributes a UI page registers a
|
|
8724
|
+
* provider here. The hub-side singleton aggregator (`addon-pages` cap,
|
|
8725
|
+
* see `addon-pages.cap.ts`) walks this collection, stamps versioned
|
|
8726
|
+
* `bundleUrl` values, and returns the enriched `AddonPageInfo[]` list
|
|
8727
|
+
* that admin-ui consumes.
|
|
8342
8728
|
*
|
|
8343
|
-
*
|
|
8344
|
-
*
|
|
8729
|
+
* The split exists because the public listing has a different output
|
|
8730
|
+
* shape than the per-provider raw declarations, and we want both ends
|
|
8731
|
+
* to flow through codegen instead of relying on a hand-written wrapper.
|
|
8345
8732
|
*/
|
|
8346
|
-
var
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
/** Originating device id — used by the classifier for per-camera concurrency tracking. */
|
|
8352
|
-
deviceId: number().optional()
|
|
8353
|
-
});
|
|
8354
|
-
var AudioLevelSchema = object({
|
|
8355
|
-
rms: number(),
|
|
8356
|
-
dbfs: number()
|
|
8357
|
-
});
|
|
8358
|
-
var AudioClassificationLabelSchema = object({
|
|
8733
|
+
var AddonPageDeclarationSchema = object({
|
|
8734
|
+
id: string(),
|
|
8735
|
+
label: string(),
|
|
8736
|
+
icon: string(),
|
|
8737
|
+
path: string(),
|
|
8359
8738
|
/**
|
|
8360
|
-
*
|
|
8361
|
-
*
|
|
8362
|
-
*
|
|
8363
|
-
* `
|
|
8739
|
+
* Module Federation remote name — must match the `name` field on the
|
|
8740
|
+
* page addon's `federation()` plugin config. Used by admin-ui's
|
|
8741
|
+
* `<AddonPageLoader>` to call `loadRemote('<remoteName>/page')`.
|
|
8742
|
+
* Conventionally `addon_<id>_page` (snake_case; MF names cannot
|
|
8743
|
+
* contain hyphens).
|
|
8364
8744
|
*/
|
|
8365
|
-
|
|
8745
|
+
remoteName: string(),
|
|
8366
8746
|
/**
|
|
8367
|
-
*
|
|
8368
|
-
*
|
|
8369
|
-
*
|
|
8370
|
-
*
|
|
8747
|
+
* Bundle filename inside the addon's `dist/` dir served at
|
|
8748
|
+
* `/api/addon-pages/<addonId>/<bundle>`. With Module Federation this
|
|
8749
|
+
* is always `'remoteEntry.js'`; the value is kept on the metadata so
|
|
8750
|
+
* the static-file route can compute an mtime-based cache-buster URL
|
|
8751
|
+
* without a separate filesystem stat.
|
|
8371
8752
|
*/
|
|
8372
|
-
|
|
8373
|
-
|
|
8753
|
+
bundle: string(),
|
|
8754
|
+
/**
|
|
8755
|
+
* Sidebar section this page docks into. Well-known ids: `'detection'`,
|
|
8756
|
+
* `'cluster'`, `'administration'` — the page renders inside that group.
|
|
8757
|
+
* Any OTHER string creates (or joins) a custom section rendered after
|
|
8758
|
+
* the built-in groups; its label comes from `sectionLabel` (first
|
|
8759
|
+
* declaration wins), falling back to the id. Absent → the legacy
|
|
8760
|
+
* "Addon Pages" group.
|
|
8761
|
+
*/
|
|
8762
|
+
section: string().optional(),
|
|
8763
|
+
/** Display label for a CUSTOM `section` id (ignored for well-known ids). */
|
|
8764
|
+
sectionLabel: string().optional()
|
|
8374
8765
|
});
|
|
8375
|
-
|
|
8376
|
-
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
|
|
8766
|
+
method(_void(), array(AddonPageDeclarationSchema).readonly());
|
|
8767
|
+
var AddonHttpRouteSchema = object({
|
|
8768
|
+
method: _enum([
|
|
8769
|
+
"GET",
|
|
8770
|
+
"POST",
|
|
8771
|
+
"PUT",
|
|
8772
|
+
"DELETE",
|
|
8773
|
+
"PATCH"
|
|
8774
|
+
]),
|
|
8775
|
+
path: string(),
|
|
8776
|
+
access: _enum([
|
|
8777
|
+
"public",
|
|
8778
|
+
"authenticated",
|
|
8779
|
+
"admin"
|
|
8780
|
+
]).optional(),
|
|
8781
|
+
description: string().optional()
|
|
8782
|
+
});
|
|
8783
|
+
/**
|
|
8784
|
+
* Cross-process route invocation envelope. The hub captures the
|
|
8785
|
+
* request as plain data, ships it to the worker via Moleculer, and
|
|
8786
|
+
* the worker runs the local handler against a capturing reply. The
|
|
8787
|
+
* envelope returned describes what the handler intended (status,
|
|
8788
|
+
* headers, body, or a redirect) so the hub can translate it back to
|
|
8789
|
+
* the Fastify reply that's actually wired to the socket.
|
|
8790
|
+
*/
|
|
8791
|
+
var InvokeRequestSchema = object({
|
|
8792
|
+
method: string(),
|
|
8793
|
+
path: string(),
|
|
8794
|
+
params: record(string(), string()),
|
|
8795
|
+
query: record(string(), string()),
|
|
8796
|
+
body: unknown(),
|
|
8797
|
+
headers: record(string(), string()),
|
|
8798
|
+
user: object({
|
|
8799
|
+
id: string(),
|
|
8800
|
+
username: string(),
|
|
8801
|
+
isAdmin: boolean()
|
|
8380
8802
|
}).optional(),
|
|
8381
|
-
|
|
8803
|
+
scopedToken: unknown().optional()
|
|
8382
8804
|
});
|
|
8383
|
-
var
|
|
8384
|
-
|
|
8385
|
-
|
|
8805
|
+
var InvokeReplyEnvelopeSchema = object({
|
|
8806
|
+
status: number().int(),
|
|
8807
|
+
headers: record(string(), string()),
|
|
8808
|
+
/** When set, the hub MUST `reply.redirect(redirectUrl)` instead of
|
|
8809
|
+
* sending `body`. Status defaults to 302 when this is set unless
|
|
8810
|
+
* the handler called `reply.code(...)` explicitly. */
|
|
8811
|
+
redirectUrl: string().nullable(),
|
|
8812
|
+
/** JSON-serializable body. `undefined` is treated as "no body". */
|
|
8813
|
+
body: unknown().optional(),
|
|
8814
|
+
/** Set when the handler called `reply.type(mime)`. */
|
|
8815
|
+
contentType: string().optional()
|
|
8386
8816
|
});
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8817
|
+
method(_void(), array(AddonHttpRouteSchema)), method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" });
|
|
8818
|
+
var ConfigTabDeclarationSchema = object({
|
|
8819
|
+
id: string(),
|
|
8820
|
+
label: string(),
|
|
8821
|
+
icon: string(),
|
|
8822
|
+
order: number().optional()
|
|
8823
|
+
});
|
|
8824
|
+
var ConfigSectionWithValuesSchema = object({
|
|
8825
|
+
id: string(),
|
|
8826
|
+
title: string(),
|
|
8827
|
+
description: string().optional(),
|
|
8828
|
+
style: _enum(["card", "accordion"]).optional(),
|
|
8829
|
+
defaultCollapsed: boolean().optional(),
|
|
8830
|
+
columns: union([
|
|
8831
|
+
literal(1),
|
|
8832
|
+
literal(2),
|
|
8833
|
+
literal(3),
|
|
8834
|
+
literal(4)
|
|
8835
|
+
]).optional(),
|
|
8836
|
+
tab: string().optional(),
|
|
8837
|
+
location: _enum(["settings", "top-tab"]).optional(),
|
|
8838
|
+
order: number().optional(),
|
|
8839
|
+
fields: array(any())
|
|
8840
|
+
});
|
|
8841
|
+
var SettingsSchemaWithValuesSchema = object({
|
|
8842
|
+
tabs: array(ConfigTabDeclarationSchema).optional(),
|
|
8843
|
+
sections: array(ConfigSectionWithValuesSchema)
|
|
8391
8844
|
});
|
|
8845
|
+
/** Patch object — keys are field names, values are the new field values. */
|
|
8846
|
+
var SettingsPatchSchema = record(string(), unknown());
|
|
8847
|
+
/** Standard success response for update operations. */
|
|
8848
|
+
var SettingsUpdateResultSchema = object({ success: literal(true) });
|
|
8392
8849
|
method(object({
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8850
|
+
addonId: string(),
|
|
8851
|
+
nodeId: string().optional(),
|
|
8852
|
+
overlay: record(string(), unknown()).optional(),
|
|
8853
|
+
cap: string().optional()
|
|
8854
|
+
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
8855
|
+
addonId: string(),
|
|
8856
|
+
nodeId: string().optional(),
|
|
8857
|
+
patch: SettingsPatchSchema
|
|
8858
|
+
}), SettingsUpdateResultSchema, {
|
|
8859
|
+
kind: "mutation",
|
|
8860
|
+
auth: "admin"
|
|
8861
|
+
}), method(object({
|
|
8862
|
+
addonId: string(),
|
|
8863
|
+
deviceId: number(),
|
|
8864
|
+
nodeId: string().optional()
|
|
8865
|
+
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
8866
|
+
addonId: string(),
|
|
8867
|
+
deviceId: number(),
|
|
8868
|
+
nodeId: string().optional(),
|
|
8869
|
+
patch: SettingsPatchSchema
|
|
8870
|
+
}), SettingsUpdateResultSchema, {
|
|
8396
8871
|
kind: "mutation",
|
|
8397
8872
|
auth: "admin"
|
|
8398
8873
|
});
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
kind: _enum(["decode", "encode"]),
|
|
8444
|
-
codec: string(),
|
|
8445
|
-
sourceSampleRate: number(),
|
|
8446
|
-
sourceChannels: number(),
|
|
8447
|
-
targetSampleRate: number(),
|
|
8448
|
-
targetChannels: number(),
|
|
8449
|
-
format: PcmSampleFormatSchema,
|
|
8450
|
-
tag: string().optional(),
|
|
8451
|
-
createdAtMs: number(),
|
|
8452
|
-
lastActivityMs: number(),
|
|
8453
|
-
framesIn: number(),
|
|
8454
|
-
framesOut: number()
|
|
8455
|
-
});
|
|
8456
|
-
method(_void(), array(AudioCodecInfoSchema).readonly()), method(object({
|
|
8457
|
-
codec: string(),
|
|
8458
|
-
kind: _enum(["decode", "encode"])
|
|
8459
|
-
}), boolean()), method(AudioDecodeSessionConfigSchema, object({
|
|
8460
|
-
sessionId: string(),
|
|
8461
|
-
nodeId: string()
|
|
8462
|
-
}), { kind: "mutation" }), method(AudioEncodeSessionConfigSchema, object({
|
|
8463
|
-
sessionId: string(),
|
|
8464
|
-
nodeId: string()
|
|
8465
|
-
}), { kind: "mutation" }), method(object({
|
|
8466
|
-
sessionId: string(),
|
|
8467
|
-
nodeId: string().optional()
|
|
8468
|
-
}), _void(), { kind: "mutation" }), method(object({
|
|
8469
|
-
sessionId: string(),
|
|
8470
|
-
nodeId: string().optional(),
|
|
8471
|
-
data: _instanceof(Uint8Array),
|
|
8472
|
-
/** Source PTS in milliseconds. Synthesised when omitted. */
|
|
8473
|
-
pts: number().optional()
|
|
8474
|
-
}), _void(), { kind: "mutation" }), method(object({
|
|
8475
|
-
sessionId: string(),
|
|
8476
|
-
nodeId: string().optional(),
|
|
8477
|
-
maxCount: number().int().positive().default(8)
|
|
8478
|
-
}), array(AudioPcmChunkSchema)), method(object({
|
|
8479
|
-
sessionId: string(),
|
|
8480
|
-
nodeId: string().optional(),
|
|
8481
|
-
data: _instanceof(Uint8Array),
|
|
8482
|
-
/** Source PTS in milliseconds. */
|
|
8483
|
-
pts: number().optional()
|
|
8484
|
-
}), _void(), { kind: "mutation" }), method(object({
|
|
8485
|
-
sessionId: string(),
|
|
8486
|
-
nodeId: string().optional(),
|
|
8487
|
-
maxCount: number().int().positive().default(8)
|
|
8488
|
-
}), array(AudioEncodedChunkSchema)), method(object({
|
|
8489
|
-
sessionId: string(),
|
|
8490
|
-
nodeId: string().optional()
|
|
8491
|
-
}), array(AudioEncodedChunkSchema), { kind: "mutation" }), method(_void(), array(SessionInventoryEntrySchema).readonly());
|
|
8492
|
-
var AuthResultSchema = object({
|
|
8493
|
-
userId: string(),
|
|
8494
|
-
username: string(),
|
|
8495
|
-
email: string().optional(),
|
|
8496
|
-
displayName: string().optional(),
|
|
8497
|
-
/**
|
|
8498
|
-
* Whether the authenticating user is an admin. The auth-provider
|
|
8499
|
-
* surface returns this so the server's login flow can mint a JWT
|
|
8500
|
-
* with the correct bypass flag. Non-admin users authenticated via
|
|
8501
|
-
* an external IdP still need their scopes assigned by an admin via
|
|
8502
|
-
* `setUserScopes` — the SSO flow doesn't carry permissions.
|
|
8503
|
-
*/
|
|
8504
|
-
isAdmin: boolean().default(false)
|
|
8874
|
+
/**
|
|
8875
|
+
* `addon-widgets-source` — collection cap exposing per-addon raw widget
|
|
8876
|
+
* declarations. Mirrors the addon-pages split: every addon shipping
|
|
8877
|
+
* widgets registers a provider on this collection cap; the hub-local
|
|
8878
|
+
* aggregator (`addon-widgets`, see `addon-widgets.cap.ts`) walks the
|
|
8879
|
+
* collection, stamps versioned `bundleUrl`s onto each declaration, and
|
|
8880
|
+
* exposes the public listing surface that admin-ui consumes.
|
|
8881
|
+
*
|
|
8882
|
+
* The split exists because the public listing has a different output
|
|
8883
|
+
* shape (flat enriched metadata with `addonId` + `bundleUrl`) than the
|
|
8884
|
+
* per-provider raw declarations. Both ends flow through codegen.
|
|
8885
|
+
*
|
|
8886
|
+
* Unified UI-contribution model (Task 10): a widget descriptor IS a
|
|
8887
|
+
* `UiContribution` with `kind:'remote'`. The host renders it through the
|
|
8888
|
+
* same `ContributionRenderer` / Module-Federation path as every other
|
|
8889
|
+
* contributed UI surface — no bespoke widget-rendering path. The widget-
|
|
8890
|
+
* only metadata (sizing hints, `requires`) lives as extra fields on the
|
|
8891
|
+
* descriptor; the `UiContribution` core (`tab` / `label` / `order` /
|
|
8892
|
+
* `kind` / `remote`) carries identity + placement + the MF remote.
|
|
8893
|
+
*/
|
|
8894
|
+
/** Where the widget makes sense to render — maps to a contribution `tab`. */
|
|
8895
|
+
var WidgetHostEnum = _enum([
|
|
8896
|
+
"device-tab",
|
|
8897
|
+
"dashboard",
|
|
8898
|
+
"integration-detail"
|
|
8899
|
+
]);
|
|
8900
|
+
var WidgetSizeEnum = _enum([
|
|
8901
|
+
"xs",
|
|
8902
|
+
"sm",
|
|
8903
|
+
"md",
|
|
8904
|
+
"lg",
|
|
8905
|
+
"xl"
|
|
8906
|
+
]);
|
|
8907
|
+
/**
|
|
8908
|
+
* MF remote descriptor — mirrors `UiContributionRemote` from
|
|
8909
|
+
* `capability-definition.ts`. Widget remotes expose a single
|
|
8910
|
+
* `'./widgets'` module whose default export is a
|
|
8911
|
+
* `Record<componentKey, Component>` map; `componentKey` (the widget
|
|
8912
|
+
* `stableId`) picks the entry the host mounts.
|
|
8913
|
+
*/
|
|
8914
|
+
var WidgetRemoteSchema = object({
|
|
8915
|
+
remoteName: string(),
|
|
8916
|
+
exposedModule: string(),
|
|
8917
|
+
componentKey: string().optional()
|
|
8505
8918
|
});
|
|
8506
|
-
method(object({
|
|
8507
|
-
username: string(),
|
|
8508
|
-
password: string()
|
|
8509
|
-
}), AuthResultSchema.nullable(), { kind: "mutation" }), method(object({ state: string() }), string()), method(record(string(), string()), AuthResultSchema, { kind: "mutation" }), method(object({ token: string() }), AuthResultSchema.nullable());
|
|
8510
8919
|
/**
|
|
8511
|
-
*
|
|
8512
|
-
*
|
|
8513
|
-
* (admin UI, restore flow) see one canonical key.
|
|
8920
|
+
* One widget declaration — a `UiContribution` (`kind:'remote'`) plus
|
|
8921
|
+
* widget-only metadata. The `UiContribution` core fields:
|
|
8514
8922
|
*
|
|
8515
|
-
*
|
|
8516
|
-
*
|
|
8517
|
-
*
|
|
8518
|
-
*
|
|
8519
|
-
*
|
|
8923
|
+
* - `tab` — where the widget hosts. A widget that runs on the
|
|
8924
|
+
* dashboard declares `tab:'dashboard'`; a device-tab
|
|
8925
|
+
* widget declares the target device-detail tab id.
|
|
8926
|
+
* - `subTab` — optional sub-tab within `tab`.
|
|
8927
|
+
* - `label` — operator-facing label.
|
|
8928
|
+
* - `order` — ordering within `(tab, subTab)`.
|
|
8929
|
+
* - `kind` — always `'remote'` for widgets.
|
|
8930
|
+
* - `remote` — the MF remote `{ remoteName, exposedModule, componentKey }`.
|
|
8520
8931
|
*
|
|
8521
|
-
*
|
|
8522
|
-
*
|
|
8523
|
-
*
|
|
8932
|
+
* Widget-only fields retained alongside the contribution core:
|
|
8933
|
+
*
|
|
8934
|
+
* - `stableId` — stable identity within the addon (the MF
|
|
8935
|
+
* `componentKey`; kept top-level so consumers have
|
|
8936
|
+
* a stable key without reaching into `remote`).
|
|
8937
|
+
* - `description` / `icon` — picker metadata.
|
|
8938
|
+
* - `bundle` — entry filename inside the addon `dist/` dir; the
|
|
8939
|
+
* aggregator stamps a versioned `bundleUrl` from it.
|
|
8940
|
+
* - `hosts` — every host the widget supports (a widget can run
|
|
8941
|
+
* both on the dashboard and a device tab). `tab`
|
|
8942
|
+
* is the PRIMARY host; `hosts` is the full set the
|
|
8943
|
+
* picker filters on.
|
|
8944
|
+
* - `requires` — host-context requirements validated at mount.
|
|
8945
|
+
* - `defaultSize` / `allowedSizes` / `defaultColumns` / `defaultRows`
|
|
8946
|
+
* — dashboard placement hints.
|
|
8524
8947
|
*/
|
|
8525
|
-
var
|
|
8948
|
+
var WidgetMetadataSchema = object({
|
|
8949
|
+
/** Primary host tab — `'dashboard'`, `'device-tab'`, or a device-detail tab id. */
|
|
8950
|
+
tab: string(),
|
|
8951
|
+
/** Optional sub-tab within `tab`. */
|
|
8952
|
+
subTab: string().optional(),
|
|
8953
|
+
/** Operator-facing label. */
|
|
8954
|
+
label: string(),
|
|
8955
|
+
/** Ordering within `(tab, subTab)`, ascending. */
|
|
8956
|
+
order: number().optional(),
|
|
8957
|
+
/** Always `'remote'` — a widget is a Module Federation remote. */
|
|
8958
|
+
kind: literal("remote"),
|
|
8959
|
+
/** MF remote descriptor. */
|
|
8960
|
+
remote: WidgetRemoteSchema,
|
|
8961
|
+
/** Stable id within the addon — kebab-case. Equals `remote.componentKey`. */
|
|
8962
|
+
stableId: string(),
|
|
8963
|
+
description: string().optional(),
|
|
8964
|
+
icon: string().optional(),
|
|
8526
8965
|
/**
|
|
8527
|
-
*
|
|
8528
|
-
*
|
|
8529
|
-
*
|
|
8530
|
-
*
|
|
8966
|
+
* Bundle filename inside the addon's `dist/` dir served at
|
|
8967
|
+
* `/api/addon-widgets/<addonId>/<bundle>`. With Module Federation
|
|
8968
|
+
* this is always `'remoteEntry.js'` — the value is kept on the
|
|
8969
|
+
* metadata so the static-file route can compute an mtime-based
|
|
8970
|
+
* cache-buster URL without a separate filesystem stat.
|
|
8531
8971
|
*/
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
id: string(),
|
|
8972
|
+
bundle: string(),
|
|
8973
|
+
/** Every host the widget supports. The picker filters on this set. */
|
|
8974
|
+
hosts: array(WidgetHostEnum).readonly(),
|
|
8975
|
+
/** Required props the host must supply. Validated at `<WidgetSlot>` mount. */
|
|
8976
|
+
requires: object({
|
|
8977
|
+
deviceContext: boolean().default(false),
|
|
8978
|
+
integrationContext: boolean().default(false)
|
|
8979
|
+
}),
|
|
8541
8980
|
/**
|
|
8542
|
-
*
|
|
8543
|
-
*
|
|
8981
|
+
* Loadable BEFORE authentication. The normal widget registry listing
|
|
8982
|
+
* (`addon-widgets.listWidgets`) is auth-gated, so a pre-auth surface
|
|
8983
|
+
* (the login page) cannot discover a widget through it. A widget that
|
|
8984
|
+
* declares `preAuth: true` marks itself as safe to mount on a pre-auth
|
|
8985
|
+
* screen — it is surfaced through the PUBLIC `auth.listLoginMethods`
|
|
8986
|
+
* login-method contribution channel (see `login-method.cap.ts`) rather
|
|
8987
|
+
* than the authenticated registry, and its bundle is served by the
|
|
8988
|
+
* public `/api/addon-widgets/:addonId/*` static route. Defaults false.
|
|
8544
8989
|
*/
|
|
8545
|
-
|
|
8546
|
-
/**
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
lastSuccessSizeBytes: number().optional(),
|
|
8556
|
-
/** Per-destination cron expression. Empty = manual-only (no schedule). */
|
|
8557
|
-
cron: string().optional(),
|
|
8558
|
-
/** ms-epoch of next computed firing for this destination's cron, if any. */
|
|
8559
|
-
nextRunAt: number().optional(),
|
|
8560
|
-
/** ms-epoch of last successful scheduled run (mirrors policy.lastRunAt). */
|
|
8561
|
-
lastRunAt: number().optional()
|
|
8990
|
+
preAuth: boolean().optional().default(false),
|
|
8991
|
+
/** Dashboard placement HINTS (operator can override per instance). */
|
|
8992
|
+
defaultSize: WidgetSizeEnum.default("md"),
|
|
8993
|
+
allowedSizes: array(WidgetSizeEnum).readonly().default([
|
|
8994
|
+
"sm",
|
|
8995
|
+
"md",
|
|
8996
|
+
"lg"
|
|
8997
|
+
]),
|
|
8998
|
+
defaultColumns: number().int().min(1).max(12).default(6),
|
|
8999
|
+
defaultRows: number().int().min(1).max(12).default(1)
|
|
8562
9000
|
});
|
|
9001
|
+
method(_void(), array(WidgetMetadataSchema).readonly());
|
|
8563
9002
|
/**
|
|
8564
|
-
*
|
|
8565
|
-
*
|
|
8566
|
-
*
|
|
9003
|
+
* `addon-widgets` — system-scoped singleton aggregator cap. Public-facing
|
|
9004
|
+
* surface that admin-ui consumes through `useAddonWidgetsListWidgets()`.
|
|
9005
|
+
*
|
|
9006
|
+
* The provider iterates every `addon-widgets-source` (collection)
|
|
9007
|
+
* provider and emits `EnrichedWidgetMetadata[]` enriched with versioned
|
|
9008
|
+
* `bundleUrl` strings pointing at
|
|
9009
|
+
* `/api/addon-widgets/<addonId>/<bundle>?v=<mtime>`. The filesystem
|
|
9010
|
+
* `mtime` cache-buster lets the browser pick up addon rebuilds without
|
|
9011
|
+
* manual reload — same scheme used by `addon-pages`.
|
|
9012
|
+
*
|
|
9013
|
+
* The hub-local builtin `addon-widgets-aggregator` (see
|
|
9014
|
+
* `@camstack/system/builtins/addon-widgets-aggregator`) registers the
|
|
9015
|
+
* provider. Splitting the public aggregator from the raw collection
|
|
9016
|
+
* keeps both ends in codegen — there's no hand-written wrapper.
|
|
8567
9017
|
*/
|
|
8568
|
-
var
|
|
8569
|
-
|
|
8570
|
-
|
|
8571
|
-
createdAt: number(),
|
|
8572
|
-
sizeBytes: number(),
|
|
8573
|
-
label: string().optional(),
|
|
8574
|
-
/** Top-level locations included in the archive (db, addons, tls, …). */
|
|
8575
|
-
locations: array(string()).readonly()
|
|
9018
|
+
var EnrichedWidgetMetadataSchema = WidgetMetadataSchema.extend({
|
|
9019
|
+
addonId: string(),
|
|
9020
|
+
bundleUrl: string()
|
|
8576
9021
|
});
|
|
8577
|
-
|
|
9022
|
+
method(_void(), array(EnrichedWidgetMetadataSchema).readonly());
|
|
9023
|
+
/**
|
|
9024
|
+
* Alerts capability — collection-based internal alert system.
|
|
9025
|
+
*
|
|
9026
|
+
* Multiple providers can register. Each provider filters by EventBus category
|
|
9027
|
+
* and creates/updates alerts. The built-in Alert Center addon persists alerts
|
|
9028
|
+
* in the DB and serves them to the admin UI.
|
|
9029
|
+
*/
|
|
9030
|
+
var AlertSeveritySchema = _enum([
|
|
9031
|
+
"info",
|
|
9032
|
+
"success",
|
|
9033
|
+
"warning",
|
|
9034
|
+
"error"
|
|
9035
|
+
]);
|
|
9036
|
+
var AlertStatusSchema = _enum([
|
|
9037
|
+
"active",
|
|
9038
|
+
"in-progress",
|
|
9039
|
+
"completed",
|
|
9040
|
+
"failed",
|
|
9041
|
+
"dismissed"
|
|
9042
|
+
]);
|
|
9043
|
+
var AlertSourceSchema = object({
|
|
9044
|
+
type: string(),
|
|
9045
|
+
id: string()
|
|
9046
|
+
});
|
|
9047
|
+
var AlertSchema = object({
|
|
8578
9048
|
id: string(),
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
|
|
9049
|
+
category: string(),
|
|
9050
|
+
severity: AlertSeveritySchema,
|
|
9051
|
+
title: string(),
|
|
9052
|
+
message: string(),
|
|
9053
|
+
status: AlertStatusSchema,
|
|
9054
|
+
progress: number().optional(),
|
|
9055
|
+
read: boolean(),
|
|
8582
9056
|
createdAt: number(),
|
|
8583
|
-
|
|
8584
|
-
|
|
9057
|
+
updatedAt: number(),
|
|
9058
|
+
source: AlertSourceSchema.optional(),
|
|
9059
|
+
metadata: record(string(), unknown()).optional()
|
|
8585
9060
|
});
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
9061
|
+
method(AlertSchema, _void(), { kind: "mutation" }), method(object({
|
|
9062
|
+
alertId: string(),
|
|
9063
|
+
patch: AlertSchema.partial()
|
|
9064
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
9065
|
+
unreadOnly: boolean().optional(),
|
|
9066
|
+
limit: number().optional()
|
|
9067
|
+
}).optional(), array(AlertSchema).readonly()), method(_void(), number()), method(object({ alertId: string() }), _void(), { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(object({ alertId: string() }), _void(), { kind: "mutation" });
|
|
9068
|
+
DeviceType.Camera, method(object({ deviceId: number() }), custom()), object({
|
|
9069
|
+
deviceId: number(),
|
|
9070
|
+
rms: number(),
|
|
9071
|
+
dbfs: number()
|
|
8595
9072
|
});
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
9073
|
+
/** Shared Zod schemas used across detection capabilities. */
|
|
9074
|
+
/**
|
|
9075
|
+
* Canonical frame-format enum mirrored on `FrameFormat` in
|
|
9076
|
+
* `packages/types/src/types/io.ts`. Kept inline (vs imported) so the
|
|
9077
|
+
* Zod runtime schema and TypeScript type stay in sync at the call site
|
|
9078
|
+
* — adding a new format requires changing both this enum and the
|
|
9079
|
+
* `FrameFormat` type alias together.
|
|
9080
|
+
*/
|
|
9081
|
+
var FrameFormatSchema = _enum([
|
|
9082
|
+
"jpeg",
|
|
9083
|
+
"rgb",
|
|
9084
|
+
"bgr",
|
|
9085
|
+
"yuv420",
|
|
9086
|
+
"gray"
|
|
9087
|
+
]);
|
|
9088
|
+
var FrameInputSchema = object({
|
|
9089
|
+
data: custom(),
|
|
9090
|
+
format: FrameFormatSchema,
|
|
9091
|
+
width: number(),
|
|
9092
|
+
height: number(),
|
|
9093
|
+
timestamp: number()
|
|
8604
9094
|
});
|
|
8605
|
-
var
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
9095
|
+
var BoundingBoxSchema = object({
|
|
9096
|
+
x: number(),
|
|
9097
|
+
y: number(),
|
|
9098
|
+
w: number(),
|
|
9099
|
+
h: number()
|
|
9100
|
+
});
|
|
9101
|
+
object({
|
|
9102
|
+
class: string(),
|
|
9103
|
+
originalClass: string(),
|
|
9104
|
+
score: number(),
|
|
9105
|
+
bbox: BoundingBoxSchema
|
|
8610
9106
|
});
|
|
8611
9107
|
/**
|
|
8612
|
-
*
|
|
8613
|
-
*
|
|
8614
|
-
* `
|
|
8615
|
-
*
|
|
8616
|
-
*
|
|
8617
|
-
*
|
|
8618
|
-
*
|
|
8619
|
-
*
|
|
8620
|
-
* this schedule's runs.
|
|
9108
|
+
* `data` carries the raw f32le bytes of the PCM samples (4 bytes per
|
|
9109
|
+
* sample, little-endian IEEE 754). `Uint8Array` is the wire-safe choice
|
|
9110
|
+
* because `@msgpack/msgpack` serialises it as a MsgPack `bin` type that
|
|
9111
|
+
* round-trips losslessly over the UDS transport. `Float32Array` is NOT
|
|
9112
|
+
* preserved — the encoder serialises it as `bin` (its raw bytes) but
|
|
9113
|
+
* the decoder returns `Uint8Array`, so treating a `Float32Array` as the
|
|
9114
|
+
* wire type causes receivers to read bytes as sample values, producing
|
|
9115
|
+
* wildly wrong RMS/dBFS results (~+43 dBFS instead of ≤0).
|
|
8621
9116
|
*
|
|
8622
|
-
*
|
|
8623
|
-
* (
|
|
8624
|
-
* default full set.
|
|
9117
|
+
* Callers that need float arithmetic reconstruct the view with:
|
|
9118
|
+
* `new Float32Array(chunk.data.buffer, chunk.data.byteOffset, chunk.data.byteLength / 4)`
|
|
8625
9119
|
*/
|
|
8626
|
-
var
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
/**
|
|
8632
|
-
|
|
8633
|
-
/** Master on/off toggle for the whole schedule. */
|
|
8634
|
-
enabled: boolean(),
|
|
8635
|
-
/** `backups`-location ids this schedule writes to (fan-out set). */
|
|
8636
|
-
locationIds: array(string()).readonly(),
|
|
8637
|
-
/** Archives kept per targeted location for this schedule. */
|
|
8638
|
-
retentionCount: number().int().min(1).max(1e3),
|
|
8639
|
-
/** Optional subset of source locations to include; omitted = all. */
|
|
8640
|
-
dataSources: array(string()).readonly().optional(),
|
|
8641
|
-
/** ms-epoch of last successful run. */
|
|
8642
|
-
lastRunAt: number().optional(),
|
|
8643
|
-
/** ms-epoch of next computed firing (read-only, filled on list). */
|
|
8644
|
-
nextRunAt: number().optional()
|
|
9120
|
+
var AudioChunkInputSchema = object({
|
|
9121
|
+
data: _instanceof(Uint8Array),
|
|
9122
|
+
sampleRate: number(),
|
|
9123
|
+
channels: number(),
|
|
9124
|
+
timestamp: number(),
|
|
9125
|
+
/** Originating device id — used by the classifier for per-camera concurrency tracking. */
|
|
9126
|
+
deviceId: number().optional()
|
|
8645
9127
|
});
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
9128
|
+
var AudioLevelSchema = object({
|
|
9129
|
+
rms: number(),
|
|
9130
|
+
dbfs: number()
|
|
9131
|
+
});
|
|
9132
|
+
var AudioClassificationLabelSchema = object({
|
|
8651
9133
|
/**
|
|
8652
|
-
*
|
|
8653
|
-
*
|
|
8654
|
-
*
|
|
8655
|
-
*
|
|
9134
|
+
* Primary display class. Depending on how the label was produced
|
|
9135
|
+
* this is either the macro category (e.g. `dog`) or the raw
|
|
9136
|
+
* backend label (e.g. `Dog bark`). Mirrors `class` on
|
|
9137
|
+
* `SpatialDetection`.
|
|
8656
9138
|
*/
|
|
8657
|
-
|
|
8658
|
-
}).optional(), array(BackupEntrySchema).readonly(), {
|
|
8659
|
-
kind: "mutation",
|
|
8660
|
-
auth: "admin"
|
|
8661
|
-
}), method(_void(), array(BackupEntrySchema).readonly(), { auth: "admin" }), method(_void(), array(LocationStatSchema).readonly(), { auth: "admin" }), method(object({
|
|
8662
|
-
destinationId: string(),
|
|
8663
|
-
backupId: string()
|
|
8664
|
-
}), ArchiveManifestSchema.nullable(), { auth: "admin" }), method(object({
|
|
8665
|
-
destinationId: string(),
|
|
8666
|
-
backupId: string(),
|
|
9139
|
+
className: string(),
|
|
8667
9140
|
/**
|
|
8668
|
-
*
|
|
8669
|
-
*
|
|
8670
|
-
*
|
|
8671
|
-
*
|
|
8672
|
-
* any of them.
|
|
9141
|
+
* Raw backend-native label the classifier actually emitted (e.g.
|
|
9142
|
+
* `Dog bark` for YAMNet, `dog_bark` for Apple SoundAnalysis). For
|
|
9143
|
+
* macro-aggregated entries this is the top raw contributor. Mirrors
|
|
9144
|
+
* `originalClass` on `SpatialDetection`.
|
|
8673
9145
|
*/
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
9146
|
+
originalClass: string().optional(),
|
|
9147
|
+
score: number()
|
|
9148
|
+
});
|
|
9149
|
+
var AudioAnalysisResultSchema = object({
|
|
9150
|
+
level: AudioLevelSchema,
|
|
9151
|
+
classification: object({
|
|
9152
|
+
labels: array(AudioClassificationLabelSchema).readonly(),
|
|
9153
|
+
inferenceMs: number()
|
|
9154
|
+
}).optional(),
|
|
9155
|
+
timestamp: number()
|
|
9156
|
+
});
|
|
9157
|
+
var AudioAnalysisSettingsSchema = object({
|
|
9158
|
+
minConfidence: number().min(0).max(1).default(.3),
|
|
9159
|
+
allowedClasses: array(string()).default([])
|
|
9160
|
+
});
|
|
9161
|
+
var AudioClassificationResultSchema = object({
|
|
9162
|
+
labels: array(AudioClassificationLabelSchema).readonly(),
|
|
9163
|
+
rawLabels: array(AudioClassificationLabelSchema).readonly().optional(),
|
|
9164
|
+
inferenceMs: number()
|
|
9165
|
+
});
|
|
9166
|
+
method(object({
|
|
9167
|
+
chunk: AudioChunkInputSchema,
|
|
9168
|
+
settings: AudioAnalysisSettingsSchema
|
|
9169
|
+
}), AudioAnalysisResultSchema.nullable(), { kind: "mutation" }), method(AudioChunkInputSchema, AudioClassificationResultSchema, { timeoutMs: 3e4 }), method(_void(), boolean()), method(_void(), _void(), { kind: "mutation" }), method(_void(), object({ backend: string() }), {
|
|
8682
9170
|
kind: "mutation",
|
|
8683
9171
|
auth: "admin"
|
|
8684
|
-
})
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
9172
|
+
});
|
|
9173
|
+
var PcmSampleFormatSchema = _enum(["f32le", "s16le"]);
|
|
9174
|
+
var AudioCodecInfoSchema = object({
|
|
9175
|
+
codec: string(),
|
|
9176
|
+
canDecode: boolean(),
|
|
9177
|
+
canEncode: boolean(),
|
|
9178
|
+
label: string().optional()
|
|
9179
|
+
});
|
|
9180
|
+
var AudioDecodeSessionConfigSchema = object({
|
|
9181
|
+
codec: string(),
|
|
9182
|
+
sourceSampleRate: number().int().positive(),
|
|
9183
|
+
sourceChannels: number().int().positive(),
|
|
9184
|
+
extraData: _instanceof(Uint8Array).optional(),
|
|
9185
|
+
targetSampleRate: number().int().positive(),
|
|
9186
|
+
targetChannels: number().int().positive(),
|
|
9187
|
+
targetFormat: PcmSampleFormatSchema.optional(),
|
|
9188
|
+
idleMs: number().int().positive().optional(),
|
|
9189
|
+
tag: string().optional()
|
|
9190
|
+
});
|
|
9191
|
+
var AudioEncodeSessionConfigSchema = object({
|
|
9192
|
+
codec: string(),
|
|
9193
|
+
sourceSampleRate: number().int().positive(),
|
|
9194
|
+
sourceChannels: number().int().positive(),
|
|
9195
|
+
sourceFormat: PcmSampleFormatSchema.optional(),
|
|
9196
|
+
targetSampleRate: number().int().positive(),
|
|
9197
|
+
targetChannels: number().int().positive(),
|
|
9198
|
+
bitrateKbps: number().int().positive().optional(),
|
|
9199
|
+
idleMs: number().int().positive().optional(),
|
|
9200
|
+
tag: string().optional()
|
|
9201
|
+
});
|
|
9202
|
+
var AudioPcmChunkSchema = object({
|
|
9203
|
+
data: _instanceof(Uint8Array),
|
|
9204
|
+
sampleRate: number().int().positive(),
|
|
9205
|
+
channels: number().int().positive(),
|
|
9206
|
+
format: PcmSampleFormatSchema,
|
|
9207
|
+
pts: number()
|
|
9208
|
+
});
|
|
9209
|
+
var AudioEncodedChunkSchema = object({
|
|
9210
|
+
data: _instanceof(Uint8Array),
|
|
9211
|
+
codec: string(),
|
|
9212
|
+
pts: number(),
|
|
9213
|
+
frameComplete: boolean()
|
|
9214
|
+
});
|
|
9215
|
+
var SessionInventoryEntrySchema = object({
|
|
9216
|
+
sessionId: string(),
|
|
9217
|
+
kind: _enum(["decode", "encode"]),
|
|
9218
|
+
codec: string(),
|
|
9219
|
+
sourceSampleRate: number(),
|
|
9220
|
+
sourceChannels: number(),
|
|
9221
|
+
targetSampleRate: number(),
|
|
9222
|
+
targetChannels: number(),
|
|
9223
|
+
format: PcmSampleFormatSchema,
|
|
9224
|
+
tag: string().optional(),
|
|
9225
|
+
createdAtMs: number(),
|
|
9226
|
+
lastActivityMs: number(),
|
|
9227
|
+
framesIn: number(),
|
|
9228
|
+
framesOut: number()
|
|
9229
|
+
});
|
|
9230
|
+
method(_void(), array(AudioCodecInfoSchema).readonly()), method(object({
|
|
9231
|
+
codec: string(),
|
|
9232
|
+
kind: _enum(["decode", "encode"])
|
|
9233
|
+
}), boolean()), method(AudioDecodeSessionConfigSchema, object({
|
|
9234
|
+
sessionId: string(),
|
|
9235
|
+
nodeId: string()
|
|
9236
|
+
}), { kind: "mutation" }), method(AudioEncodeSessionConfigSchema, object({
|
|
9237
|
+
sessionId: string(),
|
|
9238
|
+
nodeId: string()
|
|
9239
|
+
}), { kind: "mutation" }), method(object({
|
|
9240
|
+
sessionId: string(),
|
|
9241
|
+
nodeId: string().optional()
|
|
9242
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
9243
|
+
sessionId: string(),
|
|
9244
|
+
nodeId: string().optional(),
|
|
9245
|
+
data: _instanceof(Uint8Array),
|
|
9246
|
+
/** Source PTS in milliseconds. Synthesised when omitted. */
|
|
9247
|
+
pts: number().optional()
|
|
9248
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
9249
|
+
sessionId: string(),
|
|
9250
|
+
nodeId: string().optional(),
|
|
9251
|
+
maxCount: number().int().positive().default(8)
|
|
9252
|
+
}), array(AudioPcmChunkSchema)), method(object({
|
|
9253
|
+
sessionId: string(),
|
|
9254
|
+
nodeId: string().optional(),
|
|
9255
|
+
data: _instanceof(Uint8Array),
|
|
9256
|
+
/** Source PTS in milliseconds. */
|
|
9257
|
+
pts: number().optional()
|
|
9258
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
9259
|
+
sessionId: string(),
|
|
9260
|
+
nodeId: string().optional(),
|
|
9261
|
+
maxCount: number().int().positive().default(8)
|
|
9262
|
+
}), array(AudioEncodedChunkSchema)), method(object({
|
|
9263
|
+
sessionId: string(),
|
|
9264
|
+
nodeId: string().optional()
|
|
9265
|
+
}), array(AudioEncodedChunkSchema), { kind: "mutation" }), method(_void(), array(SessionInventoryEntrySchema).readonly());
|
|
9266
|
+
var AuthResultSchema = object({
|
|
9267
|
+
userId: string(),
|
|
9268
|
+
username: string(),
|
|
9269
|
+
email: string().optional(),
|
|
9270
|
+
displayName: string().optional(),
|
|
9271
|
+
/**
|
|
9272
|
+
* Whether the authenticating user is an admin. The auth-provider
|
|
9273
|
+
* surface returns this so the server's login flow can mint a JWT
|
|
9274
|
+
* with the correct bypass flag. Non-admin users authenticated via
|
|
9275
|
+
* an external IdP still need their scopes assigned by an admin via
|
|
9276
|
+
* `setUserScopes` — the SSO flow doesn't carry permissions.
|
|
8694
9277
|
*/
|
|
8695
|
-
|
|
8696
|
-
}), _void(), {
|
|
8697
|
-
kind: "mutation",
|
|
8698
|
-
auth: "admin"
|
|
8699
|
-
}), method(object({
|
|
8700
|
-
cron: string(),
|
|
8701
|
-
count: number().int().min(1).max(20).optional()
|
|
8702
|
-
}), object({
|
|
8703
|
-
ok: boolean(),
|
|
8704
|
-
error: string().optional(),
|
|
8705
|
-
nextRuns: array(number()).readonly()
|
|
8706
|
-
})), method(_void(), array(BackupScheduleSchema).readonly(), { auth: "admin" }), method(object({
|
|
8707
|
-
id: string().optional(),
|
|
8708
|
-
label: string(),
|
|
8709
|
-
cron: string(),
|
|
8710
|
-
enabled: boolean(),
|
|
8711
|
-
locationIds: array(string()).readonly(),
|
|
8712
|
-
retentionCount: number().int().min(1).max(1e3),
|
|
8713
|
-
dataSources: array(string()).readonly().optional()
|
|
8714
|
-
}), BackupScheduleSchema, {
|
|
8715
|
-
kind: "mutation",
|
|
8716
|
-
auth: "admin"
|
|
8717
|
-
}), method(object({ id: string() }), _void(), {
|
|
8718
|
-
kind: "mutation",
|
|
8719
|
-
auth: "admin"
|
|
9278
|
+
isAdmin: boolean().default(false)
|
|
8720
9279
|
});
|
|
9280
|
+
method(object({
|
|
9281
|
+
username: string(),
|
|
9282
|
+
password: string()
|
|
9283
|
+
}), AuthResultSchema.nullable(), { kind: "mutation" }), method(object({ state: string() }), string()), method(record(string(), string()), AuthResultSchema, { kind: "mutation" }), method(object({ token: string() }), AuthResultSchema.nullable());
|
|
8721
9284
|
/**
|
|
8722
|
-
*
|
|
8723
|
-
*
|
|
8724
|
-
*
|
|
8725
|
-
* register and that other addons might consume. The first two kinds
|
|
8726
|
-
* are MQTT (mosquitto / aedes embedded / cloud bridge) and
|
|
8727
|
-
* Home Assistant (HA WebSocket — subscribe_entities + call_service).
|
|
8728
|
-
* Future kinds (Zigbee2MQTT bridge, ZHA, KNX, Telegram, …) plug in
|
|
8729
|
-
* the same surface.
|
|
8730
|
-
*
|
|
8731
|
-
* Why one cap, not one cap per kind:
|
|
8732
|
-
* - The integrations page wants a single table of "every broker"
|
|
8733
|
-
* across kinds; a unified cap drives it without join logic.
|
|
8734
|
-
* - Generic admin operations (add / remove / test / status) are
|
|
8735
|
-
* identical across kinds — duplicating them per cap was busywork.
|
|
8736
|
-
* - The `network-access` cap precedent: same interface, many
|
|
8737
|
-
* `providerKind: 'ingress'` implementations (Tailscale, ngrok, …).
|
|
8738
|
-
*
|
|
8739
|
-
* What stays kind-specific:
|
|
8740
|
-
* - `getBrokerConfig` payload (kind decides whether it returns
|
|
8741
|
-
* mqtt URL+creds, HA baseUrl+token, …) — typed as
|
|
8742
|
-
* `Record<string, unknown>` at the wire; consumers narrow.
|
|
8743
|
-
* - `publish` / `subscribe` / `getState` arguments (target / filter /
|
|
8744
|
-
* key) — also typed as `Record<string, unknown>` so each kind can
|
|
8745
|
-
* evolve its surface (MQTT topic+qos vs HA entity_id+domain)
|
|
8746
|
-
* without rebreaking the cap signature.
|
|
8747
|
-
* - `add` `settings` payload (different fields per kind, validated by
|
|
8748
|
-
* the kind-specific provider).
|
|
9285
|
+
* Orchestrator-side destination metadata. The orchestrator computes
|
|
9286
|
+
* `id = <addonId>:<subId>` from its provider lookup so consumers
|
|
9287
|
+
* (admin UI, restore flow) see one canonical key.
|
|
8749
9288
|
*
|
|
8750
|
-
*
|
|
8751
|
-
*
|
|
8752
|
-
*
|
|
8753
|
-
*
|
|
8754
|
-
*
|
|
8755
|
-
* event handler. Subscriptions persist across reconnects — the
|
|
8756
|
-
* provider re-subscribes upstream after a transport drop.
|
|
9289
|
+
* Phase 4 (admin UI redesign) adds the per-destination policy fields
|
|
9290
|
+
* (`enabled`, `retentionCount`, `label`) so the destinations table can
|
|
9291
|
+
* render the joined view without a follow-up round-trip. The backend
|
|
9292
|
+
* already joins these in `listDestinations` — we now surface them on
|
|
9293
|
+
* the wire.
|
|
8757
9294
|
*
|
|
8758
|
-
*
|
|
8759
|
-
*
|
|
8760
|
-
*
|
|
8761
|
-
* drop is recoverable via `getState({key})`).
|
|
8762
|
-
* - Frees `broker.subscribe` from holding a long-lived RPC channel.
|
|
8763
|
-
* - Cross-process: events already route through `$event-bus`; no
|
|
8764
|
-
* per-broker wire-shaping needed.
|
|
9295
|
+
* `lastSuccessAt` / `lastSuccessSizeBytes` are computed from each
|
|
9296
|
+
* location's `manifests.json` (newest archive). Optional — locations
|
|
9297
|
+
* with no archives yet leave them undefined.
|
|
8765
9298
|
*/
|
|
8766
|
-
var
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
9299
|
+
var BackupDestinationInfoSchema = object({
|
|
9300
|
+
/**
|
|
9301
|
+
* Sub-id within this addon. Convention `default` for single-
|
|
9302
|
+
* destination addons; addons that host many (S3 with N buckets)
|
|
9303
|
+
* pick stable per-bucket strings. The orchestrator stitches
|
|
9304
|
+
* `<manifestId>:<subId>` for the wire format.
|
|
9305
|
+
*/
|
|
9306
|
+
subId: string(),
|
|
9307
|
+
displayName: string(),
|
|
9308
|
+
description: string().optional(),
|
|
9309
|
+
kind: string(),
|
|
9310
|
+
triggerSupported: boolean(),
|
|
9311
|
+
restoreSupported: boolean()
|
|
9312
|
+
}).extend({
|
|
9313
|
+
/** `<addonId>:<subId>` — globally-unique dispatch key. */
|
|
8776
9314
|
id: string(),
|
|
8777
|
-
/**
|
|
8778
|
-
*
|
|
8779
|
-
*
|
|
8780
|
-
|
|
8781
|
-
* brokers (mqtt-broker owns `mqtt_*`, provider-homeassistant owns
|
|
8782
|
-
* `ha_*`). A broker therefore belongs to exactly one addon — like a
|
|
8783
|
-
* device belongs to one integration. The admin UI threads this id
|
|
8784
|
-
* back as the `{ addonId }` system-collection selector on every
|
|
8785
|
-
* id-keyed call (`get` / `getSettings` / `setSettings` / `remove` /
|
|
8786
|
-
* `testConnection`), so the call routes to the OWNING provider
|
|
8787
|
-
* instead of defaulting to the first-registered one. */
|
|
9315
|
+
/**
|
|
9316
|
+
* Manifest id of the owning addon. The Settings button on the
|
|
9317
|
+
* destination card opens that addon's `globalSettingsSchema` panel.
|
|
9318
|
+
*/
|
|
8788
9319
|
addonId: string(),
|
|
8789
|
-
/**
|
|
8790
|
-
|
|
8791
|
-
/**
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
/**
|
|
8798
|
-
|
|
8799
|
-
/**
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
});
|
|
8806
|
-
/**
|
|
8807
|
-
* One entry per `broker` provider — which addon provides which broker
|
|
8808
|
-
* kind(s). The unified create picker reads this to know, for each
|
|
8809
|
-
* (addonId, kind) pair the operator can pick, where to route the
|
|
8810
|
-
* follow-up `getSettingsSchema` / `testSettings` / `add` calls (via the
|
|
8811
|
-
* `{ addonId }` selector). A provider only advertises a kind here when
|
|
8812
|
-
* it implements `getSettingsSchema` for that kind (so the picker never
|
|
8813
|
-
* offers a kind whose creation form can't be rendered).
|
|
8814
|
-
*/
|
|
8815
|
-
var BrokerProviderInfoSchema = object({
|
|
8816
|
-
/** Addon id of the `broker` provider this entry describes. */
|
|
8817
|
-
addonId: string(),
|
|
8818
|
-
/** Broker kinds this provider can create, with a display label. */
|
|
8819
|
-
kinds: array(object({
|
|
8820
|
-
kind: string(),
|
|
8821
|
-
label: string()
|
|
8822
|
-
}))
|
|
9320
|
+
/** Operator-toggled enable flag from the destination-policy table. */
|
|
9321
|
+
enabled: boolean(),
|
|
9322
|
+
/** Operator-defined retention count (archives kept per destination). */
|
|
9323
|
+
retentionCount: number(),
|
|
9324
|
+
/** Operator-defined display label (overrides storage location displayName). */
|
|
9325
|
+
label: string().optional(),
|
|
9326
|
+
/** Newest-archive timestamp from `manifests.json`, or undefined. */
|
|
9327
|
+
lastSuccessAt: number().optional(),
|
|
9328
|
+
/** Newest-archive size from `manifests.json`, or undefined. */
|
|
9329
|
+
lastSuccessSizeBytes: number().optional(),
|
|
9330
|
+
/** Per-destination cron expression. Empty = manual-only (no schedule). */
|
|
9331
|
+
cron: string().optional(),
|
|
9332
|
+
/** ms-epoch of next computed firing for this destination's cron, if any. */
|
|
9333
|
+
nextRunAt: number().optional(),
|
|
9334
|
+
/** ms-epoch of last successful scheduled run (mirrors policy.lastRunAt). */
|
|
9335
|
+
lastRunAt: number().optional()
|
|
8823
9336
|
});
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
*
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
var
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
* lost from config and re-added as `ha_001` leaves every one of its devices
|
|
8838
|
-
* bound to a broker that no longer exists. Re-entering the password under the
|
|
8839
|
-
* ORIGINAL id turns a multi-step device migration back into re-entering a
|
|
8840
|
-
* password.
|
|
8841
|
-
*
|
|
8842
|
-
* A provider MUST refuse an id that is already in use — adopting a live
|
|
8843
|
-
* broker's id would silently take it over.
|
|
8844
|
-
*/
|
|
8845
|
-
id: string().min(1).optional(),
|
|
8846
|
-
/** Kind-specific settings (e.g. MQTT `{url,username,password}` or HA
|
|
8847
|
-
* `{baseUrl,accessToken}`). Validated by the kind-specific provider
|
|
8848
|
-
* branch on receipt — invalid shape rejects the add. */
|
|
8849
|
-
settings: record(string(), unknown())
|
|
9337
|
+
/**
|
|
9338
|
+
* Per-archive entry returned by `backup.listArchives({ destinationId })`.
|
|
9339
|
+
* Same shape the destination drill-in renders. Sourced from the per-
|
|
9340
|
+
* location `manifests.json` (no per-archive tarball read required).
|
|
9341
|
+
*/
|
|
9342
|
+
var BackupArchiveEntrySchema = object({
|
|
9343
|
+
id: string(),
|
|
9344
|
+
filename: string(),
|
|
9345
|
+
createdAt: number(),
|
|
9346
|
+
sizeBytes: number(),
|
|
9347
|
+
label: string().optional(),
|
|
9348
|
+
/** Top-level locations included in the archive (db, addons, tls, …). */
|
|
9349
|
+
locations: array(string()).readonly()
|
|
8850
9350
|
});
|
|
8851
|
-
var
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
})]);
|
|
8860
|
-
var SettingsRecordSchema$1 = record(string(), unknown());
|
|
8861
|
-
var SettingsSchemaInputSchema = object({ kind: string() });
|
|
8862
|
-
var TestSettingsInputSchema = object({
|
|
8863
|
-
kind: string(),
|
|
8864
|
-
settings: SettingsRecordSchema$1
|
|
9351
|
+
var BackupEntrySchema = object({
|
|
9352
|
+
id: string(),
|
|
9353
|
+
/** Addon id of the destination that owns this backup (e.g. `local-backup`, `s3-backup`). */
|
|
9354
|
+
destinationId: string().optional(),
|
|
9355
|
+
label: string().optional(),
|
|
9356
|
+
createdAt: number(),
|
|
9357
|
+
sizeBytes: number(),
|
|
9358
|
+
locations: array(string()).optional()
|
|
8865
9359
|
});
|
|
8866
|
-
var
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
brokerId: string(),
|
|
8876
|
-
/** Kind-specific routing target.
|
|
8877
|
-
* MQTT: `{ topic: string, qos?: 0|1|2, retain?: boolean }`.
|
|
8878
|
-
* HA: `{ domain: string, service: string, entityId?: string, area?: string }`. */
|
|
8879
|
-
target: record(string(), unknown()),
|
|
8880
|
-
/** Kind-specific payload.
|
|
8881
|
-
* MQTT: raw string / number / object (provider serialises).
|
|
8882
|
-
* HA: service-call `data` object (transition, brightness, …). */
|
|
8883
|
-
payload: unknown().optional()
|
|
9360
|
+
var ArchiveEntrySchema = object({
|
|
9361
|
+
path: string(),
|
|
9362
|
+
kind: _enum([
|
|
9363
|
+
"file",
|
|
9364
|
+
"dir",
|
|
9365
|
+
"symlink"
|
|
9366
|
+
]),
|
|
9367
|
+
sizeBytes: number(),
|
|
9368
|
+
mtime: number()
|
|
8884
9369
|
});
|
|
8885
|
-
var
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
9370
|
+
var ArchiveManifestSchema = object({
|
|
9371
|
+
archiveVersion: literal(1),
|
|
9372
|
+
createdAt: number(),
|
|
9373
|
+
dataDir: string(),
|
|
9374
|
+
locations: array(string()),
|
|
9375
|
+
entries: array(ArchiveEntrySchema),
|
|
9376
|
+
totalBytes: number(),
|
|
9377
|
+
totalFiles: number()
|
|
8891
9378
|
});
|
|
8892
|
-
var
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
brokerId: string(),
|
|
8898
|
-
subscriptionId: string()
|
|
9379
|
+
var LocationStatSchema = object({
|
|
9380
|
+
name: string(),
|
|
9381
|
+
sizeBytes: number(),
|
|
9382
|
+
fileCount: number(),
|
|
9383
|
+
present: boolean()
|
|
8899
9384
|
});
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
9385
|
+
/**
|
|
9386
|
+
* A backup schedule — the N:M "entry" that binds one cron cadence to a
|
|
9387
|
+
* SET of destination locations. Supersedes the per-location cron on
|
|
9388
|
+
* `BackupDestinationPolicy`: an operator creates a schedule, picks the
|
|
9389
|
+
* `backups` locations it should write to, and the orchestrator fans a
|
|
9390
|
+
* single archive out to all of them when the cron fires.
|
|
9391
|
+
*
|
|
9392
|
+
* `retentionCount` is per-schedule (D-decision 2026-07-28): every
|
|
9393
|
+
* location targeted by this schedule keeps this many archives from
|
|
9394
|
+
* this schedule's runs.
|
|
9395
|
+
*
|
|
9396
|
+
* `dataSources` optionally narrows which top-level state locations
|
|
9397
|
+
* (db, addons, tls, …) are archived; omitted = the orchestrator's
|
|
9398
|
+
* default full set.
|
|
9399
|
+
*/
|
|
9400
|
+
var BackupScheduleSchema = object({
|
|
9401
|
+
/** Stable id. Generated by the orchestrator on first upsert if absent. */
|
|
9402
|
+
id: string(),
|
|
9403
|
+
/** Operator-facing display name. */
|
|
9404
|
+
label: string(),
|
|
9405
|
+
/** 5-field POSIX cron. Empty = disabled cadence (kept for editing). */
|
|
9406
|
+
cron: string(),
|
|
9407
|
+
/** Master on/off toggle for the whole schedule. */
|
|
9408
|
+
enabled: boolean(),
|
|
9409
|
+
/** `backups`-location ids this schedule writes to (fan-out set). */
|
|
9410
|
+
locationIds: array(string()).readonly(),
|
|
9411
|
+
/** Archives kept per targeted location for this schedule. */
|
|
9412
|
+
retentionCount: number().int().min(1).max(1e3),
|
|
9413
|
+
/** Optional subset of source locations to include; omitted = all. */
|
|
9414
|
+
dataSources: array(string()).readonly().optional(),
|
|
9415
|
+
/** ms-epoch of last successful run. */
|
|
9416
|
+
lastRunAt: number().optional(),
|
|
9417
|
+
/** ms-epoch of next computed firing (read-only, filled on list). */
|
|
9418
|
+
nextRunAt: number().optional()
|
|
8906
9419
|
});
|
|
8907
|
-
method(
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
9420
|
+
method(_void(), array(BackupDestinationInfoSchema).readonly(), { auth: "admin" }), method(object({
|
|
9421
|
+
/** Subset of registered `backup-destination` addon ids to write to. */
|
|
9422
|
+
destinations: array(string()).optional(),
|
|
9423
|
+
locations: array(string()).optional(),
|
|
9424
|
+
label: string().optional(),
|
|
9425
|
+
/**
|
|
9426
|
+
* Per-run retention override applied to every targeted
|
|
9427
|
+
* destination. Used by schedule-driven runs (per-entry
|
|
9428
|
+
* retention). Omitted = each destination's own policy
|
|
9429
|
+
* retention (manual runs).
|
|
9430
|
+
*/
|
|
9431
|
+
retentionCount: number().int().min(1).max(1e3).optional()
|
|
9432
|
+
}).optional(), array(BackupEntrySchema).readonly(), {
|
|
8914
9433
|
kind: "mutation",
|
|
8915
9434
|
auth: "admin"
|
|
8916
|
-
}), method(
|
|
8917
|
-
|
|
8918
|
-
|
|
9435
|
+
}), method(_void(), array(BackupEntrySchema).readonly(), { auth: "admin" }), method(_void(), array(LocationStatSchema).readonly(), { auth: "admin" }), method(object({
|
|
9436
|
+
destinationId: string(),
|
|
9437
|
+
backupId: string()
|
|
9438
|
+
}), ArchiveManifestSchema.nullable(), { auth: "admin" }), method(object({
|
|
9439
|
+
destinationId: string(),
|
|
9440
|
+
backupId: string(),
|
|
9441
|
+
/**
|
|
9442
|
+
* Optional whitelist — only restore these top-level locations
|
|
9443
|
+
* from the archive. Default = every location the archive
|
|
9444
|
+
* carries (full restore). The boot-time apply hook reads
|
|
9445
|
+
* this list and skips entries whose path doesn't start with
|
|
9446
|
+
* any of them.
|
|
9447
|
+
*/
|
|
9448
|
+
locations: array(string()).optional()
|
|
8919
9449
|
}), _void(), {
|
|
8920
9450
|
kind: "mutation",
|
|
8921
9451
|
auth: "admin"
|
|
8922
|
-
}), method(
|
|
9452
|
+
}), method(object({
|
|
9453
|
+
destinationId: string(),
|
|
9454
|
+
backupId: string()
|
|
9455
|
+
}), _void(), {
|
|
8923
9456
|
kind: "mutation",
|
|
8924
9457
|
auth: "admin"
|
|
8925
|
-
}), method(
|
|
9458
|
+
}), method(object({ destinationId: string() }), array(BackupArchiveEntrySchema).readonly(), { auth: "admin" }), method(object({
|
|
9459
|
+
locationId: string(),
|
|
9460
|
+
enabled: boolean(),
|
|
9461
|
+
retentionCount: number().int().min(1).max(1e3),
|
|
9462
|
+
label: string().optional(),
|
|
9463
|
+
/**
|
|
9464
|
+
* Per-destination cron expression. Empty string clears the
|
|
9465
|
+
* schedule (manual-only). Validated server-side via croner;
|
|
9466
|
+
* malformed expressions reject the upsert with an actionable
|
|
9467
|
+
* message.
|
|
9468
|
+
*/
|
|
9469
|
+
cron: string().optional()
|
|
9470
|
+
}), _void(), {
|
|
8926
9471
|
kind: "mutation",
|
|
8927
9472
|
auth: "admin"
|
|
8928
|
-
}), method(
|
|
9473
|
+
}), method(object({
|
|
9474
|
+
cron: string(),
|
|
9475
|
+
count: number().int().min(1).max(20).optional()
|
|
9476
|
+
}), object({
|
|
9477
|
+
ok: boolean(),
|
|
9478
|
+
error: string().optional(),
|
|
9479
|
+
nextRuns: array(number()).readonly()
|
|
9480
|
+
})), method(_void(), array(BackupScheduleSchema).readonly(), { auth: "admin" }), method(object({
|
|
9481
|
+
id: string().optional(),
|
|
9482
|
+
label: string(),
|
|
9483
|
+
cron: string(),
|
|
9484
|
+
enabled: boolean(),
|
|
9485
|
+
locationIds: array(string()).readonly(),
|
|
9486
|
+
retentionCount: number().int().min(1).max(1e3),
|
|
9487
|
+
dataSources: array(string()).readonly().optional()
|
|
9488
|
+
}), BackupScheduleSchema, {
|
|
8929
9489
|
kind: "mutation",
|
|
8930
9490
|
auth: "admin"
|
|
8931
|
-
}), method(
|
|
9491
|
+
}), method(object({ id: string() }), _void(), {
|
|
8932
9492
|
kind: "mutation",
|
|
8933
9493
|
auth: "admin"
|
|
8934
|
-
})
|
|
8935
|
-
|
|
9494
|
+
});
|
|
9495
|
+
/**
|
|
9496
|
+
* `broker` — unified pub/sub broker registry, system-scoped collection.
|
|
9497
|
+
*
|
|
9498
|
+
* The cap models any kind-tagged message broker the user wants to
|
|
9499
|
+
* register and that other addons might consume. The first two kinds
|
|
9500
|
+
* are MQTT (mosquitto / aedes embedded / cloud bridge) and
|
|
9501
|
+
* Home Assistant (HA WebSocket — subscribe_entities + call_service).
|
|
9502
|
+
* Future kinds (Zigbee2MQTT bridge, ZHA, KNX, Telegram, …) plug in
|
|
9503
|
+
* the same surface.
|
|
9504
|
+
*
|
|
9505
|
+
* Why one cap, not one cap per kind:
|
|
9506
|
+
* - The integrations page wants a single table of "every broker"
|
|
9507
|
+
* across kinds; a unified cap drives it without join logic.
|
|
9508
|
+
* - Generic admin operations (add / remove / test / status) are
|
|
9509
|
+
* identical across kinds — duplicating them per cap was busywork.
|
|
9510
|
+
* - The `network-access` cap precedent: same interface, many
|
|
9511
|
+
* `providerKind: 'ingress'` implementations (Tailscale, ngrok, …).
|
|
9512
|
+
*
|
|
9513
|
+
* What stays kind-specific:
|
|
9514
|
+
* - `getBrokerConfig` payload (kind decides whether it returns
|
|
9515
|
+
* mqtt URL+creds, HA baseUrl+token, …) — typed as
|
|
9516
|
+
* `Record<string, unknown>` at the wire; consumers narrow.
|
|
9517
|
+
* - `publish` / `subscribe` / `getState` arguments (target / filter /
|
|
9518
|
+
* key) — also typed as `Record<string, unknown>` so each kind can
|
|
9519
|
+
* evolve its surface (MQTT topic+qos vs HA entity_id+domain)
|
|
9520
|
+
* without rebreaking the cap signature.
|
|
9521
|
+
* - `add` `settings` payload (different fields per kind, validated by
|
|
9522
|
+
* the kind-specific provider).
|
|
9523
|
+
*
|
|
9524
|
+
* Bidirectionality:
|
|
9525
|
+
* - `publish` is RPC — synchronous result (provider-defined).
|
|
9526
|
+
* - `subscribe` returns a subscription handle; the actual message
|
|
9527
|
+
* stream flows over the typed event-bus as `broker.message` events
|
|
9528
|
+
* keyed by `(brokerId, subscriptionId)`. Consumers filter in their
|
|
9529
|
+
* event handler. Subscriptions persist across reconnects — the
|
|
9530
|
+
* provider re-subscribes upstream after a transport drop.
|
|
9531
|
+
*
|
|
9532
|
+
* Why event-bus push (not a subscription RPC stream):
|
|
9533
|
+
* - Matches the rest of CamStack's D8 contract — events for telemetry,
|
|
9534
|
+
* RPC for loss-is-a-bug. Broker messages are telemetry (a single
|
|
9535
|
+
* drop is recoverable via `getState({key})`).
|
|
9536
|
+
* - Frees `broker.subscribe` from holding a long-lived RPC channel.
|
|
9537
|
+
* - Cross-process: events already route through `$event-bus`; no
|
|
9538
|
+
* per-broker wire-shaping needed.
|
|
9539
|
+
*/
|
|
9540
|
+
var BrokerStatusEnum = _enum([
|
|
9541
|
+
"connected",
|
|
9542
|
+
"disconnected",
|
|
9543
|
+
"connecting",
|
|
9544
|
+
"auth-failed",
|
|
9545
|
+
"unreachable",
|
|
9546
|
+
"error"
|
|
9547
|
+
]);
|
|
9548
|
+
var BrokerInfoSchema$1 = object({
|
|
9549
|
+
/** Stable broker id. Persisted; survives addon restarts. */
|
|
9550
|
+
id: string(),
|
|
9551
|
+
/** Addon id of the provider that OWNS this broker.
|
|
9552
|
+
*
|
|
9553
|
+
* The `broker` cap is a system-scoped collection: several addons
|
|
9554
|
+
* register a `broker` provider, each owning a DISJOINT set of
|
|
9555
|
+
* brokers (mqtt-broker owns `mqtt_*`, provider-homeassistant owns
|
|
9556
|
+
* `ha_*`). A broker therefore belongs to exactly one addon — like a
|
|
9557
|
+
* device belongs to one integration. The admin UI threads this id
|
|
9558
|
+
* back as the `{ addonId }` system-collection selector on every
|
|
9559
|
+
* id-keyed call (`get` / `getSettings` / `setSettings` / `remove` /
|
|
9560
|
+
* `testConnection`), so the call routes to the OWNING provider
|
|
9561
|
+
* instead of defaulting to the first-registered one. */
|
|
9562
|
+
addonId: string(),
|
|
9563
|
+
/** Human-readable name (operator-chosen at add-time). */
|
|
9564
|
+
name: string(),
|
|
9565
|
+
/** Provider-defined kind tag — `mqtt` / `home-assistant` / future. */
|
|
9566
|
+
kind: string(),
|
|
9567
|
+
status: BrokerStatusEnum,
|
|
9568
|
+
/** Free-form provider-specific info (HA version, MQTT broker
|
|
9569
|
+
* flavour, latency, mTLS active, …). */
|
|
9570
|
+
info: record(string(), unknown()),
|
|
9571
|
+
/** Ms epoch of the last connection probe / status update. */
|
|
9572
|
+
lastCheckedAt: number().nullable(),
|
|
9573
|
+
/** Last error message, when `status` indicates failure. */
|
|
9574
|
+
error: string().nullable()
|
|
9575
|
+
});
|
|
9576
|
+
var RegistryStatusSchema = object({
|
|
9577
|
+
brokerCount: number().int().nonnegative(),
|
|
9578
|
+
connectedCount: number().int().nonnegative()
|
|
9579
|
+
});
|
|
9580
|
+
/**
|
|
9581
|
+
* One entry per `broker` provider — which addon provides which broker
|
|
9582
|
+
* kind(s). The unified create picker reads this to know, for each
|
|
9583
|
+
* (addonId, kind) pair the operator can pick, where to route the
|
|
9584
|
+
* follow-up `getSettingsSchema` / `testSettings` / `add` calls (via the
|
|
9585
|
+
* `{ addonId }` selector). A provider only advertises a kind here when
|
|
9586
|
+
* it implements `getSettingsSchema` for that kind (so the picker never
|
|
9587
|
+
* offers a kind whose creation form can't be rendered).
|
|
9588
|
+
*/
|
|
9589
|
+
var BrokerProviderInfoSchema = object({
|
|
9590
|
+
/** Addon id of the `broker` provider this entry describes. */
|
|
9591
|
+
addonId: string(),
|
|
9592
|
+
/** Broker kinds this provider can create, with a display label. */
|
|
9593
|
+
kinds: array(object({
|
|
9594
|
+
kind: string(),
|
|
9595
|
+
label: string()
|
|
9596
|
+
}))
|
|
9597
|
+
});
|
|
9598
|
+
var ListInputSchema = object({
|
|
9599
|
+
/** Optional kind filter — `list({kind:'home-assistant'})` returns
|
|
9600
|
+
* only HA brokers. Omit to list every broker. */
|
|
9601
|
+
kind: string().optional() });
|
|
9602
|
+
var GetInputSchema = object({ id: string() });
|
|
9603
|
+
var AddInputSchema = object({
|
|
9604
|
+
kind: string().min(1),
|
|
9605
|
+
name: string().min(1),
|
|
9606
|
+
/**
|
|
9607
|
+
* ADOPT an existing id instead of minting a new one.
|
|
9608
|
+
*
|
|
9609
|
+
* Written the day this cost an outage. A broker's id is not a detail: HA
|
|
9610
|
+
* devices carry it inside their `stableId` (`ha:ha_004:dev:…`), so a broker
|
|
9611
|
+
* lost from config and re-added as `ha_001` leaves every one of its devices
|
|
9612
|
+
* bound to a broker that no longer exists. Re-entering the password under the
|
|
9613
|
+
* ORIGINAL id turns a multi-step device migration back into re-entering a
|
|
9614
|
+
* password.
|
|
9615
|
+
*
|
|
9616
|
+
* A provider MUST refuse an id that is already in use — adopting a live
|
|
9617
|
+
* broker's id would silently take it over.
|
|
9618
|
+
*/
|
|
9619
|
+
id: string().min(1).optional(),
|
|
9620
|
+
/** Kind-specific settings (e.g. MQTT `{url,username,password}` or HA
|
|
9621
|
+
* `{baseUrl,accessToken}`). Validated by the kind-specific provider
|
|
9622
|
+
* branch on receipt — invalid shape rejects the add. */
|
|
9623
|
+
settings: record(string(), unknown())
|
|
9624
|
+
});
|
|
9625
|
+
var AddResultSchema = object({ id: string() });
|
|
9626
|
+
var RemoveInputSchema = object({ id: string() });
|
|
9627
|
+
var TestConnectionResultSchema = discriminatedUnion("ok", [object({
|
|
9628
|
+
ok: literal(true),
|
|
9629
|
+
latencyMs: number().nonnegative()
|
|
9630
|
+
}), object({
|
|
9631
|
+
ok: literal(false),
|
|
9632
|
+
error: string()
|
|
9633
|
+
})]);
|
|
9634
|
+
var SettingsRecordSchema$1 = record(string(), unknown());
|
|
9635
|
+
var SettingsSchemaInputSchema = object({ kind: string() });
|
|
9636
|
+
var TestSettingsInputSchema = object({
|
|
9637
|
+
kind: string(),
|
|
9638
|
+
settings: SettingsRecordSchema$1
|
|
9639
|
+
});
|
|
9640
|
+
var TestSettingsResultSchema = discriminatedUnion("ok", [object({
|
|
9641
|
+
ok: literal(true),
|
|
9642
|
+
latencyMs: number().nonnegative().optional()
|
|
9643
|
+
}).strict(), object({
|
|
9644
|
+
ok: literal(false),
|
|
9645
|
+
error: string()
|
|
9646
|
+
})]);
|
|
9647
|
+
var SettingsSchemaResultSchema = unknown().nullable();
|
|
9648
|
+
var PublishInputSchema = object({
|
|
9649
|
+
brokerId: string(),
|
|
9650
|
+
/** Kind-specific routing target.
|
|
9651
|
+
* MQTT: `{ topic: string, qos?: 0|1|2, retain?: boolean }`.
|
|
9652
|
+
* HA: `{ domain: string, service: string, entityId?: string, area?: string }`. */
|
|
9653
|
+
target: record(string(), unknown()),
|
|
9654
|
+
/** Kind-specific payload.
|
|
9655
|
+
* MQTT: raw string / number / object (provider serialises).
|
|
9656
|
+
* HA: service-call `data` object (transition, brightness, …). */
|
|
9657
|
+
payload: unknown().optional()
|
|
9658
|
+
});
|
|
9659
|
+
var SubscribeInputSchema = object({
|
|
9660
|
+
brokerId: string(),
|
|
9661
|
+
/** Kind-specific filter.
|
|
9662
|
+
* MQTT: `{ topic: string, qos?: 0|1|2 }` — topic can include wildcards.
|
|
9663
|
+
* HA: `{ entityIds: string[] }` or `{ domain: string }`. */
|
|
9664
|
+
filter: record(string(), unknown())
|
|
9665
|
+
});
|
|
9666
|
+
var SubscribeResultSchema = object({
|
|
9667
|
+
/** Stable subscription id. Used to unsubscribe and to filter the
|
|
9668
|
+
* matching `broker.message` events on the bus. */
|
|
9669
|
+
subscriptionId: string() });
|
|
9670
|
+
var UnsubscribeInputSchema = object({
|
|
9671
|
+
brokerId: string(),
|
|
9672
|
+
subscriptionId: string()
|
|
9673
|
+
});
|
|
9674
|
+
var GetStateInputSchema = object({
|
|
9675
|
+
brokerId: string(),
|
|
9676
|
+
/** Kind-specific lookup key.
|
|
9677
|
+
* MQTT: topic string (returns the last retained message).
|
|
9678
|
+
* HA: entity_id (returns the cached entity state). */
|
|
9679
|
+
key: string()
|
|
9680
|
+
});
|
|
9681
|
+
method(ListInputSchema, array(BrokerInfoSchema$1)), method(GetInputSchema, BrokerInfoSchema$1.nullable()), method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }), method(AddInputSchema, AddResultSchema, {
|
|
9682
|
+
kind: "mutation",
|
|
9683
|
+
auth: "admin"
|
|
9684
|
+
}), method(RemoveInputSchema, _void(), {
|
|
9685
|
+
kind: "mutation",
|
|
9686
|
+
auth: "admin"
|
|
9687
|
+
}), method(GetInputSchema, TestConnectionResultSchema, {
|
|
9688
|
+
kind: "mutation",
|
|
9689
|
+
auth: "admin"
|
|
9690
|
+
}), method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }), method(object({
|
|
9691
|
+
id: string(),
|
|
9692
|
+
settings: SettingsRecordSchema$1
|
|
9693
|
+
}), _void(), {
|
|
9694
|
+
kind: "mutation",
|
|
9695
|
+
auth: "admin"
|
|
9696
|
+
}), method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }), method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }), method(TestSettingsInputSchema, TestSettingsResultSchema, {
|
|
9697
|
+
kind: "mutation",
|
|
9698
|
+
auth: "admin"
|
|
9699
|
+
}), method(PublishInputSchema, unknown(), {
|
|
9700
|
+
kind: "mutation",
|
|
9701
|
+
auth: "admin"
|
|
9702
|
+
}), method(SubscribeInputSchema, SubscribeResultSchema, {
|
|
9703
|
+
kind: "mutation",
|
|
9704
|
+
auth: "admin"
|
|
9705
|
+
}), method(UnsubscribeInputSchema, _void(), {
|
|
9706
|
+
kind: "mutation",
|
|
9707
|
+
auth: "admin"
|
|
9708
|
+
}), method(GetStateInputSchema, unknown().nullable()), method(_void(), RegistryStatusSchema);
|
|
9709
|
+
DeviceType.Camera;
|
|
8936
9710
|
/** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
|
|
8937
9711
|
var StreamFormatSchema = _enum([
|
|
8938
9712
|
"webrtc",
|
|
@@ -11708,7 +12482,7 @@ DeviceType.Camera, method(object({
|
|
|
11708
12482
|
* Why: pub/sub routing over the system event-bus loses fidelity
|
|
11709
12483
|
* (callback shape, QoS guarantees, will/retain semantics) and adds
|
|
11710
12484
|
* refcount bookkeeping that addons would rather own themselves. The
|
|
11711
|
-
* canonical consumer
|
|
12485
|
+
* canonical consumer needs raw `mqtt.js`
|
|
11712
12486
|
* features anyway — give it the connection config, get out of the way.
|
|
11713
12487
|
*
|
|
11714
12488
|
* Consumer flow:
|
|
@@ -14248,8 +15022,26 @@ var TrackSchema = object({
|
|
|
14248
15022
|
/** Periodic snapshots at snapshotIntervalMs cadence (subject to
|
|
14249
15023
|
* saveThumbnails policy). */
|
|
14250
15024
|
snapshots: array(TrackSnapshotSchema).readonly(),
|
|
14251
|
-
/** Deduplicated zones the track has entered at least once. */
|
|
15025
|
+
/** Deduplicated zones the track has entered at least once. Zone IDS. */
|
|
14252
15026
|
zonesVisited: array(string()).readonly(),
|
|
15027
|
+
/**
|
|
15028
|
+
* Human NAMES for {@link zonesVisited}, resolved at READ time against the
|
|
15029
|
+
* `zones` capability.
|
|
15030
|
+
*
|
|
15031
|
+
* `zonesVisited` persists ids (`cfeec78c-8d69-…`), which no operator can type
|
|
15032
|
+
* and no card can render — so every free-text search surface was structurally
|
|
15033
|
+
* unable to answer "show me the tracks in Uscio", and did not fail loudly, it
|
|
15034
|
+
* just returned nothing. Resolving here rather than in each client keeps ONE
|
|
15035
|
+
* derivation and costs the clients no extra call (the `zones` cap is
|
|
15036
|
+
* per-device, so a client-side resolve would be a per-camera fan-out on a
|
|
15037
|
+
* surface built to avoid exactly that).
|
|
15038
|
+
*
|
|
15039
|
+
* Resolved, never invented: a zone deleted since the track was written has no
|
|
15040
|
+
* name and is DROPPED, so this array can be shorter than `zonesVisited` — the
|
|
15041
|
+
* two are not positionally aligned. Absent when the track visited no zone, or
|
|
15042
|
+
* when the zone catalogue could not be read.
|
|
15043
|
+
*/
|
|
15044
|
+
zoneNames: array(string()).readonly().optional(),
|
|
14253
15045
|
/** Deduplicated set of detector classes observed for this track over its
|
|
14254
15046
|
* life (a track may be reclassified, e.g. person→vehicle). Absent on
|
|
14255
15047
|
* legacy rows written before class accumulation shipped. */
|
|
@@ -16319,6 +17111,23 @@ var CameraRecordingStatusSchema = object({
|
|
|
16319
17111
|
active: boolean(),
|
|
16320
17112
|
storageBytes: number()
|
|
16321
17113
|
});
|
|
17114
|
+
/** One stage of the fan-out that could NOT be read, and how long it cost. */
|
|
17115
|
+
var CameraStatusDegradationSchema = object({
|
|
17116
|
+
stage: _enum([
|
|
17117
|
+
"source",
|
|
17118
|
+
"broker",
|
|
17119
|
+
"detection",
|
|
17120
|
+
"recording",
|
|
17121
|
+
"switches"
|
|
17122
|
+
]),
|
|
17123
|
+
reason: _enum([
|
|
17124
|
+
"timeout",
|
|
17125
|
+
"error",
|
|
17126
|
+
"partial"
|
|
17127
|
+
]),
|
|
17128
|
+
/** Wall-clock ms spent on the stage before it was abandoned. */
|
|
17129
|
+
elapsedMs: number()
|
|
17130
|
+
});
|
|
16322
17131
|
/**
|
|
16323
17132
|
* Aggregated per-camera pipeline status — server-composed, single call.
|
|
16324
17133
|
*
|
|
@@ -16349,9 +17158,28 @@ var CameraStatusSchema = object({
|
|
|
16349
17158
|
* differently — a quiet camera that looks identical to a dead one is the
|
|
16350
17159
|
* silence-reads-as-never-happened trap this repo keeps paying for.
|
|
16351
17160
|
*
|
|
16352
|
-
* Empty when nothing is off
|
|
17161
|
+
* Empty when nothing is off, and never contains a switch no provider offers
|
|
17162
|
+
* — but an empty list is only a POSITIVE claim when `degraded` does not name
|
|
17163
|
+
* `'switches'`. When it does, the switch set could not be read and nothing
|
|
17164
|
+
* here may be rendered as "the operator turned nothing off": that is the
|
|
17165
|
+
* D62 failure (a camera we could not read painted as broken) in the very
|
|
17166
|
+
* field that exists to prevent it.
|
|
16353
17167
|
*/
|
|
16354
17168
|
switchedOff: array(CameraSwitchIdSchema).readonly(),
|
|
17169
|
+
/**
|
|
17170
|
+
* Stages of the bounded fan-out that were CUT SHORT — a timeout or a
|
|
17171
|
+
* rejection — and whose block is therefore `null` because we could not
|
|
17172
|
+
* READ it, not because there is nothing there.
|
|
17173
|
+
*
|
|
17174
|
+
* Without this, three different facts arrive as the same `null`: "the stage
|
|
17175
|
+
* timed out", "the stage failed", and "this camera legitimately has no
|
|
17176
|
+
* decoder / no recording". Every surface that draws a conclusion from a null
|
|
17177
|
+
* block (or from an empty `switchedOff`) must consult this first; a stage
|
|
17178
|
+
* named here supports no conclusion at all, only "unknown".
|
|
17179
|
+
*
|
|
17180
|
+
* Empty on a clean read — the overwhelmingly common case.
|
|
17181
|
+
*/
|
|
17182
|
+
degraded: array(CameraStatusDegradationSchema).readonly(),
|
|
16355
17183
|
/** Unix timestamp (ms) when this snapshot was composed server-side. */
|
|
16356
17184
|
fetchedAt: number()
|
|
16357
17185
|
});
|
|
@@ -22825,809 +23653,180 @@ DeviceType.Weather;
|
|
|
22825
23653
|
* Counts are derived from the analytics tracker (tracked detections
|
|
22826
23654
|
* with stable trackIds), not raw detector hits — this filters out
|
|
22827
23655
|
* one-off detector flickers and gives counts that match what the user
|
|
22828
|
-
* sees on the live overlay.
|
|
22829
|
-
*
|
|
22830
|
-
* Overlap policy: a detection that intersects two zones counts in
|
|
22831
|
-
* BOTH zones' `byClass` and `totalObjects` (count-in-each). The
|
|
22832
|
-
* `frame` aggregate de-duplicates trivially since it's frame-wide.
|
|
22833
|
-
* `unzoned` counts only detections that landed in zero zones.
|
|
22834
|
-
*/
|
|
22835
|
-
var PerScopeBreakdownSchema = object({
|
|
22836
|
-
/** Total tracked objects in this scope (frame / zone / unzoned). */
|
|
22837
|
-
totalObjects: number().int().nonnegative(),
|
|
22838
|
-
/** Per-class count. Keys are macro class names (e.g. `person`, `car`). */
|
|
22839
|
-
byClass: record(string(), number().int().nonnegative())
|
|
22840
|
-
});
|
|
22841
|
-
var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
|
|
22842
|
-
zoneId: string(),
|
|
22843
|
-
zoneName: string(),
|
|
22844
|
-
/** TrackIds of objects currently inside this zone — for cross-reference
|
|
22845
|
-
* with the per-track detail panel and live overlay. */
|
|
22846
|
-
trackIds: array(string()).readonly()
|
|
22847
|
-
});
|
|
22848
|
-
/**
|
|
22849
|
-
* A parked ("stationary") object surfaced alongside occupancy — an object that
|
|
22850
|
-
* settled and stopped moving. It is NO LONGER a tracked object (the tracker was
|
|
22851
|
-
* told to forget it so it stops re-spawning tracks/events), but it IS still
|
|
22852
|
-
* physically present, so it keeps counting toward `frame` occupancy and is
|
|
22853
|
-
* listed here so the UI can show it in a dedicated "Stationary" section instead
|
|
22854
|
-
* of flooding the live event feed.
|
|
22855
|
-
*/
|
|
22856
|
-
var StationaryObjectSchema = object({
|
|
22857
|
-
id: string(),
|
|
22858
|
-
className: string(),
|
|
22859
|
-
bbox: object({
|
|
22860
|
-
x: number(),
|
|
22861
|
-
y: number(),
|
|
22862
|
-
w: number(),
|
|
22863
|
-
h: number()
|
|
22864
|
-
}),
|
|
22865
|
-
frameWidth: number().int().nonnegative(),
|
|
22866
|
-
frameHeight: number().int().nonnegative(),
|
|
22867
|
-
/** When the source track was first seen. */
|
|
22868
|
-
firstSeenAt: number().int(),
|
|
22869
|
-
/** When the object was recognised as parked (promotion time). */
|
|
22870
|
-
becameStationaryAt: number().int(),
|
|
22871
|
-
/** Last frame a detection confirmed the object is still there. */
|
|
22872
|
-
lastConfirmedAt: number().int(),
|
|
22873
|
-
/** Enrichment label carried from the source track (identity / plate). */
|
|
22874
|
-
label: string().optional(),
|
|
22875
|
-
/** Native-resolution key-frame media key for the parked object's best image. */
|
|
22876
|
-
keyFrameMediaKey: string().optional()
|
|
22877
|
-
});
|
|
22878
|
-
var CameraOccupancySnapshotSchema = object({
|
|
22879
|
-
/** Frame timestamp of the inference result that produced this snapshot. */
|
|
22880
|
-
ts: number().int(),
|
|
22881
|
-
/** Frame width/height in pixels — let the UI normalize bbox coords. */
|
|
22882
|
-
frameWidth: number().int().nonnegative(),
|
|
22883
|
-
frameHeight: number().int().nonnegative(),
|
|
22884
|
-
/** Per-zone breakdown — one entry per defined zone (user + onboard). */
|
|
22885
|
-
zones: array(ZoneScopeBreakdownSchema).readonly(),
|
|
22886
|
-
/** Frame-wide aggregate (everywhere, regardless of zone membership).
|
|
22887
|
-
* INCLUDES currently-confirmed stationary objects (they are still present). */
|
|
22888
|
-
frame: PerScopeBreakdownSchema,
|
|
22889
|
-
/** Detections that landed outside every zone. Empty when no zones defined. */
|
|
22890
|
-
unzoned: PerScopeBreakdownSchema,
|
|
22891
|
-
/** Parked objects on this camera (additive — absent on legacy snapshots).
|
|
22892
|
-
* Surfaced separately so the UI shows them in a dedicated section rather
|
|
22893
|
-
* than as repeated tracks/events. */
|
|
22894
|
-
stationaryObjects: array(StationaryObjectSchema).readonly().optional()
|
|
22895
|
-
});
|
|
22896
|
-
/**
|
|
22897
|
-
* Time-series resolution. The history methods return one bucket per
|
|
22898
|
-
* step over the requested range. Smaller resolutions cost more
|
|
22899
|
-
* memory + bandwidth; bound to discrete steps so caller cannot ask
|
|
22900
|
-
* for arbitrary fractional buckets.
|
|
22901
|
-
*/
|
|
22902
|
-
var HistoryResolutionEnum = _enum([
|
|
22903
|
-
"minute",
|
|
22904
|
-
"5min",
|
|
22905
|
-
"hour"
|
|
22906
|
-
]);
|
|
22907
|
-
var HistoryRangeSchema = object({
|
|
22908
|
-
/** Range start (epoch ms, inclusive). */
|
|
22909
|
-
from: number().int(),
|
|
22910
|
-
/** Range end (epoch ms, inclusive). Defaults to "now" at query time. */
|
|
22911
|
-
to: number().int(),
|
|
22912
|
-
resolution: HistoryResolutionEnum
|
|
22913
|
-
});
|
|
22914
|
-
var HistoryPointSchema = object({
|
|
22915
|
-
/** Bucket midpoint (epoch ms). */
|
|
22916
|
-
ts: number().int(),
|
|
22917
|
-
/** Object count averaged over the bucket (rounded to nearest integer). */
|
|
22918
|
-
count: number().int().nonnegative()
|
|
22919
|
-
});
|
|
22920
|
-
DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
|
|
22921
|
-
deviceId: number(),
|
|
22922
|
-
zoneId: string(),
|
|
22923
|
-
className: string().optional()
|
|
22924
|
-
}).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
|
|
22925
|
-
deviceId: number(),
|
|
22926
|
-
className: string().optional()
|
|
22927
|
-
}).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
|
|
22928
|
-
deviceId: number(),
|
|
22929
|
-
className: string().optional()
|
|
22930
|
-
}).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly());
|
|
22931
|
-
/**
|
|
22932
|
-
* Stages a {@link ZoneRule} can apply to. Discriminator on the rules
|
|
22933
|
-
* cap so a single CRUD surface backs every consumer; each stage has
|
|
22934
|
-
* its own dev-state mirror slice (`motion-zone-rules`,
|
|
22935
|
-
* `detection-zone-rules`, …) so consumer addons subscribe independently.
|
|
22936
|
-
*
|
|
22937
|
-
* Extend the enum here when a new gating consumer comes online (audio
|
|
22938
|
-
* gating, alert filtering, …) — no other surface needs to change.
|
|
22939
|
-
*/
|
|
22940
|
-
var ZoneRuleStageEnum = _enum([
|
|
22941
|
-
"motion",
|
|
22942
|
-
"detection",
|
|
22943
|
-
"package"
|
|
22944
|
-
]);
|
|
22945
|
-
DeviceType.Camera, method(object({
|
|
22946
|
-
deviceId: number(),
|
|
22947
|
-
stage: ZoneRuleStageEnum
|
|
22948
|
-
}), array(ZoneRuleSchema).readonly()), method(object({
|
|
22949
|
-
deviceId: number(),
|
|
22950
|
-
stage: ZoneRuleStageEnum,
|
|
22951
|
-
rules: array(ZoneRuleSchema).readonly()
|
|
22952
|
-
}), _void(), {
|
|
22953
|
-
kind: "mutation",
|
|
22954
|
-
auth: "admin"
|
|
22955
|
-
}), object({
|
|
22956
|
-
motion: array(ZoneRuleSchema).readonly(),
|
|
22957
|
-
detection: array(ZoneRuleSchema).readonly(),
|
|
22958
|
-
package: array(ZoneRuleSchema).readonly()
|
|
22959
|
-
});
|
|
22960
|
-
/**
|
|
22961
|
-
* Accessory device helpers — shared across drivers.
|
|
22962
|
-
*
|
|
22963
|
-
* Many vendor-specific drivers register accessory child devices on
|
|
22964
|
-
* top of a parent (Reolink: siren / floodlight / PIR / autotrack /
|
|
22965
|
-
* chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
|
|
22966
|
-
* driver picks the right `DeviceType` + `DeviceRole` explicitly when
|
|
22967
|
-
* spawning, builds a name derived from the parent, and produces a
|
|
22968
|
-
* stableId tied to the parent so boot-restore can reconstruct the
|
|
22969
|
-
* relationship.
|
|
22970
|
-
*
|
|
22971
|
-
* Centralised `(kind → DeviceType)` mapping was dropped on purpose:
|
|
22972
|
-
* drivers may reasonably disagree on the right type for an accessory
|
|
22973
|
-
* (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
|
|
22974
|
-
* read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
|
|
22975
|
-
* one canonical mapping was over-prescriptive and added a layer of
|
|
22976
|
-
* indirection without saving meaningful code at call sites — the
|
|
22977
|
-
* driver knows its own hardware best.
|
|
22978
|
-
*/
|
|
22979
|
-
/**
|
|
22980
|
-
* Subset of `DeviceRole` values that drivers register as child
|
|
22981
|
-
* accessories of a parent device. Sourced verbatim from `DeviceRole`
|
|
22982
|
-
* — `AccessoryKind` is the alias drivers use when building accessory
|
|
22983
|
-
* children, so the call site reads as
|
|
22984
|
-
* `accessoryStableId(parent, AccessoryKind.Siren)` rather than
|
|
22985
|
-
* `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
|
|
22986
|
-
* any role works, including non-accessory ones like Doorbell).
|
|
22987
|
-
*/
|
|
22988
|
-
var AccessoryKind = {
|
|
22989
|
-
Siren: DeviceRole.Siren,
|
|
22990
|
-
Floodlight: DeviceRole.Floodlight,
|
|
22991
|
-
Spotlight: DeviceRole.Spotlight,
|
|
22992
|
-
PirSensor: DeviceRole.PirSensor,
|
|
22993
|
-
Chime: DeviceRole.Chime,
|
|
22994
|
-
Autotrack: DeviceRole.Autotrack,
|
|
22995
|
-
Nightvision: DeviceRole.Nightvision,
|
|
22996
|
-
PrivacyMask: DeviceRole.PrivacyMask
|
|
22997
|
-
};
|
|
22998
|
-
AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
|
|
22999
|
-
DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
|
|
23000
|
-
new Set(Object.values(DeviceType));
|
|
23001
|
-
DeviceFeature.BatteryOperated;
|
|
23002
|
-
/**
|
|
23003
|
-
* Error types for the safe expression engine. Two distinct classes so callers
|
|
23004
|
-
* can tell a compile-time (grammar) failure from a runtime (evaluation)
|
|
23005
|
-
* failure — both are non-fatal to the host: read paths degrade to "skip link".
|
|
23006
|
-
*/
|
|
23007
|
-
/** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
|
|
23008
|
-
* the failure is anchored to a character (author-facing inline feedback). */
|
|
23009
|
-
var ExpressionParseError = class extends Error {
|
|
23010
|
-
position;
|
|
23011
|
-
constructor(message, position) {
|
|
23012
|
-
super(message);
|
|
23013
|
-
this.name = "ExpressionParseError";
|
|
23014
|
-
this.position = position;
|
|
23015
|
-
}
|
|
23016
|
-
};
|
|
23017
|
-
/** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
|
|
23018
|
-
* result, unknown builtin, step-budget exceeded). */
|
|
23019
|
-
var ExpressionEvalError = class extends Error {
|
|
23020
|
-
constructor(message) {
|
|
23021
|
-
super(message);
|
|
23022
|
-
this.name = "ExpressionEvalError";
|
|
23023
|
-
}
|
|
23024
|
-
};
|
|
23025
|
-
/**
|
|
23026
|
-
* Frozen, null-prototype builtin function table for the expression engine
|
|
23027
|
-
* (spec §4 rule 4). The table is the SOLE surface of callable functions: the
|
|
23028
|
-
* parser rejects any callee not in it, and the evaluator gates each call on an
|
|
23029
|
-
* own-property check against it.
|
|
23030
|
-
*
|
|
23031
|
-
* Because the object has a NULL prototype AND is `Object.freeze`d:
|
|
23032
|
-
* - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
|
|
23033
|
-
* - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
|
|
23034
|
-
* (there is no `Object.prototype` in the chain), so those names are not
|
|
23035
|
-
* callable — they are simply "unknown function" at parse time.
|
|
23036
|
-
*
|
|
23037
|
-
* Every numeric argument is validated as a finite number and every numeric
|
|
23038
|
-
* RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
|
|
23039
|
-
* (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
|
|
23040
|
-
* closed rather than emitting a garbage value.
|
|
23041
|
-
*/
|
|
23042
|
-
function asFiniteNumber(value, name, index) {
|
|
23043
|
-
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
23044
|
-
return value;
|
|
23045
|
-
}
|
|
23046
|
-
function asString$1(value, name, index) {
|
|
23047
|
-
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
23048
|
-
return value;
|
|
23049
|
-
}
|
|
23050
|
-
function finiteResult(value, name) {
|
|
23051
|
-
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
23052
|
-
return value;
|
|
23053
|
-
}
|
|
23054
|
-
function allFiniteNumbers(args, name) {
|
|
23055
|
-
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
23056
|
-
}
|
|
23057
|
-
var INF = Number.POSITIVE_INFINITY;
|
|
23058
|
-
var table = {
|
|
23059
|
-
min: {
|
|
23060
|
-
minArgs: 1,
|
|
23061
|
-
maxArgs: INF,
|
|
23062
|
-
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
23063
|
-
},
|
|
23064
|
-
max: {
|
|
23065
|
-
minArgs: 1,
|
|
23066
|
-
maxArgs: INF,
|
|
23067
|
-
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
23068
|
-
},
|
|
23069
|
-
abs: {
|
|
23070
|
-
minArgs: 1,
|
|
23071
|
-
maxArgs: 1,
|
|
23072
|
-
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
23073
|
-
},
|
|
23074
|
-
floor: {
|
|
23075
|
-
minArgs: 1,
|
|
23076
|
-
maxArgs: 1,
|
|
23077
|
-
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
23078
|
-
},
|
|
23079
|
-
ceil: {
|
|
23080
|
-
minArgs: 1,
|
|
23081
|
-
maxArgs: 1,
|
|
23082
|
-
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
23083
|
-
},
|
|
23084
|
-
sqrt: {
|
|
23085
|
-
minArgs: 1,
|
|
23086
|
-
maxArgs: 1,
|
|
23087
|
-
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
23088
|
-
},
|
|
23089
|
-
round: {
|
|
23090
|
-
minArgs: 1,
|
|
23091
|
-
maxArgs: 2,
|
|
23092
|
-
apply: (args) => {
|
|
23093
|
-
const x = asFiniteNumber(args[0], "round", 0);
|
|
23094
|
-
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
23095
|
-
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
23096
|
-
const factor = 10 ** digits;
|
|
23097
|
-
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
23098
|
-
}
|
|
23099
|
-
},
|
|
23100
|
-
pow: {
|
|
23101
|
-
minArgs: 2,
|
|
23102
|
-
maxArgs: 2,
|
|
23103
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
23104
|
-
},
|
|
23105
|
-
clamp: {
|
|
23106
|
-
minArgs: 3,
|
|
23107
|
-
maxArgs: 3,
|
|
23108
|
-
apply: (args) => {
|
|
23109
|
-
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
23110
|
-
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
23111
|
-
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
23112
|
-
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
23113
|
-
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
23114
|
-
}
|
|
23115
|
-
},
|
|
23116
|
-
avg: {
|
|
23117
|
-
minArgs: 1,
|
|
23118
|
-
maxArgs: INF,
|
|
23119
|
-
apply: (args) => {
|
|
23120
|
-
const nums = allFiniteNumbers(args, "avg");
|
|
23121
|
-
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
23122
|
-
}
|
|
23123
|
-
},
|
|
23124
|
-
sum: {
|
|
23125
|
-
minArgs: 1,
|
|
23126
|
-
maxArgs: INF,
|
|
23127
|
-
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
23128
|
-
},
|
|
23129
|
-
coalesce: {
|
|
23130
|
-
minArgs: 1,
|
|
23131
|
-
maxArgs: INF,
|
|
23132
|
-
apply: (args) => {
|
|
23133
|
-
for (const a of args) if (a !== null) return a;
|
|
23134
|
-
return null;
|
|
23135
|
-
}
|
|
23136
|
-
},
|
|
23137
|
-
age: {
|
|
23138
|
-
minArgs: 2,
|
|
23139
|
-
maxArgs: 2,
|
|
23140
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
23141
|
-
},
|
|
23142
|
-
convert: {
|
|
23143
|
-
minArgs: 3,
|
|
23144
|
-
maxArgs: 3,
|
|
23145
|
-
apply: (args, hooks) => {
|
|
23146
|
-
const x = asFiniteNumber(args[0], "convert", 0);
|
|
23147
|
-
const from = asString$1(args[1], "convert", 1).trim();
|
|
23148
|
-
const to = asString$1(args[2], "convert", 2).trim();
|
|
23149
|
-
if (hooks.convert) {
|
|
23150
|
-
const out = hooks.convert(x, from, to);
|
|
23151
|
-
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
23152
|
-
return finiteResult(out, "convert");
|
|
23153
|
-
}
|
|
23154
|
-
if (from === to) return x;
|
|
23155
|
-
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
23156
|
-
}
|
|
23157
|
-
}
|
|
23158
|
-
};
|
|
23159
|
-
Object.freeze(Object.assign(Object.create(null), table));
|
|
23160
|
-
/** The set of valid builtin names — used by the parser to reject unknown
|
|
23161
|
-
* callees at parse time (immediate author feedback). */
|
|
23162
|
-
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
23163
|
-
/**
|
|
23164
|
-
* Resource-bound constants for the safe expression engine.
|
|
23165
|
-
*
|
|
23166
|
-
* Every bound is defense-in-depth: the grammar is non-Turing-complete (no
|
|
23167
|
-
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
23168
|
-
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
23169
|
-
* work a single author-supplied expression can request, so a hostile or
|
|
23170
|
-
* accidental pathological string can never spend unbounded CPU/memory.
|
|
23171
|
-
*/
|
|
23172
|
-
/** Max source length (chars) — checked BEFORE tokenizing so a huge string is
|
|
23173
|
-
* rejected without allocation. */
|
|
23174
|
-
var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
|
|
23175
|
-
/** A legal binding / identifier name. */
|
|
23176
|
-
var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
23177
|
-
/** Binding names an author may NOT use: `now` is auto-injected; the literal
|
|
23178
|
-
* keywords lex as values, not identifiers, so binding to them is meaningless. */
|
|
23179
|
-
var RESERVED_BINDING_NAMES = new Set([
|
|
23180
|
-
"now",
|
|
23181
|
-
"true",
|
|
23182
|
-
"false",
|
|
23183
|
-
"null"
|
|
23184
|
-
]);
|
|
23185
|
-
/**
|
|
23186
|
-
* Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
|
|
23187
|
-
* zero-dependency. The grammar is deliberately boring: decimal numbers,
|
|
23188
|
-
* single/double-quoted strings with a tiny escape set, identifiers, the three
|
|
23189
|
-
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
23190
|
-
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
23191
|
-
* is a parse error with a source position, so member access / assignment /
|
|
23192
|
-
* template literals are lexically impossible.
|
|
23193
|
-
*/
|
|
23194
|
-
var KEYWORDS = new Set([
|
|
23195
|
-
"true",
|
|
23196
|
-
"false",
|
|
23197
|
-
"null"
|
|
23198
|
-
]);
|
|
23199
|
-
function isDigit(ch) {
|
|
23200
|
-
return ch >= "0" && ch <= "9";
|
|
23201
|
-
}
|
|
23202
|
-
function isIdentStart(ch) {
|
|
23203
|
-
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
23204
|
-
}
|
|
23205
|
-
function isIdentPart(ch) {
|
|
23206
|
-
return isIdentStart(ch) || isDigit(ch);
|
|
23207
|
-
}
|
|
23208
|
-
function isWhitespace(ch) {
|
|
23209
|
-
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
23210
|
-
}
|
|
23211
|
-
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
23212
|
-
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
23213
|
-
* string. */
|
|
23214
|
-
function tokenize(source) {
|
|
23215
|
-
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
23216
|
-
const tokens = [];
|
|
23217
|
-
let i = 0;
|
|
23218
|
-
const n = source.length;
|
|
23219
|
-
while (i < n) {
|
|
23220
|
-
const ch = source[i];
|
|
23221
|
-
if (isWhitespace(ch)) {
|
|
23222
|
-
i += 1;
|
|
23223
|
-
continue;
|
|
23224
|
-
}
|
|
23225
|
-
if (isDigit(ch)) {
|
|
23226
|
-
const start = i;
|
|
23227
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
23228
|
-
if (i < n && source[i] === ".") {
|
|
23229
|
-
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
23230
|
-
i += 1;
|
|
23231
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
23232
|
-
}
|
|
23233
|
-
const text = source.slice(start, i);
|
|
23234
|
-
const value = Number(text);
|
|
23235
|
-
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
23236
|
-
tokens.push({
|
|
23237
|
-
type: "number",
|
|
23238
|
-
value,
|
|
23239
|
-
pos: start
|
|
23240
|
-
});
|
|
23241
|
-
continue;
|
|
23242
|
-
}
|
|
23243
|
-
if (ch === "'" || ch === "\"") {
|
|
23244
|
-
const quote = ch;
|
|
23245
|
-
const start = i;
|
|
23246
|
-
i += 1;
|
|
23247
|
-
let out = "";
|
|
23248
|
-
let closed = false;
|
|
23249
|
-
while (i < n) {
|
|
23250
|
-
const c = source[i];
|
|
23251
|
-
if (c === "\\") {
|
|
23252
|
-
const next = i + 1 < n ? source[i + 1] : "";
|
|
23253
|
-
if (next === "\\" || next === "'" || next === "\"") {
|
|
23254
|
-
out += next;
|
|
23255
|
-
i += 2;
|
|
23256
|
-
continue;
|
|
23257
|
-
}
|
|
23258
|
-
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
23259
|
-
}
|
|
23260
|
-
if (c === quote) {
|
|
23261
|
-
closed = true;
|
|
23262
|
-
i += 1;
|
|
23263
|
-
break;
|
|
23264
|
-
}
|
|
23265
|
-
out += c;
|
|
23266
|
-
i += 1;
|
|
23267
|
-
}
|
|
23268
|
-
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
23269
|
-
tokens.push({
|
|
23270
|
-
type: "string",
|
|
23271
|
-
value: out,
|
|
23272
|
-
pos: start
|
|
23273
|
-
});
|
|
23274
|
-
continue;
|
|
23275
|
-
}
|
|
23276
|
-
if (isIdentStart(ch)) {
|
|
23277
|
-
const start = i;
|
|
23278
|
-
while (i < n && isIdentPart(source[i])) i += 1;
|
|
23279
|
-
const text = source.slice(start, i);
|
|
23280
|
-
if (KEYWORDS.has(text)) tokens.push({
|
|
23281
|
-
type: "keyword",
|
|
23282
|
-
keyword: keywordOf(text),
|
|
23283
|
-
pos: start
|
|
23284
|
-
});
|
|
23285
|
-
else tokens.push({
|
|
23286
|
-
type: "identifier",
|
|
23287
|
-
name: text,
|
|
23288
|
-
pos: start
|
|
23289
|
-
});
|
|
23290
|
-
continue;
|
|
23291
|
-
}
|
|
23292
|
-
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
23293
|
-
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
23294
|
-
tokens.push({
|
|
23295
|
-
type: "punct",
|
|
23296
|
-
punct: two,
|
|
23297
|
-
pos: i
|
|
23298
|
-
});
|
|
23299
|
-
i += 2;
|
|
23300
|
-
continue;
|
|
23301
|
-
}
|
|
23302
|
-
if (isSinglePunct(ch)) {
|
|
23303
|
-
tokens.push({
|
|
23304
|
-
type: "punct",
|
|
23305
|
-
punct: ch,
|
|
23306
|
-
pos: i
|
|
23307
|
-
});
|
|
23308
|
-
i += 1;
|
|
23309
|
-
continue;
|
|
23310
|
-
}
|
|
23311
|
-
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
23312
|
-
}
|
|
23313
|
-
tokens.push({
|
|
23314
|
-
type: "eof",
|
|
23315
|
-
pos: n
|
|
23316
|
-
});
|
|
23317
|
-
return tokens;
|
|
23318
|
-
}
|
|
23319
|
-
function keywordOf(text) {
|
|
23320
|
-
if (text === "true") return "true";
|
|
23321
|
-
if (text === "false") return "false";
|
|
23322
|
-
return "null";
|
|
23323
|
-
}
|
|
23324
|
-
function isSinglePunct(ch) {
|
|
23325
|
-
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
23326
|
-
}
|
|
23327
|
-
/**
|
|
23328
|
-
* Pratt (precedence-climbing) parser for the safe expression mini-language.
|
|
23329
|
-
*
|
|
23330
|
-
* Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
|
|
23331
|
-
* → relational → additive → multiplicative → unary `! -` → call / primary.
|
|
23332
|
-
* Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
|
|
23333
|
-
* string validated against the builtin table at parse time, so an unknown
|
|
23334
|
-
* function is rejected immediately (author feedback) and a persisted expression
|
|
23335
|
-
* that references a since-removed builtin degrades at read.
|
|
23336
|
-
*
|
|
23337
|
-
* A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
|
|
23338
|
-
* arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
|
|
23339
|
-
*/
|
|
23340
|
-
/** Binary/logical operator precedence (higher binds tighter). */
|
|
23341
|
-
var BINARY_PRECEDENCE = {
|
|
23342
|
-
"||": 1,
|
|
23343
|
-
"&&": 2,
|
|
23344
|
-
"==": 3,
|
|
23345
|
-
"!=": 3,
|
|
23346
|
-
"<": 4,
|
|
23347
|
-
"<=": 4,
|
|
23348
|
-
">": 4,
|
|
23349
|
-
">=": 4,
|
|
23350
|
-
"+": 5,
|
|
23351
|
-
"-": 5,
|
|
23352
|
-
"*": 6,
|
|
23353
|
-
"/": 6,
|
|
23354
|
-
"%": 6
|
|
23355
|
-
};
|
|
23356
|
-
function isLogicalOp(op) {
|
|
23357
|
-
return op === "&&" || op === "||";
|
|
23358
|
-
}
|
|
23359
|
-
function isBinaryOp(op) {
|
|
23360
|
-
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
23361
|
-
}
|
|
23362
|
-
var Parser = class {
|
|
23363
|
-
tokens;
|
|
23364
|
-
pos = 0;
|
|
23365
|
-
nodeCount = 0;
|
|
23366
|
-
identifiers = /* @__PURE__ */ new Set();
|
|
23367
|
-
callees = /* @__PURE__ */ new Set();
|
|
23368
|
-
constructor(tokens) {
|
|
23369
|
-
this.tokens = tokens;
|
|
23370
|
-
}
|
|
23371
|
-
parse() {
|
|
23372
|
-
const ast = this.parseTernary();
|
|
23373
|
-
const tok = this.peek();
|
|
23374
|
-
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
23375
|
-
return {
|
|
23376
|
-
ast,
|
|
23377
|
-
identifiers: this.identifiers,
|
|
23378
|
-
callees: this.callees,
|
|
23379
|
-
nodeCount: this.nodeCount
|
|
23380
|
-
};
|
|
23381
|
-
}
|
|
23382
|
-
peek() {
|
|
23383
|
-
return this.tokens[this.pos];
|
|
23384
|
-
}
|
|
23385
|
-
next() {
|
|
23386
|
-
return this.tokens[this.pos++];
|
|
23387
|
-
}
|
|
23388
|
-
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
23389
|
-
expectPunct(punct) {
|
|
23390
|
-
const tok = this.peek();
|
|
23391
|
-
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
23392
|
-
this.pos += 1;
|
|
23393
|
-
}
|
|
23394
|
-
matchPunct(punct) {
|
|
23395
|
-
const tok = this.peek();
|
|
23396
|
-
if (tok.type === "punct" && tok.punct === punct) {
|
|
23397
|
-
this.pos += 1;
|
|
23398
|
-
return true;
|
|
23399
|
-
}
|
|
23400
|
-
return false;
|
|
23401
|
-
}
|
|
23402
|
-
countNode() {
|
|
23403
|
-
this.nodeCount += 1;
|
|
23404
|
-
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
23405
|
-
}
|
|
23406
|
-
parseTernary() {
|
|
23407
|
-
const test = this.parseBinary(1);
|
|
23408
|
-
if (this.matchPunct("?")) {
|
|
23409
|
-
const consequent = this.parseTernary();
|
|
23410
|
-
this.expectPunct(":");
|
|
23411
|
-
const alternate = this.parseTernary();
|
|
23412
|
-
this.countNode();
|
|
23413
|
-
return {
|
|
23414
|
-
kind: "conditional",
|
|
23415
|
-
test,
|
|
23416
|
-
consequent,
|
|
23417
|
-
alternate
|
|
23418
|
-
};
|
|
23419
|
-
}
|
|
23420
|
-
return test;
|
|
23421
|
-
}
|
|
23422
|
-
parseBinary(minPrec) {
|
|
23423
|
-
let left = this.parseUnary();
|
|
23424
|
-
for (;;) {
|
|
23425
|
-
const tok = this.peek();
|
|
23426
|
-
if (tok.type !== "punct") break;
|
|
23427
|
-
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
23428
|
-
if (prec === void 0 || prec < minPrec) break;
|
|
23429
|
-
const op = tok.punct;
|
|
23430
|
-
this.pos += 1;
|
|
23431
|
-
const right = this.parseBinary(prec + 1);
|
|
23432
|
-
this.countNode();
|
|
23433
|
-
if (isLogicalOp(op)) left = {
|
|
23434
|
-
kind: "logical",
|
|
23435
|
-
op,
|
|
23436
|
-
left,
|
|
23437
|
-
right
|
|
23438
|
-
};
|
|
23439
|
-
else if (isBinaryOp(op)) left = {
|
|
23440
|
-
kind: "binary",
|
|
23441
|
-
op,
|
|
23442
|
-
left,
|
|
23443
|
-
right
|
|
23444
|
-
};
|
|
23445
|
-
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
23446
|
-
}
|
|
23447
|
-
return left;
|
|
23448
|
-
}
|
|
23449
|
-
parseUnary() {
|
|
23450
|
-
const tok = this.peek();
|
|
23451
|
-
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
23452
|
-
const op = tok.punct;
|
|
23453
|
-
this.pos += 1;
|
|
23454
|
-
const operand = this.parseUnary();
|
|
23455
|
-
this.countNode();
|
|
23456
|
-
return {
|
|
23457
|
-
kind: "unary",
|
|
23458
|
-
op,
|
|
23459
|
-
operand
|
|
23460
|
-
};
|
|
23461
|
-
}
|
|
23462
|
-
return this.parsePrimary();
|
|
23463
|
-
}
|
|
23464
|
-
parsePrimary() {
|
|
23465
|
-
const tok = this.next();
|
|
23466
|
-
switch (tok.type) {
|
|
23467
|
-
case "number":
|
|
23468
|
-
this.countNode();
|
|
23469
|
-
return {
|
|
23470
|
-
kind: "literal",
|
|
23471
|
-
value: tok.value
|
|
23472
|
-
};
|
|
23473
|
-
case "string":
|
|
23474
|
-
this.countNode();
|
|
23475
|
-
return {
|
|
23476
|
-
kind: "literal",
|
|
23477
|
-
value: tok.value
|
|
23478
|
-
};
|
|
23479
|
-
case "keyword":
|
|
23480
|
-
this.countNode();
|
|
23481
|
-
return {
|
|
23482
|
-
kind: "literal",
|
|
23483
|
-
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
23484
|
-
};
|
|
23485
|
-
case "identifier": {
|
|
23486
|
-
const nextTok = this.peek();
|
|
23487
|
-
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
23488
|
-
this.identifiers.add(tok.name);
|
|
23489
|
-
this.countNode();
|
|
23490
|
-
return {
|
|
23491
|
-
kind: "identifier",
|
|
23492
|
-
name: tok.name
|
|
23493
|
-
};
|
|
23494
|
-
}
|
|
23495
|
-
case "punct":
|
|
23496
|
-
if (tok.punct === "(") {
|
|
23497
|
-
const inner = this.parseTernary();
|
|
23498
|
-
this.expectPunct(")");
|
|
23499
|
-
return inner;
|
|
23500
|
-
}
|
|
23501
|
-
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
23502
|
-
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
23503
|
-
}
|
|
23504
|
-
}
|
|
23505
|
-
parseCall(callee, pos) {
|
|
23506
|
-
if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
|
|
23507
|
-
this.expectPunct("(");
|
|
23508
|
-
const args = [];
|
|
23509
|
-
if (!this.matchPunct(")")) for (;;) {
|
|
23510
|
-
args.push(this.parseTernary());
|
|
23511
|
-
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
23512
|
-
if (this.matchPunct(",")) continue;
|
|
23513
|
-
this.expectPunct(")");
|
|
23514
|
-
break;
|
|
23515
|
-
}
|
|
23516
|
-
this.callees.add(callee);
|
|
23517
|
-
this.countNode();
|
|
23518
|
-
return {
|
|
23519
|
-
kind: "call",
|
|
23520
|
-
callee,
|
|
23521
|
-
args
|
|
23522
|
-
};
|
|
23523
|
-
}
|
|
23524
|
-
};
|
|
23525
|
-
/** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
|
|
23526
|
-
* `ExpressionParseError` on any lexical or grammatical failure. */
|
|
23527
|
-
function parseExpression(source) {
|
|
23528
|
-
return new Parser(tokenize(source)).parse();
|
|
23529
|
-
}
|
|
23530
|
-
/**
|
|
23531
|
-
* LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
|
|
23532
|
-
* by expr"). The cache stores BOTH successes and failures (negative caching),
|
|
23533
|
-
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
23534
|
-
* one per read on a hot resolve path.
|
|
23656
|
+
* sees on the live overlay.
|
|
23535
23657
|
*
|
|
23536
|
-
*
|
|
23537
|
-
*
|
|
23538
|
-
*
|
|
23658
|
+
* Overlap policy: a detection that intersects two zones counts in
|
|
23659
|
+
* BOTH zones' `byClass` and `totalObjects` (count-in-each). The
|
|
23660
|
+
* `frame` aggregate de-duplicates trivially since it's frame-wide.
|
|
23661
|
+
* `unzoned` counts only detections that landed in zero zones.
|
|
23539
23662
|
*/
|
|
23540
|
-
var
|
|
23541
|
-
|
|
23542
|
-
|
|
23543
|
-
|
|
23544
|
-
|
|
23545
|
-
|
|
23546
|
-
|
|
23547
|
-
|
|
23548
|
-
|
|
23549
|
-
|
|
23550
|
-
|
|
23551
|
-
|
|
23552
|
-
|
|
23553
|
-
};
|
|
23554
|
-
} catch (err) {
|
|
23555
|
-
result = {
|
|
23556
|
-
ok: false,
|
|
23557
|
-
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
23558
|
-
};
|
|
23559
|
-
}
|
|
23560
|
-
cache.set(source, result);
|
|
23561
|
-
if (cache.size > 256) {
|
|
23562
|
-
const oldest = cache.keys().next().value;
|
|
23563
|
-
if (oldest !== void 0) cache.delete(oldest);
|
|
23564
|
-
}
|
|
23565
|
-
return result;
|
|
23566
|
-
}
|
|
23567
|
-
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
23568
|
-
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
23569
|
-
function compileExpressionSafe(source) {
|
|
23570
|
-
return getCached(source);
|
|
23571
|
-
}
|
|
23572
|
-
Object.freeze({});
|
|
23663
|
+
var PerScopeBreakdownSchema = object({
|
|
23664
|
+
/** Total tracked objects in this scope (frame / zone / unzoned). */
|
|
23665
|
+
totalObjects: number().int().nonnegative(),
|
|
23666
|
+
/** Per-class count. Keys are macro class names (e.g. `person`, `car`). */
|
|
23667
|
+
byClass: record(string(), number().int().nonnegative())
|
|
23668
|
+
});
|
|
23669
|
+
var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
|
|
23670
|
+
zoneId: string(),
|
|
23671
|
+
zoneName: string(),
|
|
23672
|
+
/** TrackIds of objects currently inside this zone — for cross-reference
|
|
23673
|
+
* with the per-track detail panel and live overlay. */
|
|
23674
|
+
trackIds: array(string()).readonly()
|
|
23675
|
+
});
|
|
23573
23676
|
/**
|
|
23574
|
-
*
|
|
23575
|
-
*
|
|
23576
|
-
*
|
|
23577
|
-
*
|
|
23578
|
-
*
|
|
23677
|
+
* A parked ("stationary") object surfaced alongside occupancy — an object that
|
|
23678
|
+
* settled and stopped moving. It is NO LONGER a tracked object (the tracker was
|
|
23679
|
+
* told to forget it so it stops re-spawning tracks/events), but it IS still
|
|
23680
|
+
* physically present, so it keeps counting toward `frame` occupancy and is
|
|
23681
|
+
* listed here so the UI can show it in a dedicated "Stationary" section instead
|
|
23682
|
+
* of flooding the live event feed.
|
|
23579
23683
|
*/
|
|
23580
|
-
|
|
23581
|
-
|
|
23582
|
-
|
|
23583
|
-
|
|
23584
|
-
|
|
23585
|
-
|
|
23586
|
-
|
|
23587
|
-
|
|
23588
|
-
const compiled = compileExpressionSafe(src.expr);
|
|
23589
|
-
if (!compiled.ok) return compiled.error;
|
|
23590
|
-
const bound = new Set(names);
|
|
23591
|
-
for (const id of compiled.parsed.identifiers) {
|
|
23592
|
-
if (id === "now") continue;
|
|
23593
|
-
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
23594
|
-
}
|
|
23595
|
-
return null;
|
|
23596
|
-
}
|
|
23597
|
-
var ExpressionBindingSourceSchema = union([
|
|
23598
|
-
object({
|
|
23599
|
-
kind: literal("field").optional(),
|
|
23600
|
-
sourceKey: string(),
|
|
23601
|
-
cap: string(),
|
|
23602
|
-
fieldPath: string()
|
|
23603
|
-
}),
|
|
23604
|
-
object({
|
|
23605
|
-
kind: literal("literal"),
|
|
23606
|
-
value: union([
|
|
23607
|
-
string(),
|
|
23608
|
-
number(),
|
|
23609
|
-
boolean(),
|
|
23610
|
-
_null()
|
|
23611
|
-
])
|
|
23684
|
+
var StationaryObjectSchema = object({
|
|
23685
|
+
id: string(),
|
|
23686
|
+
className: string(),
|
|
23687
|
+
bbox: object({
|
|
23688
|
+
x: number(),
|
|
23689
|
+
y: number(),
|
|
23690
|
+
w: number(),
|
|
23691
|
+
h: number()
|
|
23612
23692
|
}),
|
|
23613
|
-
|
|
23614
|
-
|
|
23615
|
-
|
|
23616
|
-
|
|
23617
|
-
|
|
23618
|
-
|
|
23693
|
+
frameWidth: number().int().nonnegative(),
|
|
23694
|
+
frameHeight: number().int().nonnegative(),
|
|
23695
|
+
/** When the source track was first seen. */
|
|
23696
|
+
firstSeenAt: number().int(),
|
|
23697
|
+
/** When the object was recognised as parked (promotion time). */
|
|
23698
|
+
becameStationaryAt: number().int(),
|
|
23699
|
+
/** Last frame a detection confirmed the object is still there. */
|
|
23700
|
+
lastConfirmedAt: number().int(),
|
|
23701
|
+
/** Enrichment label carried from the source track (identity / plate). */
|
|
23702
|
+
label: string().optional(),
|
|
23703
|
+
/** Native-resolution key-frame media key for the parked object's best image. */
|
|
23704
|
+
keyFrameMediaKey: string().optional()
|
|
23705
|
+
});
|
|
23706
|
+
var CameraOccupancySnapshotSchema = object({
|
|
23707
|
+
/** Frame timestamp of the inference result that produced this snapshot. */
|
|
23708
|
+
ts: number().int(),
|
|
23709
|
+
/** Frame width/height in pixels — let the UI normalize bbox coords. */
|
|
23710
|
+
frameWidth: number().int().nonnegative(),
|
|
23711
|
+
frameHeight: number().int().nonnegative(),
|
|
23712
|
+
/** Per-zone breakdown — one entry per defined zone (user + onboard). */
|
|
23713
|
+
zones: array(ZoneScopeBreakdownSchema).readonly(),
|
|
23714
|
+
/** Frame-wide aggregate (everywhere, regardless of zone membership).
|
|
23715
|
+
* INCLUDES currently-confirmed stationary objects (they are still present). */
|
|
23716
|
+
frame: PerScopeBreakdownSchema,
|
|
23717
|
+
/** Detections that landed outside every zone. Empty when no zones defined. */
|
|
23718
|
+
unzoned: PerScopeBreakdownSchema,
|
|
23719
|
+
/** Parked objects on this camera (additive — absent on legacy snapshots).
|
|
23720
|
+
* Surfaced separately so the UI shows them in a dedicated section rather
|
|
23721
|
+
* than as repeated tracks/events. */
|
|
23722
|
+
stationaryObjects: array(StationaryObjectSchema).readonly().optional()
|
|
23723
|
+
});
|
|
23724
|
+
/**
|
|
23725
|
+
* Time-series resolution. The history methods return one bucket per
|
|
23726
|
+
* step over the requested range. Smaller resolutions cost more
|
|
23727
|
+
* memory + bandwidth; bound to discrete steps so caller cannot ask
|
|
23728
|
+
* for arbitrary fractional buckets.
|
|
23729
|
+
*/
|
|
23730
|
+
var HistoryResolutionEnum = _enum([
|
|
23731
|
+
"minute",
|
|
23732
|
+
"5min",
|
|
23733
|
+
"hour"
|
|
23619
23734
|
]);
|
|
23620
|
-
object({
|
|
23621
|
-
|
|
23622
|
-
|
|
23623
|
-
|
|
23624
|
-
|
|
23625
|
-
|
|
23626
|
-
|
|
23627
|
-
|
|
23628
|
-
|
|
23629
|
-
|
|
23735
|
+
var HistoryRangeSchema = object({
|
|
23736
|
+
/** Range start (epoch ms, inclusive). */
|
|
23737
|
+
from: number().int(),
|
|
23738
|
+
/** Range end (epoch ms, inclusive). Defaults to "now" at query time. */
|
|
23739
|
+
to: number().int(),
|
|
23740
|
+
resolution: HistoryResolutionEnum
|
|
23741
|
+
});
|
|
23742
|
+
var HistoryPointSchema = object({
|
|
23743
|
+
/** Bucket midpoint (epoch ms). */
|
|
23744
|
+
ts: number().int(),
|
|
23745
|
+
/** Object count averaged over the bucket (rounded to nearest integer). */
|
|
23746
|
+
count: number().int().nonnegative()
|
|
23747
|
+
});
|
|
23748
|
+
DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
|
|
23749
|
+
deviceId: number(),
|
|
23750
|
+
zoneId: string(),
|
|
23751
|
+
className: string().optional()
|
|
23752
|
+
}).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
|
|
23753
|
+
deviceId: number(),
|
|
23754
|
+
className: string().optional()
|
|
23755
|
+
}).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
|
|
23756
|
+
deviceId: number(),
|
|
23757
|
+
className: string().optional()
|
|
23758
|
+
}).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly());
|
|
23759
|
+
/**
|
|
23760
|
+
* Stages a {@link ZoneRule} can apply to. Discriminator on the rules
|
|
23761
|
+
* cap so a single CRUD surface backs every consumer; each stage has
|
|
23762
|
+
* its own dev-state mirror slice (`motion-zone-rules`,
|
|
23763
|
+
* `detection-zone-rules`, …) so consumer addons subscribe independently.
|
|
23764
|
+
*
|
|
23765
|
+
* Extend the enum here when a new gating consumer comes online (audio
|
|
23766
|
+
* gating, alert filtering, …) — no other surface needs to change.
|
|
23767
|
+
*/
|
|
23768
|
+
var ZoneRuleStageEnum = _enum([
|
|
23769
|
+
"motion",
|
|
23770
|
+
"detection",
|
|
23771
|
+
"package"
|
|
23772
|
+
]);
|
|
23773
|
+
DeviceType.Camera, method(object({
|
|
23774
|
+
deviceId: number(),
|
|
23775
|
+
stage: ZoneRuleStageEnum
|
|
23776
|
+
}), array(ZoneRuleSchema).readonly()), method(object({
|
|
23777
|
+
deviceId: number(),
|
|
23778
|
+
stage: ZoneRuleStageEnum,
|
|
23779
|
+
rules: array(ZoneRuleSchema).readonly()
|
|
23780
|
+
}), _void(), {
|
|
23781
|
+
kind: "mutation",
|
|
23782
|
+
auth: "admin"
|
|
23783
|
+
}), object({
|
|
23784
|
+
motion: array(ZoneRuleSchema).readonly(),
|
|
23785
|
+
detection: array(ZoneRuleSchema).readonly(),
|
|
23786
|
+
package: array(ZoneRuleSchema).readonly()
|
|
23630
23787
|
});
|
|
23788
|
+
/**
|
|
23789
|
+
* Accessory device helpers — shared across drivers.
|
|
23790
|
+
*
|
|
23791
|
+
* Many vendor-specific drivers register accessory child devices on
|
|
23792
|
+
* top of a parent (Reolink: siren / floodlight / PIR / autotrack /
|
|
23793
|
+
* chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
|
|
23794
|
+
* driver picks the right `DeviceType` + `DeviceRole` explicitly when
|
|
23795
|
+
* spawning, builds a name derived from the parent, and produces a
|
|
23796
|
+
* stableId tied to the parent so boot-restore can reconstruct the
|
|
23797
|
+
* relationship.
|
|
23798
|
+
*
|
|
23799
|
+
* Centralised `(kind → DeviceType)` mapping was dropped on purpose:
|
|
23800
|
+
* drivers may reasonably disagree on the right type for an accessory
|
|
23801
|
+
* (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
|
|
23802
|
+
* read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
|
|
23803
|
+
* one canonical mapping was over-prescriptive and added a layer of
|
|
23804
|
+
* indirection without saving meaningful code at call sites — the
|
|
23805
|
+
* driver knows its own hardware best.
|
|
23806
|
+
*/
|
|
23807
|
+
/**
|
|
23808
|
+
* Subset of `DeviceRole` values that drivers register as child
|
|
23809
|
+
* accessories of a parent device. Sourced verbatim from `DeviceRole`
|
|
23810
|
+
* — `AccessoryKind` is the alias drivers use when building accessory
|
|
23811
|
+
* children, so the call site reads as
|
|
23812
|
+
* `accessoryStableId(parent, AccessoryKind.Siren)` rather than
|
|
23813
|
+
* `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
|
|
23814
|
+
* any role works, including non-accessory ones like Doorbell).
|
|
23815
|
+
*/
|
|
23816
|
+
var AccessoryKind = {
|
|
23817
|
+
Siren: DeviceRole.Siren,
|
|
23818
|
+
Floodlight: DeviceRole.Floodlight,
|
|
23819
|
+
Spotlight: DeviceRole.Spotlight,
|
|
23820
|
+
PirSensor: DeviceRole.PirSensor,
|
|
23821
|
+
Chime: DeviceRole.Chime,
|
|
23822
|
+
Autotrack: DeviceRole.Autotrack,
|
|
23823
|
+
Nightvision: DeviceRole.Nightvision,
|
|
23824
|
+
PrivacyMask: DeviceRole.PrivacyMask
|
|
23825
|
+
};
|
|
23826
|
+
AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
|
|
23827
|
+
DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
|
|
23828
|
+
new Set(Object.values(DeviceType));
|
|
23829
|
+
DeviceFeature.BatteryOperated;
|
|
23631
23830
|
Object.freeze({
|
|
23632
23831
|
"accessories.setChildHidden": {
|
|
23633
23832
|
capName: "accessories",
|
|
@@ -29254,7 +29453,7 @@ var AdminUIAddon = class extends BaseAddon {
|
|
|
29254
29453
|
capability: adminUiCapability,
|
|
29255
29454
|
provider: {
|
|
29256
29455
|
getStaticDir: async () => ({ staticDir: resolveAdminUiDistDir() }),
|
|
29257
|
-
getVersion: async () => ({ version: "1.2.
|
|
29456
|
+
getVersion: async () => ({ version: "1.2.40" })
|
|
29258
29457
|
}
|
|
29259
29458
|
}];
|
|
29260
29459
|
}
|