diff --git a/package-lock.json b/package-lock.json index 19352f1..3610bee 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#master", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", @@ -146,9 +146,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": { @@ -156,21 +156,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", @@ -233,14 +233,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" @@ -482,9 +482,9 @@ } }, "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": { @@ -507,14 +507,14 @@ } }, "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" @@ -2481,9 +2481,9 @@ } }, "node_modules/@mediapipe/tasks-vision": { - "version": "0.10.8", - "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.8.tgz", - "integrity": "sha512-Rp7ll8BHrKB3wXaRFKhrltwZl1CiXGdibPxuWXvqGnKTnv8fqa/nvftYNuSbf+pbJWKYCXdBtYTITdAUTGGh0Q==", + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", + "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==", "license": "Apache-2.0" }, "node_modules/@mui/core-downloads-tracker": { @@ -3239,28 +3239,27 @@ } }, "node_modules/@react-spring/animated": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.6.1.tgz", - "integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==", + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", + "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", "license": "MIT", "dependencies": { - "@react-spring/shared": "~9.6.1", - "@react-spring/types": "~9.6.1" + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/@react-spring/core": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.6.1.tgz", - "integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==", + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", + "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", "license": "MIT", "dependencies": { - "@react-spring/animated": "~9.6.1", - "@react-spring/rafz": "~9.6.1", - "@react-spring/shared": "~9.6.1", - "@react-spring/types": "~9.6.1" + "@react-spring/animated": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" }, "funding": { "type": "opencollective", @@ -3271,34 +3270,34 @@ } }, "node_modules/@react-spring/rafz": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.6.1.tgz", - "integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==", + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", + "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", "license": "MIT" }, "node_modules/@react-spring/shared": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.6.1.tgz", - "integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==", + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", + "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", "license": "MIT", "dependencies": { - "@react-spring/rafz": "~9.6.1", - "@react-spring/types": "~9.6.1" + "@react-spring/rafz": "~9.7.5", + "@react-spring/types": "~9.7.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/@react-spring/three": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.6.1.tgz", - "integrity": "sha512-Tyw2YhZPKJAX3t2FcqvpLRb71CyTe1GvT3V+i+xJzfALgpk10uPGdGaQQ5Xrzmok1340DAeg2pR/MCfaW7b8AA==", + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.7.5.tgz", + "integrity": "sha512-RxIsCoQfUqOS3POmhVHa1wdWS0wyHAUway73uRLp3GAL5U2iYVNdnzQsep6M2NZ994BlW8TcKuMtQHUqOsy6WA==", "license": "MIT", "dependencies": { - "@react-spring/animated": "~9.6.1", - "@react-spring/core": "~9.6.1", - "@react-spring/shared": "~9.6.1", - "@react-spring/types": "~9.6.1" + "@react-spring/animated": "~9.7.5", + "@react-spring/core": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" }, "peerDependencies": { "@react-three/fiber": ">=6.0", @@ -3307,45 +3306,44 @@ } }, "node_modules/@react-spring/types": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.6.1.tgz", - "integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==", + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", + "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", "license": "MIT" }, "node_modules/@react-three/drei": { - "version": "9.105.6", - "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.105.6.tgz", - "integrity": "sha512-JBgYeV36N9N9f1c3o1ZfLYW4rXZA7UQTq32Y8s3DEF6lwj1/y+RP/yq2VG5I8OzUPl7gsmWdy8fpWZgrlAqUpQ==", + "version": "9.122.0", + "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.122.0.tgz", + "integrity": "sha512-SEO/F/rBCTjlLez7WAlpys+iGe9hty4rNgjZvgkQeXFSiwqD4Hbk/wNHMAbdd8vprO2Aj81mihv4dF5bC7D0CA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.11.2", - "@mediapipe/tasks-vision": "0.10.8", - "@monogrid/gainmap-js": "^3.0.5", - "@react-spring/three": "~9.6.1", - "@use-gesture/react": "^10.2.24", - "camera-controls": "^2.4.2", + "@babel/runtime": "^7.26.0", + "@mediapipe/tasks-vision": "0.10.17", + "@monogrid/gainmap-js": "^3.0.6", + "@react-spring/three": "~9.7.5", + "@use-gesture/react": "^10.3.1", + "camera-controls": "^2.9.0", "cross-env": "^7.0.3", - "detect-gpu": "^5.0.28", + "detect-gpu": "^5.0.56", "glsl-noise": "^0.0.0", - "hls.js": "1.3.5", - "maath": "^0.10.7", - "meshline": "^3.1.6", + "hls.js": "^1.5.17", + "maath": "^0.10.8", + "meshline": "^3.3.1", "react-composer": "^5.0.3", - "stats-gl": "^2.0.0", + "stats-gl": "^2.2.8", "stats.js": "^0.17.0", "suspend-react": "^0.1.3", - "three-mesh-bvh": "^0.7.0", - "three-stdlib": "^2.29.9", - "troika-three-text": "^0.49.0", + "three-mesh-bvh": "^0.7.8", + "three-stdlib": "^2.35.6", + "troika-three-text": "^0.52.0", "tunnel-rat": "^0.1.2", - "utility-types": "^3.10.0", - "uuid": "^9.0.1", - "zustand": "^3.7.1" + "utility-types": "^3.11.0", + "zustand": "^5.0.1" }, "peerDependencies": { - "@react-three/fiber": ">=8.0", - "react": ">=18.0", - "react-dom": ">=18.0", + "@react-three/fiber": "^8", + "react": "^18", + "react-dom": "^18", "three": ">=0.137" }, "peerDependenciesMeta": { @@ -3366,6 +3364,35 @@ "three": ">= 0.159.0" } }, + "node_modules/@react-three/drei/node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/@react-three/fiber": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz", @@ -6986,9 +7013,9 @@ } }, "node_modules/axios": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz", - "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", + "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.16.0", @@ -6998,16 +7025,16 @@ } }, "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" @@ -8050,9 +8077,9 @@ } }, "node_modules/deeks": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/deeks/-/deeks-3.1.0.tgz", - "integrity": "sha512-e7oWH1LzIdv/prMQ7pmlDlaVoL64glqzvNgkgQNgyec9ORPHrT2jaOqMtRyqJuwWjtfb6v+2rk9pmaHj+F137A==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/deeks/-/deeks-3.2.1.tgz", + "integrity": "sha512-D/o0k3pCG1aI1cxb/dDiWmtMc4Rh7ZQBybXpfMsw9Rbtqwg8kUA9SpYkWcw0pAUjZSnPm8MluctiS0o68r69jQ==", "license": "MIT", "engines": { "node": ">= 16" @@ -8182,9 +8209,9 @@ "license": "MIT" }, "node_modules/doc-path": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/doc-path/-/doc-path-4.1.1.tgz", - "integrity": "sha512-h1ErTglQAVv2gCnOpD3sFS6uolDbOKHDU1BZq+Kl3npPqroU3dYL42lUgMfd5UimlwtRgp7C9dLGwqQ5D2HYgQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/doc-path/-/doc-path-4.1.4.tgz", + "integrity": "sha512-yw5D++UCIB6a033PvQaUvSpW2QuKW0+DOId763n0Q4z3brxS7G8oQr8yBQ1nQFkognKrAVrV6I55TLeU9cfXTg==", "license": "MIT", "engines": { "node": ">=16" @@ -8357,6 +8384,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", @@ -8416,15 +8462,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" @@ -8764,9 +8813,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "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": [ { @@ -8977,15 +9026,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": { @@ -9060,18 +9109,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" @@ -9316,9 +9368,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "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": { @@ -9535,9 +9587,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" @@ -9547,9 +9599,9 @@ } }, "node_modules/hls.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.3.5.tgz", - "integrity": "sha512-uybAvKS6uDe0MnWNEPnO0krWVr+8m2R0hJ/viql8H3MVK+itq8gGQuIYoFHL3rECkIpNH98Lw8YuuWMKZxp3Ew==", + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz", + "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==", "license": "Apache-2.0" }, "node_modules/hoist-non-react-statics": { @@ -9913,6 +9965,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", @@ -10356,10 +10424,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" @@ -10381,13 +10459,13 @@ } }, "node_modules/json-2-csv": { - "version": "5.5.10", - "resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-5.5.10.tgz", - "integrity": "sha512-Dep8wO3Fr5wNjQevO2Z8Y7yeee/nYSGRsi7q6zJDKEVHxXkXT+v21vxHmDX923UzmCXXkSo62HaTz6eTWzFLaw==", + "version": "5.5.11", + "resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-5.5.11.tgz", + "integrity": "sha512-kVuwgVL7rfad9ETf02ZZxJPuMR5ZSUn139+T34BfmVxYhb/IsAIm/LzEeQ8YLJmXfuQ5z7LUAFrgPZZM6VLJPw==", "license": "MIT", "dependencies": { - "deeks": "3.1.0", - "doc-path": "4.1.1" + "deeks": "3.2.1", + "doc-path": "4.1.4" }, "engines": { "node": ">= 16" @@ -11631,9 +11709,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -11666,9 +11744,9 @@ "license": "MIT" }, "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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": [ { @@ -11752,7 +11830,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#d6a992e46192a8ab8195235357318dc297738064", "dependencies": { "protobufjs": "^6.8.8" } @@ -12903,9 +12981,9 @@ } }, "node_modules/serialize-javascript": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", - "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", + "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", "engines": { @@ -13681,9 +13759,10 @@ "license": "MIT" }, "node_modules/three-mesh-bvh": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.6.tgz", - "integrity": "sha512-rCjsnxEqR9r1/C/lCqzGLS67NDty/S/eT6rAJfDvsanrIctTWdNoR4ZOGWewCB13h1QkVo2BpmC0wakj1+0m8A==", + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.8.tgz", + "integrity": "sha512-BGEZTOIC14U0XIRw3tO4jY7IjP7n7v24nv9JXS1CyeVRWOCkcOMhRnmENUjuV39gktAw4Ofhr0OvIAiTspQrrw==", + "deprecated": "Deprecated due to three.js version incompatibility. Please use v0.8.0, instead.", "license": "MIT", "peerDependencies": { "three": ">= 0.151.0" @@ -13861,14 +13940,14 @@ } }, "node_modules/troika-three-text": { - "version": "0.49.1", - "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.49.1.tgz", - "integrity": "sha512-lXGWxgjJP9kw4i4Wh+0k0Q/7cRfS6iOME4knKht/KozPu9GcFA9NnNpRvehIhrUawq9B0ZRw+0oiFHgRO+4Wig==", + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", + "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==", "license": "MIT", "dependencies": { "bidi-js": "^1.0.2", - "troika-three-utils": "^0.49.0", - "troika-worker-utils": "^0.49.0", + "troika-three-utils": "^0.52.4", + "troika-worker-utils": "^0.52.0", "webgl-sdf-generator": "1.1.1" }, "peerDependencies": { @@ -13876,18 +13955,18 @@ } }, "node_modules/troika-three-utils": { - "version": "0.49.0", - "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.49.0.tgz", - "integrity": "sha512-umitFL4cT+Fm/uONmaQEq4oZlyRHWwVClaS6ZrdcueRvwc2w+cpNQ47LlJKJswpqtMFWbEhOLy0TekmcPZOdYA==", + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz", + "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==", "license": "MIT", "peerDependencies": { "three": ">=0.125.0" } }, "node_modules/troika-worker-utils": { - "version": "0.49.0", - "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.49.0.tgz", - "integrity": "sha512-1xZHoJrG0HFfCvT/iyN41DvI/nRykiBtHqFkGaGgJwq5iXfIZFBiPPEHFpPpgyKM3Oo5ITHXP5wM2TNQszYdVg==", + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", + "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", "license": "MIT" }, "node_modules/ts-api-utils": { @@ -14341,19 +14420,6 @@ "node": ">= 4" } }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/package.json b/package.json index 3a76c43..76ac48b 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#master", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", 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/chat/ChatOutput.tsx b/src/chat/ChatOutput.tsx index 198690b..765809e 100644 --- a/src/chat/ChatOutput.tsx +++ b/src/chat/ChatOutput.tsx @@ -24,11 +24,11 @@ export default function ChatOutput(props: Props) { const ScrollToBottom = () => { const elementRef = useRef(null); - useEffect(() => + useEffect(() => { elementRef?.current?.scrollIntoView({ block: "end" - }) - ); + }); + }); let elem =
; return elem; }; 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/component/ComponentForm.tsx b/src/component/ComponentForm.tsx index dbebbb5..362b0ae 100644 --- a/src/component/ComponentForm.tsx +++ b/src/component/ComponentForm.tsx @@ -457,11 +457,29 @@ export default function ComponentForm(props: Props) { setForm(f); }; + const modeDictionaryValue = (dictionary: any, key: string): number | undefined => { + if (!Array.isArray(dictionary)) { + return undefined; + } + let entry = dictionary.find((elem: any) => elem.key === key); + return typeof entry?.value === "number" ? entry.value : undefined; + }; + const isCoefficientValid = () => { let calibrate = form.component.settings.calibrate; let coefficient = Number(form.coefficient); - let valid = !isNaN(coefficient) && (!calibrate || (calibrate && coefficient > 0)); - return valid; + if (isNaN(coefficient)) { + return false; + } + if (!calibrate) { + return true; + } + if (compMode && compMode.mode.entryA === "number") { + let minimum = modeDictionaryValue(compMode.mode.dictionaryA, "min") ?? 0; + let maximum = modeDictionaryValue(compMode.mode.dictionaryA, "max") ?? 100; + return coefficient >= minimum && coefficient <= maximum; + } + return coefficient > 0; }; const changeCoefficient = (event: any) => { @@ -485,10 +503,14 @@ export default function ComponentForm(props: Props) { }; const isOffsetValid = (min?: number, max?: number) => { - let minimum = min ?? 0; - let maximum = max ?? 100; + const minimum = min ?? modeDictionaryValue(compMode?.mode.dictionaryB, "min") ?? 0; + const maximum = max ?? modeDictionaryValue(compMode?.mode.dictionaryB, "max") ?? 100; if (compMode && form.component.settings.calibrate) { - return Number(form.offset) <= maximum && Number(form.offset) >= minimum; + const offset = Number(form.offset); + const meetsMinimum = compMode.mode.exclusiveMinimumB + ? offset > minimum + : offset >= minimum; + return !isNaN(offset) && offset <= maximum && meetsMinimum; } return !isNaN(Number(form.offset)); }; @@ -832,6 +854,11 @@ export default function ComponentForm(props: Props) { onFocus={selectText} margin="normal" fullWidth + inputProps={ + compMode.mode.stepB !== undefined + ? { step: compMode.mode.stepB } + : undefined + } type={compMode.mode.entryB} variant="outlined"> {compMode.mode.entryB === "select" && diff --git a/src/component/ComponentMode.json b/src/component/ComponentMode.json index 2c66a6e..4443d1f 100644 --- a/src/component/ComponentMode.json +++ b/src/component/ComponentMode.json @@ -110,7 +110,7 @@ "dictionaryA": [ { "key": "min", - "value": 1, + "value": 0, "restricted": false }, { @@ -121,10 +121,12 @@ ], "labelB": "To Node Id", "entryB": "number", + "exclusiveMinimumB": true, + "stepB": "any", "dictionaryB": [ { "key": "min", - "value": 1, + "value": 0, "restricted": false }, { diff --git a/src/component/ComponentSettings.tsx b/src/component/ComponentSettings.tsx index a779692..ce56e56 100644 --- a/src/component/ComponentSettings.tsx +++ b/src/component/ComponentSettings.tsx @@ -213,15 +213,13 @@ export default function ComponentSettings(props: Props) { ) ); setFormComponent(initComponent); - setExcludedNodes(initComponent.settings.excludedNodes) + setExcludedNodes(cloneDeep(initComponent.settings.excludedNodes)); }, [props.component, componentTypeOptions]); useEffect(() => { - if (isDialogOpen && prevIsDialogOpen) return; - if (props.component !== prevComponent || isDialogOpen !== prevIsDialogOpen) { - init(); - } - }, [props.component, prevComponent, init, isDialogOpen, prevIsDialogOpen]); + if (!isDialogOpen || prevIsDialogOpen) return; + init(); + }, [init, isDialogOpen, prevIsDialogOpen]); const close = () => { closeDialogCallback(); @@ -518,7 +516,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 +523,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) } 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/device/DeviceSettings.tsx b/src/device/DeviceSettings.tsx index 26ca591..32831f5 100644 --- a/src/device/DeviceSettings.tsx +++ b/src/device/DeviceSettings.tsx @@ -31,6 +31,7 @@ import LinearMutationBuilder from "common/LinearMutationBuilder"; import { makeStyles } from "@mui/styles"; import { useNavigate } from "react-router-dom"; import CancelSubmit from "common/CancelSubmit"; +import { cloneDeep } from "lodash"; interface TabPanelProps { children?: React.ReactNode; @@ -94,7 +95,6 @@ export default function DeviceSettings(props: Props) { const { success, error } = useSnackbar(); const deviceAPI = useDeviceAPI(); const { device, isDialogOpen, closeDialogCallback, canEdit, refreshCallback, components } = props; - const prevDevice = usePrevious(device); const prevIsDialogOpen = usePrevious(isDialogOpen); const [deviceForm, setDeviceForm] = useState(deviceFromForm(Device.clone(device))); const [isRemoveDeviceDialogOpen, setIsRemoveDeviceDialogOpen] = useState(false); @@ -103,7 +103,9 @@ export default function DeviceSettings(props: Props) { const [compExtTwo, setCompExtTwo] = useState(""); const [compExtThree, setCompExtThree] = useState(""); const [currentTab, setCurrentTab] = useState(0); - const [linearMutations, setLinearMutations] = useState([]); + const [linearMutations, setLinearMutations] = useState( + cloneDeep(device.settings.mutations) + ); const [componentsByDevice, setComponentsByDevice] = useState>( new Map() ); @@ -111,28 +113,22 @@ export default function DeviceSettings(props: Props) { const [existingMutation, setExistingMutation] = useState(); useEffect(() => { - if (isDialogOpen && prevIsDialogOpen) return; - if (prevDevice !== device || isDialogOpen !== prevIsDialogOpen) { - setDeviceForm(deviceFromForm(Device.clone(props.device))); - if (device.settings.extensionComponents[0]) { - setCompExtOne(device.settings.extensionComponents[0]); - } - if (device.settings.extensionComponents[1]) { - setCompExtTwo(device.settings.extensionComponents[1]); - } - if (device.settings.extensionComponents[2]) { - setCompExtThree(device.settings.extensionComponents[2]); - } - if (device.settings.mutations) { - setLinearMutations(device.settings.mutations); - } - if (components) { - let cbd = new Map(); - cbd.set(device.id().toString(), components); - setComponentsByDevice(cbd); - } + if (!isDialogOpen || prevIsDialogOpen) return; + + const settingsForm = deviceFromForm(Device.clone(device)); + setDeviceForm(settingsForm); + setSleeps(settingsForm.settings.sleepDurationS > 0); + setCompExtOne(settingsForm.settings.extensionComponents[0] ?? ""); + setCompExtTwo(settingsForm.settings.extensionComponents[1] ?? ""); + setCompExtThree(settingsForm.settings.extensionComponents[2] ?? ""); + setLinearMutations(cloneDeep(settingsForm.settings.mutations)); + + if (components) { + let cbd = new Map(); + cbd.set(settingsForm.id().toString(), components); + setComponentsByDevice(cbd); } - }, [device, prevDevice, props.device, components, isDialogOpen, prevIsDialogOpen]); + }, [device, components, isDialogOpen, prevIsDialogOpen]); const close = () => { closeDialogCallback(); @@ -147,7 +143,7 @@ export default function DeviceSettings(props: Props) { const minCheckPeriodS = (): number => { let defaultPeriod = 60; - switch (device.settings.platform) { + switch (deviceForm.settings.platform) { case pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON: return user.hasFeature("admin") ? defaultPeriod : 300; default: @@ -319,7 +315,7 @@ export default function DeviceSettings(props: Props) { id="name" name="name" label="Name" - defaultValue={deviceForm.settings.name} + value={deviceForm.settings.name} onChange={changeName} margin="normal" variant="outlined" @@ -332,7 +328,7 @@ export default function DeviceSettings(props: Props) { id="description" name="description" label="Description" - defaultValue={deviceForm.settings.description} + value={deviceForm.settings.description} onChange={changeDescription} multiline rows={2} @@ -550,8 +546,8 @@ export default function DeviceSettings(props: Props) { const mutationsContent = () => { let mutations: JSX.Element[] = []; - if (device.settings.mutations) { - device.settings.mutations.forEach((mut, i) => { + if (linearMutations) { + linearMutations.forEach((mut, i) => { mutations.push( { - let lm = linearMutations; - lm.splice(i, 1); - setLinearMutations([...lm]); + setLinearMutations(linearMutations.filter((_, index) => index !== i)); }}> Remove @@ -696,9 +690,7 @@ export default function DeviceSettings(props: Props) { setNewMutationDialog(false); }} onSubmit={newMutation => { - let lm = linearMutations; - lm.push(newMutation); - setLinearMutations(lm); + setLinearMutations([...linearMutations, newMutation]); }} /> 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") 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/InteractionSettings.tsx b/src/interactions/InteractionSettings.tsx index c98742f..f1f3d8c 100644 --- a/src/interactions/InteractionSettings.tsx +++ b/src/interactions/InteractionSettings.tsx @@ -32,7 +32,6 @@ import moment from "moment-timezone"; import { componentIDToString, emptyComponentId, - getComponentIDString, sameComponentID, stringToComponentId } from "pbHelpers/Component"; @@ -130,9 +129,6 @@ export default function InteractionSettings(props: Props) { } = props; const theme = useTheme(); const { success, error } = useSnackbar(); - const prevInitialInteraction = usePrevious(initialInteraction); - const prevComponents = usePrevious(components); - const prevInitialComponent = usePrevious(initialComponent); const prevIsDialogOpen = usePrevious(isDialogOpen); const classes = useStyles(); const [{ user, as }] = useGlobalState(); @@ -167,7 +163,7 @@ export default function InteractionSettings(props: Props) { const setDefaultState = useCallback(() => { let interaction = getDefaultInteraction(); if (initialInteraction && mode === "update") { - interaction = initialInteraction; + interaction = Interaction.clone(initialInteraction); if (interaction.settings.subtype === 0 || interaction.settings.subtype === 1) { setSubtypeDropdown(interaction.settings.subtype); } else { @@ -223,7 +219,7 @@ export default function InteractionSettings(props: Props) { setDutyCycle(interactionResult.dutyCycle.toString()); setMappedComponents(mappedComponents); setInteraction(interaction); - }, [components, initialComponent, initialConditions, initialInteraction, mode, /*sensor*/]); + }, [components, initialComponent, initialConditions, initialInteraction, mode, sensor, user]); const availableSources = () => { return components.filter(component => isSource(component.settings.type)); @@ -233,27 +229,12 @@ export default function InteractionSettings(props: Props) { if (user && user.settings.timezone) { setTimezone(user.settings.timezone); } - if (isDialogOpen && prevIsDialogOpen) return; - if ( - prevInitialInteraction !== initialInteraction || - (prevComponents && prevComponents.length !== components.length) || - getComponentIDString(prevInitialComponent) !== getComponentIDString(initialComponent) || - isDialogOpen !== prevIsDialogOpen - ) { - setDefaultState(); - } - }, [ - components.length, - initialComponent, - initialInteraction, - prevComponents, - prevInitialComponent, - prevInitialInteraction, - setDefaultState, - user, - isDialogOpen, - prevIsDialogOpen - ]); + }, [user]); + + useEffect(() => { + if (!isDialogOpen || prevIsDialogOpen) return; + setDefaultState(); + }, [isDialogOpen, prevIsDialogOpen, setDefaultState]); const getAvailableSinks = () => { let type = or(interaction.settings.result, pond.InteractionResult.create()).type; diff --git a/src/interactions/InteractionsOverview.tsx b/src/interactions/InteractionsOverview.tsx index 450c44f..929a58d 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"; @@ -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; }); @@ -329,17 +306,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/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; } 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/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 => { 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' ? { 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; +}