From 270df1a11876b581c5fb9476ebba6891b06f2eec Mon Sep 17 00:00:00 2001 From: csawatzky Date: Tue, 23 Jun 2026 16:40:09 -0600 Subject: [PATCH 1/4] fixed unit from V to mV for analog pressure --- package-lock.json | 2 +- src/pbHelpers/MeasurementDescriber.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 37bd044..4d9535e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11205,7 +11205,7 @@ }, "node_modules/protobuf-ts": { "version": "1.0.0", - "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#6d889de1a42adf590373ff932823e5e697634c6a", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#42f72fa7fb838b7771e6c847faaa192e93f42da7", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/src/pbHelpers/MeasurementDescriber.ts b/src/pbHelpers/MeasurementDescriber.ts index 947c545..ccc50a7 100644 --- a/src/pbHelpers/MeasurementDescriber.ts +++ b/src/pbHelpers/MeasurementDescriber.ts @@ -174,10 +174,9 @@ export class MeasurementDescriber { this.details.path = "power.inputVoltageTimes10"; this.details.decimals = 1; } - if (componentType === quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE) { + if (componentType === quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE || componentType === quack.ComponentType.COMPONENT_TYPE_ANALOG_PRESSURE) { this.details.unit = "mV"; this.details.graph = GraphType.MULTILINE; - this.details.unit = "mV"; this.details.decimals = 0 // this.details.nodeDetails = { // colours: ["white", "black", "red", "blue"], From 728d07a07e625300d98b15a0d045bdeb940c1778 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Tue, 23 Jun 2026 16:42:30 -0600 Subject: [PATCH 2/4] proto update --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 55cca88..7df4b5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11752,7 +11752,7 @@ }, "node_modules/protobuf-ts": { "version": "1.0.0", - "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#42f72fa7fb838b7771e6c847faaa192e93f42da7", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#8c5091affb9c1ef4e80c675072435b745b07e612", "dependencies": { "protobufjs": "^6.8.8" } From f73eb0fcbd80d6c37bf92aabea6a867072ad76fb Mon Sep 17 00:00:00 2001 From: csawatzky Date: Tue, 23 Jun 2026 16:44:42 -0600 Subject: [PATCH 3/4] proto update --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7df4b5b..afc0c3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", "openweathermap-ts": "^1.2.10", - "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#sendgrid_whitelabel", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", @@ -11752,7 +11752,7 @@ }, "node_modules/protobuf-ts": { "version": "1.0.0", - "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#8c5091affb9c1ef4e80c675072435b745b07e612", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#42f72fa7fb838b7771e6c847faaa192e93f42da7", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/package.json b/package.json index 1fb2505..3a76c43 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", "openweathermap-ts": "^1.2.10", - "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#sendgrid_whitelabel", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", From 895ed8e50ba269cf47cba35d6510dacdb6807626 Mon Sep 17 00:00:00 2001 From: Carter Date: Wed, 24 Jun 2026 13:13:14 -0600 Subject: [PATCH 4/4] opening interaction websocket to detect accepted pending changes --- src/interactions/InteractionsOverview.tsx | 45 ++++++++++++++++++++--- src/pages/Device.tsx | 43 +++++++++++++++++++++- 2 files changed, 81 insertions(+), 7 deletions(-) diff --git a/src/interactions/InteractionsOverview.tsx b/src/interactions/InteractionsOverview.tsx index 600c88a..db73675 100644 --- a/src/interactions/InteractionsOverview.tsx +++ b/src/interactions/InteractionsOverview.tsx @@ -4,6 +4,7 @@ import { CardActions, CardContent, CardHeader, + CircularProgress, darken, Grid2 as Grid, IconButton, @@ -14,7 +15,7 @@ import { Tooltip, Typography } from "@mui/material"; -import { Settings } from "@mui/icons-material"; +import { CheckCircleOutline, Settings } from "@mui/icons-material"; import { useInteractionsAPI, usePrevious, useSnackbar } from "hooks"; import { cloneDeep } from "lodash"; import { Component, Device, Interaction } from "models"; @@ -80,6 +81,7 @@ export default function InteractionsOverview(props: Props) { const [interactions, setInteractions] = useState(props.interactions); const [dirtyInteractions, setDirtyInteractions] = useState>(new Map()); const [mappedComponents, setMappedComponents] = useState>(new Map()); + const [acceptedInteractions, setAcceptedInteractions] = useState>(new Set()); const [renderToggle, setRenderToggle] = useState(false); const [selectedInteraction, setSelectedInteraction] = useState( undefined @@ -95,6 +97,20 @@ export default function InteractionsOverview(props: Props) { } if (props.interactions !== prevInteractions) { + if (prevInteractions) { + setAcceptedInteractions((prevAccepted) => { + const updatedAccepted = new Set(prevAccepted); + props.interactions.forEach((interaction: Interaction) => { + const previous = prevInteractions.find(prevInteraction => prevInteraction.key() === interaction.key()); + if (!interaction.status.synced) { + updatedAccepted.delete(interaction.key()); + } else if (previous && !previous.status.synced) { + updatedAccepted.add(interaction.key()); + } + }); + return updatedAccepted; + }); + } setInteractions(cloneDeep(props.interactions)); } }, [components, prevComponents, props.interactions, prevInteractions]); @@ -226,10 +242,16 @@ export default function InteractionsOverview(props: Props) { let key = interaction.key(); let source = mappedComponents.get(componentIDToString(interaction.settings.source)); let sink = mappedComponents.get(componentIDToString(interaction.settings.sink)); + const isPending = !interaction.status.synced && Boolean(interaction.status.lastUpdate); + const isAccepted = interaction.status.synced && acceptedInteractions.has(interaction.key()); let statusText = - !interaction.status.synced && interaction.status.lastUpdate - ? "Pending " + - moment(interaction.status.lastUpdate && interaction.status.lastUpdate).fromNow() + !interaction.status.synced + ? interaction.status.lastUpdate + ? "Pending " + + moment(interaction.status.lastUpdate && interaction.status.lastUpdate).fromNow() + : "" + : acceptedInteractions.has(interaction.key()) + ? "Pending change accepted" : ""; let schedule = pond.InteractionSchedule.create( interaction.settings.schedule !== null ? interaction.settings.schedule : undefined @@ -272,7 +294,20 @@ export default function InteractionsOverview(props: Props) { className={classes.header} titleTypographyProps={{ variant: "subtitle2" }} title={interactionResultText(interaction, sink)} - subheader={statusText} + subheader={ + statusText ? ( + + {isPending && } + {isAccepted && ( + + )} + + {statusText} + + + ) : ""} subheaderTypographyProps={{ variant: "caption" }} action={action} /> diff --git a/src/pages/Device.tsx b/src/pages/Device.tsx index 6c9535d..96b06b6 100644 --- a/src/pages/Device.tsx +++ b/src/pages/Device.tsx @@ -282,6 +282,45 @@ export default function DevicePage() { enabled: !!deviceID, }); + // --- Real-time interaction updates --- + // Streams interaction changes for this device, including status changes + // when the device accepts pending interaction updates. + useWebSocket<{ key: string; interaction: Interaction } | null>({ + path: `/v1/live/devices/${deviceID}/interactions`, + parse: (e) => { + try { + const raw = JSON.parse(e.data); + const interaction = Interaction.any(raw); + if (!interaction?.settings) { + console.warn("[ws] received interaction without settings:", raw); + return null; + } + return { key: interaction.key(), interaction }; + } catch (err) { + console.warn("[ws] failed to parse interaction:", err); + return null; + } + }, + onMessage: (data) => { + if (!data) return; + const { key, interaction } = data; + setInteractions((prev) => { + const index = prev.findIndex((existing) => existing.key() === key); + if (index < 0) { + return [...prev, interaction]; + } + const updated = [...prev]; + updated[index] = interaction; + return updated; + }); + }, + keys: liveContextKeys, + types: liveContextTypes, + as: liveAs, + token, + enabled: !!deviceID, + }); + const loadPortScan = () => { deviceAPI.listFoundComponents(parseInt(deviceID)) .then(resp => { @@ -395,7 +434,7 @@ export default function DevicePage() { interactions={filteredInteractions} permissions={permissions} deviceComponentPreferences={prefsMap.get(c.key())} - key={i} + key={c.key()} refreshCallback={(updatedComponent?: Component) => updatedComponent ? handleComponentChanged(updatedComponent) : loadDevice() } @@ -412,7 +451,7 @@ export default function DevicePage() { interactions={filteredInteractions} permissions={permissions} deviceComponentPreferences={prefsMap.get(c.key())} - key={i} + key={c.key()} refreshCallback={(updatedComponent?: Component) => updatedComponent ? handleComponentChanged(updatedComponent) : loadDevice() }