From 6d1c9431f23ac9be75ca8ca5056ac46e67a9e476 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 18 Jun 2026 10:54:14 -0600 Subject: [PATCH 01/25] setting up the framework for rebuilding the bin modes --- src/bin/binModes/BinModeController.tsx | 54 ++++++++++++++++++++++++++ src/bin/binModes/CooldownMode.tsx | 9 +++++ src/bin/binModes/DryingMode.tsx | 9 +++++ src/bin/binModes/HydratingMode.tsx | 9 +++++ src/bin/binModes/StorageMode.tsx | 9 +++++ 5 files changed, 90 insertions(+) create mode 100644 src/bin/binModes/BinModeController.tsx create mode 100644 src/bin/binModes/CooldownMode.tsx create mode 100644 src/bin/binModes/DryingMode.tsx create mode 100644 src/bin/binModes/HydratingMode.tsx create mode 100644 src/bin/binModes/StorageMode.tsx diff --git a/src/bin/binModes/BinModeController.tsx b/src/bin/binModes/BinModeController.tsx new file mode 100644 index 0000000..90aa3d1 --- /dev/null +++ b/src/bin/binModes/BinModeController.tsx @@ -0,0 +1,54 @@ +import { Box, Button, DialogActions, DialogContent, DialogTitle } from "@mui/material"; +import ResponsiveDialog from "common/ResponsiveDialog"; +import { Bin } from "models"; +import { pond } from "protobuf-ts/pond"; +import React from "react"; +import StorageMode from "./StorageMode"; +import DryingMode from "./DryingMode"; +import HydratingMode from "./HydratingMode"; +import CooldownMode from "./CooldownMode"; + +interface Props { + newMode: pond.BinMode //used to control which dialog to open + open: boolean + onClose: () => void + bin: Bin +} + +export default function BinModeControl(props: Props) { + const {open, onClose, newMode} = props + + //the mode controller could handle the actual api call for adding the interactions etc but the dialogs would create them and pass them up + + //steps involved for devices when changing the bin mode: + //remove conflicting interactions + //add new interactions + //updating the controllers + + const renderContent = () => { + switch(newMode){ + case pond.BinMode.BIN_MODE_DRYING: + return + case pond.BinMode.BIN_MODE_HYDRATING: + return + case pond.BinMode.BIN_MODE_COOLDOWN: + return + default: + return + } + } + + return ( + + Change Bin Mode + + {renderContent()} + + + + + + ) +} \ No newline at end of file diff --git a/src/bin/binModes/CooldownMode.tsx b/src/bin/binModes/CooldownMode.tsx new file mode 100644 index 0000000..173cf15 --- /dev/null +++ b/src/bin/binModes/CooldownMode.tsx @@ -0,0 +1,9 @@ +import { Box } from "@mui/material"; + +export default function CooldownMode(){ + return ( + + + + ) +} \ No newline at end of file diff --git a/src/bin/binModes/DryingMode.tsx b/src/bin/binModes/DryingMode.tsx new file mode 100644 index 0000000..7dd57a5 --- /dev/null +++ b/src/bin/binModes/DryingMode.tsx @@ -0,0 +1,9 @@ +import { Box } from "@mui/material"; + +export default function DryingMode(){ + return ( + + + + ) +} \ No newline at end of file diff --git a/src/bin/binModes/HydratingMode.tsx b/src/bin/binModes/HydratingMode.tsx new file mode 100644 index 0000000..f87ca98 --- /dev/null +++ b/src/bin/binModes/HydratingMode.tsx @@ -0,0 +1,9 @@ +import { Box } from "@mui/material"; + +export default function HydratingMode(){ + return ( + + + + ) +} \ No newline at end of file diff --git a/src/bin/binModes/StorageMode.tsx b/src/bin/binModes/StorageMode.tsx new file mode 100644 index 0000000..67f82dc --- /dev/null +++ b/src/bin/binModes/StorageMode.tsx @@ -0,0 +1,9 @@ +import { Box } from "@mui/material"; + +export default function StorageMode(){ + return ( + + + + ) +} \ No newline at end of file From 19eb2a7faf589c8bddba65f7a553e6670a768c7f Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 29 Jun 2026 15:30:25 -0600 Subject: [PATCH 02/25] 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 19352f1..b3671a7 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#staging", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#bin_modes_v2", "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#100126cc7f3cdf18f68af6372e5db4113db012b7", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#aa9a59d8cc0b7ba1ee27d9bdcdade6136a00d1fb", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/package.json b/package.json index 3a76c43..07cb34d 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#staging", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#bin_modes_v2", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", From 3eb66e362287e9914f78d6eaff509ac9d5f1f5ee Mon Sep 17 00:00:00 2001 From: Carter Date: Tue, 30 Jun 2026 09:43:38 -0600 Subject: [PATCH 03/25] changed header to match new theme --- src/app/Header.tsx | 4 ++-- src/services/whiteLabel.ts | 11 +++++++++++ src/theme/theme.ts | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/app/Header.tsx b/src/app/Header.tsx index 6e4dbaf..5ff6184 100644 --- a/src/app/Header.tsx +++ b/src/app/Header.tsx @@ -16,7 +16,7 @@ const useStyles = makeStyles((theme: Theme) => { appBar: { zIndex: theme.zIndex.drawer + 1, backgroundImage: "none", // This prevents de-saturation of header in dark mode - backgroundColor: theme.palette.mode === "light" ? "#3b3b3b" : "#272727" + backgroundColor: theme.palette.header.main }, toolbar: { marginLeft: theme.spacing(0.5), @@ -128,4 +128,4 @@ export default function Header() { {isMobile && {setNavOpen(isOpen)}} sideIsOpen={navOpen} />} ) -} \ No newline at end of file +} diff --git a/src/services/whiteLabel.ts b/src/services/whiteLabel.ts index 35a9c8c..90cf4ed 100644 --- a/src/services/whiteLabel.ts +++ b/src/services/whiteLabel.ts @@ -28,6 +28,8 @@ const protips: string[] = [ "Want to receive text messages about your devices? Update your phone number and enable SMS notifications!" ]; +const DEFAULT_HEADER_COLOUR = "#1f2a35"; + export interface WhiteLabelFeatures { bins: boolean; visualFarm: boolean; @@ -55,6 +57,7 @@ interface WhiteLabel { secondaryColour: any; signatureColour: any; signatureAccentColour: any; + headerColour?: any; auth0ClientId: string; redirectOnLogout: boolean; logoutRedirectTarget: string; @@ -444,6 +447,14 @@ export function getSignatureColour(): any { return getWhitelabel().signatureColour; } +export function getHeaderColour(): any { + return getWhitelabel().headerColour ?? DEFAULT_HEADER_COLOUR; +} + +export function getHeaderColor(): any { + return getHeaderColour(); +} + export function getSignatureAccentColour(): any { return getWhitelabel().signatureAccentColour; } diff --git a/src/theme/theme.ts b/src/theme/theme.ts index 739a54f..ce44c5d 100644 --- a/src/theme/theme.ts +++ b/src/theme/theme.ts @@ -2,10 +2,21 @@ import * as Colours from "@mui/material/colors" import { createTheme, ThemeOptions } from '@mui/material/styles'; import { + getHeaderColor, getPrimaryColour, getSecondaryColour, } from "../services/whiteLabel"; +declare module "@mui/material/styles" { + interface Palette { + header: Palette["primary"]; + } + + interface PaletteOptions { + header?: PaletteOptions["primary"]; + } +} + const baseTheme: ThemeOptions = { palette: { mode: 'light', // default mode; will be overridden by system if needed @@ -62,6 +73,9 @@ export const getTheme = (mode: 'light' | 'dark') => ...baseTheme.palette, primary: makePaletteColor(getPrimaryColour()), secondary: makePaletteColor(getSecondaryColour()), + header: { + main: getHeaderColor(), + }, mode, ...(mode === 'dark' ? { From 87064461087845ac424701e7c5dc28fba614250b Mon Sep 17 00:00:00 2001 From: Carter Date: Tue, 30 Jun 2026 10:50:29 -0600 Subject: [PATCH 04/25] proto update and audit --- package-lock.json | 1140 +++++++++++++++++++++++---------------------- 1 file changed, 582 insertions(+), 558 deletions(-) diff --git a/package-lock.json b/package-lock.json index 58133fb..efc9773 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,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#staging", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", @@ -128,12 +128,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -142,9 +142,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -152,21 +152,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -200,13 +200,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -229,14 +229,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -333,9 +333,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -356,28 +356,28 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -400,9 +400,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "dev": true, "license": "MIT", "engines": { @@ -460,27 +460,27 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -503,26 +503,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -1099,16 +1099,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", - "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz", + "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.29.0" + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1685,31 +1685,31 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -1717,13 +1717,13 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -3049,9 +3049,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "license": "BSD-3-Clause" }, "node_modules/@react-pdf/fns": { @@ -3223,9 +3223,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", - "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", + "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -3238,10 +3238,37 @@ "dev": true, "license": "MIT" }, + "node_modules/@rollup/plugin-babel": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.1.0.tgz", + "integrity": "sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@rollup/pluginutils": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/plugin-node-resolve": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", - "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", "dev": true, "license": "MIT", "dependencies": { @@ -3263,19 +3290,41 @@ } } }, - "node_modules/@rollup/plugin-terser": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", - "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "node_modules/@rollup/plugin-replace": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz", + "integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==", "dev": true, "license": "MIT", "dependencies": { - "serialize-javascript": "^6.0.1", + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", + "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^7.0.3", "smob": "^1.0.0", "terser": "^5.17.4" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" }, "peerDependencies": { "rollup": "^2.0.0||^3.0.0||^4.0.0" @@ -3287,9 +3336,9 @@ } }, "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", "dev": true, "license": "MIT", "dependencies": { @@ -3448,29 +3497,6 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@surma/rollup-plugin-off-main-thread": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", - "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "ejs": "^3.1.6", - "json5": "^2.2.0", - "magic-string": "^0.25.0", - "string.prototype.matchall": "^4.0.6" - } - }, - "node_modules/@surma/rollup-plugin-off-main-thread/node_modules/magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "sourcemap-codec": "^1.4.8" - } - }, "node_modules/@swc/helpers": { "version": "0.5.18", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", @@ -3492,6 +3518,22 @@ "node": ">=10" } }, + "node_modules/@trickfilm400/rollup-plugin-off-main-thread": { + "version": "3.0.0-pre1", + "resolved": "https://registry.npmjs.org/@trickfilm400/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-3.0.0-pre1.tgz", + "integrity": "sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "ejs": "^3.1.10", + "json5": "^2.2.3", + "magic-string": "^0.30.21", + "string.prototype.matchall": "^4.0.12" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@turf/along": { "version": "7.3.4", "resolved": "https://registry.npmjs.org/@turf/along/-/along-7.3.4.tgz", @@ -6198,9 +6240,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { @@ -6335,15 +6377,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", + "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -6352,13 +6394,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", + "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.4", + "@vitest/spy": "3.2.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -6379,9 +6421,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", + "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", "dev": true, "license": "MIT", "dependencies": { @@ -6392,13 +6434,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", + "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.4", + "@vitest/utils": "3.2.6", "pathe": "^2.0.3", "strip-literal": "^3.0.0" }, @@ -6407,13 +6449,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", + "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", + "@vitest/pretty-format": "3.2.6", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -6422,9 +6464,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", + "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", "dev": true, "license": "MIT", "dependencies": { @@ -6435,13 +6477,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", + "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", + "@vitest/pretty-format": "3.2.6", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, @@ -6478,6 +6520,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -6652,27 +6706,28 @@ } }, "node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, "node_modules/axios/node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -6809,9 +6864,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -6945,15 +7000,15 @@ } }, "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" }, "engines": { @@ -7263,9 +7318,9 @@ } }, "node_modules/cosmiconfig/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "license": "ISC", "engines": { "node": ">= 6" @@ -7889,9 +7944,9 @@ } }, "node_modules/es-abstract": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", "dev": true, "license": "MIT", "dependencies": { @@ -7957,6 +8012,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-cookie": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/es-cookie/-/es-cookie-1.3.2.tgz", @@ -7989,9 +8063,9 @@ "license": "MIT" }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -8016,15 +8090,18 @@ } }, "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", "dev": true, "license": "MIT", "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -8284,6 +8361,19 @@ "node": ">=0.10.0" } }, + "node_modules/eta": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-4.6.0.tgz", + "integrity": "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/bgub/eta?sponsor=1" + } + }, "node_modules/eventemitter3": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", @@ -8351,9 +8441,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", "dev": true, "funding": [ { @@ -8399,9 +8489,9 @@ } }, "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -8409,9 +8499,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { @@ -8481,16 +8571,16 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -8558,15 +8648,15 @@ } }, "node_modules/form-data": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", - "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.5.tgz", + "integrity": "sha512-j23EibVLnp4zNXGW7LjryXYa2X6U/M96yoOX+ybZxwkYajdxRNEqYY3zhh7y0i6kfISKS2jr+EJq1YTUDEv5+w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", + "hasown": "^2.0.4", "mime-types": "^2.1.35" }, "engines": { @@ -8616,6 +8706,21 @@ "node": ">=10" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -8626,18 +8731,21 @@ } }, "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -8882,16 +8990,16 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" } }, "node_modules/glob/node_modules/minimatch": { @@ -9095,9 +9203,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -9185,6 +9293,19 @@ "node": ">=10.19.0" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/hyphen": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/hyphen/-/hyphen-1.14.1.tgz", @@ -9428,6 +9549,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -9845,10 +9982,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -10158,15 +10305,15 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, "node_modules/lodash.debounce": { @@ -11022,9 +11169,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -11087,9 +11234,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -11107,7 +11254,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -11122,9 +11269,9 @@ "license": "MIT" }, "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -11198,15 +11345,15 @@ }, "node_modules/protobuf-ts": { "version": "1.0.0", - "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#29d9765ce93c04ad70d795b80e9873fc2fc5f600", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#f783a2f01deccf8c94c11b74995de45182e7cfee", "dependencies": { "protobufjs": "^6.8.8" } }, "node_modules/protobufjs": { - "version": "6.11.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", - "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "version": "6.11.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.6.tgz", + "integrity": "sha512-k8BHqgPBOtrlougZZqF2uUk5Z7bN8f0wj+3e8M3hvtSv0NBAz4VBy5f6R5Nxq/l+i7mRFTgNZb2trxqTpHNY/A==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -11230,16 +11377,19 @@ } }, "node_modules/protocol-buffers-schema": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", - "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz", + "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==", "license": "MIT" }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/pump": { "version": "3.0.3", @@ -11311,16 +11461,6 @@ "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", "license": "MIT" }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/rbush": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", @@ -11683,12 +11823,12 @@ } }, "node_modules/react-router": { - "version": "6.30.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", - "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz", + "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.2" + "@remix-run/router": "1.23.3" }, "engines": { "node": ">=14.0.0" @@ -11698,13 +11838,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", - "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz", + "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.2", - "react-router": "6.30.3" + "@remix-run/router": "1.23.3", + "react-router": "6.30.4" }, "engines": { "node": ">=14.0.0" @@ -12190,15 +12330,15 @@ "license": "BSD-3-Clause" }, "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, @@ -12304,13 +12444,13 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.7.tgz", + "integrity": "sha512-YAy8Od6KV+uuwUuU50np8fGB/Aues6Y0nAhA9y/hId74PlKUcme4pXcBD46NWKr1Q4osN/iseZ17YqO1XfmI8g==", "dev": true, "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" + "engines": { + "node": ">=20.0.0" } }, "node_modules/set-function-length": { @@ -12401,15 +12541,15 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -12421,14 +12561,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -12518,9 +12658,9 @@ "license": "MIT" }, "node_modules/smob": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.1.tgz", - "integrity": "sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.2.tgz", + "integrity": "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==", "dev": true, "license": "MIT", "engines": { @@ -12611,14 +12751,6 @@ "node": ">=0.10.0" } }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "deprecated": "Please use @jridgewell/sourcemap-codec instead", - "dev": true, - "license": "MIT" - }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -12789,19 +12921,20 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -12811,16 +12944,16 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "es-object-atoms": "^1.1.2" }, "engines": { "node": ">= 0.4" @@ -13314,18 +13447,18 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" }, "engines": { "node": ">= 0.4" @@ -14380,9 +14513,9 @@ } }, "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "dev": true, "license": "MIT", "dependencies": { @@ -14543,20 +14676,20 @@ } }, "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", + "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", + "@vitest/expect": "3.2.6", + "@vitest/mocker": "3.2.6", + "@vitest/pretty-format": "^3.2.6", + "@vitest/runner": "3.2.6", + "@vitest/snapshot": "3.2.6", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", @@ -14586,8 +14719,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", + "@vitest/browser": "3.2.6", + "@vitest/ui": "3.2.6", "happy-dom": "*", "jsdom": "*" }, @@ -14727,14 +14860,14 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.20", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", - "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", + "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", @@ -14776,30 +14909,30 @@ } }, "node_modules/workbox-background-sync": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.0.tgz", - "integrity": "sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.1.tgz", + "integrity": "sha512-HhT7KE8tOWDm02wRNshXUnUPofMlhenF2DBdUnDPOubhizzPeItkYTmAB6td1Z2cjYPa98vzEiPLEuzn5hN66g==", "dev": true, "license": "MIT", "dependencies": { "idb": "^7.0.1", - "workbox-core": "7.4.0" + "workbox-core": "7.4.1" } }, "node_modules/workbox-broadcast-update": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.0.tgz", - "integrity": "sha512-+eZQwoktlvo62cI0b+QBr40v5XjighxPq3Fzo9AWMiAosmpG5gxRHgTbGGhaJv/q/MFVxwFNGh/UwHZ/8K88lA==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.1.tgz", + "integrity": "sha512-uAlgslKLvbQY+suirIdnBCSYrcgBhjp81Nj4l1lj/Jmj0MJO2CJERnCJjT0GFVwmReV0N+zs78K6gqd5gr9/+A==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.4.0" + "workbox-core": "7.4.1" } }, "node_modules/workbox-build": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.0.tgz", - "integrity": "sha512-Ntk1pWb0caOFIvwz/hfgrov/OJ45wPEhI5PbTywQcYjyZiVhT3UrwwUPl6TRYbTm4moaFYithYnl1lvZ8UjxcA==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.1.tgz", + "integrity": "sha512-SDhxIvEAde9Gy/5w4Yo1Jh/M49Z0qE3q0oteyE8zGq0DScxFqVBcCtIXFuLtmtxRQZCMbf0prco4VyEu3KBQuw==", "dev": true, "license": "MIT", "dependencies": { @@ -14807,39 +14940,39 @@ "@babel/core": "^7.24.4", "@babel/preset-env": "^7.11.0", "@babel/runtime": "^7.11.2", - "@rollup/plugin-babel": "^5.2.0", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-replace": "^2.4.1", - "@rollup/plugin-terser": "^0.4.3", - "@surma/rollup-plugin-off-main-thread": "^2.2.3", + "@rollup/plugin-babel": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.3", + "@rollup/plugin-replace": "^6.0.3", + "@rollup/plugin-terser": "^1.0.0", + "@trickfilm400/rollup-plugin-off-main-thread": "^3.0.0-pre1", "ajv": "^8.6.0", "common-tags": "^1.8.0", + "eta": "^4.5.1", "fast-json-stable-stringify": "^2.1.0", "fs-extra": "^9.0.1", "glob": "^11.0.1", - "lodash": "^4.17.20", "pretty-bytes": "^5.3.0", - "rollup": "^2.79.2", + "rollup": "^4.53.3", "source-map": "^0.8.0-beta.0", "stringify-object": "^3.3.0", "strip-comments": "^2.0.1", "tempy": "^0.6.0", "upath": "^1.2.0", - "workbox-background-sync": "7.4.0", - "workbox-broadcast-update": "7.4.0", - "workbox-cacheable-response": "7.4.0", - "workbox-core": "7.4.0", - "workbox-expiration": "7.4.0", - "workbox-google-analytics": "7.4.0", - "workbox-navigation-preload": "7.4.0", - "workbox-precaching": "7.4.0", - "workbox-range-requests": "7.4.0", - "workbox-recipes": "7.4.0", - "workbox-routing": "7.4.0", - "workbox-strategies": "7.4.0", - "workbox-streams": "7.4.0", - "workbox-sw": "7.4.0", - "workbox-window": "7.4.0" + "workbox-background-sync": "7.4.1", + "workbox-broadcast-update": "7.4.1", + "workbox-cacheable-response": "7.4.1", + "workbox-core": "7.4.1", + "workbox-expiration": "7.4.1", + "workbox-google-analytics": "7.4.1", + "workbox-navigation-preload": "7.4.1", + "workbox-precaching": "7.4.1", + "workbox-range-requests": "7.4.1", + "workbox-recipes": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1", + "workbox-streams": "7.4.1", + "workbox-sw": "7.4.1", + "workbox-window": "7.4.1" }, "engines": { "node": ">=20.0.0" @@ -14863,69 +14996,6 @@ "ajv": ">=8" } }, - "node_modules/workbox-build/node_modules/@rollup/plugin-babel": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", - "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.10.4", - "@rollup/pluginutils": "^3.1.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "@types/babel__core": "^7.1.9", - "rollup": "^1.20.0||^2.0.0" - }, - "peerDependenciesMeta": { - "@types/babel__core": { - "optional": true - } - } - }, - "node_modules/workbox-build/node_modules/@rollup/plugin-replace": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", - "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "magic-string": "^0.25.7" - }, - "peerDependencies": { - "rollup": "^1.20.0 || ^2.0.0" - } - }, - "node_modules/workbox-build/node_modules/@rollup/pluginutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", - "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "0.0.39", - "estree-walker": "^1.0.1", - "picomatch": "^2.2.2" - }, - "engines": { - "node": ">= 8.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" - } - }, - "node_modules/workbox-build/node_modules/@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", - "dev": true, - "license": "MIT" - }, "node_modules/workbox-build/node_modules/ajv": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", @@ -14943,13 +15013,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/workbox-build/node_modules/estree-walker": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", - "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", - "dev": true, - "license": "MIT" - }, "node_modules/workbox-build/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -14957,29 +15020,6 @@ "dev": true, "license": "MIT" }, - "node_modules/workbox-build/node_modules/magic-string": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", - "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "sourcemap-codec": "^1.4.8" - } - }, - "node_modules/workbox-build/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/workbox-build/node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -14993,22 +15033,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/workbox-build/node_modules/rollup": { - "version": "2.80.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", - "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", - "dev": true, - "license": "MIT", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=10.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/workbox-build/node_modules/source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", @@ -15053,140 +15077,140 @@ } }, "node_modules/workbox-cacheable-response": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.0.tgz", - "integrity": "sha512-0Fb8795zg/x23ISFkAc7lbWes6vbw34DGFIMw31cwuHPgDEC/5EYm6m/ZkylLX0EnEbbOyOCLjKgFS/Z5g0HeQ==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.1.tgz", + "integrity": "sha512-8xaFoJdDc2OjrlbbL3gEeBO1WKcMwRqwLRupgqahYXu75yXajPLuwrbXMrIGZuWYXrQwk0xDjOxZ/ujCy/oJYw==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.4.0" + "workbox-core": "7.4.1" } }, "node_modules/workbox-core": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.0.tgz", - "integrity": "sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.1.tgz", + "integrity": "sha512-DT+vu46eh/2vRsSHTY4Xmc32Z1rr9PRlQUXr1Dx30ZuXRWwOsvZgGgcwxcasubQLQmbTNYZjv44LkBAQ4tT5tQ==", "dev": true, "license": "MIT" }, "node_modules/workbox-expiration": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.0.tgz", - "integrity": "sha512-V50p4BxYhtA80eOvulu8xVfPBgZbkxJ1Jr8UUn0rvqjGhLDqKNtfrDfjJKnLz2U8fO2xGQJTx/SKXNTzHOjnHw==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.1.tgz", + "integrity": "sha512-lRKUF7b+OGbeXkQk1s6MHXOa3d7Xxf7Of31W6c6hCfipfIyrtdWZ89stq21AHZMaoG7VNFoHply4Ox+rU31TWg==", "dev": true, "license": "MIT", "dependencies": { "idb": "^7.0.1", - "workbox-core": "7.4.0" + "workbox-core": "7.4.1" } }, "node_modules/workbox-google-analytics": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.0.tgz", - "integrity": "sha512-MVPXQslRF6YHkzGoFw1A4GIB8GrKym/A5+jYDUSL+AeJw4ytQGrozYdiZqUW1TPQHW8isBCBtyFJergUXyNoWQ==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.1.tgz", + "integrity": "sha512-Mks1JwLEt++ZAkF6sS1OpSh9RtAMIsiDgRpK+codiHGIPXeaUOgi4cPc3GFadUl8V5QPeypEk8Oxgl3HlwVzHw==", "dev": true, "license": "MIT", "dependencies": { - "workbox-background-sync": "7.4.0", - "workbox-core": "7.4.0", - "workbox-routing": "7.4.0", - "workbox-strategies": "7.4.0" + "workbox-background-sync": "7.4.1", + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" } }, "node_modules/workbox-navigation-preload": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.0.tgz", - "integrity": "sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.1.tgz", + "integrity": "sha512-C4KVsjPcYKJOhr631AxR9XoG2rLF3QiTk5aMv36MXOjtWvm8axwNFAtKUPGsWUwLXXAMgYM1En7fsvndaXeXRQ==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.4.0" + "workbox-core": "7.4.1" } }, "node_modules/workbox-precaching": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.0.tgz", - "integrity": "sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.1.tgz", + "integrity": "sha512-cdr/9qByww7yzEp7zg/qI4ukUrrNjQLgN+ONQRpjy/VqGQXwkgHwr00KksGJK8v0VifwDXBb8a4cWNZH71jn3Q==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.4.0", - "workbox-routing": "7.4.0", - "workbox-strategies": "7.4.0" + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" } }, "node_modules/workbox-range-requests": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.0.tgz", - "integrity": "sha512-3Vq854ZNuP6Y0KZOQWLaLC9FfM7ZaE+iuQl4VhADXybwzr4z/sMmnLgTeUZLq5PaDlcJBxYXQ3U91V7dwAIfvw==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.1.tgz", + "integrity": "sha512-7i2oxAUE82gHdAJBCAQ04JzNOdRPqzuOzGfoUyJpFSmeqBNYGPrAH8GPoPjUQTfp+NycwrD2H68VtuF8qxv0vQ==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.4.0" + "workbox-core": "7.4.1" } }, "node_modules/workbox-recipes": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.0.tgz", - "integrity": "sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.1.tgz", + "integrity": "sha512-gnbVfmV4/TtmQaM4x9AtuXhcdstJsep3XMVeztOrQVPT+R6+6DeBjGTCQ7fFCXm+4GEHUA5VEBTyi5+4gWGeog==", "dev": true, "license": "MIT", "dependencies": { - "workbox-cacheable-response": "7.4.0", - "workbox-core": "7.4.0", - "workbox-expiration": "7.4.0", - "workbox-precaching": "7.4.0", - "workbox-routing": "7.4.0", - "workbox-strategies": "7.4.0" + "workbox-cacheable-response": "7.4.1", + "workbox-core": "7.4.1", + "workbox-expiration": "7.4.1", + "workbox-precaching": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" } }, "node_modules/workbox-routing": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.0.tgz", - "integrity": "sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.1.tgz", + "integrity": "sha512-yubJGErZOusuidAenaL5ypfhQOa7urxP/f8E0ws7FPb4039RiWXUWBAyUkmUoOL/BcQGen3h0J8872d51IYxtA==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.4.0" + "workbox-core": "7.4.1" } }, "node_modules/workbox-strategies": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.0.tgz", - "integrity": "sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.1.tgz", + "integrity": "sha512-GZxpaw9NbmOelj7667uZ2kpk5BFpOGbO4X0qjwh5ls8XQ8C+Lha5LQchTiUzsTFSS+NlUpftYAyOVXvQUrcqOQ==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.4.0" + "workbox-core": "7.4.1" } }, "node_modules/workbox-streams": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.0.tgz", - "integrity": "sha512-QHPBQrey7hQbnTs5GrEVoWz7RhHJXnPT+12qqWM378orDMo5VMJLCkCM1cnCk+8Eq92lccx/VgRZ7WAzZWbSLg==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.1.tgz", + "integrity": "sha512-HWWtraKUbJknd9kgqGcpQ3G114HOPYvqs8HaJMDs2ebLNAimDkVDaWfAXE6Ybl+m8U6KsCE6pWyLYuigWmnAXw==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.4.0", - "workbox-routing": "7.4.0" + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1" } }, "node_modules/workbox-sw": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.0.tgz", - "integrity": "sha512-ltU+Kr3qWR6BtbdlMnCjobZKzeV1hN+S6UvDywBrwM19TTyqA03X66dzw1tEIdJvQ4lYKkBFox6IAEhoSEZ8Xw==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.1.tgz", + "integrity": "sha512-fez5f2DUlDJWTFYkCWQpY10N8gtztd849NswCbVFk0QlcSM4HT5A8x4g4ii650yem4I8tHY0R7JZahwp3ltIPw==", "dev": true, "license": "MIT" }, "node_modules/workbox-window": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.0.tgz", - "integrity": "sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.1.tgz", + "integrity": "sha512-notZDH2u8VXaqyuD7xaqIfEFi6SRM4SUSd7ewe9PDsVqADuepxX2ZMY3uvuZGxzY5ZOsGC/vD3A/3smFtJt4/A==", "dev": true, "license": "MIT", "dependencies": { "@types/trusted-types": "^2.0.2", - "workbox-core": "7.4.0" + "workbox-core": "7.4.1" } }, "node_modules/wrappy": { @@ -15212,9 +15236,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, "license": "ISC", "optional": true, From bced38d45475b78066dadb9ce7b7eff73f31084f Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 6 Jul 2026 16:41:46 -0600 Subject: [PATCH 05/25] nailed down the overall structure for the new bin modes now --- src/bin/BinConditioningInteraction.tsx | 39 +- src/bin/bin3dVisualizer.tsx | 26 +- src/bin/binModes/BinModeController.tsx | 158 +++++++- src/bin/binModes/DryingMode.tsx | 520 ++++++++++++++++++++++++- src/bin/binModes/conditionDisplay.tsx | 313 +++++++++++++++ 5 files changed, 1011 insertions(+), 45 deletions(-) create mode 100644 src/bin/binModes/conditionDisplay.tsx diff --git a/src/bin/BinConditioningInteraction.tsx b/src/bin/BinConditioningInteraction.tsx index d729462..92a4f31 100644 --- a/src/bin/BinConditioningInteraction.tsx +++ b/src/bin/BinConditioningInteraction.tsx @@ -85,10 +85,14 @@ interface Props { sink: Component; grain?: pond.Grain; customGrain?: pond.GrainSettings + /** + * if true, the button to update the interaction will be hidden because it will be assumed that the parent will handle the interaction + */ + parentUpdate?: boolean } export default function BinConditioningInteraction(props: Props) { - const { interaction, source, sink, grain, deviceId, customGrain } = props; + const { interaction, source, sink, grain, deviceId, customGrain, parentUpdate} = props; const [sliderVals, setSliderVals] = useState>(new Map()); const [sliderMarks, setSliderMarks] = useState>(new Map()); //this is the emc value calculated from the interactions temp and humidity conditions @@ -143,13 +147,13 @@ export default function BinConditioningInteraction(props: Props) { const updateInteraction = () => { interactionAPI - .updateInteraction(deviceId, interaction.settings, as) - .then(resp => { - openSnack("Updated Interaction Conditions"); - }) - .catch(err => { - openSnack("Failed to Update Interaction Conditions"); - }); + .updateInteraction(deviceId, interaction.settings, as) + .then(resp => { + openSnack("Updated Interaction Conditions"); + }) + .catch(err => { + openSnack("Failed to Update Interaction Conditions"); + }); }; const customMark = (val: string, arrowColor: string) => { @@ -238,6 +242,7 @@ export default function BinConditioningInteraction(props: Props) { max={describer.max()} value={sliderVals.get(condition.measurementType) ?? describer.min()} onChange={(_, val) => { + //note that changing it here like this is what is changing it in the interaction itself condition.value = Math.round(describer.toStored(val as number)); let sliders = cloneDeep(sliderVals); sliders.set(condition.measurementType, val as number); @@ -249,14 +254,16 @@ export default function BinConditioningInteraction(props: Props) { ); })} - + {!parentUpdate && + + } ); diff --git a/src/bin/bin3dVisualizer.tsx b/src/bin/bin3dVisualizer.tsx index 9bdfd0c..55316b8 100644 --- a/src/bin/bin3dVisualizer.tsx +++ b/src/bin/bin3dVisualizer.tsx @@ -11,6 +11,7 @@ import { CableData } from "bin/3dView/Data/BuildCableData"; import ModeChangeDialog from "bin/conditioning/modeChangeDialog"; import { cloneDeep } from "lodash"; import { useMobile } from "hooks"; +import BinModeController from "./binModes/BinModeController"; interface Props { bin: Bin @@ -35,6 +36,7 @@ export default function bin3dVisualizer(props: Props){ const [showMoistureHeatmap, setShowMoistureHeatmap] = useState(false) const [binDisplay, setBinDisplay] = useState("temp") const [binMode, setBinMode] = useState(pond.BinMode.BIN_MODE_NONE) + const [newMode, setNewMode] = useState(pond.BinMode.BIN_MODE_NONE) const [openModeChange, setOpenModeChange] = useState(false) const [selectedCable, setSelectedCable] = useState(undefined) const [selectedNode, setSelectedNode] = useState(undefined) @@ -58,10 +60,10 @@ export default function bin3dVisualizer(props: Props){ setBinMode(bin.settings.mode) },[bin]) - const updateBin = () => { + const updateBin = (newMode: pond.BinMode) => { if(updateBinCallback){ let clone = cloneDeep(bin) - clone.settings.mode = binMode + clone.settings.mode = newMode updateBinCallback(clone) } }; @@ -86,8 +88,10 @@ export default function bin3dVisualizer(props: Props){ '&.Mui-focused .MuiOutlinedInput-notchedOutline': { border: 'none' }, }} onChange={event => { - setBinMode(event.target.value as pond.BinMode) + setNewMode(event.target.value as pond.BinMode) setOpenModeChange(true) + // for the re-built modes, each one will have its own dialog box and this will just control which one to open rather than the single dialog for all of them + // and having the mode determine what to show in the dialog }} > Select Mode.. @@ -327,7 +331,7 @@ export default function bin3dVisualizer(props: Props){ }} /> } - { setModeChangeInProgress(false) }} - + /> */} + { + updateBin(newMode) + }} + onClose={()=>{setOpenModeChange(false)}} /> void bin: Bin + binPrefs?: Map + devices: Device[] + componentDevices: Map + componentMap: Map + updateMode: (newMode: pond.BinMode) => void } -export default function BinModeControl(props: Props) { - const {open, onClose, newMode} = props +export default function BinModeController(props: Props) { + const {open, onClose, newMode, updateMode, devices, componentDevices, componentMap, binPrefs, bin} = props + const [showProgress, setShowProgress] = useState(false) + const [deviceComponents, setDeviceComponents] = useState>(new Map()) + const [promiseStages, setPromiseStages] = useState([]) + const interactionAPI = useInteractionsAPI() + const componentAPI = useComponentAPI() - //the mode controller could handle the actual api call for adding the interactions etc but the dialogs would create them and pass them up + //the use effects for the initial setup of what will be needed + useEffect(()=>{ + let newMap:Map = new Map() + componentMap.forEach((comp, key) => { + //first we need to get the device id this component belongs to + let dev = componentDevices.get(key) + if(dev){ + //check if the key exists in the new map yet + if(newMap.has(dev)){ + newMap.get(dev)?.push(comp) + }else{ + newMap.set(dev, [comp]) + } + } + }) + setDeviceComponents(newMap) + },[componentDevices, componentMap]) //steps involved for devices when changing the bin mode: //remove conflicting interactions //add new interactions //updating the controllers - const renderContent = () => { + const closeDialog = () => { + setShowProgress(false) + onClose() + } + + const buildStages = (conflictingInteractions: Interaction[], deviceId: number, newInteractions: pond.MultiInteractionSettings, componentSets: ComponentSet[]) => { + //build the stages to pass into the PromiseProgress + //stage one is to remove conflicting interactions + let stages: Stage[] = [] + let stage1: Stage = { + title: "Remove Conflicting Interactions", + steps: [] + } + conflictingInteractions.forEach(i => { + let newStep: Step = { + title: "Removing Interaction", + promise: interactionAPI.removeInteraction(deviceId, i.key()) + } + stage1.steps.push(newStep); + }); + if(stage1.steps.length > 0){ + stages.push(stage1) + } + //stage two is to add the new interactions + let stage2: Stage = { + title: "Add New Interactions", + steps: [ + { + title: "Adding Interactions", + promise: interactionAPI.addMultiInteractions(deviceId, newInteractions) + } + ] + } + if(stage2.steps.length > 0){ + stages.push(stage2) + } + + //stage three is to update the controller components + let stage3: Stage = { + title: "Update Controllers", + steps: [] + } + componentSets.forEach(set => { + set.controllers.forEach(controller => { + let newStep: Step = { + title: "Updating " + controller.name(), + promise: componentAPI.update(deviceId, controller.settings) + } + stage3.steps.push(newStep) + }) + }) + if(stage3.steps.length > 0){ + stages.push(stage3) + } + //set those stages to a state variable + setPromiseStages(stages) + } + + /** + * the progress step is the last step of each mode change that shows what is being done and what succeeded/failed + * initially i was going to put it here because it would effectively be the same thing for each mode, however depending + * on how i decide to handle the api calls, and since i am thinking about passing the functions into the children it may go into each mode component + * + */ + const progressContent = () => { + return ( + + Change Mode + + { + //this is just a function that is rin as soo as the start button is clicked it can be used to disable things while the change is in progress if we want to + }} + onComplete={()=>{ + closeDialog() + }} + /> + + + + + + ) + } + + const modeContent = () => { switch(newMode){ case pond.BinMode.BIN_MODE_DRYING: - return + //setNumSteps(DryingStepCount) + return { + buildStages(conflicting, device, newInteractions, components) + setShowProgress(true) + }} + /> case pond.BinMode.BIN_MODE_HYDRATING: return case pond.BinMode.BIN_MODE_COOLDOWN: @@ -39,16 +167,8 @@ export default function BinModeControl(props: Props) { } return ( - - Change Bin Mode - - {renderContent()} - - - - + + {showProgress ? progressContent() : modeContent()} ) } \ No newline at end of file diff --git a/src/bin/binModes/DryingMode.tsx b/src/bin/binModes/DryingMode.tsx index 7dd57a5..10895a1 100644 --- a/src/bin/binModes/DryingMode.tsx +++ b/src/bin/binModes/DryingMode.tsx @@ -1,9 +1,519 @@ -import { Box } from "@mui/material"; +import { ArrowBack, ArrowForward } from "@mui/icons-material"; +import { Box, Theme, Stepper, Step, StepLabel, Typography, RadioGroup, Radio, FormControlLabel, Grid2, Button, Autocomplete, TextField, DialogTitle, DialogContent, DialogActions, CircularProgress } from "@mui/material"; +import { makeStyles } from "@mui/styles"; +import ConditioningSelector from "bin/conditioning/conditioningSelector"; +import { Component, Device, Interaction } from "models"; +import { Ambient } from "models/Ambient"; +import { Controller } from "models/Controller"; +import { Plenum } from "models/Plenum"; +import { pond, quack } from "protobuf-ts/pond"; +import { useEffect, useMemo, useState } from "react"; +import { ComponentSet } from "bin/conditioning/pickComponentSet" +import { componentIDToString, sameComponentID } from "pbHelpers/Component"; +import moment from "moment"; +import { lowerCase } from "lodash"; +import { GetGrainExtensionMap } from "grain"; +import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; +import { useGlobalState, useInteractionsAPI } from "providers"; +import BinConditioningInteraction from "bin/BinConditioningInteraction"; +import ConditionDisplay from "./conditionDisplay"; +import React from "react"; -export default function DryingMode(){ - return ( - + +const useStyles = makeStyles((theme: Theme) => { + return ({ + stepper: { + padding: theme.spacing(0.5) + }, + }) +}); + +interface Step { + label: string; + completed?: boolean; +} + +export const DryingBaseStepCount = 3 + +interface Option { + label: string; + device: Device; + icon?: string; + } + +interface Props { + devices: Device[] + deviceComponents: Map + binPrefs?: Map + grain?: pond.Grain + cancel: () => void + confirm: (conflictingInteractions: Interaction[], deviceId: number, newInteractions: pond.MultiInteractionSettings, componentSets: ComponentSet[]) => void +} + +const steps = [{label: "Style"}, {label: "Device"}, {label: "Interaction"}] + +//this is our simple drying mode the way it currently works, just building the dialog to be a little more trnasparent and customizable as to what the interaction is doing +export default function DryingMode(props: Props){ + const {devices, deviceComponents, binPrefs, grain, cancel, confirm} = props + const grainExtensionMap = GetGrainExtensionMap(); + const classes = useStyles() + const [{user}] = useGlobalState() + const interactionAPI = useInteractionsAPI() + const [dryingMethod, setDryingMethod] = useState("air")//whether they are using natural air or a heater + const [options, setOptions] = useState([]) + const [deviceOption, setDeviceOption] = useState + if(grain){ + //get the values using the grain type + let ext = grainExtensionMap.get(grain) + if (ext) { + temp = ext.setTempC + hum = ext.targetMC + } + }else{//otherwise use the default drying interaction + temp = 40 + hum = 20 + } + + let conditions = []; + let humidityCondition = pond.InteractionCondition.create({ + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PERCENT, + comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN, + value: Math.round( + describeMeasurement( + quack.MeasurementType.MEASUREMENT_TYPE_PERCENT, + sensor.settings.type, + sensor.settings.subtype, + undefined, + user + ).toStored(hum) + ) + }); + conditions.push(humidityCondition); + + //since the measurement describers function to stored does a conversion into celsius if the users pref is fahrenheit for temperature we need to convert the preset value into fahrenheit + if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) { + temp = Math.round((temp * (9 / 5) + 32) * 100) / 100; + } + let tempVal = describeMeasurement( + quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, + sensor.settings.type, + sensor.settings.subtype, + undefined, + user + ).toStored(temp); + + let tempCondition = pond.InteractionCondition.create({ + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, + comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN, + value: Math.round(tempVal) //and then round the converted value since interaction conditions wont take floats + }); + conditions.push(tempCondition); + interaction.conditions = conditions; + //the the heater output to auto so that when the components are update it uses the new mode + heater.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_AUTO + return interaction; + }; + + const buildFanInteraction = ( + sensor: Plenum | Ambient, + fan: Controller, + tempComparison: "greater" | "less", + humidityComparison: "greater" | "less", + ) => { + let interaction = pond.InteractionSettings.create({ + source: sensor.location(), + sink: fan.location(), + schedule: pond.InteractionSchedule.create({ + timeOfDayStart: "00:00", + timeOfDayEnd: "24:00", + timezone: moment.tz.guess(), + weekdays: moment.weekdays().map(d => lowerCase(d)) + }), + notifications: pond.InteractionNotifications.create({ + reports: true + }), + result: pond.InteractionResult.create({ + type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE, + value: 1 + }) + }); + + let tempPreset = 0; + let humidityPreset = 0; + if(grain){ + //get the values using the grain type + let ext = grainExtensionMap.get(grain) + if (ext) { + tempPreset = ext.setTempC + humidityPreset = ext.targetMC + } + }else{//otherwise use the default drying interaction + tempPreset = 40 + humidityPreset = 20 + } + + + let conditions = []; + let fanConditionOne = pond.InteractionCondition.create({ + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PERCENT, + comparison: + humidityComparison === "greater" + ? quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN + : quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN, + value: describeMeasurement( + quack.MeasurementType.MEASUREMENT_TYPE_PERCENT, + sensor.settings.type, + sensor.settings.subtype, + undefined, + user + ).toStored(humidityPreset) + }); + conditions.push(fanConditionOne); + + //since the measurement describers function toStored does a conversion into celsius for temperature if the users pref is fahrenheit we need to convert the preset value into fahrenheit + if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) { + tempPreset = Math.round((tempPreset * (9 / 5) + 32) * 100) / 100; + } + let tempVal = describeMeasurement( + quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, + sensor.settings.type, + sensor.settings.subtype, + undefined, + user + ).toStored(tempPreset); + + let fanConditionTwo = pond.InteractionCondition.create({ + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, + comparison: + tempComparison === "greater" + ? quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN + : quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN, + value: Math.round(tempVal) //and then round the converted value since the interaction does not take decimals + }); + conditions.push(fanConditionTwo); + + interaction.conditions = conditions; + //set the output mode to auto in the fan so that when the components are updated it uses the new mode + fan.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_AUTO + return interaction; + }; + + //this function just takes in the sets as they change and creates the list of conflicting interactions and the settings to create the new ones and returns them + const createInteractions = (sets: ComponentSet[]) => { + if(!selectedDevice) return undefined //if there is no device that was selected, do nothing + let multiInteractionSettings: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create() + let conflictingInteractions: Interaction[] = [] + let linkedComponents = deviceComponents.get(selectedDevice.id()); + //loop through the sets + sets.forEach((set) => { + //loop through the controllers + set.controllers.forEach(controller => { + //filter the conflicting interactions for that controller + let c = existingInteractions.filter(i => { + let conflicting = false; + if (linkedComponents) { + linkedComponents.forEach(comp => { + if (sameComponentID(comp.location(), i.settings.sink)) { + conflicting = true; + } + }); + } + return conflicting; + }); + conflictingInteractions = conflictingInteractions.concat(c) + // if the controller is a heater create heater interaction + if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER){ + multiInteractionSettings.interactions.push(buildHeaterInteraction(set.sensor, controller)) + }else if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN || + controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_EXHAUST_FAN){// if the controller is a fan + //if it is a combination set using a heater as well just turn the fan on + if(set.combo){ + controller.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_ON + }else{ + //otherwise make a fan interaction + multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller, "greater", "less")) + } + } + }) + }) + setComponentSets([...sets]) + return { + conflicting: conflictingInteractions, + toAdd: multiInteractionSettings + } + } + + const stepper = () => { + return ( + + {steps.map((s, i) => ( + + {s.label} + + ))} + + ); + } + + const styleStep = () => { + return ( + + Select your drying method + { + setDryingMethod(value) + }}> + } + value={"air"} + label={"Natural Air"} + /> + } + value={"heat"} + label={"Supplemental Heat"} + /> + + + ) + } + + const deviceSelector = () => { + return ( + option.label || ""} + onChange={(_, newValue) => { + if(newValue){ + setSelectedDevice(newValue.device); + } + }} + renderInput={params => } + /> + ) + } + + const deviceStep = () => { + return ( + + Devices + {deviceSelector()} + {interactionLoading ? : + { + //update the component sets that will be used for the interactions + //when the sets change it will create the interactions and any conflicting ones that need to be removed will be put into a list + //then when the submit button gets clicked it will use the conflicting list and toAdd to remove conflicting interactions and add the new ones + let i = createInteractions(sets) + if(i !== undefined){ + setConflictingInteractions(i.conflicting) + setInteractionsToAdd(i.toAdd) + } + }} + /> + } + + ) + } + + //this step will use the conflicting interactions and interactions to add to display them and allow users to make changes + const interactionStep = () => { + console.log(interactionsToAdd) + return ( + + Interactions + {interactionsToAdd?.interactions.map((interaction, index) => { + let temp = Interaction.create() + temp.settings = interaction + let sink = sinkMap.get(componentIDToString(interaction.sink)) + let source = sourceMap.get(componentIDToString(interaction.source)) + if(sink && source){ + return ( + { + console.log(newSettings) + }} + /> + ) + } + })} + + ) + } + + const stepperContent = () => { + switch(currentStep){ + case 1: + return deviceStep() + case 2: + return interactionStep() + default: + return styleStep() + } + } + + const actions = () => { + return ( + + {/* close - tells the parent to close the dialog without doing anything, always visible */} + + {/* back - goes back to the previous step, hidden on the forst step */} + {currentStep !== 0 && } + {/* next - goes to the next step, hidden on the last step */} + {currentStep !== steps.length - 1 && } + {/* confirm - tells the parent to build the stages using the data, only visible on the last step */} + {currentStep === steps.length - 1 && interactionsToAdd && selectedDevice && } + + ) + } + + + + return ( + + Bin Drying + + {stepper()} + {stepperContent()} + + + {actions()} + + ) } \ No newline at end of file diff --git a/src/bin/binModes/conditionDisplay.tsx b/src/bin/binModes/conditionDisplay.tsx new file mode 100644 index 0000000..d44c3ba --- /dev/null +++ b/src/bin/binModes/conditionDisplay.tsx @@ -0,0 +1,313 @@ +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + darken, + Grid, + Slider, + Theme, + Typography + } from "@mui/material"; + import { ExpandMore } from "@mui/icons-material"; + import { ExtractMoisture } from "grain"; + import { cloneDeep } from "lodash"; + import { Component, Interaction } from "models"; + import { interactionConditionText, interactionResultText } from "pbHelpers/Interaction"; + import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; + import { pond, quack } from "protobuf-ts/pond"; + import { useGlobalState } from "providers"; + import React, { useEffect, useState } from "react"; + import { avg, fahrenheitToCelsius } from "utils"; + import { makeStyles } from "@mui/styles"; + import { Mark } from "@mui/material/Slider/useSlider.types"; + + const useStyles = makeStyles((theme: Theme) => { + return ({ + displayBG: { + marginTop: 10, + background: darken(theme.palette.background.default, 0.05), + borderRadius: 5, + padding: 10 + }, + markContainer: { + zIndex: 2 + }, + arrowDown: { + width: 0, + height: 0, + borderLeft: "10px solid transparent", + borderRight: "10px solid transparent" + //borderTop: "10px solid yellow" + }, + sliderRoot: {}, + sliderThumb: { + height: 15, + width: 15, + backgroundColor: "yellow" + }, + sliderTrack: { + height: 3, + backgroundColor: "white" + }, + sliderRail: { + height: 3, + backgroundColor: "white" + }, + sliderValLabel: { + left: -20, + top: 40, + background: "transparent", + "& *": { + color: "#fff" + } + }, + sliderMark: { + visibility: "hidden" + }, + sliderMarked: { + marginTop: 25, + marginBottom: 0 + }, + sliderMarkLabel: { + top: -25 + } + }); + }); + + interface Props { + interaction: Interaction; + source: Component; + sink: Component; + grain?: pond.Grain; + customGrain?: pond.GrainSettings + changeConditions?: (newSettings: pond.InteractionSettings) => void + } + + export default function ConditionDisplay(props: Props) { + const { interaction, source, sink, grain, customGrain, changeConditions} = props; + const [sliderVals, setSliderVals] = useState>(new Map()); + const [sliderMarks, setSliderMarks] = useState>(new Map()); + //this is the emc value calculated from the interactions temp and humidity conditions + const [baseEMC, setBaseEMC] = useState(); + const [{ user }] = useGlobalState(); + const classes = useStyles(); + + useEffect(() => { + let passedInteraction = interaction; + let sliderVals: Map = new Map(); + let sliderMarks: Map = new Map(); + passedInteraction.conditions().forEach(condition => { + let describer = describeMeasurement(condition.measurementType, source.type(), undefined, undefined, user); + //NOTE: toDisplay will convert the temp value to fahrenheit + sliderVals.set(condition.measurementType, describer.toDisplay(condition.value)); + }); + + source.status.lastGoodMeasurement.forEach(measurement => { + let m = pond.UnitMeasurementsForComponent.fromObject(measurement); + if (m.values[0]) { + let markVal = avg(m.values[0].values); + //do this since this is how interactions handle the values so that the slider can use the toDisplay method of the describer for the marks + if (m.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) markVal = markVal * 10; + if (m.type === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) markVal = markVal * 100; + sliderMarks.set(m.type, markVal); + } + }); + setSliderVals(sliderVals); + setSliderMarks(sliderMarks); + }, [interaction, source]); + + useEffect(() => { + let temp = sliderVals.get(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE); + let hum = sliderVals.get(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT); + if ( + grain !== undefined && + grain !== pond.Grain.GRAIN_INVALID && + temp && + hum + ) { + if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) { + //the emc calc needs the temp to be in celsius + temp = fahrenheitToCelsius(temp); + } + let emc = ExtractMoisture(grain, temp, hum, customGrain) + setBaseEMC(emc === hum ? undefined : emc); + + } + }, [sliderVals, grain, user]); + + const customMark = (val: string, arrowColor: string) => { + return ( + + + + + {val} + + + + + + + + ); + }; + + + + const conditionDisplay = () => { + return ( + + {interaction.conditions().map((condition, i) => { + let describer = describeMeasurement(condition.measurementType, source?.type(), source.subType(), undefined, user); + let labelTail = describer.unit(); + let marks: Mark[] = []; + + // if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) { + // if (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) { + // labelTail = "°F"; + // } else { + // labelTail = "°C"; + // } + // } + if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) { + labelTail = "%"; + } + + let mark = sliderMarks.get(condition.measurementType); + if (mark !== undefined) { + marks.push({ + label: customMark(describer.toDisplay(mark) + labelTail, describer.colour()), + value: describer.toDisplay(mark) + }); + } + return ( + + + {interactionConditionText(source, condition, false, user)} + + + { + if ( + condition.measurementType === + quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE + ) { + if ( + user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT + ) { + return value.toFixed(1) + "°F"; + } else { + return value.toFixed(1) + "°C"; + } + } + if ( + condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT + ) { + return value.toFixed(1) + "%"; + } + }} + marks={marks} + min={describer.min()} + max={describer.max()} + value={sliderVals.get(condition.measurementType) ?? describer.min()} + onChange={(_, val) => { + //note that changing it here like this is what is changing it in the interaction itself + condition.value = Math.round(describer.toStored(val as number)); + // if(changeConditions){ + // changeConditions(interaction.settings) + // } + let sliders = cloneDeep(sliderVals); + sliders.set(condition.measurementType, val as number); + setSliderVals(sliders); + }} + onChangeCommitted={(_, val) => { + if(changeConditions){ + changeConditions(interaction.settings) + } + }} + /> + + + ); + })} + + ); + }; + + const determineEMCRelation = () => { + let relation = "Approximately:"; + let tempRelation: quack.RelationalOperator | undefined = undefined; + let humidRelation: quack.RelationalOperator | undefined = undefined; + interaction.conditions().forEach(condition => { + if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) + tempRelation = condition.comparison; + if (condition.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) + humidRelation = condition.comparison; + }); + + if ( + (tempRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN || + tempRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO) && + (humidRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN || + humidRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO) + ) { + relation = "Less Than: "; + } + if ( + (tempRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN || + tempRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO) && + (humidRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN || + humidRelation === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO) + ) { + relation = "Greater Than: "; + } + + return relation; + }; + + return ( + + + + {interactionResultText(interaction, sink)} + + {baseEMC !== undefined ? ( + + }> + + EMC {determineEMCRelation()} + + {baseEMC.toFixed(1)}% + + + + {conditionDisplay()} + + ) : ( + {conditionDisplay()} + )} + + + ); + } + \ No newline at end of file From 71fb67bba4d22d41ea5d8a6ecb0a94304a326446 Mon Sep 17 00:00:00 2001 From: Carter Date: Tue, 7 Jul 2026 11:28:34 -0600 Subject: [PATCH 06/25] pending changes indication for device and component changes --- src/common/PendingChangesChip.tsx | 46 ++++++++++++++++ src/common/PendingChangesIndicator.tsx | 27 +++++++++ src/common/StatusChip.tsx | 20 ------- src/component/ComponentCard.tsx | 40 +++++++++++--- src/device/DeviceOverview.tsx | 12 ++-- src/hooks/index.ts | 1 + src/hooks/usePendingChanges.ts | 31 +++++++++++ src/interactions/InteractionsOverview.tsx | 20 +++---- src/pages/Device.tsx | 24 ++------ src/pages/DeviceComponent.tsx | 64 ++++++++++++++++++++-- src/pbHelpers/Status.tsx | 67 ----------------------- src/utils/index.ts | 1 + src/utils/syncStatus.ts | 41 ++++++++++++++ 13 files changed, 259 insertions(+), 135 deletions(-) create mode 100644 src/common/PendingChangesChip.tsx create mode 100644 src/common/PendingChangesIndicator.tsx delete mode 100644 src/common/StatusChip.tsx create mode 100644 src/hooks/usePendingChanges.ts delete mode 100644 src/pbHelpers/Status.tsx create mode 100644 src/utils/syncStatus.ts diff --git a/src/common/PendingChangesChip.tsx b/src/common/PendingChangesChip.tsx new file mode 100644 index 0000000..f2b393a --- /dev/null +++ b/src/common/PendingChangesChip.tsx @@ -0,0 +1,46 @@ +import { Chip, CircularProgress, Tooltip } from "@mui/material"; +import { CheckCircleOutline } from "@mui/icons-material"; + +interface Props { + pending: boolean; + accepted: boolean; + size?: "small" | "medium"; +} + +/** + * Capsule tag shown alongside the other status chips while changes are + * waiting to be synced to a device or component. Shows a small loading + * circle while pending and a checkmark once the change is accepted. + * Derive the flags with usePendingChanges. + */ +export default function PendingChangesChip(props: Props) { + const { pending, accepted, size } = props; + + if (pending) { + return ( + + } + size={size} + /> + + ); + } + + if (accepted) { + return ( + + } + size={size} + /> + + ); + } + + return null; +} diff --git a/src/common/PendingChangesIndicator.tsx b/src/common/PendingChangesIndicator.tsx new file mode 100644 index 0000000..2d3ff59 --- /dev/null +++ b/src/common/PendingChangesIndicator.tsx @@ -0,0 +1,27 @@ +import { CircularProgress, Grid2 as Grid } from "@mui/material"; +import { CheckCircleOutline } from "@mui/icons-material"; + +interface Props { + pending: boolean; + accepted: boolean; + text: string; +} + +/** + * Small inline status line used inside cards: a loading circle beside the + * caption while changes are pending, and a checkmark once they are accepted. + * Derive the flags with usePendingChanges (or per-item tracking like + * InteractionsOverview does). + */ +export default function PendingChangesIndicator(props: Props) { + const { pending, accepted, text } = props; + if (!pending && !accepted) return null; + + return ( + + {pending && } + {accepted && } + {text} + + ); +} diff --git a/src/common/StatusChip.tsx b/src/common/StatusChip.tsx deleted file mode 100644 index c1b16b3..0000000 --- a/src/common/StatusChip.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Chip, Tooltip } from "@mui/material"; -import { getStatusHelper } from "pbHelpers/Status"; - -interface Props { - status: string; - size?: "small" | "medium"; -} - -export default function StatusChip(props: Props) { - const { status, size } = props; - const statusHelper = getStatusHelper(status); - const icon = statusHelper.icon; - const description = statusHelper.description; - - return icon !== null ? ( - - - - ) : null; -} diff --git a/src/component/ComponentCard.tsx b/src/component/ComponentCard.tsx index 8355abe..8b9b767 100644 --- a/src/component/ComponentCard.tsx +++ b/src/component/ComponentCard.tsx @@ -13,11 +13,13 @@ import { } from "@mui/material"; import Grid from '@mui/material/Grid2'; import EventBlocker from "common/EventBlocker"; +import PendingChangesIndicator from "common/PendingChangesIndicator"; import MeasurementSummary from "component/MeasurementSummary"; -import { useComponentAPI, useSnackbar, useThemeType } from "hooks"; +import { useComponentAPI, usePendingChanges, useSnackbar, useThemeType } from "hooks"; import InteractionsOverview from "interactions/InteractionsOverview"; import { cloneDeep } from "lodash"; import { Component, Device, Interaction } from "models"; +import moment from "moment"; import { getFriendlyAddressTypeName, getHumanReadableAddress } from "pbHelpers/AddressType"; import { controllerModeLabel } from "pbHelpers/Component"; import { @@ -121,6 +123,9 @@ export default function ComponentCard(props: Props) { const location = useLocation() const [sensors, setSensors] = useState([]); const [{ user, showErrors, as }] = useGlobalState(); + const { pending: pendingChanges, accepted: changesAccepted } = usePendingChanges( + component.status.synced + ); const updateControllerState = (checked: boolean) => { let updatedComponent = cloneDeep(component); @@ -133,6 +138,8 @@ export default function ComponentCard(props: Props) { .update(device.id(), updatedComponent.settings, undefined, undefined, as) .then(() => { success(component.name() + "'s mode was set to " + describe); + updatedComponent.status.synced = false; + updatedComponent.status.lastUpdate = moment().toISOString(); refreshCallback(updatedComponent); }) .catch(() => { @@ -287,19 +294,34 @@ export default function ComponentCard(props: Props) { className={classes.cardHeader} titleTypographyProps={{ variant: "subtitle1" }} subheader={ - // !newStructure ? ( - // - // ) : ( + + {(pendingChanges || changesAccepted) && ( + + + + )} + {/* !newStructure ? ( + + ) : ( */} - // ) + {/* ) */} + } action={ diff --git a/src/device/DeviceOverview.tsx b/src/device/DeviceOverview.tsx index 0115568..ac20e89 100644 --- a/src/device/DeviceOverview.tsx +++ b/src/device/DeviceOverview.tsx @@ -1,9 +1,9 @@ import { Chip, Grid2 as Grid, Theme, Tooltip, Skeleton } from "@mui/material"; import { DataUsage, SimCard, Launch, DeviceHub } from "@mui/icons-material"; -import StatusChip from "common/StatusChip"; +import PendingChangesChip from "common/PendingChangesChip"; import DeviceTags from "device/DeviceTags"; import VersionChip from "device/VersionChip"; -import { useSnackbar, useWidth, usePrevious } from "hooks"; +import { useSnackbar, useWidth, usePendingChanges, usePrevious } from "hooks"; import { Device, latestFirmwareVersion, Component } from "models"; import moment from "moment"; import { describeConnectivity } from "pbHelpers/Connectivity"; @@ -54,6 +54,10 @@ export default function DeviceOverview(props: Props) { // const match = useRouteMatch(); const [powerComponent, setPowerComponent] = useState(); const [modemComponent, setModemComponent] = useState(); + const { pending: pendingChanges, accepted: changesAccepted } = usePendingChanges( + device.status.synced, + !loading && device.id() > 0 + ); // const [hologramDialog, setHologramDialog] = useState(false); useEffect(() => { @@ -228,9 +232,9 @@ export default function DeviceOverview(props: Props) { )} - {!device.status.synced && ( + {(pendingChanges || changesAccepted) && ( - + )} {device.settings.needsBusControl && ( diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 0bd5baf..e93b9d5 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -26,6 +26,7 @@ export { export * from "./useDebounce"; // export * from "./useForceUpdate"; // export * from "./useInterval"; +export * from "./usePendingChanges"; export * from "./usePrevious"; export * from "./useThemeType"; export * from "./useWidth"; diff --git a/src/hooks/usePendingChanges.ts b/src/hooks/usePendingChanges.ts new file mode 100644 index 0000000..5852adb --- /dev/null +++ b/src/hooks/usePendingChanges.ts @@ -0,0 +1,31 @@ +import { useEffect, useRef, useState } from "react"; + +/** + * Tracks a synced flag and reports whether changes are pending or were + * accepted while mounted (a false -> true transition, e.g. from a websocket + * status update). Pass enabled=false until real data has loaded so the + * initial placeholder -> loaded flip is not mistaken for an accepted change. + */ +export function usePendingChanges(synced: boolean, enabled: boolean = true) { + const prevSynced = useRef(undefined); + const [accepted, setAccepted] = useState(false); + + useEffect(() => { + if (!enabled) { + prevSynced.current = undefined; + setAccepted(false); + return; + } + if (prevSynced.current === false && synced) { + setAccepted(true); + } else if (!synced) { + setAccepted(false); + } + prevSynced.current = synced; + }, [synced, enabled]); + + return { + pending: enabled && !synced, + accepted: enabled && synced && accepted + }; +} diff --git a/src/interactions/InteractionsOverview.tsx b/src/interactions/InteractionsOverview.tsx index 450c44f..68757cb 100644 --- a/src/interactions/InteractionsOverview.tsx +++ b/src/interactions/InteractionsOverview.tsx @@ -4,7 +4,6 @@ import { CardActions, CardContent, CardHeader, - CircularProgress, darken, Grid2 as Grid, IconButton, @@ -15,7 +14,8 @@ import { Tooltip, Typography } from "@mui/material"; -import { CheckCircleOutline, Settings } from "@mui/icons-material"; +import { Settings } from "@mui/icons-material"; +import PendingChangesIndicator from "common/PendingChangesIndicator"; import { useInteractionsAPI, usePrevious, useSnackbar } from "hooks"; import { cloneDeep } from "lodash"; import { Component, Device, Interaction } from "models"; @@ -329,17 +329,11 @@ export default function InteractionsOverview(props: Props) { title={interactionResultText(interaction, sink)} subheader={ statusText ? ( - - {isPending && } - {isAccepted && ( - - )} - - {statusText} - - + ) : ""} subheaderTypographyProps={{ variant: "caption" }} action={action} diff --git a/src/pages/Device.tsx b/src/pages/Device.tsx index 733d621..801c1f1 100644 --- a/src/pages/Device.tsx +++ b/src/pages/Device.tsx @@ -19,7 +19,7 @@ import { DeviceAvailabilityMap, FindAvailablePositions, OffsetAvailabilityMap } import ComponentCard from "component/ComponentCard"; import { sameComponentID, sortComponents } from "pbHelpers/Component"; import { isController } from "pbHelpers/ComponentType"; -import { or } from "utils"; +import { getTimestampMillis, isStaleStatusUpdate, or } from "utils"; import ComponentDiagnostics from "component/ComponentDiagnostics"; import DeviceWizard from "device/DeviceWizard"; import DeviceScannedComponents from "device/autoDetect/DeviceScannedComponents"; @@ -27,12 +27,6 @@ import { useWebSocket } from "hooks/useWebSocket"; type TimestampedReading = { timestamp?: string }; -function getTimestampMillis(timestamp?: string): number | undefined { - if (!timestamp) return undefined; - const parsed = Date.parse(timestamp); - return Number.isNaN(parsed) ? undefined : parsed; -} - function pickNewestReading( current: T | null | undefined, incoming: T | null | undefined @@ -270,6 +264,10 @@ export default function DevicePage() { if (!data) return; const { key, component } = data; setComponents((prev) => { + const existing = prev.get(key); + if (existing && isStaleStatusUpdate(existing.status, component.status)) { + return prev; + } const updated = new Map(prev); updated.set(key, component); return updated; @@ -315,17 +313,7 @@ export default function DevicePage() { return [...prev, interaction]; } const existing = prev[index]; - const existingLastUpdate = getTimestampMillis(existing.status.lastUpdate); - const incomingLastUpdate = getTimestampMillis(interaction.status.lastUpdate); - const incomingLastSynced = getTimestampMillis(interaction.status.lastSynced); - if (existingLastUpdate !== undefined && incomingLastUpdate !== undefined && - existingLastUpdate > incomingLastUpdate) { - return prev; - } - if (!existing.status.synced && interaction.status.synced && - incomingLastUpdate !== undefined && - existingLastUpdate === incomingLastUpdate && - (incomingLastSynced === undefined || incomingLastSynced < incomingLastUpdate)) { + if (isStaleStatusUpdate(existing.status, interaction.status)) { return prev; } const updated = [...prev]; diff --git a/src/pages/DeviceComponent.tsx b/src/pages/DeviceComponent.tsx index a245e51..e6ab9fa 100644 --- a/src/pages/DeviceComponent.tsx +++ b/src/pages/DeviceComponent.tsx @@ -4,7 +4,7 @@ import { NextMeasurementChip } from "common/NextMeasurementChip"; import ResponsiveTable, { Column } from "common/ResponsiveTable"; // import { getTableIcons } from "common/ResponsiveTable"; import SmartBreadcrumb from "common/SmartBreadcrumb"; -import StatusChip from "common/StatusChip"; +import PendingChangesChip from "common/PendingChangesChip"; import { GetDefaultDateRange } from "common/time/DateRange"; import ComponentActions from "component/ComponentActions"; import ComponentChart from "component/ComponentChart"; @@ -16,11 +16,14 @@ import { useComponentAPI, useDeviceAPI, useGroupAPI, + useHTTP, useInteractionsAPI, + usePendingChanges, usePrevious, useSnackbar, useUserAPI } from "hooks"; +import { useWebSocket } from "hooks/useWebSocket"; import InteractionChip from "interactions/InteractionChip"; import InteractionSettings from "interactions/InteractionSettings"; import { cloneDeep } from "lodash"; @@ -40,9 +43,10 @@ import { getDefaultInteraction } from "pbHelpers/Interaction"; import { pond } from "protobuf-ts/pond"; import { useGlobalState } from "providers"; import { useTeamAPI } from "providers/pond/teamAPI"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useParams } from "react-router-dom"; // import { useRouteMatch } from "react-router"; +import { isStaleStatusUpdate } from "utils/syncStatus"; import { or } from "utils/types"; const useStyles = makeStyles((theme: Theme) => { @@ -142,6 +146,55 @@ export default function DeviceComponent() { const [deviceComponentPrefs, setDeviceComponentPrefs] = useState< pond.DeviceComponentPreferences >(); + const { token } = useHTTP(); + const componentPending = usePendingChanges( + component.status.synced, + !loadingInitial && component.key() !== "" + ); + + const liveContextKeys = useMemo(() => getContextKeys(), []); + const liveContextTypes = useMemo(() => getContextTypes(), []); + /** Matches the REST calls: omit ?as= when first context type is "team". */ + const liveAs = + as && !(liveContextTypes.length > 0 && liveContextTypes[0] === "team") + ? as + : undefined; + + // --- Real-time component updates --- + // Streams component changes for this device, including status changes + // when the device accepts pending component updates. + useWebSocket<{ key: string; component: Component } | null>({ + path: `/v1/live/devices/${deviceID}/components`, + parse: (e) => { + try { + const raw = JSON.parse(e.data); + const comp = Component.any(raw); + if (!comp?.settings) { + console.warn("[ws] received component without settings:", raw); + return null; + } + return { key: comp.key(), component: comp }; + } catch (err) { + console.warn("[ws] failed to parse component:", err); + return null; + } + }, + onMessage: (data) => { + if (!data || data.key !== componentID) return; + setComponent((prev) => { + if (prev.key() !== data.key) return prev; + if (isStaleStatusUpdate(prev.status, data.component.status)) { + return prev; + } + return data.component; + }); + }, + keys: liveContextKeys, + types: liveContextTypes, + as: liveAs, + token, + enabled: !!deviceID && componentID !== "", + }); const setDefaultState = () => { setDevice(Device.create()); @@ -389,9 +442,12 @@ export default function DeviceComponent() { alignItems="center" wrap="nowrap" className={classes.overviewContainer}> - {!component.status.synced && ( + {(componentPending.pending || componentPending.accepted) && ( - + )} diff --git a/src/pbHelpers/Status.tsx b/src/pbHelpers/Status.tsx deleted file mode 100644 index 54d6f70..0000000 --- a/src/pbHelpers/Status.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { cyan } from "@mui/material/colors"; -import AcceptedIcon from "@mui/icons-material/CloudDone"; -import PendingIcon from "@mui/icons-material/CloudQueueTwoTone"; -import RejectedIcon from "@mui/icons-material/SmsFailed"; - -export interface StatusHelper { - description: string; - icon: any; -} - -const Unknown: StatusHelper = { - description: "Status unknown", - icon: null -}; - -const Pending: StatusHelper = { - description: "Pending changes", - icon: -}; - -const Stale: StatusHelper = { - description: "Stale update", - icon: null -}; - -const Accepted: StatusHelper = { - description: "Settings synced", - icon: -}; - -const Rejected: StatusHelper = { - description: "Update failed", - icon: -}; - -const Received: StatusHelper = { - description: "Settings synced", - icon: -}; - -const STATUS_MAP = new Map([ - ["pending", Pending], - ["stale", Stale], - ["accepted", Accepted], - ["rejected", Rejected], - ["received", Received] -]); - -export function getStatusHelper(status: string): StatusHelper { - const statuses = Array.from(STATUS_MAP.keys()); - for (var i = 0; i < statuses.length; i++) { - let key = statuses[i]; - if (status === key) { - return STATUS_MAP.get(key) as StatusHelper; - } - } - - return Unknown; -} - -export function getStatusDescription(status: string): string { - return getStatusHelper(status).description; -} - -export function getStatusIcon(status: string): any { - return getStatusHelper(status).icon; -} diff --git a/src/utils/index.ts b/src/utils/index.ts index 5a5ef3f..29ff37c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,5 +2,6 @@ export * from "./download"; export * from "./environment"; export * from "./numbers"; export * from "./strings"; +export * from "./syncStatus"; export * from "./types"; export * from "./units"; diff --git a/src/utils/syncStatus.ts b/src/utils/syncStatus.ts new file mode 100644 index 0000000..107c2a8 --- /dev/null +++ b/src/utils/syncStatus.ts @@ -0,0 +1,41 @@ +export function getTimestampMillis(timestamp?: string): number | undefined { + if (!timestamp) return undefined; + const parsed = Date.parse(timestamp); + return Number.isNaN(parsed) ? undefined : parsed; +} + +/** The sync-tracking fields shared by ComponentStatus and InteractionStatus. */ +export interface SyncStatusLike { + synced: boolean; + lastUpdate?: string; + lastSynced?: string; +} + +/** + * Decides whether an incoming websocket status message is stale and should + * not replace the status we already hold. A message is stale when it was + * generated before our latest local update, including a synced echo that + * predates a pending change we just submitted. + */ +export function isStaleStatusUpdate(existing: SyncStatusLike, incoming: SyncStatusLike): boolean { + const existingLastUpdate = getTimestampMillis(existing.lastUpdate); + const incomingLastUpdate = getTimestampMillis(incoming.lastUpdate); + const incomingLastSynced = getTimestampMillis(incoming.lastSynced); + if ( + existingLastUpdate !== undefined && + incomingLastUpdate !== undefined && + existingLastUpdate > incomingLastUpdate + ) { + return true; + } + if ( + !existing.synced && + incoming.synced && + incomingLastUpdate !== undefined && + existingLastUpdate === incomingLastUpdate && + (incomingLastSynced === undefined || incomingLastSynced < incomingLastUpdate) + ) { + return true; + } + return false; +} From 7e6b41273b3f95f925b36b1b51bfcf39f1d2c405 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Wed, 8 Jul 2026 10:57:23 -0600 Subject: [PATCH 07/25] defined device change data, and started working on storage --- src/bin/bin3dVisualizer.tsx | 11 --- src/bin/binModes/BinModeController.tsx | 113 ++++++++++++++-------- src/bin/binModes/DryingMode.tsx | 8 +- src/bin/binModes/StorageMode.tsx | 39 +++++++- src/bin/conditioning/modeChangeDialog.tsx | 6 +- src/common/PromiseProgress.tsx | 4 +- src/providers/pond/componentAPI.tsx | 3 +- 7 files changed, 118 insertions(+), 66 deletions(-) diff --git a/src/bin/bin3dVisualizer.tsx b/src/bin/bin3dVisualizer.tsx index 55316b8..bce1ab9 100644 --- a/src/bin/bin3dVisualizer.tsx +++ b/src/bin/bin3dVisualizer.tsx @@ -60,14 +60,6 @@ export default function bin3dVisualizer(props: Props){ setBinMode(bin.settings.mode) },[bin]) - const updateBin = (newMode: pond.BinMode) => { - if(updateBinCallback){ - let clone = cloneDeep(bin) - clone.settings.mode = newMode - updateBinCallback(clone) - } - }; - const binModeControl = () => { return ( @@ -374,9 +366,6 @@ export default function bin3dVisualizer(props: Props){ componentMap={componentMap} newMode={newMode} open={openModeChange} - updateMode={(newMode) => { - updateBin(newMode) - }} onClose={()=>{setOpenModeChange(false)}} /> diff --git a/src/bin/binModes/BinModeController.tsx b/src/bin/binModes/BinModeController.tsx index 7ae173e..f4f825e 100644 --- a/src/bin/binModes/BinModeController.tsx +++ b/src/bin/binModes/BinModeController.tsx @@ -1,15 +1,23 @@ -import { Box, Button, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material"; +import { Box, Button, DialogActions, DialogContent, DialogTitle } from "@mui/material"; import ResponsiveDialog from "common/ResponsiveDialog"; import { Bin, Component, Device, Interaction } from "models"; import { pond } from "protobuf-ts/pond"; import React, { useEffect, useState } from "react"; import StorageMode from "./StorageMode"; -import DryingMode, { DryingBaseStepCount } from "./DryingMode"; +import DryingMode from "./DryingMode"; import HydratingMode from "./HydratingMode"; import CooldownMode from "./CooldownMode"; import { PromiseProgress, Stage, Step } from "common/PromiseProgress"; -import { useComponentAPI, useInteractionsAPI } from "providers"; +import { useBinAPI, useComponentAPI, useInteractionsAPI } from "providers"; import { ComponentSet } from "bin/conditioning/pickComponentSet"; +import { cloneDeep } from "lodash"; + +export interface DeviceChangeData { + deviceId: number + toRemove: Interaction[] + toAdd?: pond.MultiInteractionSettings + componentSets: ComponentSet[] +} interface Props { newMode: pond.BinMode //used to control which dialog to open @@ -20,16 +28,17 @@ interface Props { devices: Device[] componentDevices: Map componentMap: Map - updateMode: (newMode: pond.BinMode) => void + modeUpdated?: (mode: pond.BinMode) => void } export default function BinModeController(props: Props) { - const {open, onClose, newMode, updateMode, devices, componentDevices, componentMap, binPrefs, bin} = props + const {open, onClose, newMode, devices, componentDevices, componentMap, binPrefs, bin, modeUpdated} = props const [showProgress, setShowProgress] = useState(false) const [deviceComponents, setDeviceComponents] = useState>(new Map()) const [promiseStages, setPromiseStages] = useState([]) const interactionAPI = useInteractionsAPI() const componentAPI = useComponentAPI() + const binAPI = useBinAPI(); //the use effects for the initial setup of what will be needed useEffect(()=>{ @@ -59,55 +68,79 @@ export default function BinModeController(props: Props) { onClose() } - const buildStages = (conflictingInteractions: Interaction[], deviceId: number, newInteractions: pond.MultiInteractionSettings, componentSets: ComponentSet[]) => { + const buildStages = (deviceData: DeviceChangeData[]) => { //build the stages to pass into the PromiseProgress //stage one is to remove conflicting interactions let stages: Stage[] = [] let stage1: Stage = { - title: "Remove Conflicting Interactions", + title: "Remove Interactions", steps: [] } - conflictingInteractions.forEach(i => { - let newStep: Step = { - title: "Removing Interaction", - promise: interactionAPI.removeInteraction(deviceId, i.key()) - } - stage1.steps.push(newStep); - }); - if(stage1.steps.length > 0){ - stages.push(stage1) - } - //stage two is to add the new interactions let stage2: Stage = { title: "Add New Interactions", - steps: [ - { - title: "Adding Interactions", - promise: interactionAPI.addMultiInteractions(deviceId, newInteractions) - } - ] + steps: [] } - if(stage2.steps.length > 0){ - stages.push(stage2) - } - - //stage three is to update the controller components let stage3: Stage = { title: "Update Controllers", steps: [] } - componentSets.forEach(set => { - set.controllers.forEach(controller => { - let newStep: Step = { - title: "Updating " + controller.name(), - promise: componentAPI.update(deviceId, controller.settings) - } - stage3.steps.push(newStep) + deviceData.forEach(data => { + data.toRemove.forEach(interaction => { + let newStep: Step = { + title: "Removing Interaction", + promise: () => interactionAPI.removeInteraction(data.deviceId, interaction.key()) + } + stage1.steps.push(newStep); }) + let newInteractions = data.toAdd + if(newInteractions){ + stage2.steps.push({ + title: "Adding Interactions", + promise: () => interactionAPI.addMultiInteractions(data.deviceId, newInteractions) + }) + } + data.componentSets.forEach(set => { + set.controllers.forEach(controller => { + let newStep: Step = { + title: "Updating " + controller.name(), + promise: () => componentAPI.update(data.deviceId, controller.settings) + } + stage3.steps.push(newStep) + }) + }) }) + if(stage1.steps.length > 0){ + stages.push(stage1) + } + if(stage2.steps.length > 0){ + stages.push(stage2) + } if(stage3.steps.length > 0){ stages.push(stage3) } + //stage 4 is to update the bin with the new mode + let stage4: Stage = { + title: "Update Bin With New Mode", + steps: [ + { + title: "Update " + bin.name(), + promise: () => { + let clone = cloneDeep(bin.settings) + clone.mode = newMode + //this will set the bin to use the auto top nodes for cables when the bin is going into storage mode, and turn them off if the bin is being conditioned + if(newMode === pond.BinMode.BIN_MODE_STORAGE){ + clone.autoGrainNode = true + }else{ + clone.autoGrainNode = false + } + return binAPI.updateBin(bin.key(), clone) + }, + onComplete: () => modeUpdated?.(newMode) + } + ] + } + stages.push(stage4) + //set those stages to a state variable setPromiseStages(stages) } @@ -136,7 +169,7 @@ export default function BinModeController(props: Props) { /> - + ) @@ -152,8 +185,8 @@ export default function BinModeController(props: Props) { binPrefs={binPrefs} deviceComponents={deviceComponents} cancel={closeDialog} - confirm={(conflicting, device, newInteractions, components) => { - buildStages(conflicting, device, newInteractions, components) + confirm={(deviceData) => { + buildStages(deviceData) setShowProgress(true) }} /> @@ -162,7 +195,7 @@ export default function BinModeController(props: Props) { case pond.BinMode.BIN_MODE_COOLDOWN: return default: - return + return {}}/> } } diff --git a/src/bin/binModes/DryingMode.tsx b/src/bin/binModes/DryingMode.tsx index 10895a1..40d071a 100644 --- a/src/bin/binModes/DryingMode.tsx +++ b/src/bin/binModes/DryingMode.tsx @@ -18,6 +18,7 @@ import { useGlobalState, useInteractionsAPI } from "providers"; import BinConditioningInteraction from "bin/BinConditioningInteraction"; import ConditionDisplay from "./conditionDisplay"; import React from "react"; +import { DeviceChangeData } from "./BinModeController"; const useStyles = makeStyles((theme: Theme) => { @@ -33,8 +34,6 @@ interface Step { completed?: boolean; } -export const DryingBaseStepCount = 3 - interface Option { label: string; device: Device; @@ -47,7 +46,7 @@ interface Props { binPrefs?: Map grain?: pond.Grain cancel: () => void - confirm: (conflictingInteractions: Interaction[], deviceId: number, newInteractions: pond.MultiInteractionSettings, componentSets: ComponentSet[]) => void + confirm: (deviceData: DeviceChangeData[]) => void } const steps = [{label: "Style"}, {label: "Device"}, {label: "Interaction"}] @@ -419,6 +418,7 @@ export default function DryingMode(props: Props){ } const deviceStep = () => { + return ( Devices @@ -497,7 +497,7 @@ export default function DryingMode(props: Props){ {/* next - goes to the next step, hidden on the last step */} {currentStep !== steps.length - 1 && } {/* confirm - tells the parent to build the stages using the data, only visible on the last step */} - {currentStep === steps.length - 1 && interactionsToAdd && selectedDevice && } + {currentStep === steps.length - 1 && interactionsToAdd && selectedDevice && } ) } diff --git a/src/bin/binModes/StorageMode.tsx b/src/bin/binModes/StorageMode.tsx index 67f82dc..6d61c3a 100644 --- a/src/bin/binModes/StorageMode.tsx +++ b/src/bin/binModes/StorageMode.tsx @@ -1,9 +1,38 @@ -import { Box } from "@mui/material"; +import { Box, DialogContent, DialogTitle, DialogActions } from "@mui/material"; +import { ComponentSet } from "bin/conditioning/pickComponentSet"; +import { Device, Interaction } from "models"; +import { pond } from "protobuf-ts/pond"; +import React from "react"; + +interface InteractionToRemove { + deviceID: number, + //interactions: +} + +interface Props { + // the devices connected to the bin + devices: Device[] + confirm: (conflictingInteractions: Interaction[], deviceId: number, newInteractions: pond.MultiInteractionSettings, componentSets: ComponentSet[]) => void +} + +/** + * when a bin goes into storage mode then the auto top node needs to be set, should be handled in the controller actually + * interactions with connected controllers as sink should be deleted, and controllers should be set to off + * @returns + */ +export default function StorageMode(props: Props){ + + //load the interactions for all of the bins connected devices + + // -export default function StorageMode(){ return ( - - - + + Storage Mode + + {/* could have the explanation of what changing to storage mode does here */} + + + ) } \ No newline at end of file diff --git a/src/bin/conditioning/modeChangeDialog.tsx b/src/bin/conditioning/modeChangeDialog.tsx index ef2dfbd..33b8b01 100644 --- a/src/bin/conditioning/modeChangeDialog.tsx +++ b/src/bin/conditioning/modeChangeDialog.tsx @@ -439,7 +439,7 @@ if (!selectedDevice) return; conflictingSetInteractions.forEach(i => { let newStep: PromiseStep = { title: "Removing Interaction", - promise: interactionAPI.removeInteraction(selectedDevice.id(), i.key(), as) + promise: () => interactionAPI.removeInteraction(selectedDevice.id(), i.key(), as) } stage1.steps.push(newStep); }); @@ -452,7 +452,7 @@ if (!selectedDevice) return; steps: [ { title: "Adding Interactions", - promise: interactionAPI.addMultiInteractions(selectedDevice.id(), interactionsToAdd) + promise: () => interactionAPI.addMultiInteractions(selectedDevice.id(), interactionsToAdd) } ] } @@ -469,7 +469,7 @@ if (!selectedDevice) return; set.controllers.forEach(controller => { let newStep: PromiseStep = { title: "Updating " + controller.name(), - promise: componentAPI.update(selectedDevice.id(), controller.settings) + promise: () => componentAPI.update(selectedDevice.id(), controller.settings) } stage3.steps.push(newStep) }) diff --git a/src/common/PromiseProgress.tsx b/src/common/PromiseProgress.tsx index 0bc39e9..0ade5f5 100644 --- a/src/common/PromiseProgress.tsx +++ b/src/common/PromiseProgress.tsx @@ -11,7 +11,7 @@ export interface Stage { export interface Step { title: string - promise: Promise + promise: () => Promise onComplete?: () => void onError?: () => void } @@ -52,7 +52,7 @@ export function PromiseProgress(props: Props){ // mark step as in progress progMap.set(i+"-"+k, progress.InProgress); setCompletion(new Map(progMap)); - return step.promise + return step.promise() .then(() => { // mark step as completed progMap.set(i+"-"+k, progress.Complete); diff --git a/src/providers/pond/componentAPI.tsx b/src/providers/pond/componentAPI.tsx index a224952..e78b75a 100644 --- a/src/providers/pond/componentAPI.tsx +++ b/src/providers/pond/componentAPI.tsx @@ -417,7 +417,8 @@ export default function ComponentProvider(props: PropsWithChildren) { "&end=" + end + (keys ? "&keys=" + keys : "&keys=" + [deviceId.toString()]) + - (types ? "&types=" + types : "&types=" + ["device"]) + (types ? "&types=" + types : "&types=" + ["device"]) + + (as ? "&as=" + as : "") ) return new Promise>((resolve,reject) => { get(url).then(resp => { From 2c17fb655c52871eba4fd8b7349502fc9286aee0 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Wed, 8 Jul 2026 11:01:45 -0600 Subject: [PATCH 08/25] hotfix to add as to the historyBetween function in the component API so that it works when viewing as a team without direct user permission to it --- src/providers/pond/componentAPI.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/providers/pond/componentAPI.tsx b/src/providers/pond/componentAPI.tsx index a224952..e78b75a 100644 --- a/src/providers/pond/componentAPI.tsx +++ b/src/providers/pond/componentAPI.tsx @@ -417,7 +417,8 @@ export default function ComponentProvider(props: PropsWithChildren) { "&end=" + end + (keys ? "&keys=" + keys : "&keys=" + [deviceId.toString()]) + - (types ? "&types=" + types : "&types=" + ["device"]) + (types ? "&types=" + types : "&types=" + ["device"]) + + (as ? "&as=" + as : "") ) return new Promise>((resolve,reject) => { get(url).then(resp => { From 3fc663a4b601debe6fef70bfdefa2d4f99495274 Mon Sep 17 00:00:00 2001 From: Carter Date: Wed, 8 Jul 2026 15:56:03 -0600 Subject: [PATCH 09/25] removed interaction logs --- src/interactions/InteractionsOverview.tsx | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/interactions/InteractionsOverview.tsx b/src/interactions/InteractionsOverview.tsx index 450c44f..cbdc170 100644 --- a/src/interactions/InteractionsOverview.tsx +++ b/src/interactions/InteractionsOverview.tsx @@ -87,15 +87,6 @@ export default function InteractionsOverview(props: Props) { undefined ); - useEffect(() => { - console.log("[interactions:mount] initial interactions:", props.interactions.map(i => ({ - key: i.key(), - synced: i.status.synced, - lastUpdate: i.status.lastUpdate, - lastSynced: i.status.lastSynced, - }))); - }, []); - useEffect(() => { if (components !== prevComponents) { let initMappedComponents: Map = new Map(); @@ -106,17 +97,6 @@ export default function InteractionsOverview(props: Props) { } if (props.interactions !== prevInteractions) { - console.log("[interactions:effect] props.interactions changed, prevInteractions was", prevInteractions ? "defined" : "undefined"); - props.interactions.forEach((interaction: Interaction) => { - const key = interaction.key(); - const prevInteraction = prevInteractions?.find(p => p.key() === key); - console.log("[interactions:effect]", key, { - synced: interaction.status.synced, - lastUpdate: interaction.status.lastUpdate, - prevSynced: prevInteraction?.status.synced, - prevLastUpdate: prevInteraction?.status.lastUpdate, - }); - }); setAcceptedInteractions(prev => { const updated = new Set(prev); props.interactions.forEach((interaction: Interaction) => { @@ -125,7 +105,6 @@ export default function InteractionsOverview(props: Props) { if (!interaction.status.synced) { updated.delete(key); } else if (prevInteraction && !prevInteraction.status.synced) { - console.log("[interactions:effect] ACCEPTED", key); updated.add(key); } }); @@ -240,7 +219,6 @@ export default function InteractionsOverview(props: Props) { interactionsAPI .updateInteraction(Number(deviceID), settings, as) .then((_response: any) => { - console.log("[interactions:submit] API success, setting synced=false for index", index); let updatedDirtyInteractions = cloneDeep(dirtyInteractions); updatedDirtyInteractions.set(index, false); setDirtyInteractions(updatedDirtyInteractions); @@ -249,7 +227,6 @@ export default function InteractionsOverview(props: Props) { if (updated[index]) { updated[index].status.synced = false; updated[index].status.lastUpdate = moment().toISOString(); - console.log("[interactions:submit] local state updated for", updated[index].key()); } return updated; }); From 2c6799bcc1a68e6e695a3546c226d00b8cce8255 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 9 Jul 2026 10:21:24 -0600 Subject: [PATCH 10/25] working on storage building the stuff to send up --- src/bin/binModes/StorageMode.tsx | 72 +++++++++++++++++++++++++++++--- src/models/Plenum.ts | 4 +- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/bin/binModes/StorageMode.tsx b/src/bin/binModes/StorageMode.tsx index 6d61c3a..70c4f72 100644 --- a/src/bin/binModes/StorageMode.tsx +++ b/src/bin/binModes/StorageMode.tsx @@ -1,18 +1,26 @@ -import { Box, DialogContent, DialogTitle, DialogActions } from "@mui/material"; -import { ComponentSet } from "bin/conditioning/pickComponentSet"; -import { Device, Interaction } from "models"; +import { DialogContent, DialogTitle, DialogActions } from "@mui/material"; +import { Component, Device, Interaction } from "models"; +import { Ambient } from "models/Ambient"; +import { Controller } from "models/Controller"; +import { GrainCable } from "models/GrainCable"; +import { Plenum } from "models/Plenum"; import { pond } from "protobuf-ts/pond"; -import React from "react"; +import { quack } from "protobuf-ts/quack"; +import React, { useEffect, useState } from "react"; +import { DeviceChangeData } from "./BinModeController"; +import { useInteractionsAPI } from "providers"; +import { AxiosResponse } from "axios"; interface InteractionToRemove { deviceID: number, - //interactions: } interface Props { // the devices connected to the bin devices: Device[] - confirm: (conflictingInteractions: Interaction[], deviceId: number, newInteractions: pond.MultiInteractionSettings, componentSets: ComponentSet[]) => void + deviceComponents: Map + binPrefs?: Map + confirm: (deviceData: DeviceChangeData[]) => void } /** @@ -21,6 +29,58 @@ interface Props { * @returns */ export default function StorageMode(props: Props){ + const {devices, deviceComponents, binPrefs} = props + const interactionsAPI = useInteractionsAPI() + + useEffect(()=>{ + let interactionPromises: Promise[] = [] + let deviceContext: {device: Device, plenums: Plenum[], ambients: Ambient[], cables: GrainCable[], controllers: Controller[]}[] = [] + devices.forEach(device => { + let plenums: Plenum[] = [] + let ambients: Ambient[] = [] + let cables: GrainCable[] = [] //since this mode is going to add notification interactions to cables they should be filtered as well + let controllers: Controller[] = [] //because storage is just setting things to false i dont think i need to seperate fans from heaters + //sort the components into the plenum, ambient, grain cable, and controller arrays + deviceComponents.get(device.id())!.forEach(comp => { + let pref = binPrefs?.get(comp.key()); + if (pref) { + if (pref.type) { + if (pref.type === pond.BinComponent.BIN_COMPONENT_PLENUM){ + plenums.push(Plenum.create(comp)); + //sourceMap.set(comp.locationString(), comp) + } + if (pref.type === pond.BinComponent.BIN_COMPONENT_AMBIENT){ + ambients.push(Ambient.create(comp)); + //sourceMap.set(comp.locationString(), comp) + } + if (pref.type === pond.BinComponent.BIN_COMPONENT_HEATER || pref.type === pond.BinComponent.BIN_COMPONENT_FAN) { + let controller = Controller.create(comp); + controller.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF; + controllers.push(controller); + //sinkMap.set(comp.locationString(), comp) + } + if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE){ + let cable = GrainCable.create(comp) + cables.push(cable) + } + } + } + }); + //after sorting it will need to load the interactions for the device + deviceContext.push({device, plenums, ambients, cables, controllers}) + interactionPromises.push(interactionsAPI.listInteractionsByDevice(device.id())) + }) + + Promise.all(interactionPromises).then(resp => { + //the index of this should match the device index in the devices array + resp.forEach((interactions,i) => { + interactions.forEach(interaction => { + + }) + }) + }) + + },[devices, binPrefs, deviceComponents]) //load the interactions for all of the bins connected devices diff --git a/src/models/Plenum.ts b/src/models/Plenum.ts index b88b8a0..5ec161f 100644 --- a/src/models/Plenum.ts +++ b/src/models/Plenum.ts @@ -108,7 +108,9 @@ export class Plenum { return quack.ComponentID.fromObject({ type: this.settings.type, addressType: this.settings.addressType, - address: this.settings.address + address: this.settings.address, + expansionLine: this.settings.expansionLine, + muxLine: this.settings.muxLine }); } From fc347938d2c93855c2f5edc2519711f803baf104 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 9 Jul 2026 11:32:32 -0600 Subject: [PATCH 11/25] hotfix for analog pressure, is shoudl support expansion and not cable ID --- src/component/ComponentSettings.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/component/ComponentSettings.tsx b/src/component/ComponentSettings.tsx index a779692..d66a3c1 100644 --- a/src/component/ComponentSettings.tsx +++ b/src/component/ComponentSettings.tsx @@ -518,7 +518,6 @@ export default function ComponentSettings(props: Props) { formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE || formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_PRESSURE_CABLE || - formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_ANALOG_PRESSURE || (formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_CAPACITOR_CABLE && formComponent.subType() === quack.CapacitorCableSubtype.CAPACITOR_CABLE_SUBTYPE_FROG)) @@ -526,7 +525,9 @@ export default function ComponentSettings(props: Props) { const supportsExpansion = () => { return ((formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE || - formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE) && + formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE || + formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_ANALOG_PRESSURE + ) && formComponent.settings.addressType === quack.AddressType.ADDRESS_TYPE_I2C) } From 487afc6464a973279852a1de710a0e843c0e2300 Mon Sep 17 00:00:00 2001 From: Carter Date: Thu, 9 Jul 2026 11:58:32 -0600 Subject: [PATCH 12/25] removed component type logs --- src/pbHelpers/ComponentType.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pbHelpers/ComponentType.tsx b/src/pbHelpers/ComponentType.tsx index c31154b..860e2aa 100644 --- a/src/pbHelpers/ComponentType.tsx +++ b/src/pbHelpers/ComponentType.tsx @@ -534,7 +534,7 @@ export function simpleLineChartData( } }); } - console.log(lineData) + // console.log(lineData) return lineData; } From d124db62e0f91cd25d856ff6d58fdeaf52767631 Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 10 Jul 2026 10:11:46 -0600 Subject: [PATCH 13/25] got rid of scanned logs --- src/device/autoDetect/DeviceScannedComponents.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/device/autoDetect/DeviceScannedComponents.tsx b/src/device/autoDetect/DeviceScannedComponents.tsx index 95c3759..4cb94e1 100644 --- a/src/device/autoDetect/DeviceScannedComponents.tsx +++ b/src/device/autoDetect/DeviceScannedComponents.tsx @@ -75,7 +75,6 @@ export default function DeviceScannedComponents(props: Props){ let expanders: quack.AddressData[] = [] //filter the address data let addr = scannedI2C?.settings?.foundAddresses - console.log(addr) if(addr){ addr.forEach(addrData => { if(!i2cBlacklistAddresses.includes(addrData.address)){ @@ -87,7 +86,6 @@ export default function DeviceScannedComponents(props: Props){ } }) } - console.log(valid) setExpanderAddresses(expanders) setValidI2CComponentAddresses(valid) },[scannedI2C]) @@ -213,14 +211,13 @@ export default function DeviceScannedComponents(props: Props){ }) }) } - console.log(cloneList) setComponents(cloneList) } const removeScan = (key: string, type: pond.ObjectType) => { deviceAPI.removeFoundComponents(device.settings.deviceId, key, type) .then(resp => { - console.log("Cleared this scan") + // console.log("Cleared this scan") }).catch(err => { console.log("There was a problem clearing this scan") }) @@ -229,7 +226,7 @@ export default function DeviceScannedComponents(props: Props){ const removeAllScans = () => { deviceAPI.removeAllFoundComponents(device.settings.deviceId) .then(resp => { - console.log("Cleared all scans") + // console.log("Cleared all scans") refreshCallback() }).catch(err => { console.log("There was a problem clearing scans") From 208a657528b863ae22f6dafe65c0785bdc7b0676 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Fri, 10 Jul 2026 11:59:42 -0600 Subject: [PATCH 14/25] finished the functional part of storage as well as modified the drying dialog to remove notification interactions from the cables --- src/bin/binModes/BinModeController.tsx | 43 +++++++--- src/bin/binModes/DryingMode.tsx | 29 +++++-- src/bin/binModes/StorageMode.tsx | 112 +++++++++++++++++++++---- src/models/Ambient.ts | 15 +++- src/models/Controller.ts | 15 +++- src/models/GrainCable.ts | 15 +++- src/models/Plenum.ts | 11 ++- 7 files changed, 193 insertions(+), 47 deletions(-) diff --git a/src/bin/binModes/BinModeController.tsx b/src/bin/binModes/BinModeController.tsx index f4f825e..32dd6af 100644 --- a/src/bin/binModes/BinModeController.tsx +++ b/src/bin/binModes/BinModeController.tsx @@ -11,12 +11,25 @@ import { PromiseProgress, Stage, Step } from "common/PromiseProgress"; import { useBinAPI, useComponentAPI, useInteractionsAPI } from "providers"; import { ComponentSet } from "bin/conditioning/pickComponentSet"; import { cloneDeep } from "lodash"; +import { Controller } from "models/Controller"; export interface DeviceChangeData { + /** + * the id of the device changes are being made to + */ deviceId: number + /** + * the array of interactions to remove from the device + */ toRemove: Interaction[] + /** + * the new interactions to add to the device + */ toAdd?: pond.MultiInteractionSettings - componentSets: ComponentSet[] + /** + * the controllers that need to have there state updated + */ + controllers: Controller[] } interface Props { @@ -99,16 +112,16 @@ export default function BinModeController(props: Props) { promise: () => interactionAPI.addMultiInteractions(data.deviceId, newInteractions) }) } - data.componentSets.forEach(set => { - set.controllers.forEach(controller => { - let newStep: Step = { - title: "Updating " + controller.name(), - promise: () => componentAPI.update(data.deviceId, controller.settings) - } - stage3.steps.push(newStep) - }) + + data.controllers.forEach(controller => { + let newStep: Step = { + title: "Updating " + controller.name(), + promise: () => componentAPI.update(data.deviceId, controller.settings) + } + stage3.steps.push(newStep) }) - }) + }) + if(stage1.steps.length > 0){ stages.push(stage1) } @@ -195,7 +208,15 @@ export default function BinModeController(props: Props) { case pond.BinMode.BIN_MODE_COOLDOWN: return default: - return {}}/> + return { + buildStages(deviceData) + setShowProgress(true) + }}/> } } diff --git a/src/bin/binModes/DryingMode.tsx b/src/bin/binModes/DryingMode.tsx index 40d071a..d0f7bb0 100644 --- a/src/bin/binModes/DryingMode.tsx +++ b/src/bin/binModes/DryingMode.tsx @@ -19,6 +19,7 @@ import BinConditioningInteraction from "bin/BinConditioningInteraction"; import ConditionDisplay from "./conditionDisplay"; import React from "react"; import { DeviceChangeData } from "./BinModeController"; +import { GrainCable } from "models/GrainCable"; const useStyles = makeStyles((theme: Theme) => { @@ -66,6 +67,7 @@ export default function DryingMode(props: Props){ const [ambients, setAmbients] = useState([]) const [heaters, setHeaters] = useState([]) const [fans, setFans] = useState([]) + const [cables, setCables] = useState([]) const [currentStep, setCurrentStep] = useState(0) const [interactionLoading,setInteractionsLoading] = useState(false) @@ -87,7 +89,7 @@ export default function DryingMode(props: Props){ if (!deviceComponents.get(selectedDevice.id())) return; var plenums: Plenum[] = []; var ambients: Ambient[] = []; - //var grainCables: GrainCable[] = []; + var grainCables: GrainCable[] = []; var heaters: Controller[] = []; var fans: Controller[] = []; var sinkMap: Map = new Map() @@ -105,8 +107,10 @@ export default function DryingMode(props: Props){ ambients.push(Ambient.create(comp)); sourceMap.set(comp.locationString(), comp) } - // if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE) - // grainCables.push(GrainCable.create(comp)); + if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE){ + grainCables.push(GrainCable.create(comp)); + sourceMap.set(comp.locationString(), comp) + } if (pref.type === pond.BinComponent.BIN_COMPONENT_HEATER) { let heater = Controller.create(comp); heater.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF; @@ -128,7 +132,7 @@ export default function DryingMode(props: Props){ setFans(fans); setSourceMap(sourceMap) setSinkMap(sinkMap) - + setCables(grainCables) //also load the interactions for the selected device so that we can find any conflicting ones setInteractionsLoading(true) interactionAPI.listInteractionsByDevice(selectedDevice.id()).then(resp => { @@ -315,11 +319,12 @@ export default function DryingMode(props: Props){ //this function just takes in the sets as they change and creates the list of conflicting interactions and the settings to create the new ones and returns them const createInteractions = (sets: ComponentSet[]) => { + console.log("create interactions") if(!selectedDevice) return undefined //if there is no device that was selected, do nothing let multiInteractionSettings: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create() let conflictingInteractions: Interaction[] = [] let linkedComponents = deviceComponents.get(selectedDevice.id()); - //loop through the sets + //loop through the sets to find interactions to remove as well as set the new ones sets.forEach((set) => { //loop through the controllers set.controllers.forEach(controller => { @@ -335,6 +340,7 @@ export default function DryingMode(props: Props){ } return conflicting; }); + conflictingInteractions = conflictingInteractions.concat(c) // if the controller is a heater create heater interaction if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER){ @@ -351,6 +357,17 @@ export default function DryingMode(props: Props){ } }) }) + console.log(cables) + cables.forEach(cable => { + console.log(cable.name()) + let c = existingInteractions.filter(i => { + if (sameComponentID(cable.location(), i.settings.source) && !i.settings.sink && i.settings.notifications?.notify) { + return true + } + }); + conflictingInteractions = conflictingInteractions.concat(c) + }) + setComponentSets([...sets]) return { conflicting: conflictingInteractions, @@ -497,7 +514,7 @@ export default function DryingMode(props: Props){ {/* next - goes to the next step, hidden on the last step */} {currentStep !== steps.length - 1 && } {/* confirm - tells the parent to build the stages using the data, only visible on the last step */} - {currentStep === steps.length - 1 && interactionsToAdd && selectedDevice && } + {currentStep === steps.length - 1 && interactionsToAdd && selectedDevice && } ) } diff --git a/src/bin/binModes/StorageMode.tsx b/src/bin/binModes/StorageMode.tsx index 70c4f72..e9a0926 100644 --- a/src/bin/binModes/StorageMode.tsx +++ b/src/bin/binModes/StorageMode.tsx @@ -1,5 +1,5 @@ -import { DialogContent, DialogTitle, DialogActions } from "@mui/material"; -import { Component, Device, Interaction } from "models"; +import { DialogContent, DialogTitle, DialogActions, Button } from "@mui/material"; +import { Bin, Component, Device, Interaction } from "models"; import { Ambient } from "models/Ambient"; import { Controller } from "models/Controller"; import { GrainCable } from "models/GrainCable"; @@ -10,15 +10,16 @@ import React, { useEffect, useState } from "react"; import { DeviceChangeData } from "./BinModeController"; import { useInteractionsAPI } from "providers"; import { AxiosResponse } from "axios"; - -interface InteractionToRemove { - deviceID: number, -} +import { componentIDToString } from "pbHelpers/Component"; +import { ComponentSet } from "bin/conditioning/pickComponentSet"; +import moment from "moment"; +import { lowerCase } from "lodash"; interface Props { // the devices connected to the bin devices: Device[] deviceComponents: Map + bin: Bin binPrefs?: Map confirm: (deviceData: DeviceChangeData[]) => void } @@ -29,13 +30,17 @@ interface Props { * @returns */ export default function StorageMode(props: Props){ - const {devices, deviceComponents, binPrefs} = props + const {devices, deviceComponents, bin, binPrefs, confirm} = props const interactionsAPI = useInteractionsAPI() + const [data, setData] = useState([]) + const [loading, setLoading] = useState(false) + const [loadErr, setLoadErr] = useState(false) useEffect(()=>{ let interactionPromises: Promise[] = [] let deviceContext: {device: Device, plenums: Plenum[], ambients: Ambient[], cables: GrainCable[], controllers: Controller[]}[] = [] devices.forEach(device => { + if (!deviceComponents.get(device.id())) return; let plenums: Plenum[] = [] let ambients: Ambient[] = [] let cables: GrainCable[] = [] //since this mode is going to add notification interactions to cables they should be filtered as well @@ -70,29 +75,106 @@ export default function StorageMode(props: Props){ deviceContext.push({device, plenums, ambients, cables, controllers}) interactionPromises.push(interactionsAPI.listInteractionsByDevice(device.id())) }) - + setLoading(true) + let deviceChangeData: DeviceChangeData[] = [] Promise.all(interactionPromises).then(resp => { //the index of this should match the device index in the devices array resp.forEach((interactions,i) => { + let toRemove: Interaction[] = [] + let ctx = deviceContext[i] + let sensorsAddr = ctx.plenums.concat(ctx.ambients).map(sensor => sensor.locationString()) + let controllerAddr = ctx.controllers.map(cont => cont.locationString()) + let cableAddr = ctx.cables.map(cab => cab.locationString()) + let cablesWithInteraction = new Set() + interactions.forEach(interaction => { - + //first check if the interaction has a sink in the devices fans or heaters and is being controlled by a plenum or ambient + //this should make it so that it only removes conditioning interactions and not all interactions on the device + let source = interaction.settings.source + let sink = interaction.settings.sink + //this checks if the interaction is a 'conditioning' interaction that needs to be removed + if(source && sink){ + if(controllerAddr.includes(componentIDToString(sink)) && sensorsAddr.includes(componentIDToString(source))){ + toRemove.push(interaction) + } + } + //need to make sure that it is a notification only interaction and not a controlling one + //it is possible to control things with cables and we want to leave those alone and not consider them notification interactions + //the reason we check that there is no sink is because even control interactions can still send notifications + //we want interactions that effectively ONLY send notifications + if (source && !sink && cableAddr.includes(componentIDToString(source))) { + //check if notifications is true + if(interaction.settings.notifications?.notify){ + cablesWithInteraction.add(componentIDToString(source)) + } + } }) + let cablesNeedingNotification = ctx.cables.filter( + cable => !cablesWithInteraction.has(cable.locationString()) + ) + + let dd: DeviceChangeData = { + deviceId: ctx.device.id(), + toRemove: toRemove, + controllers: ctx.controllers, + + } + + if(cablesNeedingNotification.length > 0){ + let toAdd: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create() + cablesNeedingNotification.forEach(cable => { + //create the new interaction here + let notification: pond.InteractionSettings = pond.InteractionSettings.create({ + source: cable.location(), + schedule: pond.InteractionSchedule.create({ + timeOfDayStart: "00:00", + timeOfDayEnd: "24:00", + timezone: moment.tz.guess(), + weekdays: moment.weekdays().map(d => lowerCase(d)) + }), + conditions: [pond.InteractionCondition.create({ + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, + comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN, + value: bin.settings.highTemp + })], + result: pond.InteractionResult.create({ + type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT + }), + notifications: pond.InteractionNotifications.create({ + notify: true + }), + }) + + //push it to toAdd.interactions + toAdd.interactions.push(notification) + + }) + //add toAdd to dd + dd.toAdd = toAdd + } + deviceChangeData.push(dd) }) + setData(deviceChangeData) + }).catch(err => { + //TODO: display a snackBar message saying there was an issue loading the data for one or more devices + setData([]) + setLoadErr(true) + }).finally(() => { + setLoading(false) }) - },[devices, binPrefs, deviceComponents]) - //load the interactions for all of the bins connected devices - - // - return ( Storage Mode {/* could have the explanation of what changing to storage mode does here */} - + + + ) } \ No newline at end of file diff --git a/src/models/Ambient.ts b/src/models/Ambient.ts index d2c7577..4fde1ea 100644 --- a/src/models/Ambient.ts +++ b/src/models/Ambient.ts @@ -108,18 +108,25 @@ export class Ambient { return quack.ComponentID.fromObject({ type: this.settings.type, addressType: this.settings.addressType, - address: this.settings.address + address: this.settings.address, + expansionLine: this.settings.expansionLine, + muxLine: this.settings.muxLine }); } public locationString(): string { - return ( - or(this.settings.type, 0).toString() + + let compositeLocation = or(this.settings.type, 0).toString() + "-" + or(this.settings.addressType, 0).toString() + "-" + or(this.settings.address, 0).toString() - ); + if(this.settings.expansionLine){ + compositeLocation = compositeLocation + "-" + this.settings.expansionLine + } + if(this.settings.muxLine){ + compositeLocation = compositeLocation + ":" + this.settings.muxLine + } + return compositeLocation; } public type(): quack.ComponentType { diff --git a/src/models/Controller.ts b/src/models/Controller.ts index 4e7170c..c29ba52 100644 --- a/src/models/Controller.ts +++ b/src/models/Controller.ts @@ -82,18 +82,25 @@ export class Controller { return quack.ComponentID.fromObject({ type: this.settings.type, addressType: this.settings.addressType, - address: this.settings.address + address: this.settings.address, + expansionLine: this.settings.expansionLine, + muxLine: this.settings.muxLine }); } public locationString(): string { - return ( - or(this.settings.type, 0).toString() + + let compositeLocation = or(this.settings.type, 0).toString() + "-" + or(this.settings.addressType, 0).toString() + "-" + or(this.settings.address, 0).toString() - ); + if(this.settings.expansionLine){ + compositeLocation = compositeLocation + "-" + this.settings.expansionLine + } + if(this.settings.muxLine){ + compositeLocation = compositeLocation + ":" + this.settings.muxLine + } + return compositeLocation; } public type(): quack.ComponentType { diff --git a/src/models/GrainCable.ts b/src/models/GrainCable.ts index 4de1ae4..e3e988b 100644 --- a/src/models/GrainCable.ts +++ b/src/models/GrainCable.ts @@ -230,18 +230,25 @@ export class GrainCable { return quack.ComponentID.fromObject({ type: this.settings.type, addressType: this.settings.addressType, - address: this.settings.address + address: this.settings.address, + expansionLine: this.settings.expansionLine, + muxLine: this.settings.muxLine }); } public locationString(): string { - return ( - or(this.settings.type, 0).toString() + + let compositeLocation = or(this.settings.type, 0).toString() + "-" + or(this.settings.addressType, 0).toString() + "-" + or(this.settings.address, 0).toString() - ); + if(this.settings.expansionLine){ + compositeLocation = compositeLocation + "-" + this.settings.expansionLine + } + if(this.settings.muxLine){ + compositeLocation = compositeLocation + ":" + this.settings.muxLine + } + return compositeLocation; } public type(): quack.ComponentType { diff --git a/src/models/Plenum.ts b/src/models/Plenum.ts index 5ec161f..15f99bc 100644 --- a/src/models/Plenum.ts +++ b/src/models/Plenum.ts @@ -115,13 +115,18 @@ export class Plenum { } public locationString(): string { - return ( - or(this.settings.type, 0).toString() + + let compositeLocation = or(this.settings.type, 0).toString() + "-" + or(this.settings.addressType, 0).toString() + "-" + or(this.settings.address, 0).toString() - ); + if(this.settings.expansionLine){ + compositeLocation = compositeLocation + "-" + this.settings.expansionLine + } + if(this.settings.muxLine){ + compositeLocation = compositeLocation + ":" + this.settings.muxLine + } + return compositeLocation; } public type(): quack.ComponentType { From aa346a6a5a03694820af59935ec5a375b39dd7e2 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Fri, 10 Jul 2026 15:27:47 -0600 Subject: [PATCH 15/25] set up the bones for the humidity mode --- src/bin/binModes/BinModeController.tsx | 14 +- src/bin/binModes/DryingMode.tsx | 11 +- src/bin/binModes/HydratingMode.tsx | 401 ++++++++++++++++++++++++- 3 files changed, 415 insertions(+), 11 deletions(-) diff --git a/src/bin/binModes/BinModeController.tsx b/src/bin/binModes/BinModeController.tsx index 32dd6af..6c4fead 100644 --- a/src/bin/binModes/BinModeController.tsx +++ b/src/bin/binModes/BinModeController.tsx @@ -194,6 +194,7 @@ export default function BinModeController(props: Props) { //setNumSteps(DryingStepCount) return case pond.BinMode.BIN_MODE_HYDRATING: - return + return { + buildStages(deviceData) + setShowProgress(true) + }} + /> case pond.BinMode.BIN_MODE_COOLDOWN: return default: diff --git a/src/bin/binModes/DryingMode.tsx b/src/bin/binModes/DryingMode.tsx index d0f7bb0..716e726 100644 --- a/src/bin/binModes/DryingMode.tsx +++ b/src/bin/binModes/DryingMode.tsx @@ -1,5 +1,4 @@ -import { ArrowBack, ArrowForward } from "@mui/icons-material"; -import { Box, Theme, Stepper, Step, StepLabel, Typography, RadioGroup, Radio, FormControlLabel, Grid2, Button, Autocomplete, TextField, DialogTitle, DialogContent, DialogActions, CircularProgress } from "@mui/material"; +import { Box, Theme, Stepper, Step, StepLabel, Typography, RadioGroup, Radio, FormControlLabel, Button, Autocomplete, TextField, DialogTitle, DialogContent, DialogActions, CircularProgress } from "@mui/material"; import { makeStyles } from "@mui/styles"; import ConditioningSelector from "bin/conditioning/conditioningSelector"; import { Component, Device, Interaction } from "models"; @@ -7,7 +6,7 @@ import { Ambient } from "models/Ambient"; import { Controller } from "models/Controller"; import { Plenum } from "models/Plenum"; import { pond, quack } from "protobuf-ts/pond"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { ComponentSet } from "bin/conditioning/pickComponentSet" import { componentIDToString, sameComponentID } from "pbHelpers/Component"; import moment from "moment"; @@ -15,7 +14,6 @@ import { lowerCase } from "lodash"; import { GetGrainExtensionMap } from "grain"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { useGlobalState, useInteractionsAPI } from "providers"; -import BinConditioningInteraction from "bin/BinConditioningInteraction"; import ConditionDisplay from "./conditionDisplay"; import React from "react"; import { DeviceChangeData } from "./BinModeController"; @@ -46,6 +44,7 @@ interface Props { deviceComponents: Map binPrefs?: Map grain?: pond.Grain + customGrain?: pond.GrainSettings cancel: () => void confirm: (deviceData: DeviceChangeData[]) => void } @@ -54,7 +53,7 @@ const steps = [{label: "Style"}, {label: "Device"}, {label: "Interaction"}] //this is our simple drying mode the way it currently works, just building the dialog to be a little more trnasparent and customizable as to what the interaction is doing export default function DryingMode(props: Props){ - const {devices, deviceComponents, binPrefs, grain, cancel, confirm} = props + const {devices, deviceComponents, binPrefs, grain, customGrain, cancel, confirm} = props const grainExtensionMap = GetGrainExtensionMap(); const classes = useStyles() const [{user}] = useGlobalState() @@ -478,6 +477,8 @@ export default function DryingMode(props: Props){ if(sink && source){ return ( { + return ({ + stepper: { + padding: theme.spacing(0.5) + }, + }) +}); + +interface Step { + label: string; + completed?: boolean; +} + +interface Option { + label: string; + device: Device; + icon?: string; + } + +interface Props { + devices: Device[] + deviceComponents: Map + binPrefs?: Map + grain?: pond.Grain + customGrain?: pond.GrainSettings + cancel: () => void + confirm: (deviceData: DeviceChangeData[]) => void +} + +const steps = [{label: "Style"}, {label: "Device"}, {label: "Interaction"}] + +//this is our simple drying mode the way it currently works, just building the dialog to be a little more trnasparent and customizable as to what the interaction is doing +export default function DryingMode(props: Props){ + const {devices, deviceComponents, binPrefs, grain, customGrain, cancel, confirm} = props + const grainExtensionMap = GetGrainExtensionMap(); + const classes = useStyles() + const [{user}] = useGlobalState() + const interactionAPI = useInteractionsAPI() + const [options, setOptions] = useState([]) + const [deviceOption, setDeviceOption] = useState