@camstack/ui-library 1.2.348 → 1.2.350
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/CHANGELOG.md +15 -0
- package/dist/{SceneMonitorEditor-BgqJYVdM.js → SceneMonitorEditor-A1U5eCrC.js} +1 -1
- package/dist/composites/device-controls/pet-fountain-panel.d.ts +12 -0
- package/dist/composites/device-controls/types.d.ts +1 -1
- package/dist/composites/export-entity-rows.d.ts +33 -0
- package/dist/composites/sectioned-page.d.ts +3 -0
- package/dist/generated/system-hooks.d.ts +20 -0
- package/dist/hooks/merge-detections.d.ts +16 -0
- package/dist/{icons-CWmwnulO.cjs → icons-BJpFQcLH.cjs} +1 -14
- package/dist/{icons-Bd6sbDsv.js → icons-Cf1xnXkh.js} +1 -14
- package/dist/index.cjs +879 -103
- package/dist/index.js +871 -106
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1706,6 +1706,19 @@ var EllipsisVertical = require_createLucideIcon.createLucideIcon("ellipsis-verti
|
|
|
1706
1706
|
* This source code is licensed under the ISC license.
|
|
1707
1707
|
* See the LICENSE file in the root directory of this source tree.
|
|
1708
1708
|
*/
|
|
1709
|
+
var Eraser = require_createLucideIcon.createLucideIcon("eraser", [["path", {
|
|
1710
|
+
d: "M21 21H8a2 2 0 0 1-1.42-.587l-3.994-3.999a2 2 0 0 1 0-2.828l10-10a2 2 0 0 1 2.829 0l5.999 6a2 2 0 0 1 0 2.828L12.834 21",
|
|
1711
|
+
key: "g5wo59"
|
|
1712
|
+
}], ["path", {
|
|
1713
|
+
d: "m5.082 11.09 8.828 8.828",
|
|
1714
|
+
key: "1wx5vj"
|
|
1715
|
+
}]]);
|
|
1716
|
+
/**
|
|
1717
|
+
* @license lucide-react v0.576.0 - ISC
|
|
1718
|
+
*
|
|
1719
|
+
* This source code is licensed under the ISC license.
|
|
1720
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
1721
|
+
*/
|
|
1709
1722
|
var ExternalLink = require_createLucideIcon.createLucideIcon("external-link", [
|
|
1710
1723
|
["path", {
|
|
1711
1724
|
d: "M15 3h6v6",
|
|
@@ -5400,6 +5413,11 @@ var DEVICE_TYPE_META = {
|
|
|
5400
5413
|
labelKey: "uiLibrary.deviceMeta.type.petFeeder",
|
|
5401
5414
|
labelFallback: "Pet feeder"
|
|
5402
5415
|
},
|
|
5416
|
+
[_camstack_types.DeviceType.PetFountain]: {
|
|
5417
|
+
icon: Droplets,
|
|
5418
|
+
labelKey: "uiLibrary.deviceMeta.type.petFountain",
|
|
5419
|
+
labelFallback: "Pet fountain"
|
|
5420
|
+
},
|
|
5403
5421
|
[_camstack_types.DeviceType.Dishwasher]: {
|
|
5404
5422
|
icon: Waves,
|
|
5405
5423
|
labelKey: "uiLibrary.deviceMeta.type.dishwasher",
|
|
@@ -5900,7 +5918,7 @@ function lucideExportName(key) {
|
|
|
5900
5918
|
function loadIconGlyphs() {
|
|
5901
5919
|
if (fullTable !== void 0) return Promise.resolve();
|
|
5902
5920
|
if (failure$1 !== void 0) return Promise.reject(failure$1);
|
|
5903
|
-
pending ??= Promise.resolve().then(() => require("./icons-
|
|
5921
|
+
pending ??= Promise.resolve().then(() => require("./icons-BJpFQcLH.cjs")).then((module) => {
|
|
5904
5922
|
fullTable = module;
|
|
5905
5923
|
publish();
|
|
5906
5924
|
}, (error) => {
|
|
@@ -11026,6 +11044,14 @@ var useDeviceExportListExposedDevices = trpc.deviceExport.listExposedDevices.use
|
|
|
11026
11044
|
var useDeviceExportExposeDevice = trpc.deviceExport.exposeDevice.useMutation;
|
|
11027
11045
|
/** Generated alias around `trpc.deviceExport.unexposeDevice.useMutation`. */
|
|
11028
11046
|
var useDeviceExportUnexposeDevice = trpc.deviceExport.unexposeDevice.useMutation;
|
|
11047
|
+
/** Generated alias around `trpc.deviceExport.listSupportedEntityKinds.useQuery`. */
|
|
11048
|
+
var useDeviceExportListSupportedEntityKinds = trpc.deviceExport.listSupportedEntityKinds.useQuery;
|
|
11049
|
+
/** Generated alias around `trpc.deviceExport.listExposedEntities.useQuery`. */
|
|
11050
|
+
var useDeviceExportListExposedEntities = trpc.deviceExport.listExposedEntities.useQuery;
|
|
11051
|
+
/** Generated alias around `trpc.deviceExport.exposeEntity.useMutation`. */
|
|
11052
|
+
var useDeviceExportExposeEntity = trpc.deviceExport.exposeEntity.useMutation;
|
|
11053
|
+
/** Generated alias around `trpc.deviceExport.unexposeEntity.useMutation`. */
|
|
11054
|
+
var useDeviceExportUnexposeEntity = trpc.deviceExport.unexposeEntity.useMutation;
|
|
11029
11055
|
/** Generated alias around `trpc.deviceExport.getDeviceSettingsContribution.useQuery`. */
|
|
11030
11056
|
var useDeviceExportGetDeviceSettingsContribution = trpc.deviceExport.getDeviceSettingsContribution.useQuery;
|
|
11031
11057
|
/** Generated alias around `trpc.deviceExport.getDeviceLiveContribution.useQuery`. */
|
|
@@ -11928,6 +11954,18 @@ var usePetFeederSetFeedSound = trpc.petFeeder.setFeedSound.useMutation;
|
|
|
11928
11954
|
var usePetFeederSetVolume = trpc.petFeeder.setVolume.useMutation;
|
|
11929
11955
|
/** Generated alias around `trpc.petFeeder.getStatus.useQuery`. */
|
|
11930
11956
|
var usePetFeederGetStatus = trpc.petFeeder.getStatus.useQuery;
|
|
11957
|
+
/** Generated alias around `trpc.petFountain.setPump.useMutation`. */
|
|
11958
|
+
var usePetFountainSetPump = trpc.petFountain.setPump.useMutation;
|
|
11959
|
+
/** Generated alias around `trpc.petFountain.setPumpInterval.useMutation`. */
|
|
11960
|
+
var usePetFountainSetPumpInterval = trpc.petFountain.setPumpInterval.useMutation;
|
|
11961
|
+
/** Generated alias around `trpc.petFountain.setPumpOnMinutes.useMutation`. */
|
|
11962
|
+
var usePetFountainSetPumpOnMinutes = trpc.petFountain.setPumpOnMinutes.useMutation;
|
|
11963
|
+
/** Generated alias around `trpc.petFountain.setPumpOffMinutes.useMutation`. */
|
|
11964
|
+
var usePetFountainSetPumpOffMinutes = trpc.petFountain.setPumpOffMinutes.useMutation;
|
|
11965
|
+
/** Generated alias around `trpc.petFountain.resetDailyConsumption.useMutation`. */
|
|
11966
|
+
var usePetFountainResetDailyConsumption = trpc.petFountain.resetDailyConsumption.useMutation;
|
|
11967
|
+
/** Generated alias around `trpc.petFountain.getStatus.useQuery`. */
|
|
11968
|
+
var usePetFountainGetStatus = trpc.petFountain.getStatus.useQuery;
|
|
11931
11969
|
/** Generated alias around `trpc.pipelineAnalytics.getActiveTracks.useQuery`. */
|
|
11932
11970
|
var usePipelineAnalyticsGetActiveTracks = trpc.pipelineAnalytics.getActiveTracks.useQuery;
|
|
11933
11971
|
/** Generated alias around `trpc.pipelineAnalytics.getTrack.useQuery`. */
|
|
@@ -48791,6 +48829,7 @@ var CONTROL_CAP_NAMES = [
|
|
|
48791
48829
|
"media-player",
|
|
48792
48830
|
"control",
|
|
48793
48831
|
"pet-feeder",
|
|
48832
|
+
"pet-fountain",
|
|
48794
48833
|
"dishwasher"
|
|
48795
48834
|
];
|
|
48796
48835
|
//#endregion
|
|
@@ -53081,6 +53120,10 @@ var HOLD_DELAY_MS = 350;
|
|
|
53081
53120
|
var HOLD_INTERVAL_MS = 70;
|
|
53082
53121
|
var HOLD_ACCEL_AFTER = 8;
|
|
53083
53122
|
var HOLD_FAST_STEP = 5;
|
|
53123
|
+
/** The slice stores the bowl as a mass in kilograms (D944); the card draws grams. */
|
|
53124
|
+
function bowlGrams(kilograms) {
|
|
53125
|
+
return kilograms === null ? null : Math.round(kilograms * 1e3);
|
|
53126
|
+
}
|
|
53084
53127
|
function clampPortion(grams) {
|
|
53085
53128
|
return Math.min(_camstack_types.PET_FEEDER_MANUAL_FEED_MAX, Math.max(_camstack_types.PET_FEEDER_MANUAL_FEED_MIN, grams));
|
|
53086
53129
|
}
|
|
@@ -53132,7 +53175,7 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
|
|
|
53132
53175
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UtensilsCrossed, {
|
|
53133
53176
|
className: "h-3.5 w-3.5",
|
|
53134
53177
|
"aria-hidden": true
|
|
53135
|
-
}), display.bowlFood ?? "—"]
|
|
53178
|
+
}), bowlGrams(display.bowlFood) ?? "—"]
|
|
53136
53179
|
}),
|
|
53137
53180
|
onMains && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
53138
53181
|
className: "inline-flex items-center gap-0.5 text-xs text-foreground-subtle",
|
|
@@ -53178,7 +53221,7 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
|
|
|
53178
53221
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
53179
53222
|
className: "flex items-center gap-4 rounded-lg border border-border/50 bg-surface-subtle/40 p-3",
|
|
53180
53223
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BowlGauge, {
|
|
53181
|
-
grams: display.bowlFood,
|
|
53224
|
+
grams: bowlGrams(display.bowlFood),
|
|
53182
53225
|
low: display.lowFood,
|
|
53183
53226
|
eating: display.eating
|
|
53184
53227
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -53188,7 +53231,7 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
|
|
|
53188
53231
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
53189
53232
|
className: "text-3xl font-bold leading-none tabular-nums",
|
|
53190
53233
|
style: { color: display.lowFood ? KIBBLE_LOW : KIBBLE },
|
|
53191
|
-
children: display.bowlFood ?? "—"
|
|
53234
|
+
children: bowlGrams(display.bowlFood) ?? "—"
|
|
53192
53235
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
53193
53236
|
className: "text-sm text-foreground-subtle",
|
|
53194
53237
|
children: t("uiLibrary.petFeeder.inBowl", "in bowl")
|
|
@@ -53625,6 +53668,367 @@ function PetFeederHeroCard({ trpc, deviceId, features, optimistic }) {
|
|
|
53625
53668
|
});
|
|
53626
53669
|
}
|
|
53627
53670
|
//#endregion
|
|
53671
|
+
//#region src/composites/device-controls/pet-fountain-panel.tsx
|
|
53672
|
+
/**
|
|
53673
|
+
* Pet-fountain controls for a `DeviceType.PetFountain` device.
|
|
53674
|
+
*
|
|
53675
|
+
* Reads the `pet-fountain` cap status slice (water level and volume, empty,
|
|
53676
|
+
* pump and its duty cycle, the water drunk) and renders:
|
|
53677
|
+
* - PetFountainInlineControl (device-list row): the tank level and a pump
|
|
53678
|
+
* on/off toggle.
|
|
53679
|
+
* - PetFountainHeroCard (device-detail hero): the tank gauge with the level
|
|
53680
|
+
* and the volume first, what the pet drank today / in 24 h / this hour /
|
|
53681
|
+
* last time, the pump switch, its duty-cycle switch and the run / rest
|
|
53682
|
+
* minutes. The filter is a consumable: the page's Consumables section
|
|
53683
|
+
* draws it, never this card (D915).
|
|
53684
|
+
*
|
|
53685
|
+
* Every reading may be unknown (`null`) and is drawn as a dash, never as
|
|
53686
|
+
* zero. Stored volumes are litres and durations seconds (D944); the card
|
|
53687
|
+
* shows millilitres and minutes. Actions are gated on `available`.
|
|
53688
|
+
*/
|
|
53689
|
+
var LITRES_TO_ML = 1e3;
|
|
53690
|
+
var SECONDS_PER_MINUTE = 60;
|
|
53691
|
+
var MAX_PUMP_MINUTES = 240;
|
|
53692
|
+
function millilitres(litres) {
|
|
53693
|
+
return litres === null ? "—" : `${Math.round(litres * LITRES_TO_ML)} mL`;
|
|
53694
|
+
}
|
|
53695
|
+
function minutesOf(seconds) {
|
|
53696
|
+
return seconds === null ? null : Math.round(seconds / SECONDS_PER_MINUTE);
|
|
53697
|
+
}
|
|
53698
|
+
function PetFountainPanel({ trpc, deviceId, layout, optimistic }) {
|
|
53699
|
+
const t = useUiText();
|
|
53700
|
+
const { cap, slice, available } = useDeviceCapability({
|
|
53701
|
+
trpc,
|
|
53702
|
+
deviceId,
|
|
53703
|
+
cap: (d) => d.petFountain,
|
|
53704
|
+
slice: (d) => d.state.petFountain
|
|
53705
|
+
});
|
|
53706
|
+
const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
|
|
53707
|
+
if (cap === void 0 || display === void 0) return null;
|
|
53708
|
+
const busy = !available;
|
|
53709
|
+
const fault = display.error !== null && display.error !== "no_error" ? display.error : null;
|
|
53710
|
+
const pumpOn = display.pumpRunning === true;
|
|
53711
|
+
const level = display.waterLevel;
|
|
53712
|
+
const setPump = (on) => {
|
|
53713
|
+
applyOptimistic({ pumpRunning: on });
|
|
53714
|
+
cap.setPump({ on });
|
|
53715
|
+
};
|
|
53716
|
+
if (layout === "compact") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
53717
|
+
className: "inline-flex items-center gap-2",
|
|
53718
|
+
children: [
|
|
53719
|
+
(fault !== null || display.empty === true) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, {
|
|
53720
|
+
className: "h-3.5 w-3.5 text-danger",
|
|
53721
|
+
"aria-label": fault ?? t("uiLibrary.petFountain.empty", "Tank empty")
|
|
53722
|
+
}),
|
|
53723
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
53724
|
+
className: cn("inline-flex items-center gap-1 text-xs font-medium tabular-nums", display.empty === true ? "text-danger" : "text-foreground"),
|
|
53725
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Droplet, {
|
|
53726
|
+
className: "h-3.5 w-3.5",
|
|
53727
|
+
"aria-hidden": true
|
|
53728
|
+
}), level === null ? "—" : `${Math.round(level)}%`]
|
|
53729
|
+
}),
|
|
53730
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
53731
|
+
onClick: (e) => e.stopPropagation(),
|
|
53732
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Switch, {
|
|
53733
|
+
checked: pumpOn,
|
|
53734
|
+
disabled: busy,
|
|
53735
|
+
"aria-label": t("uiLibrary.petFountain.pump", "Pump"),
|
|
53736
|
+
onCheckedChange: setPump
|
|
53737
|
+
})
|
|
53738
|
+
})
|
|
53739
|
+
]
|
|
53740
|
+
});
|
|
53741
|
+
const intervalOn = display.pumpIntervalEnabled === true;
|
|
53742
|
+
const onMinutes = minutesOf(display.pumpOnTime);
|
|
53743
|
+
const offMinutes = minutesOf(display.pumpOffTime);
|
|
53744
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
53745
|
+
className: "flex flex-col items-stretch gap-3 rounded-lg border border-border bg-surface p-4",
|
|
53746
|
+
children: [
|
|
53747
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
53748
|
+
className: "flex items-center gap-4 rounded-lg border border-border-subtle bg-background-elevated p-3",
|
|
53749
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TankGauge, {
|
|
53750
|
+
level,
|
|
53751
|
+
empty: display.empty === true
|
|
53752
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
53753
|
+
className: "flex min-w-0 flex-1 flex-col gap-1.5",
|
|
53754
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
53755
|
+
className: "flex items-baseline gap-2",
|
|
53756
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
53757
|
+
className: cn("text-3xl font-bold leading-none tabular-nums", display.empty === true ? "text-danger" : "text-info"),
|
|
53758
|
+
children: level === null ? "—" : `${Math.round(level)}%`
|
|
53759
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
53760
|
+
className: "text-sm tabular-nums text-foreground-muted",
|
|
53761
|
+
children: millilitres(display.waterVolume)
|
|
53762
|
+
})]
|
|
53763
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusLine, {
|
|
53764
|
+
empty: display.empty,
|
|
53765
|
+
pumpRunning: display.pumpRunning
|
|
53766
|
+
})]
|
|
53767
|
+
})]
|
|
53768
|
+
}),
|
|
53769
|
+
fault !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
53770
|
+
className: "flex items-start gap-2 rounded-lg border border-danger/40 bg-danger/10 px-3 py-2 text-xs font-medium text-danger",
|
|
53771
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, {
|
|
53772
|
+
className: "mt-px h-4 w-4 shrink-0",
|
|
53773
|
+
"aria-hidden": true
|
|
53774
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
53775
|
+
className: "min-w-0 break-words",
|
|
53776
|
+
children: fault
|
|
53777
|
+
})]
|
|
53778
|
+
}),
|
|
53779
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section$1, {
|
|
53780
|
+
label: t("uiLibrary.petFountain.consumption", "Water drunk"),
|
|
53781
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
53782
|
+
className: "grid grid-cols-2 gap-2 sm:grid-cols-4",
|
|
53783
|
+
children: [
|
|
53784
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
|
|
53785
|
+
label: t("uiLibrary.petFountain.today", "Today"),
|
|
53786
|
+
value: display.todayDrink
|
|
53787
|
+
}),
|
|
53788
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
|
|
53789
|
+
label: t("uiLibrary.petFountain.last24h", "Last 24 h"),
|
|
53790
|
+
value: display.last24hDrink
|
|
53791
|
+
}),
|
|
53792
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
|
|
53793
|
+
label: t("uiLibrary.petFountain.thisHour", "This hour"),
|
|
53794
|
+
value: display.currentHourDrink
|
|
53795
|
+
}),
|
|
53796
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stat, {
|
|
53797
|
+
label: t("uiLibrary.petFountain.lastDrink", "Last drink"),
|
|
53798
|
+
value: display.lastDrink
|
|
53799
|
+
})
|
|
53800
|
+
]
|
|
53801
|
+
})
|
|
53802
|
+
}),
|
|
53803
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section$1, {
|
|
53804
|
+
label: t("uiLibrary.petFountain.pumpSection", "Controls"),
|
|
53805
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
53806
|
+
className: "flex flex-col gap-2",
|
|
53807
|
+
children: [
|
|
53808
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
|
|
53809
|
+
icon: pumpOn ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Play, { className: "h-4 w-4" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, { className: "h-4 w-4" }),
|
|
53810
|
+
label: t("uiLibrary.petFountain.pump", "Pump"),
|
|
53811
|
+
checked: pumpOn,
|
|
53812
|
+
disabled: busy,
|
|
53813
|
+
onChange: setPump
|
|
53814
|
+
}),
|
|
53815
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SwitchRow, {
|
|
53816
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timer, { className: "h-4 w-4" }),
|
|
53817
|
+
label: t("uiLibrary.petFountain.cycle", "On / off cycle"),
|
|
53818
|
+
checked: intervalOn,
|
|
53819
|
+
disabled: busy,
|
|
53820
|
+
onChange: (on) => {
|
|
53821
|
+
applyOptimistic({ pumpIntervalEnabled: on });
|
|
53822
|
+
cap.setPumpInterval({ on });
|
|
53823
|
+
}
|
|
53824
|
+
}),
|
|
53825
|
+
intervalOn && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
53826
|
+
className: "flex flex-col gap-2",
|
|
53827
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(MinutesStepper, {
|
|
53828
|
+
label: t("uiLibrary.petFountain.runMinutes", "Runs for"),
|
|
53829
|
+
minutes: onMinutes,
|
|
53830
|
+
disabled: busy,
|
|
53831
|
+
onChange: (minutes) => {
|
|
53832
|
+
applyOptimistic({ pumpOnTime: minutes * SECONDS_PER_MINUTE });
|
|
53833
|
+
cap.setPumpOnMinutes({ minutes });
|
|
53834
|
+
}
|
|
53835
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MinutesStepper, {
|
|
53836
|
+
label: t("uiLibrary.petFountain.restMinutes", "Rests for"),
|
|
53837
|
+
minutes: offMinutes,
|
|
53838
|
+
disabled: busy,
|
|
53839
|
+
onChange: (minutes) => {
|
|
53840
|
+
applyOptimistic({ pumpOffTime: minutes * SECONDS_PER_MINUTE });
|
|
53841
|
+
cap.setPumpOffMinutes({ minutes });
|
|
53842
|
+
}
|
|
53843
|
+
})]
|
|
53844
|
+
})
|
|
53845
|
+
]
|
|
53846
|
+
})
|
|
53847
|
+
}),
|
|
53848
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
53849
|
+
variant: "outline",
|
|
53850
|
+
size: "sm",
|
|
53851
|
+
disabled: busy,
|
|
53852
|
+
onClick: () => void cap.resetDailyConsumption({}),
|
|
53853
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Eraser, {
|
|
53854
|
+
className: "h-4 w-4",
|
|
53855
|
+
"aria-hidden": true
|
|
53856
|
+
}), t("uiLibrary.petFountain.clearDaily", "Clear daily count")]
|
|
53857
|
+
})
|
|
53858
|
+
]
|
|
53859
|
+
});
|
|
53860
|
+
}
|
|
53861
|
+
/** A tank outline whose water rises with the level; danger-coloured when empty. */
|
|
53862
|
+
function TankGauge({ level, empty }) {
|
|
53863
|
+
const t = useUiText();
|
|
53864
|
+
const top = 58 - Math.min(1, Math.max(0, (level ?? 0) / 100)) * 40;
|
|
53865
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
53866
|
+
width: 72,
|
|
53867
|
+
height: 72,
|
|
53868
|
+
viewBox: "0 0 72 72",
|
|
53869
|
+
role: "img",
|
|
53870
|
+
"aria-label": level === null ? t("uiLibrary.petFountain.levelUnknown", "Water level unknown") : t("uiLibrary.petFountain.levelPercent", "{{percent}}% water", { percent: Math.round(level) }),
|
|
53871
|
+
className: "shrink-0",
|
|
53872
|
+
children: [
|
|
53873
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("clipPath", {
|
|
53874
|
+
id: "pfn-tank-clip",
|
|
53875
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
53876
|
+
x: "18",
|
|
53877
|
+
y: "14",
|
|
53878
|
+
width: "36",
|
|
53879
|
+
height: "46",
|
|
53880
|
+
rx: "6"
|
|
53881
|
+
})
|
|
53882
|
+
}) }),
|
|
53883
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", {
|
|
53884
|
+
clipPath: "url(#pfn-tank-clip)",
|
|
53885
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
53886
|
+
x: "18",
|
|
53887
|
+
y: top,
|
|
53888
|
+
width: "36",
|
|
53889
|
+
height: "46",
|
|
53890
|
+
className: empty ? "fill-danger" : "fill-info",
|
|
53891
|
+
opacity: .75
|
|
53892
|
+
})
|
|
53893
|
+
}),
|
|
53894
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
53895
|
+
x: "18",
|
|
53896
|
+
y: "14",
|
|
53897
|
+
width: "36",
|
|
53898
|
+
height: "46",
|
|
53899
|
+
rx: "6",
|
|
53900
|
+
fill: "none",
|
|
53901
|
+
strokeWidth: "2.5",
|
|
53902
|
+
className: "stroke-border"
|
|
53903
|
+
}),
|
|
53904
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
53905
|
+
x: "28",
|
|
53906
|
+
y: "8",
|
|
53907
|
+
width: "16",
|
|
53908
|
+
height: "6",
|
|
53909
|
+
rx: "2",
|
|
53910
|
+
className: "fill-border"
|
|
53911
|
+
})
|
|
53912
|
+
]
|
|
53913
|
+
});
|
|
53914
|
+
}
|
|
53915
|
+
function StatusLine({ empty, pumpRunning }) {
|
|
53916
|
+
const t = useUiText();
|
|
53917
|
+
if (empty === true) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
53918
|
+
className: "inline-flex items-center gap-1 text-xs font-medium text-danger",
|
|
53919
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Droplet, {
|
|
53920
|
+
className: "h-3.5 w-3.5",
|
|
53921
|
+
"aria-hidden": true
|
|
53922
|
+
}), t("uiLibrary.petFountain.empty", "Tank empty")]
|
|
53923
|
+
});
|
|
53924
|
+
if (pumpRunning === true) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
53925
|
+
className: "inline-flex items-center gap-1 text-xs font-medium text-info",
|
|
53926
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Droplets, {
|
|
53927
|
+
className: "h-3.5 w-3.5",
|
|
53928
|
+
"aria-hidden": true
|
|
53929
|
+
}), t("uiLibrary.petFountain.pumpRunning", "Pump running")]
|
|
53930
|
+
});
|
|
53931
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
53932
|
+
className: "inline-flex items-center gap-1 text-xs text-foreground-subtle",
|
|
53933
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pause, {
|
|
53934
|
+
className: "h-3.5 w-3.5",
|
|
53935
|
+
"aria-hidden": true
|
|
53936
|
+
}), pumpRunning === false ? t("uiLibrary.petFountain.pumpIdle", "Pump idle") : t("uiLibrary.petFountain.pumpUnknown", "Pump state unknown")]
|
|
53937
|
+
});
|
|
53938
|
+
}
|
|
53939
|
+
function Section$1({ label, children }) {
|
|
53940
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
53941
|
+
className: "flex flex-col gap-1",
|
|
53942
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
53943
|
+
className: "text-[10px] font-semibold uppercase tracking-wider text-foreground-subtle",
|
|
53944
|
+
children: label
|
|
53945
|
+
}), children]
|
|
53946
|
+
});
|
|
53947
|
+
}
|
|
53948
|
+
function Stat({ label, value }) {
|
|
53949
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
53950
|
+
className: "flex flex-col rounded-lg border border-border-subtle bg-background-elevated px-3 py-2",
|
|
53951
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
53952
|
+
className: "text-[11px] text-foreground-subtle",
|
|
53953
|
+
children: label
|
|
53954
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
53955
|
+
className: "text-sm font-semibold tabular-nums text-foreground",
|
|
53956
|
+
children: millilitres(value)
|
|
53957
|
+
})]
|
|
53958
|
+
});
|
|
53959
|
+
}
|
|
53960
|
+
function SwitchRow({ icon, label, checked, disabled, onChange }) {
|
|
53961
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
53962
|
+
className: "flex items-center justify-between gap-3 rounded-lg border border-border-subtle bg-background-elevated px-3 py-2",
|
|
53963
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
53964
|
+
className: "inline-flex items-center gap-2 text-sm text-foreground",
|
|
53965
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
53966
|
+
className: "text-foreground-subtle",
|
|
53967
|
+
children: icon
|
|
53968
|
+
}), label]
|
|
53969
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Switch, {
|
|
53970
|
+
checked,
|
|
53971
|
+
disabled,
|
|
53972
|
+
"aria-label": label,
|
|
53973
|
+
onCheckedChange: onChange
|
|
53974
|
+
})]
|
|
53975
|
+
});
|
|
53976
|
+
}
|
|
53977
|
+
function MinutesStepper({ label, minutes, disabled, onChange }) {
|
|
53978
|
+
const t = useUiText();
|
|
53979
|
+
const current = minutes ?? 0;
|
|
53980
|
+
const step = (delta) => onChange(Math.min(MAX_PUMP_MINUTES, Math.max(0, current + delta)));
|
|
53981
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
53982
|
+
className: "flex items-center justify-between gap-2 rounded-lg border border-border-subtle bg-background-elevated px-3 py-2",
|
|
53983
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
53984
|
+
className: "text-sm text-foreground",
|
|
53985
|
+
children: label
|
|
53986
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
53987
|
+
className: "flex items-center gap-2",
|
|
53988
|
+
children: [
|
|
53989
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
53990
|
+
variant: "outline",
|
|
53991
|
+
size: "sm",
|
|
53992
|
+
"aria-label": t("uiLibrary.petFountain.decrease", "Decrease {{label}}", { label }),
|
|
53993
|
+
disabled: disabled || minutes === null || current <= 0,
|
|
53994
|
+
onClick: () => step(-1),
|
|
53995
|
+
children: "−"
|
|
53996
|
+
}),
|
|
53997
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
53998
|
+
className: "w-14 text-center text-sm font-semibold tabular-nums text-foreground",
|
|
53999
|
+
children: minutes === null ? "—" : t("uiLibrary.petFountain.minutes", "{{count}} min", { count: current })
|
|
54000
|
+
}),
|
|
54001
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
54002
|
+
variant: "outline",
|
|
54003
|
+
size: "sm",
|
|
54004
|
+
"aria-label": t("uiLibrary.petFountain.increase", "Increase {{label}}", { label }),
|
|
54005
|
+
disabled: disabled || minutes === null || current >= MAX_PUMP_MINUTES,
|
|
54006
|
+
onClick: () => step(1),
|
|
54007
|
+
children: "+"
|
|
54008
|
+
})
|
|
54009
|
+
]
|
|
54010
|
+
})]
|
|
54011
|
+
});
|
|
54012
|
+
}
|
|
54013
|
+
function PetFountainInlineControl({ trpc, deviceId, features, optimistic }) {
|
|
54014
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PetFountainPanel, {
|
|
54015
|
+
trpc,
|
|
54016
|
+
deviceId,
|
|
54017
|
+
features,
|
|
54018
|
+
layout: "compact",
|
|
54019
|
+
optimistic
|
|
54020
|
+
});
|
|
54021
|
+
}
|
|
54022
|
+
function PetFountainHeroCard({ trpc, deviceId, features, optimistic }) {
|
|
54023
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PetFountainPanel, {
|
|
54024
|
+
trpc,
|
|
54025
|
+
deviceId,
|
|
54026
|
+
features,
|
|
54027
|
+
layout: "full",
|
|
54028
|
+
optimistic
|
|
54029
|
+
});
|
|
54030
|
+
}
|
|
54031
|
+
//#endregion
|
|
53628
54032
|
//#region src/composites/device-controls/dishwasher-control.tsx
|
|
53629
54033
|
/**
|
|
53630
54034
|
* Dishwasher controls for a `DeviceType.Dishwasher` device.
|
|
@@ -56747,6 +57151,10 @@ var CONTROL_KIND_COMPONENTS = {
|
|
|
56747
57151
|
InlineControl: PetFeederInlineControl,
|
|
56748
57152
|
HeroCard: PetFeederHeroCard
|
|
56749
57153
|
},
|
|
57154
|
+
"pet-fountain": {
|
|
57155
|
+
InlineControl: PetFountainInlineControl,
|
|
57156
|
+
HeroCard: PetFountainHeroCard
|
|
57157
|
+
},
|
|
56750
57158
|
dishwasher: {
|
|
56751
57159
|
InlineControl: DishwasherInlineControl,
|
|
56752
57160
|
HeroCard: DishwasherHeroCard
|
|
@@ -56828,6 +57236,7 @@ var CONTROL_FILLS = {
|
|
|
56828
57236
|
[_camstack_types.DeviceType.Container]: false,
|
|
56829
57237
|
[_camstack_types.DeviceType.Image]: false,
|
|
56830
57238
|
[_camstack_types.DeviceType.PetFeeder]: false,
|
|
57239
|
+
[_camstack_types.DeviceType.PetFountain]: false,
|
|
56831
57240
|
[_camstack_types.DeviceType.Dishwasher]: false,
|
|
56832
57241
|
[_camstack_types.DeviceType.Washer]: false,
|
|
56833
57242
|
[_camstack_types.DeviceType.Dryer]: false
|
|
@@ -60835,6 +61244,88 @@ function deselectShown(current, shown) {
|
|
|
60835
61244
|
return new Set([...current].filter((id) => !drop.has(id)));
|
|
60836
61245
|
}
|
|
60837
61246
|
//#endregion
|
|
61247
|
+
//#region src/composites/export-entity-rows.ts
|
|
61248
|
+
/**
|
|
61249
|
+
* The action and notification-target rows of the device export table (D968).
|
|
61250
|
+
*
|
|
61251
|
+
* An action and a notifier are list entries, never devices (D954): they are
|
|
61252
|
+
* merged into the SAME table as extra rows, built only from the kinds the
|
|
61253
|
+
* exporter answered. A kinds list that was not answered is UNKNOWN, not
|
|
61254
|
+
* "supports nothing" (D315) — it yields devices only and no filter entry, and
|
|
61255
|
+
* says nothing about what the exporter cannot do.
|
|
61256
|
+
*
|
|
61257
|
+
* Pure functions, so the rules are pinned without a DOM.
|
|
61258
|
+
*/
|
|
61259
|
+
/** The type-filter ids of the entity kinds, apart from every device type. */
|
|
61260
|
+
var ENTITY_FILTER_IDS = {
|
|
61261
|
+
action: "entity:action",
|
|
61262
|
+
notifier: "entity:notifier"
|
|
61263
|
+
};
|
|
61264
|
+
var KindsSchema = zod.z.array(_camstack_types.ExportEntityKindSchema);
|
|
61265
|
+
var NamedSchema = zod.z.array(zod.z.object({
|
|
61266
|
+
id: zod.z.string(),
|
|
61267
|
+
name: zod.z.string()
|
|
61268
|
+
}));
|
|
61269
|
+
var ExposedSchema = zod.z.array(_camstack_types.ExposedEntitySchema);
|
|
61270
|
+
/**
|
|
61271
|
+
* The non-device kinds the exporter answered, or null when it did not answer
|
|
61272
|
+
* (unknown — never an empty set).
|
|
61273
|
+
*/
|
|
61274
|
+
function supportedEntityKinds(data) {
|
|
61275
|
+
const parsed = KindsSchema.safeParse(data);
|
|
61276
|
+
if (!parsed.success) return null;
|
|
61277
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
61278
|
+
for (const kind of parsed.data) if (kind === "action" || kind === "notifier") kinds.add(kind);
|
|
61279
|
+
return kinds;
|
|
61280
|
+
}
|
|
61281
|
+
function buildEntityRows(input) {
|
|
61282
|
+
const { kinds } = input;
|
|
61283
|
+
if (kinds === null) return [];
|
|
61284
|
+
const exposed = ExposedSchema.safeParse(input.exposed);
|
|
61285
|
+
const exposedActions = /* @__PURE__ */ new Set();
|
|
61286
|
+
const exposedTargets = /* @__PURE__ */ new Set();
|
|
61287
|
+
if (exposed.success) {
|
|
61288
|
+
for (const row of exposed.data) if (row.entity.kind === "action") exposedActions.add(row.entity.actionId);
|
|
61289
|
+
else if (row.entity.kind === "notifier") exposedTargets.add(row.entity.targetId);
|
|
61290
|
+
}
|
|
61291
|
+
const rows = [];
|
|
61292
|
+
if (kinds.has("action")) {
|
|
61293
|
+
const actions = NamedSchema.safeParse(input.actions);
|
|
61294
|
+
for (const a of actions.success ? actions.data : []) rows.push({
|
|
61295
|
+
kind: "action",
|
|
61296
|
+
key: `action:${a.id}`,
|
|
61297
|
+
ref: {
|
|
61298
|
+
kind: "action",
|
|
61299
|
+
actionId: a.id
|
|
61300
|
+
},
|
|
61301
|
+
name: a.name,
|
|
61302
|
+
exposed: exposedActions.has(a.id)
|
|
61303
|
+
});
|
|
61304
|
+
}
|
|
61305
|
+
if (kinds.has("notifier")) {
|
|
61306
|
+
const targets = NamedSchema.safeParse(input.targets);
|
|
61307
|
+
for (const t of targets.success ? targets.data : []) rows.push({
|
|
61308
|
+
kind: "notifier",
|
|
61309
|
+
key: `notifier:${t.id}`,
|
|
61310
|
+
ref: {
|
|
61311
|
+
kind: "notifier",
|
|
61312
|
+
targetId: t.id
|
|
61313
|
+
},
|
|
61314
|
+
name: t.name,
|
|
61315
|
+
exposed: exposedTargets.has(t.id)
|
|
61316
|
+
});
|
|
61317
|
+
}
|
|
61318
|
+
return rows;
|
|
61319
|
+
}
|
|
61320
|
+
/** The type-filter ids to add beside the device types: one per SUPPORTED kind. */
|
|
61321
|
+
function entityFilterOptionIds(kinds) {
|
|
61322
|
+
if (kinds === null) return [];
|
|
61323
|
+
const ids = [];
|
|
61324
|
+
if (kinds.has("action")) ids.push(ENTITY_FILTER_IDS.action);
|
|
61325
|
+
if (kinds.has("notifier")) ids.push(ENTITY_FILTER_IDS.notifier);
|
|
61326
|
+
return ids;
|
|
61327
|
+
}
|
|
61328
|
+
//#endregion
|
|
60838
61329
|
//#region src/composites/device-export-panel.tsx
|
|
60839
61330
|
/**
|
|
60840
61331
|
* DeviceExportPanel — generic, addon-agnostic surface for the
|
|
@@ -61054,19 +61545,28 @@ function ExportSelectCell({ device, checked, onToggle }) {
|
|
|
61054
61545
|
* of these live in one table, and "Expose" alone names none of them.
|
|
61055
61546
|
*/
|
|
61056
61547
|
function ExportRowToggle({ addonId, device, exposed, busy, onToggle }) {
|
|
61548
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExposeToggleButton, {
|
|
61549
|
+
addonId,
|
|
61550
|
+
label: useDeviceLabel(device, "compact"),
|
|
61551
|
+
exposed,
|
|
61552
|
+
busy,
|
|
61553
|
+
onToggle: (action) => onToggle(device, action)
|
|
61554
|
+
});
|
|
61555
|
+
}
|
|
61556
|
+
/** The Expose / Exposed button every row kind (device, action, notifier) shares. */
|
|
61557
|
+
function ExposeToggleButton({ addonId, label, exposed, busy, onToggle }) {
|
|
61057
61558
|
const t = useUiText();
|
|
61058
|
-
const deviceLabel = useDeviceLabel(device, "compact");
|
|
61059
61559
|
const handleClick = (e) => {
|
|
61060
61560
|
e.stopPropagation();
|
|
61061
61561
|
if (busy) return;
|
|
61062
|
-
onToggle(
|
|
61562
|
+
onToggle(exposed ? "unexpose" : "expose");
|
|
61063
61563
|
};
|
|
61064
61564
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
61065
61565
|
size: "sm",
|
|
61066
61566
|
variant: exposed ? "ghost" : "primary",
|
|
61067
61567
|
disabled: busy,
|
|
61068
61568
|
onClick: handleClick,
|
|
61069
|
-
"aria-label": exposed ? t("uiLibrary.deviceExport.unexposeDevice", "Unexpose {{name}}", { name:
|
|
61569
|
+
"aria-label": exposed ? t("uiLibrary.deviceExport.unexposeDevice", "Unexpose {{name}}", { name: label }) : t("uiLibrary.deviceExport.exposeDevice", "Expose {{name}}", { name: label }),
|
|
61070
61570
|
title: exposed ? t("uiLibrary.deviceExport.stopTitle", "Stop exposing to \"{{addon}}\"", { addon: addonId }) : t("uiLibrary.deviceExport.exposeTitle", "Expose to \"{{addon}}\"", { addon: addonId }),
|
|
61071
61571
|
children: exposed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Unlink2, { className: "mr-1 h-3.5 w-3.5" }), t("uiLibrary.deviceExport.exposed", "Exposed")] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Share2, { className: "mr-1 h-3.5 w-3.5" }), t("uiLibrary.deviceExport.expose", "Expose")] })
|
|
61072
61572
|
});
|
|
@@ -61147,11 +61647,34 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
61147
61647
|
addonId
|
|
61148
61648
|
}, { retry: false });
|
|
61149
61649
|
const devicesQuery = useDeviceManagerListAll({ projection: "slim" }, { refetchInterval: FLEET_POLL_INTERVAL_MS });
|
|
61650
|
+
const entityKindsQuery = useDeviceExportListSupportedEntityKinds({
|
|
61651
|
+
nodeId: "hub",
|
|
61652
|
+
addonId
|
|
61653
|
+
}, { retry: false });
|
|
61654
|
+
const entityKinds = (0, react$1.useMemo)(() => supportedEntityKinds(entityKindsQuery.data), [entityKindsQuery.data]);
|
|
61655
|
+
const exposedEntitiesQuery = useDeviceExportListExposedEntities({
|
|
61656
|
+
nodeId: "hub",
|
|
61657
|
+
addonId
|
|
61658
|
+
}, {
|
|
61659
|
+
refetchInterval: STATUS_POLL_INTERVAL_MS,
|
|
61660
|
+
retry: false,
|
|
61661
|
+
enabled: entityKinds !== null
|
|
61662
|
+
});
|
|
61663
|
+
const actionsQuery = useActionsList({}, {
|
|
61664
|
+
refetchInterval: FLEET_POLL_INTERVAL_MS,
|
|
61665
|
+
enabled: entityKinds?.has("action") === true
|
|
61666
|
+
});
|
|
61667
|
+
const targetsQuery = useNotificationOutputListTargets({}, {
|
|
61668
|
+
refetchInterval: FLEET_POLL_INTERVAL_MS,
|
|
61669
|
+
enabled: entityKinds?.has("notifier") === true
|
|
61670
|
+
});
|
|
61150
61671
|
const invalidate = (0, react$1.useCallback)(() => {
|
|
61151
61672
|
queryClient.invalidateQueries({ queryKey: [["deviceExport"]] });
|
|
61152
61673
|
}, [queryClient]);
|
|
61153
61674
|
const exposeMutation = useDeviceExportExposeDevice({ onSuccess: invalidate });
|
|
61154
61675
|
const unexposeMutation = useDeviceExportUnexposeDevice({ onSuccess: invalidate });
|
|
61676
|
+
const exposeEntityMutation = useDeviceExportExposeEntity({ onSuccess: invalidate });
|
|
61677
|
+
const unexposeEntityMutation = useDeviceExportUnexposeEntity({ onSuccess: invalidate });
|
|
61155
61678
|
const status = (0, react$1.useMemo)(() => parseStatus(statusQuery.data), [statusQuery.data]);
|
|
61156
61679
|
const exposed = (0, react$1.useMemo)(() => {
|
|
61157
61680
|
const parsed = ExposedDeviceArraySchema.safeParse(exposedQuery.data);
|
|
@@ -61181,10 +61704,37 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
61181
61704
|
}
|
|
61182
61705
|
return rows;
|
|
61183
61706
|
}, [devicesQuery.data, supportedKinds]);
|
|
61707
|
+
const entityRows = (0, react$1.useMemo)(() => buildEntityRows({
|
|
61708
|
+
kinds: entityKinds,
|
|
61709
|
+
actions: actionsQuery.data,
|
|
61710
|
+
targets: targetsQuery.data,
|
|
61711
|
+
exposed: exposedEntitiesQuery.data
|
|
61712
|
+
}), [
|
|
61713
|
+
entityKinds,
|
|
61714
|
+
actionsQuery.data,
|
|
61715
|
+
targetsQuery.data,
|
|
61716
|
+
exposedEntitiesQuery.data
|
|
61717
|
+
]);
|
|
61718
|
+
const typeFilterOptions = (0, react$1.useMemo)(() => {
|
|
61719
|
+
const entityOptions = entityFilterOptionIds(entityKinds).map((id) => id === ENTITY_FILTER_IDS.action ? {
|
|
61720
|
+
id,
|
|
61721
|
+
label: t("uiLibrary.deviceExport.typeAction", "Action"),
|
|
61722
|
+
icon: Play
|
|
61723
|
+
} : {
|
|
61724
|
+
id,
|
|
61725
|
+
label: t("uiLibrary.deviceExport.typeNotifier", "Notifier"),
|
|
61726
|
+
icon: Bell
|
|
61727
|
+
});
|
|
61728
|
+
return [...supportedTypeOptions, ...entityOptions];
|
|
61729
|
+
}, [
|
|
61730
|
+
supportedTypeOptions,
|
|
61731
|
+
entityKinds,
|
|
61732
|
+
t
|
|
61733
|
+
]);
|
|
61184
61734
|
const linkState = status?.linkState ?? "unlinked";
|
|
61185
|
-
const loading = statusQuery.isLoading || exposedQuery.isLoading || kindsQuery.isLoading || devicesQuery.isLoading;
|
|
61735
|
+
const loading = statusQuery.isLoading || exposedQuery.isLoading || kindsQuery.isLoading || devicesQuery.isLoading || entityKindsQuery.isLoading;
|
|
61186
61736
|
const exposedCount = status?.exposedDeviceCount ?? exposedIds.size;
|
|
61187
|
-
const busy = exposeMutation.isPending || unexposeMutation.isPending;
|
|
61737
|
+
const busy = exposeMutation.isPending || unexposeMutation.isPending || exposeEntityMutation.isPending || unexposeEntityMutation.isPending;
|
|
61188
61738
|
const [selectedIds, setSelectedIds] = (0, react$1.useState)(() => /* @__PURE__ */ new Set());
|
|
61189
61739
|
const [outcome, setOutcome] = (0, react$1.useState)(null);
|
|
61190
61740
|
const [running, setRunning] = (0, react$1.useState)(false);
|
|
@@ -61251,10 +61801,71 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
61251
61801
|
labelFor,
|
|
61252
61802
|
running
|
|
61253
61803
|
]);
|
|
61804
|
+
/**
|
|
61805
|
+
* Expose / unexpose ONE action or notification target. Same contract as the
|
|
61806
|
+
* device runner: the result is recorded either way and a refusal is named on
|
|
61807
|
+
* screen, with the entity (it has no device id to tag).
|
|
61808
|
+
*/
|
|
61809
|
+
const runEntityExport = (0, react$1.useCallback)(async (action, row) => {
|
|
61810
|
+
if (running) return;
|
|
61811
|
+
setRunning(true);
|
|
61812
|
+
setOutcome(null);
|
|
61813
|
+
const mutation = action === "expose" ? exposeEntityMutation : unexposeEntityMutation;
|
|
61814
|
+
const succeeded = [];
|
|
61815
|
+
const failed = [];
|
|
61816
|
+
try {
|
|
61817
|
+
await mutation.mutateAsync({
|
|
61818
|
+
entity: row.ref,
|
|
61819
|
+
nodeId: "hub",
|
|
61820
|
+
addonId
|
|
61821
|
+
});
|
|
61822
|
+
succeeded.push(row.name);
|
|
61823
|
+
} catch (err) {
|
|
61824
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
61825
|
+
console.error(`[device-export] ${action} ${row.kind} failed`, {
|
|
61826
|
+
tags: row.ref.kind === "action" ? { actionId: row.ref.actionId } : { targetId: row.ref.targetId },
|
|
61827
|
+
addonId,
|
|
61828
|
+
error: message
|
|
61829
|
+
});
|
|
61830
|
+
failed.push({
|
|
61831
|
+
label: row.name,
|
|
61832
|
+
message
|
|
61833
|
+
});
|
|
61834
|
+
}
|
|
61835
|
+
setOutcome({
|
|
61836
|
+
action,
|
|
61837
|
+
total: 1,
|
|
61838
|
+
succeeded,
|
|
61839
|
+
failed
|
|
61840
|
+
});
|
|
61841
|
+
setRunning(false);
|
|
61842
|
+
}, [
|
|
61843
|
+
addonId,
|
|
61844
|
+
exposeEntityMutation,
|
|
61845
|
+
unexposeEntityMutation,
|
|
61846
|
+
running
|
|
61847
|
+
]);
|
|
61254
61848
|
const [bulkFilters, setBulkFilters] = (0, react$1.useState)(NO_EXPORT_FILTERS);
|
|
61255
61849
|
const roleOptions = (0, react$1.useMemo)(() => roleFilterOptions(candidateDevices), [candidateDevices]);
|
|
61256
61850
|
const providerOptions = (0, react$1.useMemo)(() => providerFilterOptions(candidateDevices), [candidateDevices]);
|
|
61257
61851
|
const shownDevices = (0, react$1.useMemo)(() => applyExportFilters(candidateDevices, bulkFilters), [candidateDevices, bulkFilters]);
|
|
61852
|
+
const shownRows = (0, react$1.useMemo)(() => {
|
|
61853
|
+
const deviceRows = shownDevices.map((device) => ({
|
|
61854
|
+
kind: "device",
|
|
61855
|
+
key: String(device.id),
|
|
61856
|
+
device
|
|
61857
|
+
}));
|
|
61858
|
+
const entities = bulkFilters.roles.length > 0 || bulkFilters.providers.length > 0 ? [] : entityRows.map((entity) => ({
|
|
61859
|
+
kind: "entity",
|
|
61860
|
+
key: entity.key,
|
|
61861
|
+
entity
|
|
61862
|
+
}));
|
|
61863
|
+
return [...deviceRows, ...entities];
|
|
61864
|
+
}, [
|
|
61865
|
+
shownDevices,
|
|
61866
|
+
entityRows,
|
|
61867
|
+
bulkFilters
|
|
61868
|
+
]);
|
|
61258
61869
|
const setRoleFilter = (0, react$1.useCallback)((roles) => setBulkFilters((f) => ({
|
|
61259
61870
|
...f,
|
|
61260
61871
|
roles
|
|
@@ -61273,27 +61884,35 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
61273
61884
|
const onRowToggle = (0, react$1.useCallback)((device, action) => {
|
|
61274
61885
|
runExport(action, [String(device.id)]);
|
|
61275
61886
|
}, [runExport]);
|
|
61276
|
-
const onRowClick = (0, react$1.useCallback)((
|
|
61277
|
-
|
|
61278
|
-
|
|
61279
|
-
const
|
|
61887
|
+
const onRowClick = (0, react$1.useCallback)((row) => {
|
|
61888
|
+
if (row.kind === "device") onOpenDevice?.(String(row.device.id));
|
|
61889
|
+
}, [onOpenDevice]);
|
|
61890
|
+
const getKey = (0, react$1.useCallback)((row) => row.key, []);
|
|
61891
|
+
const getSearchText = (0, react$1.useCallback)((row) => row.kind === "device" ? `${row.device.name} ${row.device.stableId}` : row.entity.name, []);
|
|
61892
|
+
const getFilterType = (0, react$1.useCallback)((row) => row.kind === "device" ? row.device.type : ENTITY_FILTER_IDS[row.entity.kind], []);
|
|
61280
61893
|
const columns = (0, react$1.useMemo)(() => [
|
|
61281
61894
|
{
|
|
61282
61895
|
key: "select",
|
|
61283
61896
|
header: "",
|
|
61284
61897
|
headerClassName: "w-8",
|
|
61285
|
-
cell: (
|
|
61286
|
-
device,
|
|
61287
|
-
checked: selectedIds.has(String(device.id)),
|
|
61288
|
-
onToggle: () => toggleSelection(String(device.id))
|
|
61289
|
-
})
|
|
61898
|
+
cell: (row) => row.kind === "device" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExportSelectCell, {
|
|
61899
|
+
device: row.device,
|
|
61900
|
+
checked: selectedIds.has(String(row.device.id)),
|
|
61901
|
+
onToggle: () => toggleSelection(String(row.device.id))
|
|
61902
|
+
}) : null
|
|
61290
61903
|
},
|
|
61291
61904
|
{
|
|
61292
61905
|
key: "type",
|
|
61293
61906
|
header: t("uiLibrary.deviceExport.colType", "Type"),
|
|
61294
61907
|
headerClassName: "w-32",
|
|
61295
|
-
cell: (
|
|
61296
|
-
const meta = deviceTypeMetaOf(device.type, t)
|
|
61908
|
+
cell: (row) => {
|
|
61909
|
+
const meta = row.kind === "device" ? deviceTypeMetaOf(row.device.type, t) : row.entity.kind === "action" ? {
|
|
61910
|
+
label: t("uiLibrary.deviceExport.typeAction", "Action"),
|
|
61911
|
+
icon: Play
|
|
61912
|
+
} : {
|
|
61913
|
+
label: t("uiLibrary.deviceExport.typeNotifier", "Notifier"),
|
|
61914
|
+
icon: Bell
|
|
61915
|
+
};
|
|
61297
61916
|
const TypeIcon = meta.icon;
|
|
61298
61917
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
61299
61918
|
className: "inline-flex items-center gap-1.5 text-[11px] text-foreground-subtle",
|
|
@@ -61304,10 +61923,13 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
61304
61923
|
{
|
|
61305
61924
|
key: "name",
|
|
61306
61925
|
header: t("uiLibrary.deviceExport.colName", "Name"),
|
|
61307
|
-
cell: (
|
|
61308
|
-
device,
|
|
61926
|
+
cell: (row) => row.kind === "device" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceName, {
|
|
61927
|
+
device: row.device,
|
|
61309
61928
|
form: "compact",
|
|
61310
61929
|
className: "text-xs font-medium text-foreground"
|
|
61930
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
61931
|
+
className: "text-xs font-medium text-foreground",
|
|
61932
|
+
children: row.entity.name
|
|
61311
61933
|
})
|
|
61312
61934
|
},
|
|
61313
61935
|
{
|
|
@@ -61315,12 +61937,18 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
61315
61937
|
header: t("uiLibrary.deviceExport.colExport", "Export"),
|
|
61316
61938
|
headerClassName: "w-32 text-right",
|
|
61317
61939
|
cellClassName: "text-right",
|
|
61318
|
-
cell: (
|
|
61940
|
+
cell: (row) => row.kind === "device" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExportRowToggle, {
|
|
61319
61941
|
addonId,
|
|
61320
|
-
device,
|
|
61321
|
-
exposed: exposedIds.has(String(device.id)),
|
|
61942
|
+
device: row.device,
|
|
61943
|
+
exposed: exposedIds.has(String(row.device.id)),
|
|
61322
61944
|
busy,
|
|
61323
61945
|
onToggle: onRowToggle
|
|
61946
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExposeToggleButton, {
|
|
61947
|
+
addonId,
|
|
61948
|
+
label: row.entity.name,
|
|
61949
|
+
exposed: row.entity.exposed,
|
|
61950
|
+
busy,
|
|
61951
|
+
onToggle: (action) => void runEntityExport(action, row.entity)
|
|
61324
61952
|
})
|
|
61325
61953
|
}
|
|
61326
61954
|
], [
|
|
@@ -61328,67 +61956,71 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
61328
61956
|
busy,
|
|
61329
61957
|
exposedIds,
|
|
61330
61958
|
onRowToggle,
|
|
61959
|
+
runEntityExport,
|
|
61331
61960
|
selectedIds,
|
|
61332
61961
|
toggleSelection,
|
|
61333
61962
|
t
|
|
61334
61963
|
]);
|
|
61335
|
-
const renderToolbar = (0, react$1.useCallback)(({ filtered }) =>
|
|
61336
|
-
|
|
61337
|
-
|
|
61338
|
-
|
|
61339
|
-
|
|
61340
|
-
|
|
61341
|
-
|
|
61342
|
-
|
|
61343
|
-
|
|
61344
|
-
|
|
61345
|
-
label: t("uiLibrary.deviceExport.filterProvider", "Provider"),
|
|
61346
|
-
options: providerOptions,
|
|
61347
|
-
selected: bulkFilters.providers,
|
|
61348
|
-
onChange: setProviderFilter
|
|
61349
|
-
}),
|
|
61350
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
61351
|
-
size: "sm",
|
|
61352
|
-
variant: "secondary",
|
|
61353
|
-
disabled: filtered.length === 0,
|
|
61354
|
-
onClick: () => setSelectedIds((current) => selectShown(current, filtered)),
|
|
61355
|
-
children: t("uiLibrary.deviceExport.selectShown", "Select {{count}} shown", { count: filtered.length })
|
|
61356
|
-
}),
|
|
61357
|
-
selectedIds.size > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
61358
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
61359
|
-
size: "sm",
|
|
61360
|
-
variant: "ghost",
|
|
61361
|
-
onClick: () => setSelectedIds((current) => deselectShown(current, filtered)),
|
|
61362
|
-
children: t("uiLibrary.deviceExport.deselectShown", "Deselect shown")
|
|
61363
|
-
}),
|
|
61364
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
61365
|
-
"data-testid": "export-selection-count",
|
|
61366
|
-
className: "text-[11px] text-foreground-subtle",
|
|
61367
|
-
children: t("uiLibrary.deviceExport.selected", "{{count}} selected", { count: selectedIds.size })
|
|
61964
|
+
const renderToolbar = (0, react$1.useCallback)(({ filtered: filteredRows }) => {
|
|
61965
|
+
const filtered = filteredRows.flatMap((row) => row.kind === "device" ? [row.device] : []);
|
|
61966
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
61967
|
+
className: "inline-flex flex-wrap items-center gap-1.5",
|
|
61968
|
+
children: [
|
|
61969
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(MultiSelectDropdown, {
|
|
61970
|
+
label: t("uiLibrary.deviceExport.filterRole", "Role"),
|
|
61971
|
+
options: roleOptions,
|
|
61972
|
+
selected: bulkFilters.roles,
|
|
61973
|
+
onChange: setRoleFilter
|
|
61368
61974
|
}),
|
|
61369
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
61370
|
-
|
|
61371
|
-
|
|
61372
|
-
|
|
61373
|
-
|
|
61374
|
-
children: t("uiLibrary.deviceExport.exposeN", "Expose {{count}}", { count: selectedIds.size })
|
|
61975
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(MultiSelectDropdown, {
|
|
61976
|
+
label: t("uiLibrary.deviceExport.filterProvider", "Provider"),
|
|
61977
|
+
options: providerOptions,
|
|
61978
|
+
selected: bulkFilters.providers,
|
|
61979
|
+
onChange: setProviderFilter
|
|
61375
61980
|
}),
|
|
61376
61981
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
61377
61982
|
size: "sm",
|
|
61378
61983
|
variant: "secondary",
|
|
61379
|
-
disabled:
|
|
61380
|
-
onClick:
|
|
61381
|
-
children: t("uiLibrary.deviceExport.
|
|
61984
|
+
disabled: filtered.length === 0,
|
|
61985
|
+
onClick: () => setSelectedIds((current) => selectShown(current, filtered)),
|
|
61986
|
+
children: t("uiLibrary.deviceExport.selectShown", "Select {{count}} shown", { count: filtered.length })
|
|
61382
61987
|
}),
|
|
61383
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
61384
|
-
|
|
61385
|
-
|
|
61386
|
-
|
|
61387
|
-
|
|
61388
|
-
|
|
61389
|
-
|
|
61390
|
-
|
|
61391
|
-
|
|
61988
|
+
selectedIds.size > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
61989
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
61990
|
+
size: "sm",
|
|
61991
|
+
variant: "ghost",
|
|
61992
|
+
onClick: () => setSelectedIds((current) => deselectShown(current, filtered)),
|
|
61993
|
+
children: t("uiLibrary.deviceExport.deselectShown", "Deselect shown")
|
|
61994
|
+
}),
|
|
61995
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
61996
|
+
"data-testid": "export-selection-count",
|
|
61997
|
+
className: "text-[11px] text-foreground-subtle",
|
|
61998
|
+
children: t("uiLibrary.deviceExport.selected", "{{count}} selected", { count: selectedIds.size })
|
|
61999
|
+
}),
|
|
62000
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
62001
|
+
size: "sm",
|
|
62002
|
+
variant: "primary",
|
|
62003
|
+
disabled: busy,
|
|
62004
|
+
onClick: exposeSelected,
|
|
62005
|
+
children: t("uiLibrary.deviceExport.exposeN", "Expose {{count}}", { count: selectedIds.size })
|
|
62006
|
+
}),
|
|
62007
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
62008
|
+
size: "sm",
|
|
62009
|
+
variant: "secondary",
|
|
62010
|
+
disabled: busy,
|
|
62011
|
+
onClick: unexposeSelected,
|
|
62012
|
+
children: t("uiLibrary.deviceExport.unexposeN", "Unexpose {{count}}", { count: selectedIds.size })
|
|
62013
|
+
}),
|
|
62014
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
62015
|
+
size: "sm",
|
|
62016
|
+
variant: "ghost",
|
|
62017
|
+
onClick: clearSelection,
|
|
62018
|
+
children: t("uiLibrary.deviceExport.clear", "Clear")
|
|
62019
|
+
})
|
|
62020
|
+
] })
|
|
62021
|
+
]
|
|
62022
|
+
});
|
|
62023
|
+
}, [
|
|
61392
62024
|
t,
|
|
61393
62025
|
roleOptions,
|
|
61394
62026
|
providerOptions,
|
|
@@ -61437,16 +62069,16 @@ function DeviceExportPanel({ addonId, onOpenDevice, showSetup = true }) {
|
|
|
61437
62069
|
}),
|
|
61438
62070
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
61439
62071
|
className: "p-2 sm:p-3",
|
|
61440
|
-
children: loading && candidateDevices.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
62072
|
+
children: loading && candidateDevices.length === 0 && entityRows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
61441
62073
|
className: "flex items-center gap-2 px-1 py-6 text-xs text-foreground-subtle",
|
|
61442
62074
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), t("uiLibrary.deviceExport.loading", "Loading exportable devices…")]
|
|
61443
|
-
}) : hasSupportedKinds && candidateDevices.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceList, {
|
|
62075
|
+
}) : hasSupportedKinds && candidateDevices.length > 0 || entityRows.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceList, {
|
|
61444
62076
|
mode: "generic",
|
|
61445
|
-
items:
|
|
62077
|
+
items: shownRows,
|
|
61446
62078
|
getKey,
|
|
61447
62079
|
getSearchText,
|
|
61448
62080
|
getFilterType,
|
|
61449
|
-
filterOptions:
|
|
62081
|
+
filterOptions: typeFilterOptions,
|
|
61450
62082
|
columns,
|
|
61451
62083
|
onRowClick: onOpenDevice ? onRowClick : void 0,
|
|
61452
62084
|
toolbar: renderToolbar,
|
|
@@ -63590,7 +64222,8 @@ function SectionedPage({ icon, title, subtitle, actions, sections, defaultSectio
|
|
|
63590
64222
|
tabKey: activeId,
|
|
63591
64223
|
fill,
|
|
63592
64224
|
children: fill ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
63593
|
-
className: "flex min-h-0 flex-1 flex-col",
|
|
64225
|
+
className: "flex min-h-0 flex-1 flex-col overflow-y-auto",
|
|
64226
|
+
"data-testid": "sectioned-page-body",
|
|
63594
64227
|
children: active?.content
|
|
63595
64228
|
}) : active?.content
|
|
63596
64229
|
});
|
|
@@ -71030,6 +71663,90 @@ function useDeviceCapSlice(trpc, deviceId, capName) {
|
|
|
71030
71663
|
return useDeviceStateSlice(handleRef.current);
|
|
71031
71664
|
}
|
|
71032
71665
|
//#endregion
|
|
71666
|
+
//#region src/hooks/merge-detections.ts
|
|
71667
|
+
/** Minimum IoU between an enriched root and a raw root to call them one subject
|
|
71668
|
+
* when no trackId pairs them. */
|
|
71669
|
+
var EXTRA_MATCH_IOU_THRESHOLD = .3;
|
|
71670
|
+
function iou(a, b) {
|
|
71671
|
+
const x1 = Math.max(a.x, b.x);
|
|
71672
|
+
const y1 = Math.max(a.y, b.y);
|
|
71673
|
+
const x2 = Math.min(a.x + a.width, b.x + b.width);
|
|
71674
|
+
const y2 = Math.min(a.y + a.height, b.y + b.height);
|
|
71675
|
+
const inter = Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
|
|
71676
|
+
const union = a.width * a.height + b.width * b.height - inter;
|
|
71677
|
+
return union > 0 ? inter / union : 0;
|
|
71678
|
+
}
|
|
71679
|
+
function isRoot(d) {
|
|
71680
|
+
return d.kind !== "detail";
|
|
71681
|
+
}
|
|
71682
|
+
function scaleBox(b, sx, sy) {
|
|
71683
|
+
return {
|
|
71684
|
+
x: b.x * sx,
|
|
71685
|
+
y: b.y * sy,
|
|
71686
|
+
width: b.width * sx,
|
|
71687
|
+
height: b.height * sy
|
|
71688
|
+
};
|
|
71689
|
+
}
|
|
71690
|
+
/** The raw root an enriched root describes: same trackId, else best IoU. */
|
|
71691
|
+
function matchRawRoot(enrichedRoot, rawRoots, sx, sy) {
|
|
71692
|
+
const trackId = enrichedRoot.track?.trackId;
|
|
71693
|
+
if (trackId !== void 0) {
|
|
71694
|
+
const byTrack = rawRoots.find((r) => r.track?.trackId === trackId);
|
|
71695
|
+
if (byTrack) return byTrack;
|
|
71696
|
+
}
|
|
71697
|
+
const scaled = scaleBox(enrichedRoot.bbox, sx, sy);
|
|
71698
|
+
let best = null;
|
|
71699
|
+
let bestIou = EXTRA_MATCH_IOU_THRESHOLD;
|
|
71700
|
+
for (const r of rawRoots) {
|
|
71701
|
+
const v = iou(scaled, r.bbox);
|
|
71702
|
+
if (v >= bestIou) {
|
|
71703
|
+
best = r;
|
|
71704
|
+
bestIou = v;
|
|
71705
|
+
}
|
|
71706
|
+
}
|
|
71707
|
+
return best;
|
|
71708
|
+
}
|
|
71709
|
+
function mergeDetections(raw, extras) {
|
|
71710
|
+
if (!extras) return raw;
|
|
71711
|
+
const frameWidth = raw?.frameWidth ?? extras.frameWidth;
|
|
71712
|
+
const frameHeight = raw?.frameHeight ?? extras.frameHeight;
|
|
71713
|
+
const sx = extras.frameWidth > 0 ? frameWidth / extras.frameWidth : 1;
|
|
71714
|
+
const sy = extras.frameHeight > 0 ? frameHeight / extras.frameHeight : 1;
|
|
71715
|
+
const rawDets = raw?.detections ?? [];
|
|
71716
|
+
const rawRoots = rawDets.filter(isRoot);
|
|
71717
|
+
const rawRootFor = /* @__PURE__ */ new Map();
|
|
71718
|
+
const extraLabels = /* @__PURE__ */ new Map();
|
|
71719
|
+
for (const root of extras.detections.filter(isRoot)) {
|
|
71720
|
+
const match = matchRawRoot(root, rawRoots, sx, sy);
|
|
71721
|
+
rawRootFor.set(root.id, match);
|
|
71722
|
+
if (match && root.labels.length > 0) extraLabels.set(match, [...extraLabels.get(match) ?? [], ...root.labels]);
|
|
71723
|
+
}
|
|
71724
|
+
const out = rawDets.map((d) => {
|
|
71725
|
+
const add = extraLabels.get(d);
|
|
71726
|
+
if (!add) return d;
|
|
71727
|
+
const have = new Set(d.labels.map((l) => l.label));
|
|
71728
|
+
const fresh = add.filter((l) => !have.has(l.label));
|
|
71729
|
+
return fresh.length > 0 ? {
|
|
71730
|
+
...d,
|
|
71731
|
+
labels: [...d.labels, ...fresh]
|
|
71732
|
+
} : d;
|
|
71733
|
+
});
|
|
71734
|
+
for (const d of extras.detections) {
|
|
71735
|
+
if (isRoot(d)) continue;
|
|
71736
|
+
const parent = d.parentId !== void 0 ? rawRootFor.get(d.parentId) : void 0;
|
|
71737
|
+
out.push({
|
|
71738
|
+
...d,
|
|
71739
|
+
bbox: scaleBox(d.bbox, sx, sy),
|
|
71740
|
+
...parent ? { parentId: parent.id } : {}
|
|
71741
|
+
});
|
|
71742
|
+
}
|
|
71743
|
+
return {
|
|
71744
|
+
detections: out,
|
|
71745
|
+
frameWidth,
|
|
71746
|
+
frameHeight
|
|
71747
|
+
};
|
|
71748
|
+
}
|
|
71749
|
+
//#endregion
|
|
71033
71750
|
//#region src/hooks/use-device-detections.ts
|
|
71034
71751
|
/**
|
|
71035
71752
|
* useDeviceDetections — subscribes to motion, raw motion, and detection events for a device.
|
|
@@ -71038,12 +71755,18 @@ function useDeviceCapSlice(trpc, deviceId, capName) {
|
|
|
71038
71755
|
* admin-ui (BackendClient.trpc) and addon pages (AddonPageProps.trpc).
|
|
71039
71756
|
*/
|
|
71040
71757
|
var MOTION_FADE_MS = 500;
|
|
71041
|
-
/**
|
|
71042
|
-
*
|
|
71043
|
-
* frames are ignored (post-analysis is the richer source; raw remains the
|
|
71044
|
-
* fallback when post-analysis is absent). Keeping the two equal means raw
|
|
71045
|
-
* resumes exactly when the enriched display fades — no blank gap. */
|
|
71758
|
+
/** Fade of each detection source (raw roots, enriched extras), counted from the
|
|
71759
|
+
* frame's capture time so a late frame is not kept for the full window. */
|
|
71046
71760
|
var DETECTION_FADE_MS = 800;
|
|
71761
|
+
/** A frame older than this at arrival is replayed pre-roll and never drawn.
|
|
71762
|
+
* Same meaning as the server's OVERLAY_MAX_FRAME_AGE_MS. */
|
|
71763
|
+
var OVERLAY_MAX_FRAME_AGE_MS = 1e3;
|
|
71764
|
+
/**
|
|
71765
|
+
* The most of a frame's age (arrival minus its capture timestamp) the fade
|
|
71766
|
+
* deducts. Bounded so a skewed server/browser clock can never make boxes vanish
|
|
71767
|
+
* on arrival.
|
|
71768
|
+
*/
|
|
71769
|
+
var MAX_FRAME_AGE_FADE_DEDUCTION_MS = DETECTION_FADE_MS / 2;
|
|
71047
71770
|
/** Helper: subscribe to live.onEvent without optional chaining on subscribe() */
|
|
71048
71771
|
function safeSubscribe(trpc, category, onData) {
|
|
71049
71772
|
try {
|
|
@@ -71066,8 +71789,26 @@ function useDeviceDetections(trpc, deviceId) {
|
|
|
71066
71789
|
const [phase, setPhase] = (0, react$1.useState)("watching");
|
|
71067
71790
|
const motionTimer = (0, react$1.useRef)(void 0);
|
|
71068
71791
|
const motionRawTimer = (0, react$1.useRef)(void 0);
|
|
71069
|
-
const
|
|
71070
|
-
const
|
|
71792
|
+
const rawTimer = (0, react$1.useRef)(void 0);
|
|
71793
|
+
const extrasTimer = (0, react$1.useRef)(void 0);
|
|
71794
|
+
/** Live part of each source: raw roots and enriched extras fade independently. */
|
|
71795
|
+
const rawPart = (0, react$1.useRef)(null);
|
|
71796
|
+
const extrasPart = (0, react$1.useRef)(null);
|
|
71797
|
+
/** Capture timestamp of the newest frame shown, per source; older arrivals are dropped. */
|
|
71798
|
+
const lastRawTs = (0, react$1.useRef)(0);
|
|
71799
|
+
const lastExtrasTs = (0, react$1.useRef)(0);
|
|
71800
|
+
const publish = (0, react$1.useCallback)(() => {
|
|
71801
|
+
setDetection(mergeDetections(rawPart.current, extrasPart.current));
|
|
71802
|
+
}, []);
|
|
71803
|
+
/** Admit a frame: null = rejected (stale or out of order), else its fade in ms. */
|
|
71804
|
+
const admit = (0, react$1.useCallback)((frameTs, lastTs) => {
|
|
71805
|
+
if (frameTs !== null) {
|
|
71806
|
+
if (Date.now() - frameTs > OVERLAY_MAX_FRAME_AGE_MS) return null;
|
|
71807
|
+
if (frameTs < lastTs.current) return null;
|
|
71808
|
+
lastTs.current = frameTs;
|
|
71809
|
+
}
|
|
71810
|
+
return DETECTION_FADE_MS - (frameTs === null ? 0 : Math.min(Math.max(Date.now() - frameTs, 0), MAX_FRAME_AGE_FADE_DEDUCTION_MS));
|
|
71811
|
+
}, []);
|
|
71071
71812
|
(0, react$1.useEffect)(() => {
|
|
71072
71813
|
if (deviceId === null) return;
|
|
71073
71814
|
const sub = safeSubscribe(trpc, "detection.motion-analysis", (event) => {
|
|
@@ -71116,48 +71857,72 @@ function useDeviceDetections(trpc, deviceId) {
|
|
|
71116
71857
|
};
|
|
71117
71858
|
}, [trpc, deviceId]);
|
|
71118
71859
|
(0, react$1.useEffect)(() => {
|
|
71119
|
-
|
|
71860
|
+
extrasPart.current = null;
|
|
71861
|
+
rawPart.current = null;
|
|
71862
|
+
lastExtrasTs.current = 0;
|
|
71863
|
+
lastRawTs.current = 0;
|
|
71864
|
+
setDetection(null);
|
|
71120
71865
|
if (deviceId === null) return;
|
|
71121
71866
|
const sub = safeSubscribe(trpc, "pipeline-analytics.frame-tracked", (event) => {
|
|
71122
71867
|
const data = event?.data;
|
|
71123
71868
|
if (!data || data.deviceId !== deviceId) return;
|
|
71124
71869
|
if (!data.detections?.length) return;
|
|
71125
71870
|
if (typeof data.detections[0]?.kind !== "string") return;
|
|
71126
|
-
|
|
71127
|
-
|
|
71871
|
+
const fadeInMs = admit(typeof data.timestamp === "number" ? data.timestamp : null, lastExtrasTs);
|
|
71872
|
+
if (fadeInMs === null) return;
|
|
71873
|
+
extrasPart.current = {
|
|
71128
71874
|
detections: data.detections,
|
|
71129
71875
|
frameWidth: data.frameWidth,
|
|
71130
71876
|
frameHeight: data.frameHeight
|
|
71131
|
-
}
|
|
71132
|
-
|
|
71133
|
-
|
|
71877
|
+
};
|
|
71878
|
+
publish();
|
|
71879
|
+
if (extrasTimer.current) clearTimeout(extrasTimer.current);
|
|
71880
|
+
extrasTimer.current = setTimeout(() => {
|
|
71881
|
+
extrasPart.current = null;
|
|
71882
|
+
publish();
|
|
71883
|
+
}, fadeInMs);
|
|
71134
71884
|
});
|
|
71135
71885
|
return () => {
|
|
71136
71886
|
sub?.unsubscribe();
|
|
71137
|
-
if (
|
|
71887
|
+
if (extrasTimer.current) clearTimeout(extrasTimer.current);
|
|
71138
71888
|
};
|
|
71139
|
-
}, [
|
|
71889
|
+
}, [
|
|
71890
|
+
trpc,
|
|
71891
|
+
deviceId,
|
|
71892
|
+
admit,
|
|
71893
|
+
publish
|
|
71894
|
+
]);
|
|
71140
71895
|
(0, react$1.useEffect)(() => {
|
|
71141
71896
|
if (deviceId === null) return;
|
|
71142
71897
|
const sub = safeSubscribe(trpc, "detection.result", (event) => {
|
|
71143
71898
|
const e = event;
|
|
71144
71899
|
if (!matchesDevice(e?.source, deviceId)) return;
|
|
71145
|
-
if (Date.now() - lastEnrichedAt.current < DETECTION_FADE_MS) return;
|
|
71146
71900
|
const frame = e.data?.frame;
|
|
71147
71901
|
if (!frame?.detections?.length) return;
|
|
71148
|
-
|
|
71902
|
+
const fadeInMs = admit(typeof frame.timestamp === "number" ? frame.timestamp : null, lastRawTs);
|
|
71903
|
+
if (fadeInMs === null) return;
|
|
71904
|
+
rawPart.current = {
|
|
71149
71905
|
detections: frame.detections,
|
|
71150
71906
|
frameWidth: frame.width,
|
|
71151
71907
|
frameHeight: frame.height
|
|
71152
|
-
}
|
|
71153
|
-
|
|
71154
|
-
|
|
71908
|
+
};
|
|
71909
|
+
publish();
|
|
71910
|
+
if (rawTimer.current) clearTimeout(rawTimer.current);
|
|
71911
|
+
rawTimer.current = setTimeout(() => {
|
|
71912
|
+
rawPart.current = null;
|
|
71913
|
+
publish();
|
|
71914
|
+
}, fadeInMs);
|
|
71155
71915
|
});
|
|
71156
71916
|
return () => {
|
|
71157
71917
|
sub?.unsubscribe();
|
|
71158
|
-
if (
|
|
71918
|
+
if (rawTimer.current) clearTimeout(rawTimer.current);
|
|
71159
71919
|
};
|
|
71160
|
-
}, [
|
|
71920
|
+
}, [
|
|
71921
|
+
trpc,
|
|
71922
|
+
deviceId,
|
|
71923
|
+
admit,
|
|
71924
|
+
publish
|
|
71925
|
+
]);
|
|
71161
71926
|
(0, react$1.useEffect)(() => {
|
|
71162
71927
|
if (deviceId === null) return;
|
|
71163
71928
|
const sub = safeSubscribe(trpc, "detection.phase-transition", (event) => {
|
|
@@ -71826,6 +72591,7 @@ exports.EmptyState = EmptyState;
|
|
|
71826
72591
|
exports.EndpointResultsCard = EndpointResultsCard;
|
|
71827
72592
|
exports.EndpointTestCard = EndpointTestCard;
|
|
71828
72593
|
exports.EntryJsonSchemaForm = EntryJsonSchemaForm;
|
|
72594
|
+
exports.Eraser = Eraser;
|
|
71829
72595
|
exports.ErrorBox = ErrorBox;
|
|
71830
72596
|
exports.EventKindGlyph = EventKindGlyph;
|
|
71831
72597
|
exports.EventStream = EventStream;
|
|
@@ -72842,12 +73608,16 @@ exports.useDeviceDiscoveryRefreshDiscovery = useDeviceDiscoveryRefreshDiscovery;
|
|
|
72842
73608
|
exports.useDeviceDiscoveryReleaseDevice = useDeviceDiscoveryReleaseDevice;
|
|
72843
73609
|
exports.useDeviceExportApplyDeviceSettingsPatch = useDeviceExportApplyDeviceSettingsPatch;
|
|
72844
73610
|
exports.useDeviceExportExposeDevice = useDeviceExportExposeDevice;
|
|
73611
|
+
exports.useDeviceExportExposeEntity = useDeviceExportExposeEntity;
|
|
72845
73612
|
exports.useDeviceExportGetDeviceLiveContribution = useDeviceExportGetDeviceLiveContribution;
|
|
72846
73613
|
exports.useDeviceExportGetDeviceSettingsContribution = useDeviceExportGetDeviceSettingsContribution;
|
|
72847
73614
|
exports.useDeviceExportGetStatus = useDeviceExportGetStatus;
|
|
72848
73615
|
exports.useDeviceExportListExposedDevices = useDeviceExportListExposedDevices;
|
|
73616
|
+
exports.useDeviceExportListExposedEntities = useDeviceExportListExposedEntities;
|
|
72849
73617
|
exports.useDeviceExportListSupportedDeviceKinds = useDeviceExportListSupportedDeviceKinds;
|
|
73618
|
+
exports.useDeviceExportListSupportedEntityKinds = useDeviceExportListSupportedEntityKinds;
|
|
72850
73619
|
exports.useDeviceExportUnexposeDevice = useDeviceExportUnexposeDevice;
|
|
73620
|
+
exports.useDeviceExportUnexposeEntity = useDeviceExportUnexposeEntity;
|
|
72851
73621
|
exports.useDeviceExtensionListCandidates = useDeviceExtensionListCandidates;
|
|
72852
73622
|
exports.useDeviceExtensionListSlots = useDeviceExtensionListSlots;
|
|
72853
73623
|
exports.useDeviceExtensionListSources = useDeviceExtensionListSources;
|
|
@@ -73338,6 +74108,12 @@ exports.usePetFeederSetChildLock = usePetFeederSetChildLock;
|
|
|
73338
74108
|
exports.usePetFeederSetFeedSound = usePetFeederSetFeedSound;
|
|
73339
74109
|
exports.usePetFeederSetIndicatorLight = usePetFeederSetIndicatorLight;
|
|
73340
74110
|
exports.usePetFeederSetVolume = usePetFeederSetVolume;
|
|
74111
|
+
exports.usePetFountainGetStatus = usePetFountainGetStatus;
|
|
74112
|
+
exports.usePetFountainResetDailyConsumption = usePetFountainResetDailyConsumption;
|
|
74113
|
+
exports.usePetFountainSetPump = usePetFountainSetPump;
|
|
74114
|
+
exports.usePetFountainSetPumpInterval = usePetFountainSetPumpInterval;
|
|
74115
|
+
exports.usePetFountainSetPumpOffMinutes = usePetFountainSetPumpOffMinutes;
|
|
74116
|
+
exports.usePetFountainSetPumpOnMinutes = usePetFountainSetPumpOnMinutes;
|
|
73341
74117
|
exports.usePickerSiteZones = usePickerSiteZones;
|
|
73342
74118
|
exports.usePipelineAnalyticsAddRetrainFrameFromRecording = usePipelineAnalyticsAddRetrainFrameFromRecording;
|
|
73343
74119
|
exports.usePipelineAnalyticsAddRetrainFramesToCollection = usePipelineAnalyticsAddRetrainFramesToCollection;
|