From 3765758d36ada4a8ff09f160a4783f57fa0d4e66 Mon Sep 17 00:00:00 2001 From: abishek Date: Fri, 19 Sep 2025 11:02:01 +0530 Subject: [PATCH 01/28] refactor: migrate availability chart from VisX to Chakra UI v3 charts --- .../obj/Debug/package.g.props | 29 +- thingconnect.pulse.client/package-lock.json | 1064 +++++++---------- thingconnect.pulse.client/package.json | 7 +- .../src/components/AvailabilityChart.tsx | 143 +-- .../src/components/OutageList.tsx | 3 + .../src/components/RecentChecksTable.tsx | 3 + 6 files changed, 501 insertions(+), 748 deletions(-) diff --git a/thingconnect.pulse.client/obj/Debug/package.g.props b/thingconnect.pulse.client/obj/Debug/package.g.props index b0a9162..854218e 100644 --- a/thingconnect.pulse.client/obj/Debug/package.g.props +++ b/thingconnect.pulse.client/obj/Debug/package.g.props @@ -11,17 +11,13 @@ vite preview prettier --write . cd .. && husky ./thingconnect.pulse.client/.husky + ^3.27.0 ^3.24.2 ^11.14.0 ^5.2.1 ^4.7.0 ^10.11.0 ^5.84.2 - ^3.12.0 - ^3.12.0 - ^3.12.0 - ^3.12.0 - ^3.12.0 ^1.11.0 ^4.1.0 ^4.17.21 @@ -36,31 +32,32 @@ ^7.62.0 ^5.5.0 ^7.8.1 - ^4.0.17 + ^3.2.1 + ^4.1.9 ^3.24.0 ^9.33.0 ^5.83.1 ^5.85.5 ^4.17.20 ^3.7.1 - ^22 + ^24 ^2.0.5 - ^19.1.10 - ^19.1.7 - ^3.11.0 - ^9.33.0 + ^19.1.13 + ^19.1.9 + ^4.1.0 + ^9.35.0 ^10.1.8 - ^1.52.3 + ^1.53.1 ^5.2.0 ^0.4.20 - ^1.52.3 + ^1.53.1 ^16.3.0 ^9.1.7 ^16.1.4 ^3.6.2 - ~5.8.3 - ^8.39.1 - ^7.1.2 + ~5.9.2 + ^8.44.0 + ^7.1.6 ^5.1.4 [ "eslint --fix", diff --git a/thingconnect.pulse.client/package-lock.json b/thingconnect.pulse.client/package-lock.json index f026816..8ad51a3 100644 --- a/thingconnect.pulse.client/package-lock.json +++ b/thingconnect.pulse.client/package-lock.json @@ -8,17 +8,13 @@ "name": "thingconnect.pulse.client", "version": "0.1.0", "dependencies": { + "@chakra-ui/charts": "^3.27.0", "@chakra-ui/react": "^3.24.2", "@emotion/react": "^11.14.0", "@hookform/resolvers": "^5.2.1", "@monaco-editor/react": "^4.7.0", "@sentry/react": "^10.11.0", "@tanstack/react-query": "^5.84.2", - "@visx/axis": "^3.12.0", - "@visx/responsive": "^3.12.0", - "@visx/scale": "^3.12.0", - "@visx/shape": "^3.12.0", - "@visx/tooltip": "^3.12.0", "axios": "^1.11.0", "date-fns": "^4.1.0", "lodash": "^4.17.21", @@ -33,6 +29,7 @@ "react-hook-form": "^7.62.0", "react-icons": "^5.5.0", "react-router-dom": "^7.8.1", + "recharts": "^3.2.1", "zod": "^4.1.9" }, "devDependencies": { @@ -262,6 +259,18 @@ "node": ">=6.9.0" } }, + "node_modules/@chakra-ui/charts": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/charts/-/charts-3.27.0.tgz", + "integrity": "sha512-nCn4TbbQZIbnr89ynETD4rrW3Rh+it+w55q3QUc76GbqMTfcs4I148UsP/nb5YURQ9WAHwmMXhzlW9T62JqSvw==", + "license": "MIT", + "peerDependencies": { + "@chakra-ui/react": ">=3", + "react": ">=18", + "react-dom": ">=18", + "recharts": ">=2" + } + }, "node_modules/@chakra-ui/cli": { "version": "3.25.0", "resolved": "https://registry.npmjs.org/@chakra-ui/cli/-/cli-3.25.0.tgz", @@ -1466,6 +1475,32 @@ "node": ">=14" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz", + "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.35", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.35.tgz", @@ -1829,6 +1864,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, "node_modules/@standard-schema/utils": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", @@ -2134,81 +2175,66 @@ "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==" }, "node_modules/@types/d3-array": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz", - "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", "license": "MIT" }, "node_modules/@types/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==", - "license": "MIT" - }, - "node_modules/@types/d3-delaunay": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz", - "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "license": "MIT" }, - "node_modules/@types/d3-format": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", - "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==", + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", "license": "MIT" }, - "node_modules/@types/d3-geo": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, "node_modules/@types/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", "license": "MIT", "dependencies": { "@types/d3-color": "*" } }, "node_modules/@types/d3-path": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz", - "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", "license": "MIT" }, "node_modules/@types/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", "license": "MIT", "dependencies": { "@types/d3-time": "*" } }, "node_modules/@types/d3-shape": { - "version": "1.3.12", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz", - "integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", "license": "MIT", "dependencies": { - "@types/d3-path": "^1" + "@types/d3-path": "*" } }, "node_modules/@types/d3-time": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", - "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", "license": "MIT" }, - "node_modules/@types/d3-time-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.1.0.tgz", - "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==", + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, "node_modules/@types/debug": { @@ -2226,12 +2252,6 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "license": "MIT" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2241,7 +2261,8 @@ "node_modules/@types/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==" + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true }, "node_modules/@types/luxon": { "version": "3.7.1", @@ -2281,6 +2302,7 @@ "version": "19.1.13", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", + "devOptional": true, "dependencies": { "csstype": "^3.0.2" } @@ -2289,10 +2311,17 @@ "version": "19.1.9", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", + "dev": true, "peerDependencies": { "@types/react": "^19.0.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.44.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.0.tgz", @@ -2550,178 +2579,6 @@ "node": ">=20.18 <=24.x" } }, - "node_modules/@visx/axis": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/axis/-/axis-3.12.0.tgz", - "integrity": "sha512-8MoWpfuaJkhm2Yg+HwyytK8nk+zDugCqTT/tRmQX7gW4LYrHYLXFUXOzbDyyBakCVaUbUaAhVFxpMASJiQKf7A==", - "license": "MIT", - "dependencies": { - "@types/react": "*", - "@visx/group": "3.12.0", - "@visx/point": "3.12.0", - "@visx/scale": "3.12.0", - "@visx/shape": "3.12.0", - "@visx/text": "3.12.0", - "classnames": "^2.3.1", - "prop-types": "^15.6.0" - }, - "peerDependencies": { - "react": "^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0" - } - }, - "node_modules/@visx/bounds": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/bounds/-/bounds-3.12.0.tgz", - "integrity": "sha512-peAlNCUbYaaZ0IO6c1lDdEAnZv2iGPDiLIM8a6gu7CaMhtXZJkqrTh+AjidNcIqITktrICpGxJE/Qo9D099dvQ==", - "license": "MIT", - "dependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "prop-types": "^15.5.10" - }, - "peerDependencies": { - "react": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0", - "react-dom": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0" - } - }, - "node_modules/@visx/curve": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-3.12.0.tgz", - "integrity": "sha512-Ng1mefXIzoIoAivw7dJ+ZZYYUbfuwXgZCgQynShr6ZIVw7P4q4HeQfJP3W24ON+1uCSrzoycHSXRelhR9SBPcw==", - "license": "MIT", - "dependencies": { - "@types/d3-shape": "^1.3.1", - "d3-shape": "^1.0.6" - } - }, - "node_modules/@visx/group": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/group/-/group-3.12.0.tgz", - "integrity": "sha512-Dye8iS1alVXPv7nj/7M37gJe6sSKqJLH7x6sEWAsRQ9clI0kFvjbKcKgF+U3aAVQr0NCohheFV+DtR8trfK/Ag==", - "license": "MIT", - "dependencies": { - "@types/react": "*", - "classnames": "^2.3.1", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0" - } - }, - "node_modules/@visx/point": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/point/-/point-3.12.0.tgz", - "integrity": "sha512-I6UrHoJAEVbx3RORQNupgTiX5EzjuZpiwLPxn8L2mI5nfERotPKi1Yus12Cq2WtXqEBR/WgqTnoImlqOXBykcA==", - "license": "MIT" - }, - "node_modules/@visx/responsive": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-3.12.0.tgz", - "integrity": "sha512-GV1BTYwAGlk/K5c9vH8lS2syPnTuIqEacI7L6LRPbsuaLscXMNi+i9fZyzo0BWvAdtRV8v6Urzglo++lvAXT1Q==", - "license": "MIT", - "dependencies": { - "@types/lodash": "^4.14.172", - "@types/react": "*", - "lodash": "^4.17.21", - "prop-types": "^15.6.1" - }, - "peerDependencies": { - "react": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0" - } - }, - "node_modules/@visx/scale": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-3.12.0.tgz", - "integrity": "sha512-+ubijrZ2AwWCsNey0HGLJ0YKNeC/XImEFsr9rM+Uef1CM3PNM43NDdNTrdBejSlzRq0lcfQPWYMYQFSlkLcPOg==", - "license": "MIT", - "dependencies": { - "@visx/vendor": "3.12.0" - } - }, - "node_modules/@visx/shape": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-3.12.0.tgz", - "integrity": "sha512-/1l0lrpX9tPic6SJEalryBKWjP/ilDRnQA+BGJTI1tj7i23mJ/J0t4nJHyA1GrL4QA/bM/qTJ35eyz5dEhJc4g==", - "license": "MIT", - "dependencies": { - "@types/d3-path": "^1.0.8", - "@types/d3-shape": "^1.3.1", - "@types/lodash": "^4.14.172", - "@types/react": "*", - "@visx/curve": "3.12.0", - "@visx/group": "3.12.0", - "@visx/scale": "3.12.0", - "classnames": "^2.3.1", - "d3-path": "^1.0.5", - "d3-shape": "^1.2.0", - "lodash": "^4.17.21", - "prop-types": "^15.5.10" - }, - "peerDependencies": { - "react": "^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0" - } - }, - "node_modules/@visx/text": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/text/-/text-3.12.0.tgz", - "integrity": "sha512-0rbDYQlbuKPhBqXkkGYKFec1gQo05YxV45DORzr6hCyaizdJk1G+n9VkuKSHKBy1vVQhBA0W3u/WXd7tiODQPA==", - "license": "MIT", - "dependencies": { - "@types/lodash": "^4.14.172", - "@types/react": "*", - "classnames": "^2.3.1", - "lodash": "^4.17.21", - "prop-types": "^15.7.2", - "reduce-css-calc": "^1.3.0" - }, - "peerDependencies": { - "react": "^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0" - } - }, - "node_modules/@visx/tooltip": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/tooltip/-/tooltip-3.12.0.tgz", - "integrity": "sha512-pWhsYhgl0Shbeqf80qy4QCB6zpq6tQtMQQxKlh3UiKxzkkfl+Metaf9p0/S0HexNi4vewOPOo89xWx93hBeh3A==", - "license": "MIT", - "dependencies": { - "@types/react": "*", - "@visx/bounds": "3.12.0", - "classnames": "^2.3.1", - "prop-types": "^15.5.10", - "react-use-measure": "^2.0.4" - }, - "peerDependencies": { - "react": "^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0", - "react-dom": "^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0" - } - }, - "node_modules/@visx/vendor": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-3.12.0.tgz", - "integrity": "sha512-SVO+G0xtnL9dsNpGDcjCgoiCnlB3iLSM9KLz1sLbSrV7RaVXwY3/BTm2X9OWN1jH2a9M+eHt6DJ6sE6CXm4cUg==", - "license": "MIT and ISC", - "dependencies": { - "@types/d3-array": "3.0.3", - "@types/d3-color": "3.1.0", - "@types/d3-delaunay": "6.0.1", - "@types/d3-format": "3.0.1", - "@types/d3-geo": "3.1.0", - "@types/d3-interpolate": "3.0.1", - "@types/d3-scale": "4.0.2", - "@types/d3-time": "3.0.0", - "@types/d3-time-format": "2.1.0", - "d3-array": "3.2.1", - "d3-color": "3.1.0", - "d3-delaunay": "6.0.2", - "d3-format": "3.1.0", - "d3-geo": "3.1.0", - "d3-interpolate": "3.0.1", - "d3-scale": "4.0.2", - "d3-time": "3.1.0", - "d3-time-format": "4.1.0", - "internmap": "2.0.3" - } - }, "node_modules/@vitejs/plugin-react-swc": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.1.0.tgz", @@ -3673,7 +3530,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/base64-arraybuffer": { "version": "1.0.2", @@ -3792,12 +3650,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", - "license": "MIT" - }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -3841,6 +3693,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3962,9 +3823,9 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", "license": "ISC", "dependencies": { "internmap": "1 - 2" @@ -3982,14 +3843,11 @@ "node": ">=12" } }, - "node_modules/d3-delaunay": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", - "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", - "license": "ISC", - "dependencies": { - "delaunator": "5" - }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", "engines": { "node": ">=12" } @@ -4003,18 +3861,6 @@ "node": ">=12" } }, - "node_modules/d3-geo": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", - "license": "ISC", - "dependencies": { - "d3-array": "2.5.0 - 3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/d3-interpolate": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", @@ -4028,10 +3874,13 @@ } }, "node_modules/d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", - "license": "BSD-3-Clause" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } }, "node_modules/d3-scale": { "version": "4.0.2", @@ -4050,12 +3899,15 @@ } }, "node_modules/d3-shape": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", - "license": "BSD-3-Clause", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", "dependencies": { - "d3-path": "1" + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-time": { @@ -4082,6 +3934,15 @@ "node": ">=12" } }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -4116,21 +3977,18 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", - "license": "ISC", - "dependencies": { - "robust-predicates": "^3.0.2" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -4225,6 +4083,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.39.10", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz", + "integrity": "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", @@ -4597,8 +4465,7 @@ "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -5064,6 +4931,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -5455,18 +5332,6 @@ "integrity": "sha512-nMoGWW2HurtuJf6XAL56FWTDCWLOTSsanrgwOyaR5Y4e3zfG5N/0cU5xWZSEU3tBxhQugRbV1xL9jb+ug7yZww==", "dev": true }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -5490,12 +5355,6 @@ "node": ">=12" } }, - "node_modules/math-expression-evaluator": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.4.0.tgz", - "integrity": "sha512-4vRUvPyxdO8cWULGTh9dZWL2tZK6LDBvj+OGHBER7poH9Qdt7kXEoj20wiz4lQUbUXQZFjPbe5mVDo9nutizCw==", - "license": "MIT" - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5731,6 +5590,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -5987,17 +5847,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, "node_modules/proxy-compare": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz", @@ -6093,6 +5942,29 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.2.tgz", @@ -6129,21 +6001,6 @@ "react-dom": ">=18" } }, - "node_modules/react-use-measure": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", - "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.13", - "react-dom": ">=16.13" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -6156,32 +6013,54 @@ "node": ">=8.10.0" } }, - "node_modules/reduce-css-calc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", - "integrity": "sha512-0dVfwYVOlf/LBA2ec4OwQ6p3X9mYxn/wOl2xTcLwjnPYrkgEfPx3VI4eGCH3rQLlPISG5v9I9bkZosKsNRTRKA==", + "node_modules/recharts": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.1.tgz", + "integrity": "sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw==", "license": "MIT", "dependencies": { - "balanced-match": "^0.4.2", - "math-expression-evaluator": "^1.2.14", - "reduce-function-call": "^1.0.1" + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/reduce-css-calc/node_modules/balanced-match": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "integrity": "sha512-STw03mQKnGUYtoNjmowo4F2cRmIIxYEGiMsjjwla/u5P1lxadj/05WkNaFjNiKTgJkj8KiXbgAiRTmcQRwQNtg==", + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "license": "MIT" }, - "node_modules/reduce-function-call": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz", - "integrity": "sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==", + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + "peerDependencies": { + "redux": "^5.0.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -6241,12 +6120,6 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true }, - "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", - "license": "Unlicense" - }, "node_modules/rollup": { "version": "4.48.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.48.0.tgz", @@ -6649,6 +6522,12 @@ "node": ">=0.8" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -6872,6 +6751,37 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.6.tgz", @@ -7317,6 +7227,12 @@ "@babel/helper-validator-identifier": "^7.27.1" } }, + "@chakra-ui/charts": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/charts/-/charts-3.27.0.tgz", + "integrity": "sha512-nCn4TbbQZIbnr89ynETD4rrW3Rh+it+w55q3QUc76GbqMTfcs4I148UsP/nb5YURQ9WAHwmMXhzlW9T62JqSvw==", + "requires": {} + }, "@chakra-ui/cli": { "version": "3.25.0", "resolved": "https://registry.npmjs.org/@chakra-ui/cli/-/cli-3.25.0.tgz", @@ -8133,6 +8049,19 @@ "dev": true, "optional": true }, + "@reduxjs/toolkit": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz", + "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==", + "requires": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + } + }, "@rolldown/pluginutils": { "version": "1.0.0-beta.35", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.35.tgz", @@ -8346,6 +8275,11 @@ "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", "dev": true }, + "@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==" + }, "@standard-schema/utils": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", @@ -8513,71 +8447,58 @@ "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==" }, "@types/d3-array": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz", - "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==" + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==" }, "@types/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==" - }, - "@types/d3-delaunay": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz", - "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==" - }, - "@types/d3-format": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", - "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" }, - "@types/d3-geo": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", - "requires": { - "@types/geojson": "*" - } + "@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" }, "@types/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", "requires": { "@types/d3-color": "*" } }, "@types/d3-path": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz", - "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" }, "@types/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", "requires": { "@types/d3-time": "*" } }, "@types/d3-shape": { - "version": "1.3.12", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz", - "integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", "requires": { - "@types/d3-path": "^1" + "@types/d3-path": "*" } }, "@types/d3-time": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", - "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==" + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" }, - "@types/d3-time-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.1.0.tgz", - "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==" + "@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" }, "@types/debug": { "version": "4.1.12", @@ -8594,11 +8515,6 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, - "@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==" - }, "@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -8608,7 +8524,8 @@ "@types/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==" + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true }, "@types/luxon": { "version": "3.7.1", @@ -8646,6 +8563,7 @@ "version": "19.1.13", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", + "devOptional": true, "requires": { "csstype": "^3.0.2" } @@ -8654,8 +8572,14 @@ "version": "19.1.9", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", + "dev": true, "requires": {} }, + "@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==" + }, "@typescript-eslint/eslint-plugin": { "version": "8.44.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.0.tgz", @@ -8795,144 +8719,6 @@ "integrity": "sha512-eqm/OzwETl1Zd5ehW5CUXhYf8tqb+seBCkHBKXh1rEMS94n+OhyCY0KAlZv/17qPoN73WT2nGDN9SdYlvoWbTQ==", "dev": true }, - "@visx/axis": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/axis/-/axis-3.12.0.tgz", - "integrity": "sha512-8MoWpfuaJkhm2Yg+HwyytK8nk+zDugCqTT/tRmQX7gW4LYrHYLXFUXOzbDyyBakCVaUbUaAhVFxpMASJiQKf7A==", - "requires": { - "@types/react": "*", - "@visx/group": "3.12.0", - "@visx/point": "3.12.0", - "@visx/scale": "3.12.0", - "@visx/shape": "3.12.0", - "@visx/text": "3.12.0", - "classnames": "^2.3.1", - "prop-types": "^15.6.0" - } - }, - "@visx/bounds": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/bounds/-/bounds-3.12.0.tgz", - "integrity": "sha512-peAlNCUbYaaZ0IO6c1lDdEAnZv2iGPDiLIM8a6gu7CaMhtXZJkqrTh+AjidNcIqITktrICpGxJE/Qo9D099dvQ==", - "requires": { - "@types/react": "*", - "@types/react-dom": "*", - "prop-types": "^15.5.10" - } - }, - "@visx/curve": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-3.12.0.tgz", - "integrity": "sha512-Ng1mefXIzoIoAivw7dJ+ZZYYUbfuwXgZCgQynShr6ZIVw7P4q4HeQfJP3W24ON+1uCSrzoycHSXRelhR9SBPcw==", - "requires": { - "@types/d3-shape": "^1.3.1", - "d3-shape": "^1.0.6" - } - }, - "@visx/group": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/group/-/group-3.12.0.tgz", - "integrity": "sha512-Dye8iS1alVXPv7nj/7M37gJe6sSKqJLH7x6sEWAsRQ9clI0kFvjbKcKgF+U3aAVQr0NCohheFV+DtR8trfK/Ag==", - "requires": { - "@types/react": "*", - "classnames": "^2.3.1", - "prop-types": "^15.6.2" - } - }, - "@visx/point": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/point/-/point-3.12.0.tgz", - "integrity": "sha512-I6UrHoJAEVbx3RORQNupgTiX5EzjuZpiwLPxn8L2mI5nfERotPKi1Yus12Cq2WtXqEBR/WgqTnoImlqOXBykcA==" - }, - "@visx/responsive": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-3.12.0.tgz", - "integrity": "sha512-GV1BTYwAGlk/K5c9vH8lS2syPnTuIqEacI7L6LRPbsuaLscXMNi+i9fZyzo0BWvAdtRV8v6Urzglo++lvAXT1Q==", - "requires": { - "@types/lodash": "^4.14.172", - "@types/react": "*", - "lodash": "^4.17.21", - "prop-types": "^15.6.1" - } - }, - "@visx/scale": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-3.12.0.tgz", - "integrity": "sha512-+ubijrZ2AwWCsNey0HGLJ0YKNeC/XImEFsr9rM+Uef1CM3PNM43NDdNTrdBejSlzRq0lcfQPWYMYQFSlkLcPOg==", - "requires": { - "@visx/vendor": "3.12.0" - } - }, - "@visx/shape": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-3.12.0.tgz", - "integrity": "sha512-/1l0lrpX9tPic6SJEalryBKWjP/ilDRnQA+BGJTI1tj7i23mJ/J0t4nJHyA1GrL4QA/bM/qTJ35eyz5dEhJc4g==", - "requires": { - "@types/d3-path": "^1.0.8", - "@types/d3-shape": "^1.3.1", - "@types/lodash": "^4.14.172", - "@types/react": "*", - "@visx/curve": "3.12.0", - "@visx/group": "3.12.0", - "@visx/scale": "3.12.0", - "classnames": "^2.3.1", - "d3-path": "^1.0.5", - "d3-shape": "^1.2.0", - "lodash": "^4.17.21", - "prop-types": "^15.5.10" - } - }, - "@visx/text": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/text/-/text-3.12.0.tgz", - "integrity": "sha512-0rbDYQlbuKPhBqXkkGYKFec1gQo05YxV45DORzr6hCyaizdJk1G+n9VkuKSHKBy1vVQhBA0W3u/WXd7tiODQPA==", - "requires": { - "@types/lodash": "^4.14.172", - "@types/react": "*", - "classnames": "^2.3.1", - "lodash": "^4.17.21", - "prop-types": "^15.7.2", - "reduce-css-calc": "^1.3.0" - } - }, - "@visx/tooltip": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/tooltip/-/tooltip-3.12.0.tgz", - "integrity": "sha512-pWhsYhgl0Shbeqf80qy4QCB6zpq6tQtMQQxKlh3UiKxzkkfl+Metaf9p0/S0HexNi4vewOPOo89xWx93hBeh3A==", - "requires": { - "@types/react": "*", - "@visx/bounds": "3.12.0", - "classnames": "^2.3.1", - "prop-types": "^15.5.10", - "react-use-measure": "^2.0.4" - } - }, - "@visx/vendor": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-3.12.0.tgz", - "integrity": "sha512-SVO+G0xtnL9dsNpGDcjCgoiCnlB3iLSM9KLz1sLbSrV7RaVXwY3/BTm2X9OWN1jH2a9M+eHt6DJ6sE6CXm4cUg==", - "requires": { - "@types/d3-array": "3.0.3", - "@types/d3-color": "3.1.0", - "@types/d3-delaunay": "6.0.1", - "@types/d3-format": "3.0.1", - "@types/d3-geo": "3.1.0", - "@types/d3-interpolate": "3.0.1", - "@types/d3-scale": "4.0.2", - "@types/d3-time": "3.0.0", - "@types/d3-time-format": "2.1.0", - "d3-array": "3.2.1", - "d3-color": "3.1.0", - "d3-delaunay": "6.0.2", - "d3-format": "3.1.0", - "d3-geo": "3.1.0", - "d3-interpolate": "3.0.1", - "d3-scale": "4.0.2", - "d3-time": "3.1.0", - "d3-time-format": "4.1.0", - "internmap": "2.0.3" - } - }, "@vitejs/plugin-react-swc": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.1.0.tgz", @@ -9825,7 +9611,8 @@ "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "base64-arraybuffer": { "version": "1.0.2", @@ -9912,11 +9699,6 @@ "readdirp": "~3.6.0" } }, - "classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" - }, "cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -9945,6 +9727,11 @@ "string-width": "^7.0.0" } }, + "clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -10044,9 +9831,9 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", "requires": { "internmap": "1 - 2" } @@ -10056,27 +9843,16 @@ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" }, - "d3-delaunay": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", - "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", - "requires": { - "delaunator": "5" - } + "d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" }, "d3-format": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" }, - "d3-geo": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", - "requires": { - "d3-array": "2.5.0 - 3" - } - }, "d3-interpolate": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", @@ -10086,9 +9862,9 @@ } }, "d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==" }, "d3-scale": { "version": "4.0.2", @@ -10103,11 +9879,11 @@ } }, "d3-shape": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "requires": { - "d3-path": "1" + "d3-path": "^3.1.0" } }, "d3-time": { @@ -10126,6 +9902,11 @@ "d3-time": "1 - 3" } }, + "d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" + }, "data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -10145,20 +9926,17 @@ "ms": "^2.1.3" } }, + "decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", - "requires": { - "robust-predicates": "^3.0.2" - } - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -10229,6 +10007,11 @@ "hasown": "^2.0.2" } }, + "es-toolkit": { + "version": "1.39.10", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz", + "integrity": "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==" + }, "esbuild": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", @@ -10478,8 +10261,7 @@ "eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" }, "fast-deep-equal": { "version": "3.1.3", @@ -10796,6 +10578,11 @@ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true }, + "immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==" + }, "import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -11076,14 +10863,6 @@ "integrity": "sha512-nMoGWW2HurtuJf6XAL56FWTDCWLOTSsanrgwOyaR5Y4e3zfG5N/0cU5xWZSEU3tBxhQugRbV1xL9jb+ug7yZww==", "dev": true }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, "lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -11101,11 +10880,6 @@ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==" }, - "math-expression-evaluator": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.4.0.tgz", - "integrity": "sha512-4vRUvPyxdO8cWULGTh9dZWL2tZK6LDBvj+OGHBER7poH9Qdt7kXEoj20wiz4lQUbUXQZFjPbe5mVDo9nutizCw==" - }, "math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -11258,7 +11032,8 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true }, "onetime": { "version": "7.0.0", @@ -11420,16 +11195,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true }, - "prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, "proxy-compare": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz", @@ -11490,6 +11255,15 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "requires": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + } + }, "react-router": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.2.tgz", @@ -11507,12 +11281,6 @@ "react-router": "7.8.2" } }, - "react-use-measure": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", - "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", - "requires": {} - }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -11522,30 +11290,39 @@ "picomatch": "^2.2.1" } }, - "reduce-css-calc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", - "integrity": "sha512-0dVfwYVOlf/LBA2ec4OwQ6p3X9mYxn/wOl2xTcLwjnPYrkgEfPx3VI4eGCH3rQLlPISG5v9I9bkZosKsNRTRKA==", + "recharts": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.1.tgz", + "integrity": "sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw==", "requires": { - "balanced-match": "^0.4.2", - "math-expression-evaluator": "^1.2.14", - "reduce-function-call": "^1.0.1" - }, - "dependencies": { - "balanced-match": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "integrity": "sha512-STw03mQKnGUYtoNjmowo4F2cRmIIxYEGiMsjjwla/u5P1lxadj/05WkNaFjNiKTgJkj8KiXbgAiRTmcQRwQNtg==" - } + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" } }, - "reduce-function-call": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz", - "integrity": "sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==", - "requires": { - "balanced-match": "^1.0.0" - } + "redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "requires": {} + }, + "reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" }, "resolve": { "version": "1.22.10", @@ -11584,11 +11361,6 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true }, - "robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" - }, "rollup": { "version": "4.48.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.48.0.tgz", @@ -11871,6 +11643,11 @@ "thenify": ">= 3.1.0 < 4" } }, + "tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, "tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -12006,6 +11783,33 @@ "punycode": "^2.1.0" } }, + "use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "requires": {} + }, + "victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "requires": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "vite": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.6.tgz", diff --git a/thingconnect.pulse.client/package.json b/thingconnect.pulse.client/package.json index 76ebb24..2c73d34 100644 --- a/thingconnect.pulse.client/package.json +++ b/thingconnect.pulse.client/package.json @@ -12,17 +12,13 @@ "prepare": "cd .. && husky ./thingconnect.pulse.client/.husky" }, "dependencies": { + "@chakra-ui/charts": "^3.27.0", "@chakra-ui/react": "^3.24.2", "@emotion/react": "^11.14.0", "@hookform/resolvers": "^5.2.1", "@monaco-editor/react": "^4.7.0", "@sentry/react": "^10.11.0", "@tanstack/react-query": "^5.84.2", - "@visx/axis": "^3.12.0", - "@visx/responsive": "^3.12.0", - "@visx/scale": "^3.12.0", - "@visx/shape": "^3.12.0", - "@visx/tooltip": "^3.12.0", "axios": "^1.11.0", "date-fns": "^4.1.0", "lodash": "^4.17.21", @@ -37,6 +33,7 @@ "react-hook-form": "^7.62.0", "react-icons": "^5.5.0", "react-router-dom": "^7.8.1", + "recharts": "^3.2.1", "zod": "^4.1.9" }, "devDependencies": { diff --git a/thingconnect.pulse.client/src/components/AvailabilityChart.tsx b/thingconnect.pulse.client/src/components/AvailabilityChart.tsx index f57cdb0..1e3c25b 100644 --- a/thingconnect.pulse.client/src/components/AvailabilityChart.tsx +++ b/thingconnect.pulse.client/src/components/AvailabilityChart.tsx @@ -1,9 +1,7 @@ import { useMemo } from 'react'; import { Box, Text, VStack, Skeleton } from '@chakra-ui/react'; -import { ParentSize } from '@visx/responsive'; -import { Group } from '@visx/group'; -import { AxisLeft, AxisBottom } from '@visx/axis'; -import { scaleBand, scaleLinear } from '@visx/scale'; +import { Chart, useChart } from '@chakra-ui/charts'; +import { Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts'; import type { HistoryResponse } from '@/api/types'; import type { BucketType } from '@/types/bucket'; import { CloudOff } from 'lucide-react'; @@ -18,37 +16,42 @@ export interface AvailabilityChartProps { export function AvailabilityChart({ data, bucket, isLoading }: AvailabilityChartProps) { const chartData = useMemo(() => { - if (!data) return null; + if (!data) return []; switch (bucket) { case 'raw': return data.raw.map(check => ({ - xaxis: new Date(check.ts).toLocaleTimeString('en-US', { + label: new Date(check.ts).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', }), - yaxis: check.status === 'up' ? 100 : 0, + uptime: check.status === 'up' ? 100 : 0, })); case '15m': return data.rollup15m.map(bucket => ({ - xaxis: new Date(bucket.bucketTs).toLocaleTimeString('en-US', { + label: new Date(bucket.bucketTs).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', }), - yaxis: bucket.upPct, + uptime: bucket.upPct, })); case 'daily': return data.rollupDaily.map(bucket => ({ - xaxis: new Date(bucket.bucketDate).toLocaleDateString('en-US', { + label: new Date(bucket.bucketDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', }), - yaxis: bucket.upPct, + uptime: bucket.upPct, })); default: return []; } }, [data, bucket]); + const chart = useChart({ + data: chartData, + series: [{ name: 'uptime', color: 'blue.500' }], + }); + if (chartData?.length === 0) { return ( - - {({ width, height }) => { - const xMax = width - margin.left - margin.right; - const yMax = height - margin.top - margin.bottom; - - const xScale = scaleBand({ - range: [0, xMax], - domain: chartData?.map(d => d.xaxis) ?? [], - padding: 0.2, - }); - - const yScale = scaleLinear({ - range: [yMax, 0], - domain: [0, 100], - }); - - return ( - - - {chartData?.map((d, i) => { - const barWidth = xScale.bandwidth(); - const barHeight = yMax - (yScale(d.yaxis) ?? 0); - const x = xScale(d.xaxis) ?? 0; - const y = yMax - barHeight; - return ( - - ); - })} - - `${d}%`} - stroke='#718096' - tickStroke='transparent' - tickLabelProps={{ - fill: '#718096', - textAnchor: 'end', - dx: -4, - style: { - fontSize: '12px', - }, - }} - /> - - {/* Y-axis label */} - - Uptime % - - - ''} - tickLabelProps={{ - fill: '#718096', - textAnchor: 'middle', - style: { - fontSize: '12px', - }, - }} - /> - - - ); - }} - - + + + + + `${v}%`} + /> + { + if (active && payload && payload.length) { + const uptime = payload[0].payload.uptime; + return ( + + {`Time: ${label}`} + {`Uptime: ${uptime.toFixed(3)}%`} + + ); + } + return null; + }} + /> + {chart.series.map(s => ( + + ))} + + + ); } diff --git a/thingconnect.pulse.client/src/components/OutageList.tsx b/thingconnect.pulse.client/src/components/OutageList.tsx index cbdd921..cccd760 100644 --- a/thingconnect.pulse.client/src/components/OutageList.tsx +++ b/thingconnect.pulse.client/src/components/OutageList.tsx @@ -30,6 +30,9 @@ export function OutagesList({ outages, isLoading }: OutagesListProps) { gap={1} py={5} h='100%' + bg='gray.50' + _dark={{ bg: 'gray.800' }} + borderRadius='md' > diff --git a/thingconnect.pulse.client/src/components/RecentChecksTable.tsx b/thingconnect.pulse.client/src/components/RecentChecksTable.tsx index 4e78efa..a716568 100644 --- a/thingconnect.pulse.client/src/components/RecentChecksTable.tsx +++ b/thingconnect.pulse.client/src/components/RecentChecksTable.tsx @@ -36,6 +36,9 @@ export function RecentChecksTable({ checks, pageSize = 10 }: RecentChecksTablePr gap={1} py={5} h='100%' + bg='gray.50' + _dark={{ bg: 'gray.800' }} + borderRadius='md' > From ce1c3e25eaff11fd0c7533772a6fd649adb6e0fe Mon Sep 17 00:00:00 2001 From: abishek Date: Fri, 19 Sep 2025 11:10:07 +0530 Subject: [PATCH 02/28] fix: endpoint details page scroll fix --- thingconnect.pulse.client/src/pages/EndpointDetail.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/thingconnect.pulse.client/src/pages/EndpointDetail.tsx b/thingconnect.pulse.client/src/pages/EndpointDetail.tsx index ab76fbf..8f7901c 100644 --- a/thingconnect.pulse.client/src/pages/EndpointDetail.tsx +++ b/thingconnect.pulse.client/src/pages/EndpointDetail.tsx @@ -371,25 +371,25 @@ export default function EndpointDetail() { {/* Recent Checks and Outages */} {/* Recent Checks */} - + Recent Checks - + {/* Recent Outages */} - + Recent Outages - + From 571c5855517c263a7ec176d9cdb48fe00caa47bc Mon Sep 17 00:00:00 2001 From: abishek Date: Fri, 19 Sep 2025 11:11:38 +0530 Subject: [PATCH 03/28] fix: dashboard button --- thingconnect.pulse.client/src/pages/EndpointDetail.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thingconnect.pulse.client/src/pages/EndpointDetail.tsx b/thingconnect.pulse.client/src/pages/EndpointDetail.tsx index 8f7901c..a968717 100644 --- a/thingconnect.pulse.client/src/pages/EndpointDetail.tsx +++ b/thingconnect.pulse.client/src/pages/EndpointDetail.tsx @@ -107,7 +107,7 @@ export default function EndpointDetail() { const backButton = ( - From 0cc07c9a16ac0fcd139cd0bb3efd92e417db02cb Mon Sep 17 00:00:00 2001 From: abishek Date: Fri, 19 Sep 2025 11:30:34 +0530 Subject: [PATCH 04/28] added reddit link --- .../src/icons/Discord.tsx | 16 ++++++++ thingconnect.pulse.client/src/pages/About.tsx | 38 +++++++++++++++---- 2 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 thingconnect.pulse.client/src/icons/Discord.tsx diff --git a/thingconnect.pulse.client/src/icons/Discord.tsx b/thingconnect.pulse.client/src/icons/Discord.tsx new file mode 100644 index 0000000..abe2bc6 --- /dev/null +++ b/thingconnect.pulse.client/src/icons/Discord.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +export function Discord(props: React.SVGProps) { + return ( + + Discord + + + ); +} diff --git a/thingconnect.pulse.client/src/pages/About.tsx b/thingconnect.pulse.client/src/pages/About.tsx index 9ad68fc..af09136 100644 --- a/thingconnect.pulse.client/src/pages/About.tsx +++ b/thingconnect.pulse.client/src/pages/About.tsx @@ -30,6 +30,7 @@ import { import { PageHeader } from '@/components/layout/PageHeader'; import { useForceRefreshNotifications, useNotificationStats } from '@/hooks/useNotifications'; import thingConnectLogo from '@/assets/thingconnect-pulse-logo.svg'; +import { Discord } from '@/icons/Discord'; export default function About() { const { data: stats } = useNotificationStats(); @@ -122,7 +123,7 @@ export default function About() { {[ { - icon: MessageCircle, + icon: Discord, title: 'Discord', desc: 'Community support and real-time help', tags: ['Community Support', 'Q&A', 'General Chat', 'Networking'], @@ -133,7 +134,7 @@ export default function About() { title: 'Reddit', desc: 'Share questions and experiences', tags: ['Discussions', 'Tips', 'Troubleshooting'], - link: 'https://reddit.com', + link: 'https://www.reddit.com/r/thingconnectio/', }, { icon: Linkedin, @@ -340,7 +341,10 @@ export default function About() { Unread Count: - + {stats?.unreadNotifications || 0} @@ -350,7 +354,9 @@ export default function About() { Last Sync: - {stats?.lastFetch ? new Date(stats.lastFetch).toLocaleDateString() : 'Never'} + {stats?.lastFetch + ? new Date(stats.lastFetch).toLocaleDateString() + : 'Never'} @@ -389,7 +395,8 @@ export default function About() { - Notifications are automatically synced every 6 hours. Use the button below to trigger an immediate refresh. + Notifications are automatically synced every 6 hours. Use the button below to + trigger an immediate refresh. @@ -405,18 +412,33 @@ export default function About() { {refreshMutation.isSuccess && ( - + Notifications refreshed successfully! )} {refreshMutation.isError && ( - + Failed to refresh notifications. Please try again. )} - + Syncs from: thingconnect-pulse.s3.ap-south-1.amazonaws.com From d988f74cc3167466c8ab56a5d222adca86c22c67 Mon Sep 17 00:00:00 2001 From: abishek Date: Sat, 20 Sep 2025 09:42:33 +0530 Subject: [PATCH 05/28] collapsible --- thingconnect.pulse.client/src/pages/History.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/thingconnect.pulse.client/src/pages/History.tsx b/thingconnect.pulse.client/src/pages/History.tsx index aeeea75..37aac87 100644 --- a/thingconnect.pulse.client/src/pages/History.tsx +++ b/thingconnect.pulse.client/src/pages/History.tsx @@ -170,7 +170,7 @@ export default function History() { {/* History Data */} - + Date: Sat, 20 Sep 2025 15:27:03 +0530 Subject: [PATCH 06/28] made the recent perfomacne as collapsible --- thingconnect.pulse.client/src/pages/EndpointDetail.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/thingconnect.pulse.client/src/pages/EndpointDetail.tsx b/thingconnect.pulse.client/src/pages/EndpointDetail.tsx index a968717..a6dd536 100644 --- a/thingconnect.pulse.client/src/pages/EndpointDetail.tsx +++ b/thingconnect.pulse.client/src/pages/EndpointDetail.tsx @@ -38,6 +38,7 @@ import { Skeleton } from '@/components/ui/skeleton'; import { EmptyState } from '@/components/ui/empty-state'; import { RecentChecksTable } from '@/components/RecentChecksTable'; import { OutagesList } from '@/components/OutageList'; +import { PageSection } from '@/components/layout/PageSection'; function getStatusColor(status: string) { switch (status.toLowerCase()) { @@ -339,8 +340,7 @@ export default function EndpointDetail() { {/* Statistics */} - - Recent Performance + {stats.map(stat => ( @@ -367,7 +367,8 @@ export default function EndpointDetail() { ))} - + + {/* Recent Checks and Outages */} {/* Recent Checks */} From b07aa4dcd5ff4ff4ad522fd841dd788696afa3e0 Mon Sep 17 00:00:00 2001 From: abishek Date: Fri, 26 Sep 2025 12:32:47 +0530 Subject: [PATCH 07/28] added a md file for icmp fallback --- ...icmp-fallback-and-outage-classification.md | 175 ++++++++++++++++++ .../outage-probe-flow-analysis.md | 0 2 files changed, 175 insertions(+) create mode 100644 docs/icmp-fallback-and-outage-classification.md rename outage-probe-flow-analysis.md => docs/outage-probe-flow-analysis.md (100%) diff --git a/docs/icmp-fallback-and-outage-classification.md b/docs/icmp-fallback-and-outage-classification.md new file mode 100644 index 0000000..44d6b8c --- /dev/null +++ b/docs/icmp-fallback-and-outage-classification.md @@ -0,0 +1,175 @@ +# 📡 ICMP Fallback – ThingConnect Pulse + +## 📝 Description + +ICMP fallback enhances outage classification accuracy by automatically performing ICMP ping tests when TCP or HTTP probes fail. +This enables **precise root cause analysis** by distinguishing between: + +- 🟠 **Service Outage** → Service down, host still reachable +- 🔴 **Network Outage** → Host completely unreachable +- 🟡 **Mixed Outage** → Flapping or unstable network + +Without fallback, failed HTTP probes are ambiguous (could mean web service down *or* full network isolation). +With ICMP fallback, ThingConnect Pulse intelligently classifies failures for **better diagnostics and reduced false positives**. + +--- + +## 🎯 Scope of Work + +### Core Implementation +- Add automatic ICMP fallback logic on TCP/HTTP failures +- Extend `CheckResult` model with fallback probe results +- Implement outage classification system (Network, Service, Mixed, Unknown) +- Update outage detection to consider fallback outcomes + +### Probe Enhancement +- Modify `ProbeService` to run ICMP fallback after failed TCP/HTTP +- Configurable fallback timeout (default **500ms**) +- Respect concurrency limits & jitter scheduling +- Preserve original error messages + add fallback context + +### Database Schema +- Extend **`CheckResultRaw`** with fallback probe fields +- Add **`OutageClassification`** enum +- Update **`Outage`** table with classification results +- Maintain backwards compatibility + +### UI Integration +- Dashboard shows classification badges (🔴, 🟠, 🟡) +- Endpoint details display fallback probe results +- History view filter by outage type +- CSV exports include `OutageType` + `FallbackResult` + +--- + +## ✅ Acceptance Criteria + +### Functional +- TCP/HTTP failures trigger ICMP fallback within **2s** +- ICMP fallback timeout = **500ms** (configurable) +- Correct outage classification applied: + - **Network Outage** → Primary + ICMP fail + - **Service Outage** → Primary fail, ICMP succeed + - **Mixed Outage** → Inconsistent results over multiple checks + - **Unknown** → Errors/timeout in fallback +- Jitter & concurrency respected +- Original error messages preserved + +### Performance +- <1s overhead on failed probes +- No extra cost on successful probes +- +20B memory per endpoint for fallback tracking +- ~30% DB storage increase for failed probe records + +### UX +- Dashboard shows color-coded outage types +- Endpoint details: "Last seen via ICMP" timestamps +- History charts distinguish outage types +- CSV exports include new fields + +--- + + +# 🔥 Outage Classification Decision Logic + +## 🌐 1. Primary Classification (Immediate Fallback) + +### For TCP/HTTP Probe Failures +TCP/HTTP Probe Fails +└── Execute ICMP Fallback +├── ICMP Succeeds → Service (2) +├── ICMP Fails → Network (1) +└── ICMP Timeout/Error → Unknown (0) + +### For ICMP Probe Failures +ICMP Probe Fails +└── No fallback needed → Network (1) + +### For Successful Probes +Probe Succeeds +├── RTT > Performance Threshold → Performance (4) +└── RTT Normal → No Classification + +--- + +## ⚙️ 2. Advanced Classification (Historical / Contextual Analysis) + +These rules are applied **after primary classification**, and may **override** the immediate result. + +### 🟡 Intermittent (3) +- **Trigger**: ≥4 UP/DOWN transitions in 15 minutes +- **Logic**: Analyze `CheckResultRaw` history +- **Override**: Reclassify `Network`/`Service` → `Intermittent` + +--- + +### 🟠 PartialService (5) *(HTTP only)* +- **Trigger**: HTTP 5xx errors + TCP succeeds +- **Logic**: Parse HTTP error codes from primary failure +- **Classification**: `PartialService` + +--- + +### 🔵 DnsResolution (6) +- **Trigger**: Hostname probe fails, IP probe succeeds +- **Logic**: Compare DNS resolution with fallback reachability +- **Classification**: `DnsResolution` + +--- + +### 🟣 Congestion (7) +- **Trigger**: RTT increase >50% across multiple endpoints simultaneously +- **Logic**: Cross-endpoint RTT correlation +- **Classification**: `Congestion` + +--- + +### 🟢 Maintenance (8) +- **Trigger**: Outage overlaps with maintenance window +- **Logic**: Check YAML `maintenance` schedule +- **Classification**: `Maintenance` + +--- + +## 📌 3. Classification Precedence + +When multiple conditions apply, the following precedence is used: + +1. **Maintenance (8)** – Highest priority +2. **DnsResolution (6)** / **PartialService (5)** – Specific service-layer issues +3. **Intermittent (3)** – Overrides unstable host classifications +4. **Congestion (7)** – Correlated cross-host slowdown +5. **Primary Classification (0, 1, 2, 4)** – Default result + +--- + +## 🗺️ 4. Mermaid Flowchart + +```mermaid +flowchart TD + A[Probe Result] -->|TCP/HTTP Fail| B[Run ICMP Fallback] + A -->|ICMP Fail| N[Network (1)] + A -->|Success| S[Check RTT] + + B -->|ICMP Success| Svc[Service (2)] + B -->|ICMP Fail| Net[Network (1)] + B -->|ICMP Timeout/Error| Unk[Unknown (0)] + + S -->|RTT > Threshold| Perf[Performance (4)] + S -->|RTT Normal| NoClass[No Classification] + + %% Advanced Overrides + subgraph Advanced + I[Intermittent (3)] + P[PartialService (5)] + D[DnsResolution (6)] + C[Congestion (7)] + M[Maintenance (8)] + end + + Net --> I + Svc --> I + A --> P + A --> D + A --> C + A --> M \ No newline at end of file diff --git a/outage-probe-flow-analysis.md b/docs/outage-probe-flow-analysis.md similarity index 100% rename from outage-probe-flow-analysis.md rename to docs/outage-probe-flow-analysis.md From eea29ac4caa24b6906efea0da64e6d1c6f806a40 Mon Sep 17 00:00:00 2001 From: abishek Date: Fri, 26 Sep 2025 12:42:35 +0530 Subject: [PATCH 08/28] updated entity, db context and migration for ICMP fallback and outage classification support --- ThingConnect.Pulse.Server/Data/Entities.cs | 27 +- .../Data/PulseDbContext.cs | 15 + ...allbackAndOutageClassification.Designer.cs | 768 +++++++++++++++ ...0803_AddFallbackAndOutageClassification.cs | 78 ++ .../Migrations/PulseDbContextModelSnapshot.cs | 18 + .../Monitoring/OutageCalssifaction.cs | 90 ++ .../obj/Debug/package.g.props | 27 +- thingconnect.pulse.client/package-lock.json | 872 +----------------- thingconnect.pulse.client/package.json | 5 - 9 files changed, 1021 insertions(+), 879 deletions(-) create mode 100644 ThingConnect.Pulse.Server/Migrations/20250926070803_AddFallbackAndOutageClassification.Designer.cs create mode 100644 ThingConnect.Pulse.Server/Migrations/20250926070803_AddFallbackAndOutageClassification.cs create mode 100644 ThingConnect.Pulse.Server/Services/Monitoring/OutageCalssifaction.cs diff --git a/ThingConnect.Pulse.Server/Data/Entities.cs b/ThingConnect.Pulse.Server/Data/Entities.cs index 29a344d..656f107 100644 --- a/ThingConnect.Pulse.Server/Data/Entities.cs +++ b/ThingConnect.Pulse.Server/Data/Entities.cs @@ -1,9 +1,26 @@ -// ThingConnect Pulse - EF Core Entities (v1) +// ThingConnect Pulse - EF Core Entities (v2) +// Updated for ICMP Fallback + Outage Classification namespace ThingConnect.Pulse.Server.Data; public enum ProbeType { icmp, tcp, http } public enum UpDown { up, down } +/// +/// Outage classification for failed probe analysis. +/// +public enum OutageClassification +{ + Unknown = 0, + Network = 1, // Host unreachable (ICMP + service fail) + Service = 2, // Service down, host reachable via ICMP + Intermittent = 3, // Flapping / unstable + Performance = 4, // RTT above threshold + PartialService = 5, // HTTP error, TCP works + DnsResolution = 6, // DNS fails, IP works + Congestion = 7, // Correlated latency + Maintenance = 8 // Planned downtime +} + public record GroupVm(string Id, string Name, string? ParentId, string? Color); public record EndpointVm(Guid Id, string Name, GroupVm Group, ProbeType Type, string Host, int? Port, string? HttpPath, string? HttpMatch, @@ -50,6 +67,13 @@ public sealed class CheckResultRaw public UpDown Status { get; set; } public double? RttMs { get; set; } public string? Error { get; set; } + + // 🔹 New fields for fallback probe + public bool? FallbackAttempted { get; set; } + public UpDown? FallbackStatus { get; set; } + public double? FallbackRttMs { get; set; } + public string? FallbackError { get; set; } + public OutageClassification? Classification { get; set; } } public sealed class Outage @@ -61,6 +85,7 @@ public sealed class Outage public long? EndedTs { get; set; } public int? DurationSeconds { get; set; } public string? LastError { get; set; } + public OutageClassification? Classification { get; set; } /// /// Gets or sets timestamp when monitoring was lost during this outage (service downtime). diff --git a/ThingConnect.Pulse.Server/Data/PulseDbContext.cs b/ThingConnect.Pulse.Server/Data/PulseDbContext.cs index ebbaf7c..f21ad85 100644 --- a/ThingConnect.Pulse.Server/Data/PulseDbContext.cs +++ b/ThingConnect.Pulse.Server/Data/PulseDbContext.cs @@ -65,6 +65,17 @@ protected override void OnModelCreating(ModelBuilder b) e.HasKey(x => x.Id); e.Property(x => x.Status).HasConversion().IsRequired(); e.Property(x => x.RttMs).HasColumnType("double precision"); + + // New Fallback fields + e.Property(x => x.FallbackAttempted); + e.Property(x => x.FallbackStatus).HasConversion(); + e.Property(x => x.FallbackRttMs).HasColumnType("double precision"); + e.Property(x => x.FallbackError); + + // Classification field + e.Property(x => x.Classification) + .HasConversion(); + e.HasIndex(x => new { x.EndpointId, x.Ts }); }); @@ -74,6 +85,10 @@ protected override void OnModelCreating(ModelBuilder b) e.HasKey(x => x.Id); e.HasIndex(x => new { x.EndpointId, x.StartedTs }); e.HasIndex(x => new { x.EndpointId, x.EndedTs }); + + // New Classification field + e.Property(x => x.Classification) + .HasConversion(); }); b.Entity(e => diff --git a/ThingConnect.Pulse.Server/Migrations/20250926070803_AddFallbackAndOutageClassification.Designer.cs b/ThingConnect.Pulse.Server/Migrations/20250926070803_AddFallbackAndOutageClassification.Designer.cs new file mode 100644 index 0000000..ae19086 --- /dev/null +++ b/ThingConnect.Pulse.Server/Migrations/20250926070803_AddFallbackAndOutageClassification.Designer.cs @@ -0,0 +1,768 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using ThingConnect.Pulse.Server.Data; + +#nullable disable + +namespace ThingConnect.Pulse.Server.Migrations +{ + [DbContext(typeof(PulseDbContext))] + [Migration("20250926070803_AddFallbackAndOutageClassification")] + partial class AddFallbackAndOutageClassification + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("ThingConnect.Pulse.Server.Data.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("LastLoginAt") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("Role"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("ThingConnect.Pulse.Server.Data.CheckResultRaw", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Classification") + .HasColumnType("INTEGER"); + + b.Property("EndpointId") + .HasColumnType("TEXT"); + + b.Property("Error") + .HasColumnType("TEXT"); + + b.Property("FallbackAttempted") + .HasColumnType("INTEGER"); + + b.Property("FallbackError") + .HasColumnType("TEXT"); + + b.Property("FallbackRttMs") + .HasColumnType("double precision"); + + b.Property("FallbackStatus") + .HasColumnType("TEXT"); + + b.Property("RttMs") + .HasColumnType("double precision"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Ts") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EndpointId", "Ts"); + + b.ToTable("check_result_raw", (string)null); + }); + + modelBuilder.Entity("ThingConnect.Pulse.Server.Data.ConfigVersion", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Actor") + .HasColumnType("TEXT"); + + b.Property("AppliedTs") + .HasColumnType("INTEGER"); + + b.Property("FileHash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppliedTs"); + + b.ToTable("config_version", (string)null); + }); + + modelBuilder.Entity("ThingConnect.Pulse.Server.Data.Endpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExpectedRttMs") + .HasColumnType("INTEGER"); + + b.Property("GroupId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Host") + .IsRequired() + .HasMaxLength(253) + .HasColumnType("TEXT"); + + b.Property("HttpMatch") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("HttpPath") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("IntervalSeconds") + .HasColumnType("INTEGER"); + + b.Property("LastChangeTs") + .HasColumnType("INTEGER"); + + b.Property("LastRttMs") + .HasColumnType("REAL"); + + b.Property("LastStatus") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("Retries") + .HasColumnType("INTEGER"); + + b.Property("TimeoutMs") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Host"); + + b.HasIndex("GroupId", "Name"); + + b.ToTable("endpoint", (string)null); + }); + + modelBuilder.Entity("ThingConnect.Pulse.Server.Data.Group", b => + { + b.Property("Id") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Color") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("group", (string)null); + }); + + modelBuilder.Entity("ThingConnect.Pulse.Server.Data.MonitoringSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EndedTs") + .HasColumnType("INTEGER"); + + b.Property("LastActivityTs") + .HasColumnType("INTEGER"); + + b.Property("ShutdownReason") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("StartedTs") + .HasColumnType("INTEGER"); + + b.Property("Version") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("EndedTs"); + + b.HasIndex("StartedTs"); + + b.ToTable("monitoring_session", (string)null); + }); + + modelBuilder.Entity("ThingConnect.Pulse.Server.Data.Notification", b => + { + b.Property("Id") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("ActionText") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ActionUrl") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("CreatedTs") + .HasColumnType("INTEGER"); + + b.Property("IsRead") + .HasColumnType("INTEGER"); + + b.Property("IsShown") + .HasColumnType("INTEGER"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ReadTs") + .HasColumnType("INTEGER"); + + b.Property("ShowOnce") + .HasColumnType("INTEGER"); + + b.Property("ShownTs") + .HasColumnType("INTEGER"); + + b.Property("TargetVersions") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ValidFromTs") + .HasColumnType("INTEGER"); + + b.Property("ValidUntilTs") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ValidFromTs"); + + b.HasIndex("ValidUntilTs"); + + b.HasIndex("IsRead", "ValidFromTs"); + + b.HasIndex("Priority", "ValidFromTs"); + + b.ToTable("notification", (string)null); + }); + + modelBuilder.Entity("ThingConnect.Pulse.Server.Data.NotificationFetch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Error") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("FetchTs") + .HasColumnType("INTEGER"); + + b.Property("NotificationCount") + .HasColumnType("INTEGER"); + + b.Property("RemoteLastUpdated") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RemoteVersion") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Success") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("FetchTs"); + + b.HasIndex("Success"); + + b.ToTable("notification_fetch", (string)null); + }); + + modelBuilder.Entity("ThingConnect.Pulse.Server.Data.Outage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Classification") + .HasColumnType("INTEGER"); + + b.Property("DurationSeconds") + .HasColumnType("INTEGER"); + + b.Property("EndedTs") + .HasColumnType("INTEGER"); + + b.Property("EndpointId") + .HasColumnType("TEXT"); + + b.Property("HasMonitoringGap") + .HasColumnType("INTEGER"); + + b.Property("LastError") + .HasColumnType("TEXT"); + + b.Property("MonitoringStoppedTs") + .HasColumnType("INTEGER"); + + b.Property("StartedTs") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EndpointId", "EndedTs"); + + b.HasIndex("EndpointId", "StartedTs"); + + b.ToTable("outage", (string)null); + }); + + modelBuilder.Entity("ThingConnect.Pulse.Server.Data.Rollup15m", b => + { + b.Property("EndpointId") + .HasColumnType("TEXT"); + + b.Property("BucketTs") + .HasColumnType("INTEGER"); + + b.Property("AvgRttMs") + .HasColumnType("double precision"); + + b.Property("DownEvents") + .HasColumnType("INTEGER"); + + b.Property("UpPct") + .HasColumnType("REAL"); + + b.HasKey("EndpointId", "BucketTs"); + + b.HasIndex("BucketTs"); + + b.ToTable("rollup_15m", (string)null); + }); + + modelBuilder.Entity("ThingConnect.Pulse.Server.Data.RollupDaily", b => + { + b.Property("EndpointId") + .HasColumnType("TEXT"); + + b.Property("BucketDate") + .HasColumnType("INTEGER"); + + b.Property("AvgRttMs") + .HasColumnType("double precision"); + + b.Property("DownEvents") + .HasColumnType("INTEGER"); + + b.Property("UpPct") + .HasColumnType("REAL"); + + b.HasKey("EndpointId", "BucketDate"); + + b.HasIndex("BucketDate"); + + b.ToTable("rollup_daily", (string)null); + }); + + modelBuilder.Entity("ThingConnect.Pulse.Server.Data.Setting", b => + { + b.Property("K") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("V") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("K"); + + b.ToTable("setting", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("ThingConnect.Pulse.Server.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("ThingConnect.Pulse.Server.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ThingConnect.Pulse.Server.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("ThingConnect.Pulse.Server.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ThingConnect.Pulse.Server.Data.CheckResultRaw", b => + { + b.HasOne("ThingConnect.Pulse.Server.Data.Endpoint", "Endpoint") + .WithMany() + .HasForeignKey("EndpointId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Endpoint"); + }); + + modelBuilder.Entity("ThingConnect.Pulse.Server.Data.Endpoint", b => + { + b.HasOne("ThingConnect.Pulse.Server.Data.Group", "Group") + .WithMany("Endpoints") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("ThingConnect.Pulse.Server.Data.Outage", b => + { + b.HasOne("ThingConnect.Pulse.Server.Data.Endpoint", "Endpoint") + .WithMany() + .HasForeignKey("EndpointId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Endpoint"); + }); + + modelBuilder.Entity("ThingConnect.Pulse.Server.Data.Rollup15m", b => + { + b.HasOne("ThingConnect.Pulse.Server.Data.Endpoint", "Endpoint") + .WithMany() + .HasForeignKey("EndpointId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Endpoint"); + }); + + modelBuilder.Entity("ThingConnect.Pulse.Server.Data.RollupDaily", b => + { + b.HasOne("ThingConnect.Pulse.Server.Data.Endpoint", "Endpoint") + .WithMany() + .HasForeignKey("EndpointId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Endpoint"); + }); + + modelBuilder.Entity("ThingConnect.Pulse.Server.Data.Group", b => + { + b.Navigation("Endpoints"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ThingConnect.Pulse.Server/Migrations/20250926070803_AddFallbackAndOutageClassification.cs b/ThingConnect.Pulse.Server/Migrations/20250926070803_AddFallbackAndOutageClassification.cs new file mode 100644 index 0000000..c8b60c8 --- /dev/null +++ b/ThingConnect.Pulse.Server/Migrations/20250926070803_AddFallbackAndOutageClassification.cs @@ -0,0 +1,78 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ThingConnect.Pulse.Server.Migrations +{ + /// + public partial class AddFallbackAndOutageClassification : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Classification", + table: "outage", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "Classification", + table: "check_result_raw", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "FallbackAttempted", + table: "check_result_raw", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "FallbackError", + table: "check_result_raw", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "FallbackRttMs", + table: "check_result_raw", + type: "double precision", + nullable: true); + + migrationBuilder.AddColumn( + name: "FallbackStatus", + table: "check_result_raw", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Classification", + table: "outage"); + + migrationBuilder.DropColumn( + name: "Classification", + table: "check_result_raw"); + + migrationBuilder.DropColumn( + name: "FallbackAttempted", + table: "check_result_raw"); + + migrationBuilder.DropColumn( + name: "FallbackError", + table: "check_result_raw"); + + migrationBuilder.DropColumn( + name: "FallbackRttMs", + table: "check_result_raw"); + + migrationBuilder.DropColumn( + name: "FallbackStatus", + table: "check_result_raw"); + } + } +} diff --git a/ThingConnect.Pulse.Server/Migrations/PulseDbContextModelSnapshot.cs b/ThingConnect.Pulse.Server/Migrations/PulseDbContextModelSnapshot.cs index 51bbbb1..aeccc31 100644 --- a/ThingConnect.Pulse.Server/Migrations/PulseDbContextModelSnapshot.cs +++ b/ThingConnect.Pulse.Server/Migrations/PulseDbContextModelSnapshot.cs @@ -238,12 +238,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("Classification") + .HasColumnType("INTEGER"); + b.Property("EndpointId") .HasColumnType("TEXT"); b.Property("Error") .HasColumnType("TEXT"); + b.Property("FallbackAttempted") + .HasColumnType("INTEGER"); + + b.Property("FallbackError") + .HasColumnType("TEXT"); + + b.Property("FallbackRttMs") + .HasColumnType("double precision"); + + b.Property("FallbackStatus") + .HasColumnType("TEXT"); + b.Property("RttMs") .HasColumnType("double precision"); @@ -538,6 +553,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("Classification") + .HasColumnType("INTEGER"); + b.Property("DurationSeconds") .HasColumnType("INTEGER"); diff --git a/ThingConnect.Pulse.Server/Services/Monitoring/OutageCalssifaction.cs b/ThingConnect.Pulse.Server/Services/Monitoring/OutageCalssifaction.cs new file mode 100644 index 0000000..5d5dc34 --- /dev/null +++ b/ThingConnect.Pulse.Server/Services/Monitoring/OutageCalssifaction.cs @@ -0,0 +1,90 @@ +// public static OutageClassification ClassifyOutage( +// CheckResult primaryResult, +// CheckResult? fallbackResult, +// Endpoint endpoint, +// IEnumerable recentHistory) +// { +// // 1. ICMP probes always Network on failure +// if (endpoint.Type == ProbeType.icmp && primaryResult.Status == UpDown.down) +// return OutageClassification.Network; + +// // 2. Successful probes - check performance +// if (primaryResult.Status == UpDown.up) +// { +// if (IsPerformanceDegraded(primaryResult, endpoint)) +// return OutageClassification.Performance; +// return OutageClassification.Unknown; // No outage +// } + +// // 3. Failed TCP/HTTP probes - use fallback +// if (fallbackResult?.IsExecuted == true) +// { +// var baseClassification = fallbackResult.IsSuccess ? +// OutageClassification.Service : OutageClassification.Network; + +// // 4. Check for advanced patterns +// if (IsIntermittent(recentHistory)) +// return OutageClassification.Intermittent; + +// if (IsPartialService(primaryResult, fallbackResult, endpoint)) +// return OutageClassification.PartialService; + +// if (IsDnsIssue(endpoint, fallbackResult)) +// return OutageClassification.DnsResolution; + +// return baseClassification; +// } + +// // 5. Fallback failed or no fallback +// return OutageClassification.Unknown; +// } + +// // Helper Methods +// private static bool IsPerformanceDegraded(CheckResult result, Endpoint endpoint) +// { +// if (!result.RttMs.HasValue) return false; + +// double threshold = endpoint.Type == ProbeType.icmp ? 2.0 : 3.0; +// double baselineRtt = endpoint.ExpectedRttMs ?? 100; // Default 100ms baseline + +// return result.RttMs > baselineRtt * threshold; +// } + +// private static bool IsIntermittent(IEnumerable recentHistory) +// { +// var last15Min = recentHistory +// .Where(r => r.Timestamp > DateTimeOffset.UtcNow.AddMinutes(-15)) +// .OrderBy(r => r.Timestamp) +// .ToList(); + +// if (last15Min.Count < 4) return false; + +// int transitions = 0; +// for (int i = 1; i < last15Min.Count; i++) +// { +// if (last15Min[i].Status != last15Min[i-1].Status) +// transitions++; +// } + +// return transitions >= 4; +// } + +// private static bool IsPartialService(CheckResult primary, CheckResult fallback, Endpoint endpoint) +// { +// return endpoint.Type == ProbeType.http && +// primary.Error?.Contains("50") == true && // 5xx HTTP errors +// fallback.IsSuccess == true; +// } + +// private static bool IsDnsIssue(Endpoint endpoint, CheckResult fallbackResult) +// { +// // Only for hostnames, not IP addresses +// return IsHostname(endpoint.Host) && +// fallbackResult.IsSuccess == false && +// fallbackResult.Error?.Contains("resolve") == true; +// } + +// private static bool IsHostname(string host) +// { +// return !IPAddress.TryParse(host, out _); +// } diff --git a/thingconnect.pulse.client/obj/Debug/package.g.props b/thingconnect.pulse.client/obj/Debug/package.g.props index b0a9162..9f3e00f 100644 --- a/thingconnect.pulse.client/obj/Debug/package.g.props +++ b/thingconnect.pulse.client/obj/Debug/package.g.props @@ -17,11 +17,6 @@ ^4.7.0 ^10.11.0 ^5.84.2 - ^3.12.0 - ^3.12.0 - ^3.12.0 - ^3.12.0 - ^3.12.0 ^1.11.0 ^4.1.0 ^4.17.21 @@ -36,31 +31,31 @@ ^7.62.0 ^5.5.0 ^7.8.1 - ^4.0.17 + ^4.1.9 ^3.24.0 ^9.33.0 ^5.83.1 ^5.85.5 ^4.17.20 ^3.7.1 - ^22 + ^24 ^2.0.5 - ^19.1.10 - ^19.1.7 - ^3.11.0 - ^9.33.0 + ^19.1.13 + ^19.1.9 + ^4.1.0 + ^9.35.0 ^10.1.8 - ^1.52.3 + ^1.53.1 ^5.2.0 ^0.4.20 - ^1.52.3 + ^1.53.1 ^16.3.0 ^9.1.7 ^16.1.4 ^3.6.2 - ~5.8.3 - ^8.39.1 - ^7.1.2 + ~5.9.2 + ^8.44.0 + ^7.1.6 ^5.1.4 [ "eslint --fix", diff --git a/thingconnect.pulse.client/package-lock.json b/thingconnect.pulse.client/package-lock.json index f026816..0c030f2 100644 --- a/thingconnect.pulse.client/package-lock.json +++ b/thingconnect.pulse.client/package-lock.json @@ -14,11 +14,6 @@ "@monaco-editor/react": "^4.7.0", "@sentry/react": "^10.11.0", "@tanstack/react-query": "^5.84.2", - "@visx/axis": "^3.12.0", - "@visx/responsive": "^3.12.0", - "@visx/scale": "^3.12.0", - "@visx/shape": "^3.12.0", - "@visx/tooltip": "^3.12.0", "axios": "^1.11.0", "date-fns": "^4.1.0", "lodash": "^4.17.21", @@ -2133,84 +2128,6 @@ "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==" }, - "node_modules/@types/d3-array": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz", - "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==", - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==", - "license": "MIT" - }, - "node_modules/@types/d3-delaunay": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz", - "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==", - "license": "MIT" - }, - "node_modules/@types/d3-format": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", - "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==", - "license": "MIT" - }, - "node_modules/@types/d3-geo": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz", - "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==", - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-shape": { - "version": "1.3.12", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz", - "integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==", - "license": "MIT", - "dependencies": { - "@types/d3-path": "^1" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", - "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==", - "license": "MIT" - }, - "node_modules/@types/d3-time-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.1.0.tgz", - "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==", - "license": "MIT" - }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -2226,12 +2143,6 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "license": "MIT" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2241,7 +2152,8 @@ "node_modules/@types/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==" + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true }, "node_modules/@types/luxon": { "version": "3.7.1", @@ -2281,6 +2193,7 @@ "version": "19.1.13", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", + "dev": true, "dependencies": { "csstype": "^3.0.2" } @@ -2289,6 +2202,7 @@ "version": "19.1.9", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", + "dev": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -2550,178 +2464,6 @@ "node": ">=20.18 <=24.x" } }, - "node_modules/@visx/axis": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/axis/-/axis-3.12.0.tgz", - "integrity": "sha512-8MoWpfuaJkhm2Yg+HwyytK8nk+zDugCqTT/tRmQX7gW4LYrHYLXFUXOzbDyyBakCVaUbUaAhVFxpMASJiQKf7A==", - "license": "MIT", - "dependencies": { - "@types/react": "*", - "@visx/group": "3.12.0", - "@visx/point": "3.12.0", - "@visx/scale": "3.12.0", - "@visx/shape": "3.12.0", - "@visx/text": "3.12.0", - "classnames": "^2.3.1", - "prop-types": "^15.6.0" - }, - "peerDependencies": { - "react": "^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0" - } - }, - "node_modules/@visx/bounds": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/bounds/-/bounds-3.12.0.tgz", - "integrity": "sha512-peAlNCUbYaaZ0IO6c1lDdEAnZv2iGPDiLIM8a6gu7CaMhtXZJkqrTh+AjidNcIqITktrICpGxJE/Qo9D099dvQ==", - "license": "MIT", - "dependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "prop-types": "^15.5.10" - }, - "peerDependencies": { - "react": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0", - "react-dom": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0" - } - }, - "node_modules/@visx/curve": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-3.12.0.tgz", - "integrity": "sha512-Ng1mefXIzoIoAivw7dJ+ZZYYUbfuwXgZCgQynShr6ZIVw7P4q4HeQfJP3W24ON+1uCSrzoycHSXRelhR9SBPcw==", - "license": "MIT", - "dependencies": { - "@types/d3-shape": "^1.3.1", - "d3-shape": "^1.0.6" - } - }, - "node_modules/@visx/group": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/group/-/group-3.12.0.tgz", - "integrity": "sha512-Dye8iS1alVXPv7nj/7M37gJe6sSKqJLH7x6sEWAsRQ9clI0kFvjbKcKgF+U3aAVQr0NCohheFV+DtR8trfK/Ag==", - "license": "MIT", - "dependencies": { - "@types/react": "*", - "classnames": "^2.3.1", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0" - } - }, - "node_modules/@visx/point": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/point/-/point-3.12.0.tgz", - "integrity": "sha512-I6UrHoJAEVbx3RORQNupgTiX5EzjuZpiwLPxn8L2mI5nfERotPKi1Yus12Cq2WtXqEBR/WgqTnoImlqOXBykcA==", - "license": "MIT" - }, - "node_modules/@visx/responsive": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-3.12.0.tgz", - "integrity": "sha512-GV1BTYwAGlk/K5c9vH8lS2syPnTuIqEacI7L6LRPbsuaLscXMNi+i9fZyzo0BWvAdtRV8v6Urzglo++lvAXT1Q==", - "license": "MIT", - "dependencies": { - "@types/lodash": "^4.14.172", - "@types/react": "*", - "lodash": "^4.17.21", - "prop-types": "^15.6.1" - }, - "peerDependencies": { - "react": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0" - } - }, - "node_modules/@visx/scale": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-3.12.0.tgz", - "integrity": "sha512-+ubijrZ2AwWCsNey0HGLJ0YKNeC/XImEFsr9rM+Uef1CM3PNM43NDdNTrdBejSlzRq0lcfQPWYMYQFSlkLcPOg==", - "license": "MIT", - "dependencies": { - "@visx/vendor": "3.12.0" - } - }, - "node_modules/@visx/shape": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-3.12.0.tgz", - "integrity": "sha512-/1l0lrpX9tPic6SJEalryBKWjP/ilDRnQA+BGJTI1tj7i23mJ/J0t4nJHyA1GrL4QA/bM/qTJ35eyz5dEhJc4g==", - "license": "MIT", - "dependencies": { - "@types/d3-path": "^1.0.8", - "@types/d3-shape": "^1.3.1", - "@types/lodash": "^4.14.172", - "@types/react": "*", - "@visx/curve": "3.12.0", - "@visx/group": "3.12.0", - "@visx/scale": "3.12.0", - "classnames": "^2.3.1", - "d3-path": "^1.0.5", - "d3-shape": "^1.2.0", - "lodash": "^4.17.21", - "prop-types": "^15.5.10" - }, - "peerDependencies": { - "react": "^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0" - } - }, - "node_modules/@visx/text": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/text/-/text-3.12.0.tgz", - "integrity": "sha512-0rbDYQlbuKPhBqXkkGYKFec1gQo05YxV45DORzr6hCyaizdJk1G+n9VkuKSHKBy1vVQhBA0W3u/WXd7tiODQPA==", - "license": "MIT", - "dependencies": { - "@types/lodash": "^4.14.172", - "@types/react": "*", - "classnames": "^2.3.1", - "lodash": "^4.17.21", - "prop-types": "^15.7.2", - "reduce-css-calc": "^1.3.0" - }, - "peerDependencies": { - "react": "^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0" - } - }, - "node_modules/@visx/tooltip": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/tooltip/-/tooltip-3.12.0.tgz", - "integrity": "sha512-pWhsYhgl0Shbeqf80qy4QCB6zpq6tQtMQQxKlh3UiKxzkkfl+Metaf9p0/S0HexNi4vewOPOo89xWx93hBeh3A==", - "license": "MIT", - "dependencies": { - "@types/react": "*", - "@visx/bounds": "3.12.0", - "classnames": "^2.3.1", - "prop-types": "^15.5.10", - "react-use-measure": "^2.0.4" - }, - "peerDependencies": { - "react": "^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0", - "react-dom": "^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0" - } - }, - "node_modules/@visx/vendor": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-3.12.0.tgz", - "integrity": "sha512-SVO+G0xtnL9dsNpGDcjCgoiCnlB3iLSM9KLz1sLbSrV7RaVXwY3/BTm2X9OWN1jH2a9M+eHt6DJ6sE6CXm4cUg==", - "license": "MIT and ISC", - "dependencies": { - "@types/d3-array": "3.0.3", - "@types/d3-color": "3.1.0", - "@types/d3-delaunay": "6.0.1", - "@types/d3-format": "3.0.1", - "@types/d3-geo": "3.1.0", - "@types/d3-interpolate": "3.0.1", - "@types/d3-scale": "4.0.2", - "@types/d3-time": "3.0.0", - "@types/d3-time-format": "2.1.0", - "d3-array": "3.2.1", - "d3-color": "3.1.0", - "d3-delaunay": "6.0.2", - "d3-format": "3.1.0", - "d3-geo": "3.1.0", - "d3-interpolate": "3.0.1", - "d3-scale": "4.0.2", - "d3-time": "3.1.0", - "d3-time-format": "4.1.0", - "internmap": "2.0.3" - } - }, "node_modules/@vitejs/plugin-react-swc": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.1.0.tgz", @@ -3673,7 +3415,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/base64-arraybuffer": { "version": "1.0.2", @@ -3792,12 +3535,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", - "license": "MIT" - }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -3961,127 +3698,6 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, - "node_modules/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-delaunay": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", - "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", - "license": "ISC", - "dependencies": { - "delaunator": "5" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-geo": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", - "license": "ISC", - "dependencies": { - "d3-array": "2.5.0 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", - "license": "BSD-3-Clause" - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-path": "1" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -4122,15 +3738,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", - "license": "ISC", - "dependencies": { - "robust-predicates": "^3.0.2" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5088,15 +4695,6 @@ "node": ">=0.8.19" } }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -5455,18 +5053,6 @@ "integrity": "sha512-nMoGWW2HurtuJf6XAL56FWTDCWLOTSsanrgwOyaR5Y4e3zfG5N/0cU5xWZSEU3tBxhQugRbV1xL9jb+ug7yZww==", "dev": true }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -5490,12 +5076,6 @@ "node": ">=12" } }, - "node_modules/math-expression-evaluator": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.4.0.tgz", - "integrity": "sha512-4vRUvPyxdO8cWULGTh9dZWL2tZK6LDBvj+OGHBER7poH9Qdt7kXEoj20wiz4lQUbUXQZFjPbe5mVDo9nutizCw==", - "license": "MIT" - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5731,6 +5311,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -5987,17 +5568,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, "node_modules/proxy-compare": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz", @@ -6129,21 +5699,6 @@ "react-dom": ">=18" } }, - "node_modules/react-use-measure": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", - "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.13", - "react-dom": ">=16.13" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -6156,32 +5711,6 @@ "node": ">=8.10.0" } }, - "node_modules/reduce-css-calc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", - "integrity": "sha512-0dVfwYVOlf/LBA2ec4OwQ6p3X9mYxn/wOl2xTcLwjnPYrkgEfPx3VI4eGCH3rQLlPISG5v9I9bkZosKsNRTRKA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^0.4.2", - "math-expression-evaluator": "^1.2.14", - "reduce-function-call": "^1.0.1" - } - }, - "node_modules/reduce-css-calc/node_modules/balanced-match": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "integrity": "sha512-STw03mQKnGUYtoNjmowo4F2cRmIIxYEGiMsjjwla/u5P1lxadj/05WkNaFjNiKTgJkj8KiXbgAiRTmcQRwQNtg==", - "license": "MIT" - }, - "node_modules/reduce-function-call": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz", - "integrity": "sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -6241,12 +5770,6 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true }, - "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", - "license": "Unlicense" - }, "node_modules/rollup": { "version": "4.48.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.48.0.tgz", @@ -8512,73 +8035,6 @@ "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==" }, - "@types/d3-array": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz", - "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==" - }, - "@types/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==" - }, - "@types/d3-delaunay": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz", - "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==" - }, - "@types/d3-format": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", - "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==" - }, - "@types/d3-geo": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", - "requires": { - "@types/geojson": "*" - } - }, - "@types/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", - "requires": { - "@types/d3-color": "*" - } - }, - "@types/d3-path": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz", - "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==" - }, - "@types/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==", - "requires": { - "@types/d3-time": "*" - } - }, - "@types/d3-shape": { - "version": "1.3.12", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz", - "integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==", - "requires": { - "@types/d3-path": "^1" - } - }, - "@types/d3-time": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", - "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==" - }, - "@types/d3-time-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.1.0.tgz", - "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==" - }, "@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -8594,11 +8050,6 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, - "@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==" - }, "@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -8608,7 +8059,8 @@ "@types/lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==" + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true }, "@types/luxon": { "version": "3.7.1", @@ -8646,6 +8098,7 @@ "version": "19.1.13", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", + "dev": true, "requires": { "csstype": "^3.0.2" } @@ -8654,6 +8107,7 @@ "version": "19.1.9", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", + "dev": true, "requires": {} }, "@typescript-eslint/eslint-plugin": { @@ -8795,144 +8249,6 @@ "integrity": "sha512-eqm/OzwETl1Zd5ehW5CUXhYf8tqb+seBCkHBKXh1rEMS94n+OhyCY0KAlZv/17qPoN73WT2nGDN9SdYlvoWbTQ==", "dev": true }, - "@visx/axis": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/axis/-/axis-3.12.0.tgz", - "integrity": "sha512-8MoWpfuaJkhm2Yg+HwyytK8nk+zDugCqTT/tRmQX7gW4LYrHYLXFUXOzbDyyBakCVaUbUaAhVFxpMASJiQKf7A==", - "requires": { - "@types/react": "*", - "@visx/group": "3.12.0", - "@visx/point": "3.12.0", - "@visx/scale": "3.12.0", - "@visx/shape": "3.12.0", - "@visx/text": "3.12.0", - "classnames": "^2.3.1", - "prop-types": "^15.6.0" - } - }, - "@visx/bounds": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/bounds/-/bounds-3.12.0.tgz", - "integrity": "sha512-peAlNCUbYaaZ0IO6c1lDdEAnZv2iGPDiLIM8a6gu7CaMhtXZJkqrTh+AjidNcIqITktrICpGxJE/Qo9D099dvQ==", - "requires": { - "@types/react": "*", - "@types/react-dom": "*", - "prop-types": "^15.5.10" - } - }, - "@visx/curve": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-3.12.0.tgz", - "integrity": "sha512-Ng1mefXIzoIoAivw7dJ+ZZYYUbfuwXgZCgQynShr6ZIVw7P4q4HeQfJP3W24ON+1uCSrzoycHSXRelhR9SBPcw==", - "requires": { - "@types/d3-shape": "^1.3.1", - "d3-shape": "^1.0.6" - } - }, - "@visx/group": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/group/-/group-3.12.0.tgz", - "integrity": "sha512-Dye8iS1alVXPv7nj/7M37gJe6sSKqJLH7x6sEWAsRQ9clI0kFvjbKcKgF+U3aAVQr0NCohheFV+DtR8trfK/Ag==", - "requires": { - "@types/react": "*", - "classnames": "^2.3.1", - "prop-types": "^15.6.2" - } - }, - "@visx/point": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/point/-/point-3.12.0.tgz", - "integrity": "sha512-I6UrHoJAEVbx3RORQNupgTiX5EzjuZpiwLPxn8L2mI5nfERotPKi1Yus12Cq2WtXqEBR/WgqTnoImlqOXBykcA==" - }, - "@visx/responsive": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-3.12.0.tgz", - "integrity": "sha512-GV1BTYwAGlk/K5c9vH8lS2syPnTuIqEacI7L6LRPbsuaLscXMNi+i9fZyzo0BWvAdtRV8v6Urzglo++lvAXT1Q==", - "requires": { - "@types/lodash": "^4.14.172", - "@types/react": "*", - "lodash": "^4.17.21", - "prop-types": "^15.6.1" - } - }, - "@visx/scale": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-3.12.0.tgz", - "integrity": "sha512-+ubijrZ2AwWCsNey0HGLJ0YKNeC/XImEFsr9rM+Uef1CM3PNM43NDdNTrdBejSlzRq0lcfQPWYMYQFSlkLcPOg==", - "requires": { - "@visx/vendor": "3.12.0" - } - }, - "@visx/shape": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-3.12.0.tgz", - "integrity": "sha512-/1l0lrpX9tPic6SJEalryBKWjP/ilDRnQA+BGJTI1tj7i23mJ/J0t4nJHyA1GrL4QA/bM/qTJ35eyz5dEhJc4g==", - "requires": { - "@types/d3-path": "^1.0.8", - "@types/d3-shape": "^1.3.1", - "@types/lodash": "^4.14.172", - "@types/react": "*", - "@visx/curve": "3.12.0", - "@visx/group": "3.12.0", - "@visx/scale": "3.12.0", - "classnames": "^2.3.1", - "d3-path": "^1.0.5", - "d3-shape": "^1.2.0", - "lodash": "^4.17.21", - "prop-types": "^15.5.10" - } - }, - "@visx/text": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/text/-/text-3.12.0.tgz", - "integrity": "sha512-0rbDYQlbuKPhBqXkkGYKFec1gQo05YxV45DORzr6hCyaizdJk1G+n9VkuKSHKBy1vVQhBA0W3u/WXd7tiODQPA==", - "requires": { - "@types/lodash": "^4.14.172", - "@types/react": "*", - "classnames": "^2.3.1", - "lodash": "^4.17.21", - "prop-types": "^15.7.2", - "reduce-css-calc": "^1.3.0" - } - }, - "@visx/tooltip": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/tooltip/-/tooltip-3.12.0.tgz", - "integrity": "sha512-pWhsYhgl0Shbeqf80qy4QCB6zpq6tQtMQQxKlh3UiKxzkkfl+Metaf9p0/S0HexNi4vewOPOo89xWx93hBeh3A==", - "requires": { - "@types/react": "*", - "@visx/bounds": "3.12.0", - "classnames": "^2.3.1", - "prop-types": "^15.5.10", - "react-use-measure": "^2.0.4" - } - }, - "@visx/vendor": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-3.12.0.tgz", - "integrity": "sha512-SVO+G0xtnL9dsNpGDcjCgoiCnlB3iLSM9KLz1sLbSrV7RaVXwY3/BTm2X9OWN1jH2a9M+eHt6DJ6sE6CXm4cUg==", - "requires": { - "@types/d3-array": "3.0.3", - "@types/d3-color": "3.1.0", - "@types/d3-delaunay": "6.0.1", - "@types/d3-format": "3.0.1", - "@types/d3-geo": "3.1.0", - "@types/d3-interpolate": "3.0.1", - "@types/d3-scale": "4.0.2", - "@types/d3-time": "3.0.0", - "@types/d3-time-format": "2.1.0", - "d3-array": "3.2.1", - "d3-color": "3.1.0", - "d3-delaunay": "6.0.2", - "d3-format": "3.1.0", - "d3-geo": "3.1.0", - "d3-interpolate": "3.0.1", - "d3-scale": "4.0.2", - "d3-time": "3.1.0", - "d3-time-format": "4.1.0", - "internmap": "2.0.3" - } - }, "@vitejs/plugin-react-swc": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.1.0.tgz", @@ -9825,7 +9141,8 @@ "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "base64-arraybuffer": { "version": "1.0.2", @@ -9912,11 +9229,6 @@ "readdirp": "~3.6.0" } }, - "classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" - }, "cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -10043,89 +9355,6 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, - "d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", - "requires": { - "internmap": "1 - 2" - } - }, - "d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" - }, - "d3-delaunay": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", - "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", - "requires": { - "delaunator": "5" - } - }, - "d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" - }, - "d3-geo": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", - "requires": { - "d3-array": "2.5.0 - 3" - } - }, - "d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "requires": { - "d3-color": "1 - 3" - } - }, - "d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" - }, - "d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "requires": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - } - }, - "d3-shape": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", - "requires": { - "d3-path": "1" - } - }, - "d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "requires": { - "d3-array": "2 - 3" - } - }, - "d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "requires": { - "d3-time": "1 - 3" - } - }, "data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -10151,14 +9380,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", - "requires": { - "robust-predicates": "^3.0.2" - } - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -10811,11 +10032,6 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true }, - "internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" - }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -11076,14 +10292,6 @@ "integrity": "sha512-nMoGWW2HurtuJf6XAL56FWTDCWLOTSsanrgwOyaR5Y4e3zfG5N/0cU5xWZSEU3tBxhQugRbV1xL9jb+ug7yZww==", "dev": true }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, "lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -11101,11 +10309,6 @@ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==" }, - "math-expression-evaluator": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.4.0.tgz", - "integrity": "sha512-4vRUvPyxdO8cWULGTh9dZWL2tZK6LDBvj+OGHBER7poH9Qdt7kXEoj20wiz4lQUbUXQZFjPbe5mVDo9nutizCw==" - }, "math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -11258,7 +10461,8 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true }, "onetime": { "version": "7.0.0", @@ -11420,16 +10624,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true }, - "prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, "proxy-compare": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz", @@ -11507,12 +10701,6 @@ "react-router": "7.8.2" } }, - "react-use-measure": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", - "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", - "requires": {} - }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -11522,31 +10710,6 @@ "picomatch": "^2.2.1" } }, - "reduce-css-calc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", - "integrity": "sha512-0dVfwYVOlf/LBA2ec4OwQ6p3X9mYxn/wOl2xTcLwjnPYrkgEfPx3VI4eGCH3rQLlPISG5v9I9bkZosKsNRTRKA==", - "requires": { - "balanced-match": "^0.4.2", - "math-expression-evaluator": "^1.2.14", - "reduce-function-call": "^1.0.1" - }, - "dependencies": { - "balanced-match": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "integrity": "sha512-STw03mQKnGUYtoNjmowo4F2cRmIIxYEGiMsjjwla/u5P1lxadj/05WkNaFjNiKTgJkj8KiXbgAiRTmcQRwQNtg==" - } - } - }, - "reduce-function-call": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz", - "integrity": "sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==", - "requires": { - "balanced-match": "^1.0.0" - } - }, "resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -11584,11 +10747,6 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true }, - "robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" - }, "rollup": { "version": "4.48.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.48.0.tgz", diff --git a/thingconnect.pulse.client/package.json b/thingconnect.pulse.client/package.json index 76ebb24..99f9f58 100644 --- a/thingconnect.pulse.client/package.json +++ b/thingconnect.pulse.client/package.json @@ -18,11 +18,6 @@ "@monaco-editor/react": "^4.7.0", "@sentry/react": "^10.11.0", "@tanstack/react-query": "^5.84.2", - "@visx/axis": "^3.12.0", - "@visx/responsive": "^3.12.0", - "@visx/scale": "^3.12.0", - "@visx/shape": "^3.12.0", - "@visx/tooltip": "^3.12.0", "axios": "^1.11.0", "date-fns": "^4.1.0", "lodash": "^4.17.21", From 4a876546127bf51b8c870c335885e49fbba33142 Mon Sep 17 00:00:00 2001 From: abishek Date: Fri, 26 Sep 2025 14:58:35 +0530 Subject: [PATCH 09/28] feat: icmp fallback if tcp and http fails(down) --- ThingConnect.Pulse.Server/Data/Entities.cs | 3 +- .../Models/CheckResult.cs | 25 ++++ .../Models/HistoryDtos.cs | 1 + .../Monitoring/OutageCalssifaction.cs | 90 -------------- .../Services/Monitoring/OutageClassifier.cs | 112 ++++++++++++++++++ .../Services/Monitoring/ProbeService.cs | 30 ++++- 6 files changed, 167 insertions(+), 94 deletions(-) delete mode 100644 ThingConnect.Pulse.Server/Services/Monitoring/OutageCalssifaction.cs create mode 100644 ThingConnect.Pulse.Server/Services/Monitoring/OutageClassifier.cs diff --git a/ThingConnect.Pulse.Server/Data/Entities.cs b/ThingConnect.Pulse.Server/Data/Entities.cs index 656f107..785beac 100644 --- a/ThingConnect.Pulse.Server/Data/Entities.cs +++ b/ThingConnect.Pulse.Server/Data/Entities.cs @@ -10,7 +10,8 @@ public enum UpDown { up, down } /// public enum OutageClassification { - Unknown = 0, + None = -1, // Explicitly healthy, no outage detected + Unknown = 0, // Not enough information to classify Network = 1, // Host unreachable (ICMP + service fail) Service = 2, // Service down, host reachable via ICMP Intermittent = 3, // Flapping / unstable diff --git a/ThingConnect.Pulse.Server/Models/CheckResult.cs b/ThingConnect.Pulse.Server/Models/CheckResult.cs index 6a9639e..f7fa607 100644 --- a/ThingConnect.Pulse.Server/Models/CheckResult.cs +++ b/ThingConnect.Pulse.Server/Models/CheckResult.cs @@ -2,6 +2,20 @@ namespace ThingConnect.Pulse.Server.Models; +public enum OutageClassificationDto +{ + None = -1, // Explicitly healthy, no outage detected + Unknown = 0, // Not enough information to classify + Network = 1, // Host unreachable (ICMP + service fail) + Service = 2, // Service down, host reachable via ICMP + Intermittent = 3, // Flapping / unstable + Performance = 4, // RTT above threshold + PartialService = 5, // HTTP error, TCP works + DnsResolution = 6, // DNS fails, IP works + Congestion = 7, // Correlated latency + Maintenance = 8 // Planned downtime +} + /// /// Result of a single probe check (ICMP, TCP, or HTTP). /// @@ -32,6 +46,17 @@ public sealed class CheckResult /// public string? Error { get; set; } + // 🔹 Fallback probe info + public bool? FallbackAttempted { get; set; } + public UpDown? FallbackStatus { get; set; } + public double? FallbackRttMs { get; set; } + public string? FallbackError { get; set; } + + /// + /// Gets or sets the outage classification determined after primary and fallback probes. + /// + public OutageClassificationDto? Classification { get; set; } + /// /// Creates a successful check result. /// diff --git a/ThingConnect.Pulse.Server/Models/HistoryDtos.cs b/ThingConnect.Pulse.Server/Models/HistoryDtos.cs index 55109c7..1a58e2e 100644 --- a/ThingConnect.Pulse.Server/Models/HistoryDtos.cs +++ b/ThingConnect.Pulse.Server/Models/HistoryDtos.cs @@ -39,4 +39,5 @@ public sealed class HistoryResponseDto public List Rollup15m { get; set; } = new(); public List RollupDaily { get; set; } = new(); public List Outages { get; set; } = new(); + public OutageClassificationDto? Classification { get; set; } } diff --git a/ThingConnect.Pulse.Server/Services/Monitoring/OutageCalssifaction.cs b/ThingConnect.Pulse.Server/Services/Monitoring/OutageCalssifaction.cs deleted file mode 100644 index 5d5dc34..0000000 --- a/ThingConnect.Pulse.Server/Services/Monitoring/OutageCalssifaction.cs +++ /dev/null @@ -1,90 +0,0 @@ -// public static OutageClassification ClassifyOutage( -// CheckResult primaryResult, -// CheckResult? fallbackResult, -// Endpoint endpoint, -// IEnumerable recentHistory) -// { -// // 1. ICMP probes always Network on failure -// if (endpoint.Type == ProbeType.icmp && primaryResult.Status == UpDown.down) -// return OutageClassification.Network; - -// // 2. Successful probes - check performance -// if (primaryResult.Status == UpDown.up) -// { -// if (IsPerformanceDegraded(primaryResult, endpoint)) -// return OutageClassification.Performance; -// return OutageClassification.Unknown; // No outage -// } - -// // 3. Failed TCP/HTTP probes - use fallback -// if (fallbackResult?.IsExecuted == true) -// { -// var baseClassification = fallbackResult.IsSuccess ? -// OutageClassification.Service : OutageClassification.Network; - -// // 4. Check for advanced patterns -// if (IsIntermittent(recentHistory)) -// return OutageClassification.Intermittent; - -// if (IsPartialService(primaryResult, fallbackResult, endpoint)) -// return OutageClassification.PartialService; - -// if (IsDnsIssue(endpoint, fallbackResult)) -// return OutageClassification.DnsResolution; - -// return baseClassification; -// } - -// // 5. Fallback failed or no fallback -// return OutageClassification.Unknown; -// } - -// // Helper Methods -// private static bool IsPerformanceDegraded(CheckResult result, Endpoint endpoint) -// { -// if (!result.RttMs.HasValue) return false; - -// double threshold = endpoint.Type == ProbeType.icmp ? 2.0 : 3.0; -// double baselineRtt = endpoint.ExpectedRttMs ?? 100; // Default 100ms baseline - -// return result.RttMs > baselineRtt * threshold; -// } - -// private static bool IsIntermittent(IEnumerable recentHistory) -// { -// var last15Min = recentHistory -// .Where(r => r.Timestamp > DateTimeOffset.UtcNow.AddMinutes(-15)) -// .OrderBy(r => r.Timestamp) -// .ToList(); - -// if (last15Min.Count < 4) return false; - -// int transitions = 0; -// for (int i = 1; i < last15Min.Count; i++) -// { -// if (last15Min[i].Status != last15Min[i-1].Status) -// transitions++; -// } - -// return transitions >= 4; -// } - -// private static bool IsPartialService(CheckResult primary, CheckResult fallback, Endpoint endpoint) -// { -// return endpoint.Type == ProbeType.http && -// primary.Error?.Contains("50") == true && // 5xx HTTP errors -// fallback.IsSuccess == true; -// } - -// private static bool IsDnsIssue(Endpoint endpoint, CheckResult fallbackResult) -// { -// // Only for hostnames, not IP addresses -// return IsHostname(endpoint.Host) && -// fallbackResult.IsSuccess == false && -// fallbackResult.Error?.Contains("resolve") == true; -// } - -// private static bool IsHostname(string host) -// { -// return !IPAddress.TryParse(host, out _); -// } diff --git a/ThingConnect.Pulse.Server/Services/Monitoring/OutageClassifier.cs b/ThingConnect.Pulse.Server/Services/Monitoring/OutageClassifier.cs new file mode 100644 index 0000000..62b1d64 --- /dev/null +++ b/ThingConnect.Pulse.Server/Services/Monitoring/OutageClassifier.cs @@ -0,0 +1,112 @@ +using System.Net; +using ThingConnect.Pulse.Server.Data; +using ThingConnect.Pulse.Server.Models; + +namespace ThingConnect.Pulse.Server.Services.Monitoring; + +/// +/// Provides classification logic for probe results (primary + fallback + history). +/// +public static class OutageClassifier +{ + public static OutageClassificationDto ClassifyOutage( + CheckResult primaryResult, + CheckResult? fallbackResult, + Data.Endpoint endpoint, + IEnumerable recentHistory) + { + // 1. ICMP probes → always Network on failure + if (endpoint.Type == ProbeType.icmp && primaryResult.Status == UpDown.down) + { + return OutageClassificationDto.Network; + } + + // 2. Successful probes → check performance + if (primaryResult.Status == UpDown.up) + { + if (IsPerformanceDegraded(primaryResult, endpoint)) + { + return OutageClassificationDto.Performance; + } + + return OutageClassificationDto.None; // explicitly healthy + } + + // 3. Failed TCP/HTTP probes → use fallback + if (fallbackResult != null) + { + var baseClassification = fallbackResult.Status == UpDown.up + ? OutageClassificationDto.Service + : OutageClassificationDto.Network; + + // 4. Advanced patterns + if (IsIntermittent(recentHistory)) + { + return OutageClassificationDto.Intermittent; + } + + if (IsPartialService(primaryResult, fallbackResult, endpoint)) + { + return OutageClassificationDto.PartialService; + } + + if (IsDnsIssue(endpoint, fallbackResult)) + { + return OutageClassificationDto.DnsResolution; + } + + return baseClassification; + } + + // 5. Fallback missing or failed → default + return OutageClassificationDto.Unknown; + } + + private static bool IsPerformanceDegraded(CheckResult result, Data.Endpoint endpoint) + { + if (!result.RttMs.HasValue) return false; + + double threshold = endpoint.Type == ProbeType.icmp ? 2.0 : 3.0; + double baselineRtt = endpoint.ExpectedRttMs ?? 100; // default baseline + + return result.RttMs > baselineRtt * threshold; + } + + private static bool IsIntermittent(IEnumerable recentHistory) + { + var last15Min = recentHistory + .Where(r => r.Timestamp > DateTimeOffset.UtcNow.AddMinutes(-15)) + .OrderBy(r => r.Timestamp) + .ToList(); + + if (last15Min.Count < 4) return false; + + int transitions = 0; + for (int i = 1; i < last15Min.Count; i++) + { + if (last15Min[i].Status != last15Min[i - 1].Status) + transitions++; + } + + return transitions >= 4; + } + + private static bool IsPartialService(CheckResult primary, CheckResult fallback, Data.Endpoint endpoint) + { + return endpoint.Type == ProbeType.http && + primary.Error?.Contains("50") == true && // HTTP 5xx + fallback.Status == UpDown.up; + } + + private static bool IsDnsIssue(Data.Endpoint endpoint, CheckResult fallbackResult) + { + return IsHostname(endpoint.Host) && + fallbackResult.Status == UpDown.down && + (fallbackResult.Error?.Contains("resolve", StringComparison.OrdinalIgnoreCase) ?? false); + } + + private static bool IsHostname(string host) + { + return !IPAddress.TryParse(host, out _); + } +} diff --git a/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs b/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs index 33f3aab..b5609a5 100644 --- a/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs +++ b/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs @@ -24,10 +24,11 @@ public ProbeService(ILogger logger, IHttpClientFactory httpClientF public async Task ProbeAsync(Data.Endpoint endpoint, CancellationToken cancellationToken = default) { DateTimeOffset timestamp = DateTimeOffset.UtcNow; + CheckResult primaryResult; try { - return endpoint.Type switch + primaryResult = endpoint.Type switch { ProbeType.icmp => await PingAsync(endpoint.Id, endpoint.Host, endpoint.TimeoutMs, cancellationToken), ProbeType.tcp => await TcpConnectAsync(endpoint.Id, endpoint.Host, @@ -40,9 +41,32 @@ public async Task ProbeAsync(Data.Endpoint endpoint, CancellationTo } catch (Exception ex) { - _logger.LogError(ex, "Probe failed for endpoint {EndpointId} ({Host})", endpoint.Id, endpoint.Host); - return CheckResult.Failure(endpoint.Id, timestamp, ex.Message); + _logger.LogError(ex, "Primary probe failed for endpoint {EndpointId} ({Host})", endpoint.Id, endpoint.Host); + primaryResult = CheckResult.Failure(endpoint.Id, timestamp, ex.Message); } + + CheckResult? fallbackResult = null; + + // Trigger ICMP fallback only if primary failed and endpoint is not ICMP itself + if (primaryResult.Status == UpDown.down && endpoint.Type != ProbeType.icmp) + { + fallbackResult = await PingAsync(endpoint.Id, endpoint.Host, endpoint.TimeoutMs, cancellationToken); + + primaryResult.FallbackAttempted = true; + primaryResult.FallbackStatus = fallbackResult.Status; + primaryResult.FallbackRttMs = fallbackResult.RttMs; + primaryResult.FallbackError = fallbackResult.Error; + } + + // Centralized classifier + primaryResult.Classification = OutageClassifier.ClassifyOutage( + primaryResult, + fallbackResult, + endpoint, + Enumerable.Empty() // later you can feed history here + ); + + return primaryResult; } public async Task PingAsync(Guid endpointId, string host, int timeoutMs, CancellationToken cancellationToken = default) From e764056220deac582de8a3dc3423257e2f15caae Mon Sep 17 00:00:00 2001 From: abishek Date: Fri, 26 Sep 2025 15:25:42 +0530 Subject: [PATCH 10/28] updated the fall back result in outage --- .../Monitoring/OutageDetectionService.cs | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs b/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs index d2d6a67..28c57de 100644 --- a/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs +++ b/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs @@ -22,6 +22,10 @@ public OutageDetectionService(IServiceProvider serviceProvider, ILogger + /// Processes a single check result: updates streaks, transitions UP/DOWN with flap damping, + /// and persists the raw result including fallback details and classification. + /// public async Task ProcessCheckResultAsync(CheckResult result, CancellationToken cancellationToken = default) { MonitorState state = _states.GetOrAdd(result.EndpointId, _ => new MonitorState()); @@ -207,10 +211,6 @@ public async Task InitializeStatesFromDatabaseAsync(CancellationToken cancellati if (inconsistenciesFixed > 0) { await context.SaveChangesAsync(cancellationToken); - } - - if (inconsistenciesFixed > 0) - { _logger.LogInformation("Started monitoring session {SessionId}, initialized {Count} states, fixed {InconsistencyCount} state inconsistencies", newSession.Id, initializedCount, inconsistenciesFixed); } @@ -248,7 +248,6 @@ public async Task InitializeStatesFromDatabaseAsync(CancellationToken cancellati "{GapDuration}s gap > {Threshold}s threshold ({IntervalSeconds}s interval), " + "missed ~{MissedChecks} checks", endpoint.Id, endpoint.Name, gapDuration, gapThreshold, endpoint.IntervalSeconds, missedChecks); - affectedEndpoints.Add(endpoint); } } @@ -265,8 +264,8 @@ private async Task HandleMonitoringGapAsync(PulseDbContext context, long lastMon // Handle open outages only for affected endpoints List outagesForAffectedEndpoints = await context.Outages .Where(o => o.EndedTs == null && - o.StartedTs < lastMonitoringTime && - affectedEndpointIds.Contains(o.EndpointId)) + o.StartedTs < lastMonitoringTime && + affectedEndpointIds.Contains(o.EndpointId)) .ToListAsync(cancellationToken); foreach (Outage? outage in outagesForAffectedEndpoints) @@ -365,7 +364,8 @@ private async Task TransitionToDownAsync(Guid endpointId, MonitorState state, lo { EndpointId = endpointId, StartedTs = timestamp, - LastError = error + LastError = error, + Classification = state.LastClassification ?? OutageClassificationDto.Unknown }; context.Outages.Add(outage); @@ -515,18 +515,31 @@ private async Task UpdateEndpointStatusAsync(PulseDbContext context, Guid endpoi return (endpointStatus, openOutageId, false); } + /// + /// Persists the raw check result including fallback probe fields and classification. + /// Also updates endpoint's LastRttMs for successful probes. + /// private async Task SaveCheckResultAsync(CheckResult result, CancellationToken cancellationToken) { using IServiceScope scope = _serviceProvider.CreateScope(); PulseDbContext context = scope.ServiceProvider.GetRequiredService(); - CheckResultRaw rawResult = new CheckResultRaw + var rawResult = new CheckResultRaw { EndpointId = result.EndpointId, Ts = UnixTimestamp.ToUnixSeconds(result.Timestamp), Status = result.Status, RttMs = result.RttMs, - Error = result.Error + Error = result.Error, + + // New fallback fields + FallbackAttempted = result.FallbackAttempted, + FallbackStatus = result.FallbackStatus, + FallbackRttMs = result.FallbackRttMs, + FallbackError = result.FallbackError, + Classification = result.Classification.HasValue + ? (OutageClassification?)((OutageClassification)(int)result.Classification.Value) + : null }; context.CheckResultsRaw.Add(rawResult); From e4429f26318f8579facea8470020e01875e19da3 Mon Sep 17 00:00:00 2001 From: abishek Date: Fri, 26 Sep 2025 16:05:49 +0530 Subject: [PATCH 11/28] icmp fallback time out made half of the primary timeout --- ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs b/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs index b5609a5..4568f22 100644 --- a/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs +++ b/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs @@ -50,7 +50,9 @@ public async Task ProbeAsync(Data.Endpoint endpoint, CancellationTo // Trigger ICMP fallback only if primary failed and endpoint is not ICMP itself if (primaryResult.Status == UpDown.down && endpoint.Type != ProbeType.icmp) { - fallbackResult = await PingAsync(endpoint.Id, endpoint.Host, endpoint.TimeoutMs, cancellationToken); + // Use half of the primary timeout, minimum 100ms + int fallbackTimeout = Math.Max(endpoint.TimeoutMs / 2, 100); + fallbackResult = await PingAsync(endpoint.Id, endpoint.Host, fallbackTimeout, cancellationToken); primaryResult.FallbackAttempted = true; primaryResult.FallbackStatus = fallbackResult.Status; From 8fc54a47f69a4e53690cefe2694bada6fb4bc592 Mon Sep 17 00:00:00 2001 From: abishek Date: Fri, 26 Sep 2025 16:56:32 +0530 Subject: [PATCH 12/28] code refactored --- .../Monitoring/OutageDetectionService.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs b/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs index 28c57de..115070b 100644 --- a/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs +++ b/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs @@ -58,7 +58,13 @@ public async Task ProcessCheckResultAsync(CheckResult result, Cancellation // Check for DOWN transition if (state.ShouldTransitionToDown()) { - await TransitionToDownAsync(result.EndpointId, state, UnixTimestamp.ToUnixSeconds(result.Timestamp), result.Error, cancellationToken); + await TransitionToDownAsync( + result.EndpointId, + state, + UnixTimestamp.ToUnixSeconds(result.Timestamp), + result.Error, + result.Classification, + cancellationToken); stateChanged = true; _logger.LogWarning("Endpoint {EndpointId} transitioned to DOWN after {FailStreak} consecutive failures", result.EndpointId, state.FailStreak); @@ -348,8 +354,7 @@ public async Task HandleGracefulShutdownAsync(string? shutdownReason = null, Can } } - private async Task TransitionToDownAsync(Guid endpointId, MonitorState state, long timestamp, - string? error, CancellationToken cancellationToken) + private async Task TransitionToDownAsync(Guid endpointId, MonitorState state, long timestamp, string? error, OutageClassification classification, CancellationToken cancellationToken) { using IServiceScope scope = _serviceProvider.CreateScope(); PulseDbContext context = scope.ServiceProvider.GetRequiredService(); @@ -365,7 +370,7 @@ private async Task TransitionToDownAsync(Guid endpointId, MonitorState state, lo EndpointId = endpointId, StartedTs = timestamp, LastError = error, - Classification = state.LastClassification ?? OutageClassificationDto.Unknown + Classification = classification }; context.Outages.Add(outage); @@ -537,9 +542,7 @@ private async Task SaveCheckResultAsync(CheckResult result, CancellationToken ca FallbackStatus = result.FallbackStatus, FallbackRttMs = result.FallbackRttMs, FallbackError = result.FallbackError, - Classification = result.Classification.HasValue - ? (OutageClassification?)((OutageClassification)(int)result.Classification.Value) - : null + Classification = result.Classification }; context.CheckResultsRaw.Add(rawResult); From 8e20d4c77b339cf0d5a10e699d5b012a8c075f9f Mon Sep 17 00:00:00 2001 From: abishek Date: Fri, 26 Sep 2025 17:02:29 +0530 Subject: [PATCH 13/28] code refactord --- .../Services/Monitoring/OutageDetectionService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs b/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs index 115070b..fd214f4 100644 --- a/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs +++ b/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs @@ -63,7 +63,7 @@ await TransitionToDownAsync( state, UnixTimestamp.ToUnixSeconds(result.Timestamp), result.Error, - result.Classification, + (OutageClassification?)result.Classification, cancellationToken); stateChanged = true; _logger.LogWarning("Endpoint {EndpointId} transitioned to DOWN after {FailStreak} consecutive failures", @@ -354,7 +354,7 @@ public async Task HandleGracefulShutdownAsync(string? shutdownReason = null, Can } } - private async Task TransitionToDownAsync(Guid endpointId, MonitorState state, long timestamp, string? error, OutageClassification classification, CancellationToken cancellationToken) + private async Task TransitionToDownAsync(Guid endpointId, MonitorState state, long timestamp, string? error, OutageClassification? classification, CancellationToken cancellationToken) { using IServiceScope scope = _serviceProvider.CreateScope(); PulseDbContext context = scope.ServiceProvider.GetRequiredService(); @@ -542,7 +542,7 @@ private async Task SaveCheckResultAsync(CheckResult result, CancellationToken ca FallbackStatus = result.FallbackStatus, FallbackRttMs = result.FallbackRttMs, FallbackError = result.FallbackError, - Classification = result.Classification + Classification = (OutageClassification?)result.Classification }; context.CheckResultsRaw.Add(rawResult); From b0311160c1cd37594e5567b9065fea515237b84f Mon Sep 17 00:00:00 2001 From: abishek Date: Mon, 29 Sep 2025 11:18:25 +0530 Subject: [PATCH 14/28] udpate the models --- ThingConnect.Pulse.Server/Data/Entities.cs | 20 +++++++++---------- .../Models/HistoryDtos.cs | 7 ++++++- thingconnect.pulse.client/src/api/types.ts | 17 ++++++++++++++++ 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/ThingConnect.Pulse.Server/Data/Entities.cs b/ThingConnect.Pulse.Server/Data/Entities.cs index 785beac..aba80b1 100644 --- a/ThingConnect.Pulse.Server/Data/Entities.cs +++ b/ThingConnect.Pulse.Server/Data/Entities.cs @@ -10,16 +10,16 @@ public enum UpDown { up, down } /// public enum OutageClassification { - None = -1, // Explicitly healthy, no outage detected - Unknown = 0, // Not enough information to classify - Network = 1, // Host unreachable (ICMP + service fail) - Service = 2, // Service down, host reachable via ICMP - Intermittent = 3, // Flapping / unstable - Performance = 4, // RTT above threshold - PartialService = 5, // HTTP error, TCP works - DnsResolution = 6, // DNS fails, IP works - Congestion = 7, // Correlated latency - Maintenance = 8 // Planned downtime + None = -1, // Explicitly healthy, no outage detected + Unknown = 0, // Not enough information to classify + Network = 1, // Host unreachable (ICMP + service fail) + Service = 2, // Service down, host reachable via ICMP + Intermittent = 3, // Flapping / unstable + Performance = 4, // RTT above threshold + PartialService = 5, // HTTP error, TCP works + DnsResolution = 6, // DNS fails, IP works + Congestion = 7, // Correlated latency + Maintenance = 8 // Planned downtime } public record GroupVm(string Id, string Name, string? ParentId, string? Color); diff --git a/ThingConnect.Pulse.Server/Models/HistoryDtos.cs b/ThingConnect.Pulse.Server/Models/HistoryDtos.cs index 1a58e2e..b3fb0f4 100644 --- a/ThingConnect.Pulse.Server/Models/HistoryDtos.cs +++ b/ThingConnect.Pulse.Server/Models/HistoryDtos.cs @@ -6,6 +6,11 @@ public sealed class RawCheckDto public string Status { get; set; } = default!; public double? RttMs { get; set; } public string? Error { get; set; } + public bool? FallbackAttempted { get; set; } + public string? FallbackStatus { get; set; } + public double? FallbackRttMs { get; set; } + public string? FallbackError { get; set; } + public OutageClassification? Classification { get; set; } } public sealed class RollupBucketDto @@ -30,6 +35,7 @@ public sealed class OutageDto public DateTimeOffset? EndedTs { get; set; } public int? DurationS { get; set; } public string? LastError { get; set; } + public OutageClassification? Classification { get; set; } } public sealed class HistoryResponseDto @@ -39,5 +45,4 @@ public sealed class HistoryResponseDto public List Rollup15m { get; set; } = new(); public List RollupDaily { get; set; } = new(); public List Outages { get; set; } = new(); - public OutageClassificationDto? Classification { get; set; } } diff --git a/thingconnect.pulse.client/src/api/types.ts b/thingconnect.pulse.client/src/api/types.ts index 67b2dc7..97924f9 100644 --- a/thingconnect.pulse.client/src/api/types.ts +++ b/thingconnect.pulse.client/src/api/types.ts @@ -82,11 +82,27 @@ export interface StateChange { error?: string; } +export type OutageClassification = + | -1 // None + | 0 // Unknown + | 1 // Network + | 2 // Service + | 3 // Intermittent + | 4 // Performance + | 5 // PartialService + | 6 // DnsResolution + | 7 // Congestion + | 8; // Maintenance export interface RawCheck { ts: string; status: 'up' | 'down'; rttMs?: number | null; error?: string | null; + fallbackAttempted?: boolean; + fallbackSuccess?: boolean; + fallbackRttMs?: number | null; + classification?: OutageClassification | null; + lastSeenViaIcmp?: string | null; // ISO timestamp when last reachable via ICMP } export interface Outage { @@ -94,6 +110,7 @@ export interface Outage { endedTs?: string | null; durationS?: number | null; lastError?: string | null; + classification: OutageClassification; } export interface EndpointDetail { From aacc52a40095c901da05f668987f47b347956cf6 Mon Sep 17 00:00:00 2001 From: abishek Date: Mon, 29 Sep 2025 11:33:50 +0530 Subject: [PATCH 15/28] code refactored --- ThingConnect.Pulse.Server/Models/HistoryDtos.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ThingConnect.Pulse.Server/Models/HistoryDtos.cs b/ThingConnect.Pulse.Server/Models/HistoryDtos.cs index b3fb0f4..03a5a65 100644 --- a/ThingConnect.Pulse.Server/Models/HistoryDtos.cs +++ b/ThingConnect.Pulse.Server/Models/HistoryDtos.cs @@ -10,7 +10,7 @@ public sealed class RawCheckDto public string? FallbackStatus { get; set; } public double? FallbackRttMs { get; set; } public string? FallbackError { get; set; } - public OutageClassification? Classification { get; set; } + public OutageClassificationDto? Classification { get; set; } } public sealed class RollupBucketDto @@ -35,7 +35,7 @@ public sealed class OutageDto public DateTimeOffset? EndedTs { get; set; } public int? DurationS { get; set; } public string? LastError { get; set; } - public OutageClassification? Classification { get; set; } + public OutageClassificationDto? Classification { get; set; } } public sealed class HistoryResponseDto From 3db55656b224b45ad93fa2918520acb037977acc Mon Sep 17 00:00:00 2001 From: abishek Date: Mon, 29 Sep 2025 15:08:52 +0530 Subject: [PATCH 16/28] icpm fallback works correctyl need to display it in teh forntend --- .../Models/CheckResult.cs | 47 +++++++++++-------- .../Models/HistoryDtos.cs | 6 ++- .../Services/EndpointService.cs | 7 ++- .../Services/Monitoring/OutageClassifier.cs | 22 ++++----- .../Monitoring/OutageDetectionService.cs | 4 +- .../Services/Monitoring/ProbeService.cs | 37 ++++++++------- 6 files changed, 70 insertions(+), 53 deletions(-) diff --git a/ThingConnect.Pulse.Server/Models/CheckResult.cs b/ThingConnect.Pulse.Server/Models/CheckResult.cs index f7fa607..f10b9e3 100644 --- a/ThingConnect.Pulse.Server/Models/CheckResult.cs +++ b/ThingConnect.Pulse.Server/Models/CheckResult.cs @@ -2,20 +2,6 @@ namespace ThingConnect.Pulse.Server.Models; -public enum OutageClassificationDto -{ - None = -1, // Explicitly healthy, no outage detected - Unknown = 0, // Not enough information to classify - Network = 1, // Host unreachable (ICMP + service fail) - Service = 2, // Service down, host reachable via ICMP - Intermittent = 3, // Flapping / unstable - Performance = 4, // RTT above threshold - PartialService = 5, // HTTP error, TCP works - DnsResolution = 6, // DNS fails, IP works - Congestion = 7, // Correlated latency - Maintenance = 8 // Planned downtime -} - /// /// Result of a single probe check (ICMP, TCP, or HTTP). /// @@ -47,7 +33,7 @@ public sealed class CheckResult public string? Error { get; set; } // 🔹 Fallback probe info - public bool? FallbackAttempted { get; set; } + public bool FallbackAttempted { get; set; } = false; public UpDown? FallbackStatus { get; set; } public double? FallbackRttMs { get; set; } public string? FallbackError { get; set; } @@ -55,12 +41,11 @@ public sealed class CheckResult /// /// Gets or sets the outage classification determined after primary and fallback probes. /// - public OutageClassificationDto? Classification { get; set; } + public OutageClassification? Classification { get; set; } /// /// Creates a successful check result. /// - /// public static CheckResult Success(Guid endpointId, DateTimeOffset timestamp, double? rttMs = null) { return new CheckResult @@ -69,14 +54,18 @@ public static CheckResult Success(Guid endpointId, DateTimeOffset timestamp, dou Timestamp = timestamp, Status = UpDown.up, RttMs = rttMs, - Error = null + Error = null, + FallbackAttempted = false, + FallbackStatus = null, + FallbackRttMs = null, + FallbackError = null, + Classification = null }; } /// /// Creates a failed check result. /// - /// public static CheckResult Failure(Guid endpointId, DateTimeOffset timestamp, string error) { return new CheckResult @@ -85,7 +74,25 @@ public static CheckResult Failure(Guid endpointId, DateTimeOffset timestamp, str Timestamp = timestamp, Status = UpDown.down, RttMs = null, - Error = error + Error = error, + FallbackAttempted = false, + FallbackStatus = null, + FallbackRttMs = null, + FallbackError = null, + Classification = null }; } + + /// + /// Updates the current CheckResult with fallback info. + /// + public void ApplyFallback(CheckResult fallback) + { + if (fallback == null) return; + + FallbackAttempted = true; + FallbackStatus = fallback.Status; + FallbackRttMs = fallback.RttMs; + FallbackError = fallback.Error; + } } diff --git a/ThingConnect.Pulse.Server/Models/HistoryDtos.cs b/ThingConnect.Pulse.Server/Models/HistoryDtos.cs index 03a5a65..341147f 100644 --- a/ThingConnect.Pulse.Server/Models/HistoryDtos.cs +++ b/ThingConnect.Pulse.Server/Models/HistoryDtos.cs @@ -1,3 +1,5 @@ +using ThingConnect.Pulse.Server.Data; + namespace ThingConnect.Pulse.Server.Models; public sealed class RawCheckDto @@ -10,7 +12,7 @@ public sealed class RawCheckDto public string? FallbackStatus { get; set; } public double? FallbackRttMs { get; set; } public string? FallbackError { get; set; } - public OutageClassificationDto? Classification { get; set; } + public OutageClassification? Classification { get; set; } } public sealed class RollupBucketDto @@ -35,7 +37,7 @@ public sealed class OutageDto public DateTimeOffset? EndedTs { get; set; } public int? DurationS { get; set; } public string? LastError { get; set; } - public OutageClassificationDto? Classification { get; set; } + public OutageClassification? Classification { get; set; } } public sealed class HistoryResponseDto diff --git a/ThingConnect.Pulse.Server/Services/EndpointService.cs b/ThingConnect.Pulse.Server/Services/EndpointService.cs index 48bdd71..b50f3da 100644 --- a/ThingConnect.Pulse.Server/Services/EndpointService.cs +++ b/ThingConnect.Pulse.Server/Services/EndpointService.cs @@ -44,7 +44,12 @@ public EndpointService(PulseDbContext context) Ts = ConvertToDateTimeOffset(c.Ts), Status = c.Status.ToString().ToLower(), RttMs = c.RttMs, - Error = c.Error + Error = c.Error, + FallbackAttempted = c.FallbackAttempted, + FallbackStatus = c.FallbackStatus?.ToString().ToLower(), + FallbackRttMs = c.FallbackRttMs, + FallbackError = c.FallbackError, + Classification = c.Classification }) .Where(r => r.Ts >= windowStart) .OrderByDescending(r => r.Ts) diff --git a/ThingConnect.Pulse.Server/Services/Monitoring/OutageClassifier.cs b/ThingConnect.Pulse.Server/Services/Monitoring/OutageClassifier.cs index 62b1d64..9a5f110 100644 --- a/ThingConnect.Pulse.Server/Services/Monitoring/OutageClassifier.cs +++ b/ThingConnect.Pulse.Server/Services/Monitoring/OutageClassifier.cs @@ -9,16 +9,16 @@ namespace ThingConnect.Pulse.Server.Services.Monitoring; /// public static class OutageClassifier { - public static OutageClassificationDto ClassifyOutage( + public static OutageClassification ClassifyOutage( CheckResult primaryResult, - CheckResult? fallbackResult, + CheckResult fallbackResult, Data.Endpoint endpoint, IEnumerable recentHistory) { // 1. ICMP probes → always Network on failure if (endpoint.Type == ProbeType.icmp && primaryResult.Status == UpDown.down) { - return OutageClassificationDto.Network; + return OutageClassification.Network; } // 2. Successful probes → check performance @@ -26,40 +26,40 @@ public static OutageClassificationDto ClassifyOutage( { if (IsPerformanceDegraded(primaryResult, endpoint)) { - return OutageClassificationDto.Performance; + return OutageClassification.Performance; } - return OutageClassificationDto.None; // explicitly healthy + return OutageClassification.None; // explicitly healthy } // 3. Failed TCP/HTTP probes → use fallback if (fallbackResult != null) { var baseClassification = fallbackResult.Status == UpDown.up - ? OutageClassificationDto.Service - : OutageClassificationDto.Network; + ? OutageClassification.Service + : OutageClassification.Network; // 4. Advanced patterns if (IsIntermittent(recentHistory)) { - return OutageClassificationDto.Intermittent; + return OutageClassification.Intermittent; } if (IsPartialService(primaryResult, fallbackResult, endpoint)) { - return OutageClassificationDto.PartialService; + return OutageClassification.PartialService; } if (IsDnsIssue(endpoint, fallbackResult)) { - return OutageClassificationDto.DnsResolution; + return OutageClassification.DnsResolution; } return baseClassification; } // 5. Fallback missing or failed → default - return OutageClassificationDto.Unknown; + return OutageClassification.Unknown; } private static bool IsPerformanceDegraded(CheckResult result, Data.Endpoint endpoint) diff --git a/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs b/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs index fd214f4..f8ed1c5 100644 --- a/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs +++ b/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs @@ -63,7 +63,7 @@ await TransitionToDownAsync( state, UnixTimestamp.ToUnixSeconds(result.Timestamp), result.Error, - (OutageClassification?)result.Classification, + result.Classification, cancellationToken); stateChanged = true; _logger.LogWarning("Endpoint {EndpointId} transitioned to DOWN after {FailStreak} consecutive failures", @@ -542,7 +542,7 @@ private async Task SaveCheckResultAsync(CheckResult result, CancellationToken ca FallbackStatus = result.FallbackStatus, FallbackRttMs = result.FallbackRttMs, FallbackError = result.FallbackError, - Classification = (OutageClassification?)result.Classification + Classification = result.Classification }; context.CheckResultsRaw.Add(rawResult); diff --git a/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs b/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs index 4568f22..8d01dbd 100644 --- a/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs +++ b/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Net.NetworkInformation; using System.Net.Sockets; +using System.Threading; using ThingConnect.Pulse.Server.Data; using ThingConnect.Pulse.Server.Models; @@ -25,16 +26,15 @@ public async Task ProbeAsync(Data.Endpoint endpoint, CancellationTo { DateTimeOffset timestamp = DateTimeOffset.UtcNow; CheckResult primaryResult; + CheckResult? fallbackResult = null; try { primaryResult = endpoint.Type switch { ProbeType.icmp => await PingAsync(endpoint.Id, endpoint.Host, endpoint.TimeoutMs, cancellationToken), - ProbeType.tcp => await TcpConnectAsync(endpoint.Id, endpoint.Host, - endpoint.Port ?? 80, endpoint.TimeoutMs, cancellationToken), - ProbeType.http => await HttpCheckAsync(endpoint.Id, endpoint.Host, - endpoint.Port ?? (endpoint.Host.StartsWith("https://") ? 443 : 80), + ProbeType.tcp => await TcpConnectAsync(endpoint.Id, endpoint.Host, endpoint.Port ?? 80, endpoint.TimeoutMs, cancellationToken), + ProbeType.http => await HttpCheckAsync(endpoint.Id, endpoint.Host, endpoint.Port ?? (endpoint.Host.StartsWith("https://") ? 443 : 80), endpoint.HttpPath, endpoint.HttpMatch, endpoint.TimeoutMs, cancellationToken), _ => CheckResult.Failure(endpoint.Id, timestamp, $"Unknown probe type: {endpoint.Type}") }; @@ -45,27 +45,30 @@ public async Task ProbeAsync(Data.Endpoint endpoint, CancellationTo primaryResult = CheckResult.Failure(endpoint.Id, timestamp, ex.Message); } - CheckResult? fallbackResult = null; - - // Trigger ICMP fallback only if primary failed and endpoint is not ICMP itself + // TCP/HTTP fallback to ICMP if primary failed if (primaryResult.Status == UpDown.down && endpoint.Type != ProbeType.icmp) { - // Use half of the primary timeout, minimum 100ms - int fallbackTimeout = Math.Max(endpoint.TimeoutMs / 2, 100); - fallbackResult = await PingAsync(endpoint.Id, endpoint.Host, fallbackTimeout, cancellationToken); - - primaryResult.FallbackAttempted = true; - primaryResult.FallbackStatus = fallbackResult.Status; - primaryResult.FallbackRttMs = fallbackResult.RttMs; - primaryResult.FallbackError = fallbackResult.Error; + try + { + int fallbackTimeout = Math.Max(endpoint.TimeoutMs / 2, 100); + fallbackResult = await PingAsync(endpoint.Id, endpoint.Host, endpoint.TimeoutMs, cancellationToken); + + // Apply fallback only if it actually ran + primaryResult.ApplyFallback(fallbackResult); + } + catch (Exception ex) + { + fallbackResult = CheckResult.Failure(endpoint.Id, DateTimeOffset.UtcNow, $"Fallback ping failed: {ex.Message}"); + primaryResult.ApplyFallback(fallbackResult); + } } - // Centralized classifier + // Centralized classification primaryResult.Classification = OutageClassifier.ClassifyOutage( primaryResult, fallbackResult, endpoint, - Enumerable.Empty() // later you can feed history here + Enumerable.Empty() // feed history later if available ); return primaryResult; From 72c459e30fc5a79cd8099080be1e989c2e5467b2 Mon Sep 17 00:00:00 2001 From: abishek Date: Mon, 29 Sep 2025 18:18:25 +0530 Subject: [PATCH 17/28] udpated the enpoint and status service --- .../Models/EndpointDetailDto.cs | 30 +- .../Models/StatusDtos.cs | 14 +- .../Services/EndpointService.cs | 201 +++++++++++-- .../Services/StatusService.cs | 272 +++++++++++++----- 4 files changed, 427 insertions(+), 90 deletions(-) diff --git a/ThingConnect.Pulse.Server/Models/EndpointDetailDto.cs b/ThingConnect.Pulse.Server/Models/EndpointDetailDto.cs index 6309039..d4a5f09 100644 --- a/ThingConnect.Pulse.Server/Models/EndpointDetailDto.cs +++ b/ThingConnect.Pulse.Server/Models/EndpointDetailDto.cs @@ -3,6 +3,34 @@ public sealed class EndpointDetailDto { public required EndpointDto Endpoint { get; set; } - public List Recent { get; set; } = []; + public CurrentStateDto CurrentState { get; set; } = default!; + public List Recent { get; set; } = []; public List Outages { get; set; } = []; } + +public sealed class CheckResultStructuredDto +{ + public DateTimeOffset Ts { get; set; } + public int Classification { get; set; } + public ProbeResultDto Primary { get; set; } = default!; + public FallbackResultDto Fallback { get; set; } = default!; +} + +public sealed class ProbeResultDto +{ + public string Type { get; set; } = default!; + public string Target { get; set; } = default!; + public string Status { get; set; } = default!; + public double? RttMs { get; set; } + public string? Error { get; set; } +} + +public sealed class FallbackResultDto +{ + public bool Attempted { get; set; } + public string? Type { get; set; } + public string? Target { get; set; } + public string? Status { get; set; } + public double? RttMs { get; set; } + public string? Error { get; set; } +} diff --git a/ThingConnect.Pulse.Server/Models/StatusDtos.cs b/ThingConnect.Pulse.Server/Models/StatusDtos.cs index 11df3f8..aabe418 100644 --- a/ThingConnect.Pulse.Server/Models/StatusDtos.cs +++ b/ThingConnect.Pulse.Server/Models/StatusDtos.cs @@ -3,8 +3,9 @@ namespace ThingConnect.Pulse.Server.Models; public sealed class LiveStatusItemDto { public EndpointDto Endpoint { get; set; } = default!; - public string Status { get; set; } = default!; - public double? RttMs { get; set; } + public string Status { get; set; } = default!; // need to remove + public double? RttMs { get; set; } // need to remove + public CurrentStateDto CurrentState { get; set; } = default!; public DateTimeOffset LastChangeTs { get; set; } public List Sparkline { get; set; } = new(); } @@ -51,3 +52,12 @@ public sealed class PagedLiveDto public PageMetaDto Meta { get; set; } = default!; public List Items { get; set; } = new(); } + +public sealed class CurrentStateDto +{ + public string EffectiveStatus { get; set; } = default!; // "up" or "down" + public double? EffectiveRtt { get; set; } // Priority-based RTT + public int Classification { get; set; } // OutageClassification enum value + public bool HostReachable { get; set; } // Quick connectivity check + public DateTimeOffset LastCheck { get; set; } +} diff --git a/ThingConnect.Pulse.Server/Services/EndpointService.cs b/ThingConnect.Pulse.Server/Services/EndpointService.cs index b50f3da..aaaa955 100644 --- a/ThingConnect.Pulse.Server/Services/EndpointService.cs +++ b/ThingConnect.Pulse.Server/Services/EndpointService.cs @@ -39,21 +39,38 @@ public EndpointService(PulseDbContext context) .ToListAsync(); var recent = rawChecks - .Select(c => new RawCheckDto - { - Ts = ConvertToDateTimeOffset(c.Ts), - Status = c.Status.ToString().ToLower(), - RttMs = c.RttMs, - Error = c.Error, - FallbackAttempted = c.FallbackAttempted, - FallbackStatus = c.FallbackStatus?.ToString().ToLower(), - FallbackRttMs = c.FallbackRttMs, - FallbackError = c.FallbackError, - Classification = c.Classification - }) - .Where(r => r.Ts >= windowStart) - .OrderByDescending(r => r.Ts) - .ToList(); + .Where(c => ConvertToDateTimeOffset(c.Ts) >= windowStart) + .OrderByDescending(c => ConvertToDateTimeOffset(c.Ts)) + .Select(c => new CheckResultStructuredDto + { + Ts = ConvertToDateTimeOffset(c.Ts), + Classification = (int)(c.Classification ?? OutageClassification.Unknown), + Primary = new ProbeResultDto + { + Type = endpoint.Type.ToString().ToLower(), + Target = endpoint.Type == ProbeType.http + ? $"{endpoint.Host}{endpoint.HttpPath ?? ""}" + : endpoint.Type == ProbeType.tcp + ? $"{endpoint.Host}:{endpoint.Port ?? 80}" + : endpoint.Host, // For ICMP + Status = c.Status.ToString().ToLower(), + RttMs = c.RttMs, + Error = c.Error + }, + Fallback = new FallbackResultDto + { + Attempted = c.FallbackAttempted ?? false, + Type = c.FallbackAttempted == true ? "icmp" : null, + Target = c.FallbackAttempted == true ? endpoint.Host : null, + Status = c.FallbackStatus?.ToString().ToLower(), + RttMs = c.FallbackRttMs, + Error = c.FallbackError + } + }) + .ToList(); + + // 🔹 ENHANCED: Proper current state calculation with flapping detection + var currentState = await BuildCurrentStateAsync(recent, endpoint.Id, endpoint.IntervalSeconds); // --- Fetch outages within window --- var outageRaw = await _context.Outages @@ -73,7 +90,8 @@ public EndpointService(PulseDbContext context) StartedTs = ConvertToDateTimeOffset(o.StartedTs), EndedTs = o.EndedTs != null ? ConvertToDateTimeOffset(o.EndedTs) : null, DurationS = NormalizeDurationToInt(o.DurationSeconds), - LastError = o.LastError + LastError = o.LastError, + Classification = o.Classification }) .ToList(); @@ -83,11 +101,162 @@ public EndpointService(PulseDbContext context) return new EndpointDetailDto { Endpoint = endpointDto, + CurrentState = currentState, Recent = recent, Outages = outages }; } + // 🔹 NEW: Enhanced current state builder with flapping detection + private async Task BuildCurrentStateAsync( + List recent, + Guid endpointId, + int intervalSeconds) + { + var lastCheck = recent.FirstOrDefault(); + + // Handle no data case + if (lastCheck == null) + { + return new CurrentStateDto + { + EffectiveStatus = "down", + EffectiveRtt = null, + Classification = (int)OutageClassification.Unknown, + HostReachable = false, + LastCheck = DateTimeOffset.UtcNow + }; + } + + // Check if data is stale (older than 2x interval) + var expectedInterval = TimeSpan.FromSeconds(intervalSeconds * 2); + bool isStale = DateTimeOffset.UtcNow - lastCheck.Ts > expectedInterval; + + if (isStale) + { + return new CurrentStateDto + { + EffectiveStatus = "down", + EffectiveRtt = null, + Classification = (int)OutageClassification.Unknown, + HostReachable = false, + LastCheck = lastCheck.Ts + }; + } + + // 🔹 PRIORITY 1: Check for flapping using recent data + bool isFlapping = await IsFlappingAsync(endpointId, recent); + + if (isFlapping) + { + var (effectiveStatus, effectiveRtt) = DetermineEffectiveStatusAndRtt(lastCheck); + + return new CurrentStateDto + { + EffectiveStatus = "flapping", + EffectiveRtt = effectiveRtt, + Classification = (int)OutageClassification.Intermittent, + HostReachable = lastCheck.Primary.Status == "up" || + (lastCheck.Fallback.Attempted && lastCheck.Fallback.Status == "up"), + LastCheck = lastCheck.Ts + }; + } + + // 🔹 PRIORITY 2: Normal status logic + var (status, rtt) = DetermineEffectiveStatusAndRtt(lastCheck); + + return new CurrentStateDto + { + EffectiveStatus = status, + EffectiveRtt = rtt, + Classification = lastCheck.Classification, + HostReachable = lastCheck.Primary.Status == "up" || + (lastCheck.Fallback.Attempted && lastCheck.Fallback.Status == "up"), + LastCheck = lastCheck.Ts + }; + } + + // 🔹 NEW: Proper effective status determination + private (string status, double? rtt) DetermineEffectiveStatusAndRtt(CheckResultStructuredDto check) + { + // If primary DOWN but fallback UP → show as UP (service issue, host reachable) + if (check.Primary.Status == "down" && + check.Fallback.Attempted && + check.Fallback.Status == "up") + { + return ("up", check.Fallback.RttMs); + } + + // Otherwise use primary status and RTT + return (check.Primary.Status, check.Primary.RttMs); + } + + // 🔹 NEW: Flapping detection using structured data + private async Task IsFlappingAsync(Guid endpointId, List recent) + { + // Use recent data if we have enough, otherwise query database + var checksForFlapping = recent.Count >= 10 + ? recent.Take(10).ToList() + : await GetRecentChecksForFlappingAsync(endpointId); + + if (checksForFlapping.Count < 4) + { + return false; + } + + // Apply effective status logic to each check + var effectiveStatuses = checksForFlapping + .OrderBy(c => c.Ts) + .Select(c => { + var (effectiveStatus, _) = DetermineEffectiveStatusAndRtt(c); + return effectiveStatus; + }) + .ToList(); + + // Count state changes in effective status + int stateChanges = 0; + for (int i = 1; i < effectiveStatuses.Count; i++) + { + if (effectiveStatuses[i] != effectiveStatuses[i - 1]) + { + stateChanges++; + } + } + + return stateChanges > 3; + } + + // 🔹 NEW: Helper to get recent checks for flapping detection + private async Task> GetRecentChecksForFlappingAsync(Guid endpointId) + { + var cutoffTime = DateTimeOffset.UtcNow.AddMinutes(-5); + var checks = await _context.CheckResultsRaw + .Where(c => c.EndpointId == endpointId && ConvertToDateTimeOffset(c.Ts) >= cutoffTime) + .OrderByDescending(c => c.Ts) + .Take(10) + .ToListAsync(); + + // Convert to structured format for consistency + return checks.Select(c => new CheckResultStructuredDto + { + Ts = ConvertToDateTimeOffset(c.Ts), + Classification = (int)(c.Classification ?? OutageClassification.Unknown), + Primary = new ProbeResultDto + { + Status = c.Status.ToString().ToLower(), + RttMs = c.RttMs, + Error = c.Error + }, + Fallback = new FallbackResultDto + { + Attempted = c.FallbackAttempted ?? false, + Status = c.FallbackStatus?.ToString().ToLower(), + RttMs = c.FallbackRttMs, + Error = c.FallbackError + } + }).ToList(); + } + private EndpointDto MapToEndpointDto(Data.Endpoint endpoint) { return new EndpointDto diff --git a/ThingConnect.Pulse.Server/Services/StatusService.cs b/ThingConnect.Pulse.Server/Services/StatusService.cs index 3fade8c..4e51ead 100644 --- a/ThingConnect.Pulse.Server/Services/StatusService.cs +++ b/ThingConnect.Pulse.Server/Services/StatusService.cs @@ -9,6 +9,8 @@ namespace ThingConnect.Pulse.Server.Services; public interface IStatusService { Task> GetLiveStatusAsync(string? group, string? search); + Task> GetGroupsCachedAsync(); + void InvalidateGroupsCache(); } public sealed class StatusService : IStatusService @@ -63,7 +65,7 @@ public async Task> GetLiveStatusAsync(string? group, str var items = new List(); var endpointIds = endpoints.Select(e => e.Id).ToList(); - // Get latest checks for all endpoints - optimized query using window functions in SQLite + // Get latest checks for all endpoints - optimized query var latestChecks = await _context.CheckResultsRaw .Where(c => endpointIds.Contains(c.EndpointId)) .AsNoTracking() @@ -82,21 +84,35 @@ public async Task> GetLiveStatusAsync(string? group, str foreach (Data.Endpoint? endpoint in endpoints) { - StatusType status = DetermineStatus(endpoint, latestCheckDict); + CheckResultRaw? latestCheck = latestCheckDict.ContainsKey(endpoint.Id) + ? latestCheckDict[endpoint.Id] + : null; + + // 🔹 NEW: Build enhanced status with fallback + flapping + classification + var statusInfo = await DetermineEnhancedStatusAsync(endpoint, latestCheck); + List sparkline = sparklineData.ContainsKey(endpoint.Id) ? sparklineData[endpoint.Id] : new List(); _logger.LogInformation( - "Endpoint {EndpointName}: Status = {Status}, LastRttMs = {RttMs}, LastChangeTs = {LastChangeTs}", - endpoint.Name, status, endpoint.LastRttMs, endpoint.LastChangeTs); + "Endpoint {EndpointName}: EffectiveStatus = {EffectiveStatus}, EffectiveRtt = {EffectiveRtt}, Classification = {Classification}", + endpoint.Name, statusInfo.CurrentState.EffectiveStatus, statusInfo.CurrentState.EffectiveRtt, statusInfo.CurrentState.Classification); items.Add(new LiveStatusItemDto { Endpoint = MapToEndpointDto(endpoint), - Status = status.ToString().ToLower(), - RttMs = endpoint.LastRttMs, - LastChangeTs = endpoint.LastChangeTs.HasValue ? UnixTimestamp.FromUnixSeconds(endpoint.LastChangeTs.Value) : DateTimeOffset.Now, + + // 🔹 LEGACY: Keep temporarily for backward compatibility + Status = statusInfo.CurrentState.EffectiveStatus, + RttMs = statusInfo.CurrentState.EffectiveRtt, + + // 🔹 NEW: Rich current state + CurrentState = statusInfo.CurrentState, + + LastChangeTs = endpoint.LastChangeTs.HasValue + ? UnixTimestamp.FromUnixSeconds(endpoint.LastChangeTs.Value) + : DateTimeOffset.Now, Sparkline = sparkline }); } @@ -104,10 +120,175 @@ public async Task> GetLiveStatusAsync(string? group, str return items; } + // 🔹 NEW: Enhanced status determination with all logic combined + private async Task<(CurrentStateDto CurrentState, StatusType LegacyStatus)> DetermineEnhancedStatusAsync(Data.Endpoint endpoint, CheckResultRaw? latestCheck) + { + // Handle no data case + if (latestCheck == null) + { + return (new CurrentStateDto + { + EffectiveStatus = "down", + EffectiveRtt = null, + Classification = (int)OutageClassification.Unknown, + HostReachable = false, + LastCheck = DateTimeOffset.UtcNow + }, StatusType.Down); + } + + // Check if data is stale (older than 2x interval) + var expectedInterval = TimeSpan.FromSeconds(endpoint.IntervalSeconds * 2); + bool isStale = UnixTimestamp.Now() - latestCheck.Ts > (long)expectedInterval.TotalSeconds; + + if (isStale) + { + return (new CurrentStateDto + { + EffectiveStatus = "down", + EffectiveRtt = null, + Classification = (int)OutageClassification.Unknown, + HostReachable = false, + LastCheck = UnixTimestamp.FromUnixSeconds(latestCheck.Ts) + }, StatusType.Down); + } + + // 🔹 PRIORITY 1: Check for flapping (using your existing logic) + bool isFlapping = await IsFlappingAsync(endpoint.Id); + + if (isFlapping) + { + return (new CurrentStateDto + { + EffectiveStatus = "flapping", + EffectiveRtt = CalculateEffectiveRtt(latestCheck), + Classification = (int)OutageClassification.Intermittent, + HostReachable = latestCheck.FallbackStatus == UpDown.up, + LastCheck = UnixTimestamp.FromUnixSeconds(latestCheck.Ts) + }, StatusType.Flapping); + } + + // 🔹 PRIORITY 2: Normal status logic with fallback awareness + string effectiveStatus = DetermineEffectiveStatus(latestCheck); + StatusType legacyStatus = latestCheck.Status == UpDown.up ? StatusType.Up : StatusType.Down; + + // Override legacy status if we show as "up" due to fallback + if (effectiveStatus == "up" && latestCheck.Status == UpDown.down) + { + legacyStatus = StatusType.Up; // Show as UP in legacy field too + } + + return (new CurrentStateDto + { + EffectiveStatus = effectiveStatus, + EffectiveRtt = CalculateEffectiveRtt(latestCheck), + Classification = DetermineClassification(latestCheck), + HostReachable = latestCheck.FallbackStatus == UpDown.up, + LastCheck = UnixTimestamp.FromUnixSeconds(latestCheck.Ts) + }, legacyStatus); + } + + // 🔹 NEW: Determine effective status with fallback logic + private string DetermineEffectiveStatus(CheckResultRaw check) + { + // If primary failed but fallback succeeded → show as UP (service issue, host reachable) + if (check.Status == UpDown.down && check.FallbackStatus == UpDown.up) + { + return "up"; // Host is reachable, it's a service issue + } + + // Otherwise use primary status + return check.Status.ToString().ToLower(); + } + + // 🔹 NEW: Smart classification logic + private int DetermineClassification(CheckResultRaw check) + { + // Use database classification if available + if (check.Classification.HasValue) + { + return (int)check.Classification.Value; + } + + // Fallback to simple classification logic + if (check.Status == UpDown.up) + { + return (int)OutageClassification.None; // Healthy + } + + if (check.Status == UpDown.down && check.FallbackStatus == UpDown.up) + { + return (int)OutageClassification.Service; // Service down, host up + } + + if (check.Status == UpDown.down && check.FallbackStatus == UpDown.down) + { + return (int)OutageClassification.Network; // Both down = network issue + } + + return (int)OutageClassification.Unknown; + } + + // 🔹 NEW: Calculate effective RTT (priority-based) + private double? CalculateEffectiveRtt(CheckResultRaw check) + { + // Priority 1: Primary probe RTT (if successful) + if (check.Status == UpDown.up && check.RttMs.HasValue) + { + return check.RttMs.Value; + } + + // Priority 2: Fallback RTT (if primary failed but fallback succeeded) + if (check.Status == UpDown.down && + check.FallbackStatus == UpDown.up && + check.FallbackRttMs.HasValue) + { + return check.FallbackRttMs.Value; + } + + // Priority 3: Both failed or no RTT available + return null; + } + + // 🔹 KEEP: Your existing flapping logic (enhanced) + private async Task IsFlappingAsync(Guid endpointId) + { + // Enhanced: Use your existing 5-minute window with fallback consideration + long cutoffTime = UnixTimestamp.Subtract(UnixTimestamp.Now(), TimeSpan.FromMinutes(5)); + var checks = await _context.CheckResultsRaw + .Where(c => c.EndpointId == endpointId && c.Ts >= cutoffTime) + .AsNoTracking() + .Select(c => new { c.Ts, c.Status, c.FallbackStatus }) + .OrderBy(c => c.Ts) + .ToListAsync(); + + if (checks.Count < 4) + { + return false; + } + + // 🔹 ENHANCED: Consider effective status for flapping (not just primary) + var effectiveStatuses = checks.Select(c => { + // Apply same logic: if primary down but fallback up = effective up + if (c.Status == UpDown.down && c.FallbackStatus == UpDown.up) + return UpDown.up; + return c.Status; + }).ToList(); + + int stateChanges = 0; + for (int i = 1; i < effectiveStatuses.Count; i++) + { + if (effectiveStatuses[i] != effectiveStatuses[i - 1]) + { + stateChanges++; + } + } + + return stateChanges > 3; + } + /// /// Gets all groups with caching for better performance. /// - /// A representing the asynchronous operation. public async Task> GetGroupsCachedAsync() { const string cacheKey = "all_groups"; @@ -147,12 +328,12 @@ private async Task>> GetSparklineDataAsync return sparklineData; } - // Get last 20 checks for each endpoint - optimized with time filter in query + // Get last 20 checks for each endpoint - optimized with time filter long cutoffTime = UnixTimestamp.Subtract(UnixTimestamp.Now(), TimeSpan.FromHours(2)); var recentChecks = await _context.CheckResultsRaw .Where(c => endpointIds.Contains(c.EndpointId) && c.Ts >= cutoffTime) .AsNoTracking() - .Select(c => new { c.EndpointId, c.Ts, c.Status }) + .Select(c => new { c.EndpointId, c.Ts, c.Status, c.FallbackStatus }) .ToListAsync(); recentChecks = recentChecks @@ -167,10 +348,16 @@ private async Task>> GetSparklineDataAsync var points = group .Take(20) // Maximum 20 points for sparkline .OrderBy(c => c.Ts) // Order chronologically for display - .Select(c => new SparklinePoint - { - Ts = UnixTimestamp.FromUnixSeconds(c.Ts), - S = c.Status == UpDown.up ? "u" : "d" + .Select(c => { + // 🔹 ENHANCED: Sparkline shows effective status + var effectiveStatus = (c.Status == UpDown.down && c.FallbackStatus == UpDown.up) + ? UpDown.up : c.Status; + + return new SparklinePoint + { + Ts = UnixTimestamp.FromUnixSeconds(c.Ts), + S = effectiveStatus == UpDown.up ? "u" : "d" + }; }) .ToList(); @@ -180,63 +367,6 @@ private async Task>> GetSparklineDataAsync return sparklineData; } - private StatusType DetermineStatus(Data.Endpoint endpoint, Dictionary latestChecks) - { - // Check if we have recent check data - if (!latestChecks.TryGetValue(endpoint.Id, out CheckResultRaw? latestCheck) || latestCheck == null) - { - return StatusType.Down; // No data means down - } - - // Check if the latest check is recent enough (within 2x interval) - var expectedInterval = TimeSpan.FromSeconds(endpoint.IntervalSeconds * 2); - if (UnixTimestamp.Now() - latestCheck.Ts > (long)expectedInterval.TotalSeconds) - { - return StatusType.Down; // Stale data means down - } - - // Check for flapping (multiple state changes in short period) - // This is simplified - in production you'd want more sophisticated flap detection - if (IsFlapping(endpoint.Id).Result) - { - return StatusType.Flapping; - } - - return latestCheck.Status == UpDown.up ? StatusType.Up : StatusType.Down; - } - - private async Task IsFlapping(Guid endpointId) - { - // Simple flap detection: check if there were > 3 state changes in last 5 minutes - long cutoffTime = UnixTimestamp.Subtract(UnixTimestamp.Now(), TimeSpan.FromMinutes(5)); - var checks = await _context.CheckResultsRaw - .Where(c => c.EndpointId == endpointId && c.Ts >= cutoffTime) - .AsNoTracking() - .Select(c => new { c.Ts, c.Status }) - .ToListAsync(); - - var recentChecks = checks - .OrderBy(c => c.Ts) - .Select(c => c.Status) - .ToList(); - - if (recentChecks.Count < 4) - { - return false; - } - - int stateChanges = 0; - for (int i = 1; i < recentChecks.Count; i++) - { - if (recentChecks[i] != recentChecks[i - 1]) - { - stateChanges++; - } - } - - return stateChanges > 3; - } - private EndpointDto MapToEndpointDto(Data.Endpoint endpoint) { return new EndpointDto From e9ecd402442d971255f5aea251fadd8eb2fef0a4 Mon Sep 17 00:00:00 2001 From: abishek Date: Tue, 30 Sep 2025 11:14:09 +0530 Subject: [PATCH 18/28] renamed outage classifaciton status classification --- ThingConnect.Pulse.Server/Data/Entities.cs | 6 ++--- .../Models/CheckResult.cs | 2 +- .../Models/HistoryDtos.cs | 4 ++-- .../Models/StatusDtos.cs | 2 +- .../Services/EndpointService.cs | 12 +++++----- .../Monitoring/OutageDetectionService.cs | 2 +- .../Services/Monitoring/ProbeService.cs | 2 +- ...utageClassifier.cs => StatusClassifier.cs} | 22 +++++++++---------- .../Services/StatusService.cs | 14 ++++++------ thingconnect.pulse.client/src/api/types.ts | 6 ++--- 10 files changed, 36 insertions(+), 36 deletions(-) rename ThingConnect.Pulse.Server/Services/Monitoring/{OutageClassifier.cs => StatusClassifier.cs} (83%) diff --git a/ThingConnect.Pulse.Server/Data/Entities.cs b/ThingConnect.Pulse.Server/Data/Entities.cs index aba80b1..420faac 100644 --- a/ThingConnect.Pulse.Server/Data/Entities.cs +++ b/ThingConnect.Pulse.Server/Data/Entities.cs @@ -8,7 +8,7 @@ public enum UpDown { up, down } /// /// Outage classification for failed probe analysis. /// -public enum OutageClassification +public enum Classification { None = -1, // Explicitly healthy, no outage detected Unknown = 0, // Not enough information to classify @@ -74,7 +74,7 @@ public sealed class CheckResultRaw public UpDown? FallbackStatus { get; set; } public double? FallbackRttMs { get; set; } public string? FallbackError { get; set; } - public OutageClassification? Classification { get; set; } + public Classification? Classification { get; set; } } public sealed class Outage @@ -86,7 +86,7 @@ public sealed class Outage public long? EndedTs { get; set; } public int? DurationSeconds { get; set; } public string? LastError { get; set; } - public OutageClassification? Classification { get; set; } + public Classification? Classification { get; set; } /// /// Gets or sets timestamp when monitoring was lost during this outage (service downtime). diff --git a/ThingConnect.Pulse.Server/Models/CheckResult.cs b/ThingConnect.Pulse.Server/Models/CheckResult.cs index f10b9e3..2c6df70 100644 --- a/ThingConnect.Pulse.Server/Models/CheckResult.cs +++ b/ThingConnect.Pulse.Server/Models/CheckResult.cs @@ -41,7 +41,7 @@ public sealed class CheckResult /// /// Gets or sets the outage classification determined after primary and fallback probes. /// - public OutageClassification? Classification { get; set; } + public Classification? Classification { get; set; } /// /// Creates a successful check result. diff --git a/ThingConnect.Pulse.Server/Models/HistoryDtos.cs b/ThingConnect.Pulse.Server/Models/HistoryDtos.cs index 341147f..170e255 100644 --- a/ThingConnect.Pulse.Server/Models/HistoryDtos.cs +++ b/ThingConnect.Pulse.Server/Models/HistoryDtos.cs @@ -12,7 +12,7 @@ public sealed class RawCheckDto public string? FallbackStatus { get; set; } public double? FallbackRttMs { get; set; } public string? FallbackError { get; set; } - public OutageClassification? Classification { get; set; } + public Classification? Classification { get; set; } } public sealed class RollupBucketDto @@ -37,7 +37,7 @@ public sealed class OutageDto public DateTimeOffset? EndedTs { get; set; } public int? DurationS { get; set; } public string? LastError { get; set; } - public OutageClassification? Classification { get; set; } + public Classification? Classification { get; set; } } public sealed class HistoryResponseDto diff --git a/ThingConnect.Pulse.Server/Models/StatusDtos.cs b/ThingConnect.Pulse.Server/Models/StatusDtos.cs index aabe418..9dd7069 100644 --- a/ThingConnect.Pulse.Server/Models/StatusDtos.cs +++ b/ThingConnect.Pulse.Server/Models/StatusDtos.cs @@ -57,7 +57,7 @@ public sealed class CurrentStateDto { public string EffectiveStatus { get; set; } = default!; // "up" or "down" public double? EffectiveRtt { get; set; } // Priority-based RTT - public int Classification { get; set; } // OutageClassification enum value + public int Classification { get; set; } // Classification enum value public bool HostReachable { get; set; } // Quick connectivity check public DateTimeOffset LastCheck { get; set; } } diff --git a/ThingConnect.Pulse.Server/Services/EndpointService.cs b/ThingConnect.Pulse.Server/Services/EndpointService.cs index aaaa955..ea1f349 100644 --- a/ThingConnect.Pulse.Server/Services/EndpointService.cs +++ b/ThingConnect.Pulse.Server/Services/EndpointService.cs @@ -44,7 +44,7 @@ public EndpointService(PulseDbContext context) .Select(c => new CheckResultStructuredDto { Ts = ConvertToDateTimeOffset(c.Ts), - Classification = (int)(c.Classification ?? OutageClassification.Unknown), + Classification = (int)(c.Classification ?? Classification.Unknown), Primary = new ProbeResultDto { Type = endpoint.Type.ToString().ToLower(), @@ -110,7 +110,7 @@ public EndpointService(PulseDbContext context) // 🔹 NEW: Enhanced current state builder with flapping detection private async Task BuildCurrentStateAsync( List recent, - Guid endpointId, + Guid endpointId, int intervalSeconds) { var lastCheck = recent.FirstOrDefault(); @@ -122,7 +122,7 @@ private async Task BuildCurrentStateAsync( { EffectiveStatus = "down", EffectiveRtt = null, - Classification = (int)OutageClassification.Unknown, + Classification = (int)Classification.Unknown, HostReachable = false, LastCheck = DateTimeOffset.UtcNow }; @@ -138,7 +138,7 @@ private async Task BuildCurrentStateAsync( { EffectiveStatus = "down", EffectiveRtt = null, - Classification = (int)OutageClassification.Unknown, + Classification = (int)Classification.Unknown, HostReachable = false, LastCheck = lastCheck.Ts }; @@ -155,7 +155,7 @@ private async Task BuildCurrentStateAsync( { EffectiveStatus = "flapping", EffectiveRtt = effectiveRtt, - Classification = (int)OutageClassification.Intermittent, + Classification = (int)Classification.Intermittent, HostReachable = lastCheck.Primary.Status == "up" || (lastCheck.Fallback.Attempted && lastCheck.Fallback.Status == "up"), LastCheck = lastCheck.Ts @@ -240,7 +240,7 @@ private async Task> GetRecentChecksForFlappingAsy return checks.Select(c => new CheckResultStructuredDto { Ts = ConvertToDateTimeOffset(c.Ts), - Classification = (int)(c.Classification ?? OutageClassification.Unknown), + Classification = (int)(c.Classification ?? Classification.Unknown), Primary = new ProbeResultDto { Status = c.Status.ToString().ToLower(), diff --git a/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs b/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs index f8ed1c5..cd93e27 100644 --- a/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs +++ b/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs @@ -354,7 +354,7 @@ public async Task HandleGracefulShutdownAsync(string? shutdownReason = null, Can } } - private async Task TransitionToDownAsync(Guid endpointId, MonitorState state, long timestamp, string? error, OutageClassification? classification, CancellationToken cancellationToken) + private async Task TransitionToDownAsync(Guid endpointId, MonitorState state, long timestamp, string? error, Classification? classification, CancellationToken cancellationToken) { using IServiceScope scope = _serviceProvider.CreateScope(); PulseDbContext context = scope.ServiceProvider.GetRequiredService(); diff --git a/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs b/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs index 8d01dbd..c48b70a 100644 --- a/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs +++ b/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs @@ -64,7 +64,7 @@ public async Task ProbeAsync(Data.Endpoint endpoint, CancellationTo } // Centralized classification - primaryResult.Classification = OutageClassifier.ClassifyOutage( + primaryResult.Classification = StatusClassifier.ClassifyStatus( primaryResult, fallbackResult, endpoint, diff --git a/ThingConnect.Pulse.Server/Services/Monitoring/OutageClassifier.cs b/ThingConnect.Pulse.Server/Services/Monitoring/StatusClassifier.cs similarity index 83% rename from ThingConnect.Pulse.Server/Services/Monitoring/OutageClassifier.cs rename to ThingConnect.Pulse.Server/Services/Monitoring/StatusClassifier.cs index 9a5f110..24bb2ea 100644 --- a/ThingConnect.Pulse.Server/Services/Monitoring/OutageClassifier.cs +++ b/ThingConnect.Pulse.Server/Services/Monitoring/StatusClassifier.cs @@ -7,9 +7,9 @@ namespace ThingConnect.Pulse.Server.Services.Monitoring; /// /// Provides classification logic for probe results (primary + fallback + history). /// -public static class OutageClassifier +public static class StatusClassifier { - public static OutageClassification ClassifyOutage( + public static Classification ClassifyStatus( CheckResult primaryResult, CheckResult fallbackResult, Data.Endpoint endpoint, @@ -18,7 +18,7 @@ public static OutageClassification ClassifyOutage( // 1. ICMP probes → always Network on failure if (endpoint.Type == ProbeType.icmp && primaryResult.Status == UpDown.down) { - return OutageClassification.Network; + return Classification.Network; } // 2. Successful probes → check performance @@ -26,40 +26,40 @@ public static OutageClassification ClassifyOutage( { if (IsPerformanceDegraded(primaryResult, endpoint)) { - return OutageClassification.Performance; + return Classification.Performance; } - return OutageClassification.None; // explicitly healthy + return Classification.None; // explicitly healthy } // 3. Failed TCP/HTTP probes → use fallback if (fallbackResult != null) { var baseClassification = fallbackResult.Status == UpDown.up - ? OutageClassification.Service - : OutageClassification.Network; + ? Classification.Service + : Classification.Network; // 4. Advanced patterns if (IsIntermittent(recentHistory)) { - return OutageClassification.Intermittent; + return Classification.Intermittent; } if (IsPartialService(primaryResult, fallbackResult, endpoint)) { - return OutageClassification.PartialService; + return Classification.PartialService; } if (IsDnsIssue(endpoint, fallbackResult)) { - return OutageClassification.DnsResolution; + return Classification.DnsResolution; } return baseClassification; } // 5. Fallback missing or failed → default - return OutageClassification.Unknown; + return Classification.Unknown; } private static bool IsPerformanceDegraded(CheckResult result, Data.Endpoint endpoint) diff --git a/ThingConnect.Pulse.Server/Services/StatusService.cs b/ThingConnect.Pulse.Server/Services/StatusService.cs index 4e51ead..1fce67a 100644 --- a/ThingConnect.Pulse.Server/Services/StatusService.cs +++ b/ThingConnect.Pulse.Server/Services/StatusService.cs @@ -130,7 +130,7 @@ public async Task> GetLiveStatusAsync(string? group, str { EffectiveStatus = "down", EffectiveRtt = null, - Classification = (int)OutageClassification.Unknown, + Classification = (int)Classification.Unknown, HostReachable = false, LastCheck = DateTimeOffset.UtcNow }, StatusType.Down); @@ -146,7 +146,7 @@ public async Task> GetLiveStatusAsync(string? group, str { EffectiveStatus = "down", EffectiveRtt = null, - Classification = (int)OutageClassification.Unknown, + Classification = (int)Classification.Unknown, HostReachable = false, LastCheck = UnixTimestamp.FromUnixSeconds(latestCheck.Ts) }, StatusType.Down); @@ -161,7 +161,7 @@ public async Task> GetLiveStatusAsync(string? group, str { EffectiveStatus = "flapping", EffectiveRtt = CalculateEffectiveRtt(latestCheck), - Classification = (int)OutageClassification.Intermittent, + Classification = (int)Classification.Intermittent, HostReachable = latestCheck.FallbackStatus == UpDown.up, LastCheck = UnixTimestamp.FromUnixSeconds(latestCheck.Ts) }, StatusType.Flapping); @@ -212,20 +212,20 @@ private int DetermineClassification(CheckResultRaw check) // Fallback to simple classification logic if (check.Status == UpDown.up) { - return (int)OutageClassification.None; // Healthy + return (int)Classification.None; // Healthy } if (check.Status == UpDown.down && check.FallbackStatus == UpDown.up) { - return (int)OutageClassification.Service; // Service down, host up + return (int)Classification.Service; // Service down, host up } if (check.Status == UpDown.down && check.FallbackStatus == UpDown.down) { - return (int)OutageClassification.Network; // Both down = network issue + return (int)Classification.Network; // Both down = network issue } - return (int)OutageClassification.Unknown; + return (int)Classification.Unknown; } // 🔹 NEW: Calculate effective RTT (priority-based) diff --git a/thingconnect.pulse.client/src/api/types.ts b/thingconnect.pulse.client/src/api/types.ts index 97924f9..3c6385c 100644 --- a/thingconnect.pulse.client/src/api/types.ts +++ b/thingconnect.pulse.client/src/api/types.ts @@ -82,7 +82,7 @@ export interface StateChange { error?: string; } -export type OutageClassification = +export type Classification = | -1 // None | 0 // Unknown | 1 // Network @@ -101,7 +101,7 @@ export interface RawCheck { fallbackAttempted?: boolean; fallbackSuccess?: boolean; fallbackRttMs?: number | null; - classification?: OutageClassification | null; + classification?: Classification | null; lastSeenViaIcmp?: string | null; // ISO timestamp when last reachable via ICMP } @@ -110,7 +110,7 @@ export interface Outage { endedTs?: string | null; durationS?: number | null; lastError?: string | null; - classification: OutageClassification; + classification: Classification; } export interface EndpointDetail { From 01b834857d481a8ece4795eca364c8453077c6bb Mon Sep 17 00:00:00 2001 From: abishek Date: Tue, 30 Sep 2025 15:16:28 +0530 Subject: [PATCH 19/28] refactor the dto models --- .../Models/CheckResult.cs | 87 +++++++++++++------ .../Models/EndpointDetailDto.cs | 3 +- .../Models/StatusDtos.cs | 5 +- .../Services/EndpointService.cs | 4 +- .../Monitoring/OutageDetectionService.cs | 16 ++-- .../Services/Monitoring/ProbeService.cs | 33 +++---- .../Services/Monitoring/StatusClassifier.cs | 3 +- 7 files changed, 93 insertions(+), 58 deletions(-) diff --git a/ThingConnect.Pulse.Server/Models/CheckResult.cs b/ThingConnect.Pulse.Server/Models/CheckResult.cs index 2c6df70..aef0075 100644 --- a/ThingConnect.Pulse.Server/Models/CheckResult.cs +++ b/ThingConnect.Pulse.Server/Models/CheckResult.cs @@ -7,29 +7,10 @@ namespace ThingConnect.Pulse.Server.Models; /// public sealed class CheckResult { - /// - /// Gets or sets the endpoint that was checked. - /// public Guid EndpointId { get; set; } - - /// - /// Gets or sets timestamp when the check was performed. - /// public DateTimeOffset Timestamp { get; set; } - - /// - /// Gets or sets result status: UP or DOWN. - /// public UpDown Status { get; set; } - - /// - /// Gets or sets round-trip time in milliseconds. Null if not applicable or failed. - /// public double? RttMs { get; set; } - - /// - /// Gets or sets error message if the check failed. Null if successful. - /// public string? Error { get; set; } // 🔹 Fallback probe info @@ -37,10 +18,6 @@ public sealed class CheckResult public UpDown? FallbackStatus { get; set; } public double? FallbackRttMs { get; set; } public string? FallbackError { get; set; } - - /// - /// Gets or sets the outage classification determined after primary and fallback probes. - /// public Classification? Classification { get; set; } /// @@ -59,7 +36,7 @@ public static CheckResult Success(Guid endpointId, DateTimeOffset timestamp, dou FallbackStatus = null, FallbackRttMs = null, FallbackError = null, - Classification = null + Classification = Data.Classification.None }; } @@ -79,7 +56,7 @@ public static CheckResult Failure(Guid endpointId, DateTimeOffset timestamp, str FallbackStatus = null, FallbackRttMs = null, FallbackError = null, - Classification = null + Classification = Data.Classification.Unknown // 🔹 FIXED: Set to unknown }; } @@ -94,5 +71,65 @@ public void ApplyFallback(CheckResult fallback) FallbackStatus = fallback.Status; FallbackRttMs = fallback.RttMs; FallbackError = fallback.Error; + Classification = DetermineClassification(); + } + + /// + /// 🔹 NEW: Helper to calculate effective status + /// + public UpDown GetEffectiveStatus() + { + // Primary DOWN + Fallback UP = Effective UP (service issue) + if (Status == UpDown.down && FallbackAttempted && FallbackStatus == UpDown.up) + { + return UpDown.up; + } + + return Status; + } + + /// + /// 🔹 NEW: Helper to get effective RTT + /// + public double? GetEffectiveRtt() + { + // Priority 1: Primary RTT if successful + if (Status == UpDown.up && RttMs.HasValue) + { + return RttMs; + } + + // Priority 2: Fallback RTT if primary failed but fallback succeeded + if (Status == UpDown.down && FallbackAttempted && FallbackStatus == UpDown.up && FallbackRttMs.HasValue) + { + return FallbackRttMs; + } + + return null; + } + + /// + /// 🔹 NEW: Auto-classification based on probe results + /// + private Classification DetermineClassification() + { + if (Status == UpDown.up) + { + return Data.Classification.None; // Healthy + } + + if (FallbackAttempted) + { + if (FallbackStatus == UpDown.up) + { + return Data.Classification.Service; // Service down, host up + } + else + { + return Data.Classification.Network; // Both down + } + } + + return Data.Classification.Unknown; // No fallback info } } diff --git a/ThingConnect.Pulse.Server/Models/EndpointDetailDto.cs b/ThingConnect.Pulse.Server/Models/EndpointDetailDto.cs index d4a5f09..369224d 100644 --- a/ThingConnect.Pulse.Server/Models/EndpointDetailDto.cs +++ b/ThingConnect.Pulse.Server/Models/EndpointDetailDto.cs @@ -1,3 +1,4 @@ +using ThingConnect.Pulse.Server.Data; using ThingConnect.Pulse.Server.Models; public sealed class EndpointDetailDto @@ -11,7 +12,7 @@ public sealed class EndpointDetailDto public sealed class CheckResultStructuredDto { public DateTimeOffset Ts { get; set; } - public int Classification { get; set; } + public Classification Classification { get; set; } public ProbeResultDto Primary { get; set; } = default!; public FallbackResultDto Fallback { get; set; } = default!; } diff --git a/ThingConnect.Pulse.Server/Models/StatusDtos.cs b/ThingConnect.Pulse.Server/Models/StatusDtos.cs index 9dd7069..7c04179 100644 --- a/ThingConnect.Pulse.Server/Models/StatusDtos.cs +++ b/ThingConnect.Pulse.Server/Models/StatusDtos.cs @@ -3,8 +3,9 @@ namespace ThingConnect.Pulse.Server.Models; public sealed class LiveStatusItemDto { public EndpointDto Endpoint { get; set; } = default!; - public string Status { get; set; } = default!; // need to remove - public double? RttMs { get; set; } // need to remove + // 🔹 LEGACY: Keep for backward compatibility (remove later) + public string Status { get; set; } = default!; + public double? RttMs { get; set; } public CurrentStateDto CurrentState { get; set; } = default!; public DateTimeOffset LastChangeTs { get; set; } public List Sparkline { get; set; } = new(); diff --git a/ThingConnect.Pulse.Server/Services/EndpointService.cs b/ThingConnect.Pulse.Server/Services/EndpointService.cs index ea1f349..9bb9114 100644 --- a/ThingConnect.Pulse.Server/Services/EndpointService.cs +++ b/ThingConnect.Pulse.Server/Services/EndpointService.cs @@ -242,13 +242,13 @@ private async Task> GetRecentChecksForFlappingAsy Ts = ConvertToDateTimeOffset(c.Ts), Classification = (int)(c.Classification ?? Classification.Unknown), Primary = new ProbeResultDto - { + { Status = c.Status.ToString().ToLower(), RttMs = c.RttMs, Error = c.Error }, Fallback = new FallbackResultDto - { + { Attempted = c.FallbackAttempted ?? false, Status = c.FallbackStatus?.ToString().ToLower(), RttMs = c.FallbackRttMs, diff --git a/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs b/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs index cd93e27..4471a30 100644 --- a/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs +++ b/ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs @@ -37,21 +37,21 @@ public async Task ProcessCheckResultAsync(CheckResult result, Cancellation try { - // Update streak counters based on result - if (result.Status == UpDown.up) + var effectiveStatus = result.GetEffectiveStatus(); + if (effectiveStatus == UpDown.up) { state.RecordSuccess(); _logger.LogDebug( - "RecordSuccess called for endpoint {EndpointId}. SuccessStreak={SuccessStreak}, FailStreak={FailStreak}", - result.EndpointId, state.SuccessStreak, state.FailStreak + "RecordSuccess called for endpoint {EndpointId}. EffectiveStatus={EffectiveStatus}, SuccessStreak={SuccessStreak}, FailStreak={FailStreak}", + result.EndpointId, effectiveStatus, state.SuccessStreak, state.FailStreak ); } else { state.RecordFailure(); _logger.LogDebug( - "RecordFailure called for endpoint {EndpointId}. SuccessStreak={SuccessStreak}, FailStreak={FailStreak}, Error={Error}", - result.EndpointId, state.SuccessStreak, state.FailStreak, result.Error + "RecordFailure called for endpoint {EndpointId}. EffectiveStatus={EffectiveStatus}, SuccessStreak={SuccessStreak}, FailStreak={FailStreak}, Error={Error}", + result.EndpointId, effectiveStatus, state.SuccessStreak, state.FailStreak, result.Error ); } @@ -66,7 +66,7 @@ await TransitionToDownAsync( result.Classification, cancellationToken); stateChanged = true; - _logger.LogWarning("Endpoint {EndpointId} transitioned to DOWN after {FailStreak} consecutive failures", + _logger.LogWarning("Endpoint {EndpointId} transitioned to DOWN after {FailStreak} consecutive effective failures", result.EndpointId, state.FailStreak); } @@ -75,7 +75,7 @@ await TransitionToDownAsync( { await TransitionToUpAsync(result.EndpointId, state, UnixTimestamp.ToUnixSeconds(result.Timestamp), cancellationToken); stateChanged = true; - _logger.LogInformation("Endpoint {EndpointId} transitioned to UP after {SuccessStreak} consecutive successes", + _logger.LogInformation("Endpoint {EndpointId} transitioned to UP after {SuccessStreak} consecutive effective successes", result.EndpointId, state.SuccessStreak); } diff --git a/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs b/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs index c48b70a..9df3739 100644 --- a/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs +++ b/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs @@ -25,12 +25,11 @@ public ProbeService(ILogger logger, IHttpClientFactory httpClientF public async Task ProbeAsync(Data.Endpoint endpoint, CancellationToken cancellationToken = default) { DateTimeOffset timestamp = DateTimeOffset.UtcNow; - CheckResult primaryResult; - CheckResult? fallbackResult = null; + CheckResult probeResult; try { - primaryResult = endpoint.Type switch + probeResult = endpoint.Type switch { ProbeType.icmp => await PingAsync(endpoint.Id, endpoint.Host, endpoint.TimeoutMs, cancellationToken), ProbeType.tcp => await TcpConnectAsync(endpoint.Id, endpoint.Host, endpoint.Port ?? 80, endpoint.TimeoutMs, cancellationToken), @@ -42,36 +41,32 @@ public async Task ProbeAsync(Data.Endpoint endpoint, CancellationTo catch (Exception ex) { _logger.LogError(ex, "Primary probe failed for endpoint {EndpointId} ({Host})", endpoint.Id, endpoint.Host); - primaryResult = CheckResult.Failure(endpoint.Id, timestamp, ex.Message); + probeResult = CheckResult.Failure(endpoint.Id, timestamp, ex.Message); } // TCP/HTTP fallback to ICMP if primary failed - if (primaryResult.Status == UpDown.down && endpoint.Type != ProbeType.icmp) + if (probeResult.Status == UpDown.down && endpoint.Type != ProbeType.icmp) { try { - int fallbackTimeout = Math.Max(endpoint.TimeoutMs / 2, 100); - fallbackResult = await PingAsync(endpoint.Id, endpoint.Host, endpoint.TimeoutMs, cancellationToken); + int fallbackTimeout = Math.Max(endpoint.TimeoutMs / 2, 1000); + CheckResult fallbackResult = await PingAsync(endpoint.Id, endpoint.Host, fallbackTimeout, cancellationToken); - // Apply fallback only if it actually ran - primaryResult.ApplyFallback(fallbackResult); + // ApplyFallback automatically sets classification + probeResult.ApplyFallback(fallbackResult); } catch (Exception ex) { - fallbackResult = CheckResult.Failure(endpoint.Id, DateTimeOffset.UtcNow, $"Fallback ping failed: {ex.Message}"); - primaryResult.ApplyFallback(fallbackResult); + _logger.LogWarning(ex, "Fallback ICMP probe failed for endpoint {EndpointId} ({Host})", endpoint.Id, endpoint.Host); + CheckResult fallbackResult = CheckResult.Failure(endpoint.Id, DateTimeOffset.UtcNow, $"Fallback ping failed: {ex.Message}"); + probeResult.ApplyFallback(fallbackResult); } } - // Centralized classification - primaryResult.Classification = StatusClassifier.ClassifyStatus( - primaryResult, - fallbackResult, - endpoint, - Enumerable.Empty() // feed history later if available - ); + _logger.LogDebug("Probe completed for {EndpointId}: Status={Status}, Classification={Classification}, EffectiveStatus={EffectiveStatus}", + endpoint.Id, probeResult.Status, probeResult.Classification, probeResult.GetEffectiveStatus()); - return primaryResult; + return probeResult; } public async Task PingAsync(Guid endpointId, string host, int timeoutMs, CancellationToken cancellationToken = default) diff --git a/ThingConnect.Pulse.Server/Services/Monitoring/StatusClassifier.cs b/ThingConnect.Pulse.Server/Services/Monitoring/StatusClassifier.cs index 24bb2ea..b50bbc3 100644 --- a/ThingConnect.Pulse.Server/Services/Monitoring/StatusClassifier.cs +++ b/ThingConnect.Pulse.Server/Services/Monitoring/StatusClassifier.cs @@ -5,7 +5,8 @@ namespace ThingConnect.Pulse.Server.Services.Monitoring; /// -/// Provides classification logic for probe results (primary + fallback + history). +// Complex logic with history, performance, DNS, etc. +// Save for Phase 2 /// public static class StatusClassifier { From 12a5f4caf0eaceabf8ef8af4690b3700907e4327 Mon Sep 17 00:00:00 2001 From: abishek Date: Tue, 30 Sep 2025 15:39:19 +0530 Subject: [PATCH 20/28] reverted the serivce --- .../Models/CheckResult.cs | 66 ++++- .../Services/EndpointService.cs | 198 +------------ .../Services/StatusService.cs | 274 +++++------------- 3 files changed, 137 insertions(+), 401 deletions(-) diff --git a/ThingConnect.Pulse.Server/Models/CheckResult.cs b/ThingConnect.Pulse.Server/Models/CheckResult.cs index aef0075..e307f94 100644 --- a/ThingConnect.Pulse.Server/Models/CheckResult.cs +++ b/ThingConnect.Pulse.Server/Models/CheckResult.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Linq; using ThingConnect.Pulse.Server.Data; namespace ThingConnect.Pulse.Server.Models; @@ -75,7 +78,7 @@ public void ApplyFallback(CheckResult fallback) } /// - /// 🔹 NEW: Helper to calculate effective status + /// 🔹 Helper to calculate effective status /// public UpDown GetEffectiveStatus() { @@ -84,12 +87,11 @@ public UpDown GetEffectiveStatus() { return UpDown.up; } - return Status; } /// - /// 🔹 NEW: Helper to get effective RTT + /// 🔹 Helper to get effective RTT /// public double? GetEffectiveRtt() { @@ -98,38 +100,76 @@ public UpDown GetEffectiveStatus() { return RttMs; } - // Priority 2: Fallback RTT if primary failed but fallback succeeded if (Status == UpDown.down && FallbackAttempted && FallbackStatus == UpDown.up && FallbackRttMs.HasValue) { return FallbackRttMs; } - return null; } /// - /// 🔹 NEW: Auto-classification based on probe results + /// 🔹 Auto-classification based on probe results /// - private Classification DetermineClassification() + public Classification DetermineClassification() { if (Status == UpDown.up) { return Data.Classification.None; // Healthy } - if (FallbackAttempted) { if (FallbackStatus == UpDown.up) { return Data.Classification.Service; // Service down, host up } - else - { - return Data.Classification.Network; // Both down - } + return Data.Classification.Network; // Both down } - return Data.Classification.Unknown; // No fallback info } + + /// + /// 🔹 Common flapping detection utility (uses recent check list) + /// + public static bool IsFlapping(List recent) + { + if (recent.Count < 4) return false; + var effectiveStatuses = recent + .OrderBy(c => c.Timestamp) + .Select(c => c.GetEffectiveStatus().ToString()) + .ToList(); + int stateChanges = 0; + for (int i = 1; i < effectiveStatuses.Count; i++) + if (effectiveStatuses[i] != effectiveStatuses[i - 1]) + stateChanges++; + return stateChanges > 3; + } + + /// + /// 🔹 Map endpoint entity to EndpointDto (call anywhere you need) + /// + public static EndpointDto MapToEndpointDto(Data.Endpoint endpoint) + { + return new EndpointDto + { + Id = endpoint.Id, + Name = endpoint.Name, + Group = new GroupDto + { + Id = endpoint.Group.Id, + Name = endpoint.Group.Name, + ParentId = endpoint.Group.ParentId, + Color = endpoint.Group.Color + }, + Type = endpoint.Type.ToString().ToLower(), + Host = endpoint.Host, + Port = endpoint.Port, + HttpPath = endpoint.HttpPath, + HttpMatch = endpoint.HttpMatch, + IntervalSeconds = endpoint.IntervalSeconds, + TimeoutMs = endpoint.TimeoutMs, + Retries = endpoint.Retries, + Enabled = endpoint.Enabled + }; + } } diff --git a/ThingConnect.Pulse.Server/Services/EndpointService.cs b/ThingConnect.Pulse.Server/Services/EndpointService.cs index 9bb9114..7b502e3 100644 --- a/ThingConnect.Pulse.Server/Services/EndpointService.cs +++ b/ThingConnect.Pulse.Server/Services/EndpointService.cs @@ -39,38 +39,16 @@ public EndpointService(PulseDbContext context) .ToListAsync(); var recent = rawChecks - .Where(c => ConvertToDateTimeOffset(c.Ts) >= windowStart) - .OrderByDescending(c => ConvertToDateTimeOffset(c.Ts)) - .Select(c => new CheckResultStructuredDto - { - Ts = ConvertToDateTimeOffset(c.Ts), - Classification = (int)(c.Classification ?? Classification.Unknown), - Primary = new ProbeResultDto - { - Type = endpoint.Type.ToString().ToLower(), - Target = endpoint.Type == ProbeType.http - ? $"{endpoint.Host}{endpoint.HttpPath ?? ""}" - : endpoint.Type == ProbeType.tcp - ? $"{endpoint.Host}:{endpoint.Port ?? 80}" - : endpoint.Host, // For ICMP - Status = c.Status.ToString().ToLower(), - RttMs = c.RttMs, - Error = c.Error - }, - Fallback = new FallbackResultDto - { - Attempted = c.FallbackAttempted ?? false, - Type = c.FallbackAttempted == true ? "icmp" : null, - Target = c.FallbackAttempted == true ? endpoint.Host : null, - Status = c.FallbackStatus?.ToString().ToLower(), - RttMs = c.FallbackRttMs, - Error = c.FallbackError - } - }) - .ToList(); - - // 🔹 ENHANCED: Proper current state calculation with flapping detection - var currentState = await BuildCurrentStateAsync(recent, endpoint.Id, endpoint.IntervalSeconds); + .Select(c => new RawCheckDto + { + Ts = ConvertToDateTimeOffset(c.Ts), + Status = c.Status.ToString().ToLower(), + RttMs = c.RttMs, + Error = c.Error + }) + .Where(r => r.Ts >= windowStart) + .OrderByDescending(r => r.Ts) + .ToList(); // --- Fetch outages within window --- var outageRaw = await _context.Outages @@ -90,8 +68,7 @@ public EndpointService(PulseDbContext context) StartedTs = ConvertToDateTimeOffset(o.StartedTs), EndedTs = o.EndedTs != null ? ConvertToDateTimeOffset(o.EndedTs) : null, DurationS = NormalizeDurationToInt(o.DurationSeconds), - LastError = o.LastError, - Classification = o.Classification + LastError = o.LastError }) .ToList(); @@ -101,162 +78,11 @@ public EndpointService(PulseDbContext context) return new EndpointDetailDto { Endpoint = endpointDto, - CurrentState = currentState, Recent = recent, Outages = outages }; } - // 🔹 NEW: Enhanced current state builder with flapping detection - private async Task BuildCurrentStateAsync( - List recent, - Guid endpointId, - int intervalSeconds) - { - var lastCheck = recent.FirstOrDefault(); - - // Handle no data case - if (lastCheck == null) - { - return new CurrentStateDto - { - EffectiveStatus = "down", - EffectiveRtt = null, - Classification = (int)Classification.Unknown, - HostReachable = false, - LastCheck = DateTimeOffset.UtcNow - }; - } - - // Check if data is stale (older than 2x interval) - var expectedInterval = TimeSpan.FromSeconds(intervalSeconds * 2); - bool isStale = DateTimeOffset.UtcNow - lastCheck.Ts > expectedInterval; - - if (isStale) - { - return new CurrentStateDto - { - EffectiveStatus = "down", - EffectiveRtt = null, - Classification = (int)Classification.Unknown, - HostReachable = false, - LastCheck = lastCheck.Ts - }; - } - - // 🔹 PRIORITY 1: Check for flapping using recent data - bool isFlapping = await IsFlappingAsync(endpointId, recent); - - if (isFlapping) - { - var (effectiveStatus, effectiveRtt) = DetermineEffectiveStatusAndRtt(lastCheck); - - return new CurrentStateDto - { - EffectiveStatus = "flapping", - EffectiveRtt = effectiveRtt, - Classification = (int)Classification.Intermittent, - HostReachable = lastCheck.Primary.Status == "up" || - (lastCheck.Fallback.Attempted && lastCheck.Fallback.Status == "up"), - LastCheck = lastCheck.Ts - }; - } - - // 🔹 PRIORITY 2: Normal status logic - var (status, rtt) = DetermineEffectiveStatusAndRtt(lastCheck); - - return new CurrentStateDto - { - EffectiveStatus = status, - EffectiveRtt = rtt, - Classification = lastCheck.Classification, - HostReachable = lastCheck.Primary.Status == "up" || - (lastCheck.Fallback.Attempted && lastCheck.Fallback.Status == "up"), - LastCheck = lastCheck.Ts - }; - } - - // 🔹 NEW: Proper effective status determination - private (string status, double? rtt) DetermineEffectiveStatusAndRtt(CheckResultStructuredDto check) - { - // If primary DOWN but fallback UP → show as UP (service issue, host reachable) - if (check.Primary.Status == "down" && - check.Fallback.Attempted && - check.Fallback.Status == "up") - { - return ("up", check.Fallback.RttMs); - } - - // Otherwise use primary status and RTT - return (check.Primary.Status, check.Primary.RttMs); - } - - // 🔹 NEW: Flapping detection using structured data - private async Task IsFlappingAsync(Guid endpointId, List recent) - { - // Use recent data if we have enough, otherwise query database - var checksForFlapping = recent.Count >= 10 - ? recent.Take(10).ToList() - : await GetRecentChecksForFlappingAsync(endpointId); - - if (checksForFlapping.Count < 4) - { - return false; - } - - // Apply effective status logic to each check - var effectiveStatuses = checksForFlapping - .OrderBy(c => c.Ts) - .Select(c => { - var (effectiveStatus, _) = DetermineEffectiveStatusAndRtt(c); - return effectiveStatus; - }) - .ToList(); - - // Count state changes in effective status - int stateChanges = 0; - for (int i = 1; i < effectiveStatuses.Count; i++) - { - if (effectiveStatuses[i] != effectiveStatuses[i - 1]) - { - stateChanges++; - } - } - - return stateChanges > 3; - } - - // 🔹 NEW: Helper to get recent checks for flapping detection - private async Task> GetRecentChecksForFlappingAsync(Guid endpointId) - { - var cutoffTime = DateTimeOffset.UtcNow.AddMinutes(-5); - var checks = await _context.CheckResultsRaw - .Where(c => c.EndpointId == endpointId && ConvertToDateTimeOffset(c.Ts) >= cutoffTime) - .OrderByDescending(c => c.Ts) - .Take(10) - .ToListAsync(); - - // Convert to structured format for consistency - return checks.Select(c => new CheckResultStructuredDto - { - Ts = ConvertToDateTimeOffset(c.Ts), - Classification = (int)(c.Classification ?? Classification.Unknown), - Primary = new ProbeResultDto - { - Status = c.Status.ToString().ToLower(), - RttMs = c.RttMs, - Error = c.Error - }, - Fallback = new FallbackResultDto - { - Attempted = c.FallbackAttempted ?? false, - Status = c.FallbackStatus?.ToString().ToLower(), - RttMs = c.FallbackRttMs, - Error = c.FallbackError - } - }).ToList(); - } - private EndpointDto MapToEndpointDto(Data.Endpoint endpoint) { return new EndpointDto @@ -311,4 +137,4 @@ private static DateTimeOffset ConvertToDateTimeOffset(T value) _ => int.TryParse(value.ToString(), out var v) ? v : null }; } -} +} \ No newline at end of file diff --git a/ThingConnect.Pulse.Server/Services/StatusService.cs b/ThingConnect.Pulse.Server/Services/StatusService.cs index 1fce67a..1139d2a 100644 --- a/ThingConnect.Pulse.Server/Services/StatusService.cs +++ b/ThingConnect.Pulse.Server/Services/StatusService.cs @@ -9,8 +9,6 @@ namespace ThingConnect.Pulse.Server.Services; public interface IStatusService { Task> GetLiveStatusAsync(string? group, string? search); - Task> GetGroupsCachedAsync(); - void InvalidateGroupsCache(); } public sealed class StatusService : IStatusService @@ -65,7 +63,7 @@ public async Task> GetLiveStatusAsync(string? group, str var items = new List(); var endpointIds = endpoints.Select(e => e.Id).ToList(); - // Get latest checks for all endpoints - optimized query + // Get latest checks for all endpoints - optimized query using window functions in SQLite var latestChecks = await _context.CheckResultsRaw .Where(c => endpointIds.Contains(c.EndpointId)) .AsNoTracking() @@ -84,35 +82,21 @@ public async Task> GetLiveStatusAsync(string? group, str foreach (Data.Endpoint? endpoint in endpoints) { - CheckResultRaw? latestCheck = latestCheckDict.ContainsKey(endpoint.Id) - ? latestCheckDict[endpoint.Id] - : null; - - // 🔹 NEW: Build enhanced status with fallback + flapping + classification - var statusInfo = await DetermineEnhancedStatusAsync(endpoint, latestCheck); - + StatusType status = DetermineStatus(endpoint, latestCheckDict); List sparkline = sparklineData.ContainsKey(endpoint.Id) ? sparklineData[endpoint.Id] : new List(); _logger.LogInformation( - "Endpoint {EndpointName}: EffectiveStatus = {EffectiveStatus}, EffectiveRtt = {EffectiveRtt}, Classification = {Classification}", - endpoint.Name, statusInfo.CurrentState.EffectiveStatus, statusInfo.CurrentState.EffectiveRtt, statusInfo.CurrentState.Classification); + "Endpoint {EndpointName}: Status = {Status}, LastRttMs = {RttMs}, LastChangeTs = {LastChangeTs}", + endpoint.Name, status, endpoint.LastRttMs, endpoint.LastChangeTs); items.Add(new LiveStatusItemDto { Endpoint = MapToEndpointDto(endpoint), - - // 🔹 LEGACY: Keep temporarily for backward compatibility - Status = statusInfo.CurrentState.EffectiveStatus, - RttMs = statusInfo.CurrentState.EffectiveRtt, - - // 🔹 NEW: Rich current state - CurrentState = statusInfo.CurrentState, - - LastChangeTs = endpoint.LastChangeTs.HasValue - ? UnixTimestamp.FromUnixSeconds(endpoint.LastChangeTs.Value) - : DateTimeOffset.Now, + Status = status.ToString().ToLower(), + RttMs = endpoint.LastRttMs, + LastChangeTs = endpoint.LastChangeTs.HasValue ? UnixTimestamp.FromUnixSeconds(endpoint.LastChangeTs.Value) : DateTimeOffset.Now, Sparkline = sparkline }); } @@ -120,175 +104,10 @@ public async Task> GetLiveStatusAsync(string? group, str return items; } - // 🔹 NEW: Enhanced status determination with all logic combined - private async Task<(CurrentStateDto CurrentState, StatusType LegacyStatus)> DetermineEnhancedStatusAsync(Data.Endpoint endpoint, CheckResultRaw? latestCheck) - { - // Handle no data case - if (latestCheck == null) - { - return (new CurrentStateDto - { - EffectiveStatus = "down", - EffectiveRtt = null, - Classification = (int)Classification.Unknown, - HostReachable = false, - LastCheck = DateTimeOffset.UtcNow - }, StatusType.Down); - } - - // Check if data is stale (older than 2x interval) - var expectedInterval = TimeSpan.FromSeconds(endpoint.IntervalSeconds * 2); - bool isStale = UnixTimestamp.Now() - latestCheck.Ts > (long)expectedInterval.TotalSeconds; - - if (isStale) - { - return (new CurrentStateDto - { - EffectiveStatus = "down", - EffectiveRtt = null, - Classification = (int)Classification.Unknown, - HostReachable = false, - LastCheck = UnixTimestamp.FromUnixSeconds(latestCheck.Ts) - }, StatusType.Down); - } - - // 🔹 PRIORITY 1: Check for flapping (using your existing logic) - bool isFlapping = await IsFlappingAsync(endpoint.Id); - - if (isFlapping) - { - return (new CurrentStateDto - { - EffectiveStatus = "flapping", - EffectiveRtt = CalculateEffectiveRtt(latestCheck), - Classification = (int)Classification.Intermittent, - HostReachable = latestCheck.FallbackStatus == UpDown.up, - LastCheck = UnixTimestamp.FromUnixSeconds(latestCheck.Ts) - }, StatusType.Flapping); - } - - // 🔹 PRIORITY 2: Normal status logic with fallback awareness - string effectiveStatus = DetermineEffectiveStatus(latestCheck); - StatusType legacyStatus = latestCheck.Status == UpDown.up ? StatusType.Up : StatusType.Down; - - // Override legacy status if we show as "up" due to fallback - if (effectiveStatus == "up" && latestCheck.Status == UpDown.down) - { - legacyStatus = StatusType.Up; // Show as UP in legacy field too - } - - return (new CurrentStateDto - { - EffectiveStatus = effectiveStatus, - EffectiveRtt = CalculateEffectiveRtt(latestCheck), - Classification = DetermineClassification(latestCheck), - HostReachable = latestCheck.FallbackStatus == UpDown.up, - LastCheck = UnixTimestamp.FromUnixSeconds(latestCheck.Ts) - }, legacyStatus); - } - - // 🔹 NEW: Determine effective status with fallback logic - private string DetermineEffectiveStatus(CheckResultRaw check) - { - // If primary failed but fallback succeeded → show as UP (service issue, host reachable) - if (check.Status == UpDown.down && check.FallbackStatus == UpDown.up) - { - return "up"; // Host is reachable, it's a service issue - } - - // Otherwise use primary status - return check.Status.ToString().ToLower(); - } - - // 🔹 NEW: Smart classification logic - private int DetermineClassification(CheckResultRaw check) - { - // Use database classification if available - if (check.Classification.HasValue) - { - return (int)check.Classification.Value; - } - - // Fallback to simple classification logic - if (check.Status == UpDown.up) - { - return (int)Classification.None; // Healthy - } - - if (check.Status == UpDown.down && check.FallbackStatus == UpDown.up) - { - return (int)Classification.Service; // Service down, host up - } - - if (check.Status == UpDown.down && check.FallbackStatus == UpDown.down) - { - return (int)Classification.Network; // Both down = network issue - } - - return (int)Classification.Unknown; - } - - // 🔹 NEW: Calculate effective RTT (priority-based) - private double? CalculateEffectiveRtt(CheckResultRaw check) - { - // Priority 1: Primary probe RTT (if successful) - if (check.Status == UpDown.up && check.RttMs.HasValue) - { - return check.RttMs.Value; - } - - // Priority 2: Fallback RTT (if primary failed but fallback succeeded) - if (check.Status == UpDown.down && - check.FallbackStatus == UpDown.up && - check.FallbackRttMs.HasValue) - { - return check.FallbackRttMs.Value; - } - - // Priority 3: Both failed or no RTT available - return null; - } - - // 🔹 KEEP: Your existing flapping logic (enhanced) - private async Task IsFlappingAsync(Guid endpointId) - { - // Enhanced: Use your existing 5-minute window with fallback consideration - long cutoffTime = UnixTimestamp.Subtract(UnixTimestamp.Now(), TimeSpan.FromMinutes(5)); - var checks = await _context.CheckResultsRaw - .Where(c => c.EndpointId == endpointId && c.Ts >= cutoffTime) - .AsNoTracking() - .Select(c => new { c.Ts, c.Status, c.FallbackStatus }) - .OrderBy(c => c.Ts) - .ToListAsync(); - - if (checks.Count < 4) - { - return false; - } - - // 🔹 ENHANCED: Consider effective status for flapping (not just primary) - var effectiveStatuses = checks.Select(c => { - // Apply same logic: if primary down but fallback up = effective up - if (c.Status == UpDown.down && c.FallbackStatus == UpDown.up) - return UpDown.up; - return c.Status; - }).ToList(); - - int stateChanges = 0; - for (int i = 1; i < effectiveStatuses.Count; i++) - { - if (effectiveStatuses[i] != effectiveStatuses[i - 1]) - { - stateChanges++; - } - } - - return stateChanges > 3; - } - /// /// Gets all groups with caching for better performance. /// + /// A representing the asynchronous operation. public async Task> GetGroupsCachedAsync() { const string cacheKey = "all_groups"; @@ -328,12 +147,12 @@ private async Task>> GetSparklineDataAsync return sparklineData; } - // Get last 20 checks for each endpoint - optimized with time filter + // Get last 20 checks for each endpoint - optimized with time filter in query long cutoffTime = UnixTimestamp.Subtract(UnixTimestamp.Now(), TimeSpan.FromHours(2)); var recentChecks = await _context.CheckResultsRaw .Where(c => endpointIds.Contains(c.EndpointId) && c.Ts >= cutoffTime) .AsNoTracking() - .Select(c => new { c.EndpointId, c.Ts, c.Status, c.FallbackStatus }) + .Select(c => new { c.EndpointId, c.Ts, c.Status }) .ToListAsync(); recentChecks = recentChecks @@ -348,16 +167,10 @@ private async Task>> GetSparklineDataAsync var points = group .Take(20) // Maximum 20 points for sparkline .OrderBy(c => c.Ts) // Order chronologically for display - .Select(c => { - // 🔹 ENHANCED: Sparkline shows effective status - var effectiveStatus = (c.Status == UpDown.down && c.FallbackStatus == UpDown.up) - ? UpDown.up : c.Status; - - return new SparklinePoint - { - Ts = UnixTimestamp.FromUnixSeconds(c.Ts), - S = effectiveStatus == UpDown.up ? "u" : "d" - }; + .Select(c => new SparklinePoint + { + Ts = UnixTimestamp.FromUnixSeconds(c.Ts), + S = c.Status == UpDown.up ? "u" : "d" }) .ToList(); @@ -367,6 +180,63 @@ private async Task>> GetSparklineDataAsync return sparklineData; } + private StatusType DetermineStatus(Data.Endpoint endpoint, Dictionary latestChecks) + { + // Check if we have recent check data + if (!latestChecks.TryGetValue(endpoint.Id, out CheckResultRaw? latestCheck) || latestCheck == null) + { + return StatusType.Down; // No data means down + } + + // Check if the latest check is recent enough (within 2x interval) + var expectedInterval = TimeSpan.FromSeconds(endpoint.IntervalSeconds * 2); + if (UnixTimestamp.Now() - latestCheck.Ts > (long)expectedInterval.TotalSeconds) + { + return StatusType.Down; // Stale data means down + } + + // Check for flapping (multiple state changes in short period) + // This is simplified - in production you'd want more sophisticated flap detection + if (IsFlapping(endpoint.Id).Result) + { + return StatusType.Flapping; + } + + return latestCheck.Status == UpDown.up ? StatusType.Up : StatusType.Down; + } + + private async Task IsFlapping(Guid endpointId) + { + // Simple flap detection: check if there were > 3 state changes in last 5 minutes + long cutoffTime = UnixTimestamp.Subtract(UnixTimestamp.Now(), TimeSpan.FromMinutes(5)); + var checks = await _context.CheckResultsRaw + .Where(c => c.EndpointId == endpointId && c.Ts >= cutoffTime) + .AsNoTracking() + .Select(c => new { c.Ts, c.Status }) + .ToListAsync(); + + var recentChecks = checks + .OrderBy(c => c.Ts) + .Select(c => c.Status) + .ToList(); + + if (recentChecks.Count < 4) + { + return false; + } + + int stateChanges = 0; + for (int i = 1; i < recentChecks.Count; i++) + { + if (recentChecks[i] != recentChecks[i - 1]) + { + stateChanges++; + } + } + + return stateChanges > 3; + } + private EndpointDto MapToEndpointDto(Data.Endpoint endpoint) { return new EndpointDto @@ -398,4 +268,4 @@ private enum StatusType Down, Flapping } -} +} \ No newline at end of file From d5632690931b47b93b805f6f678bca943d1dd3b6 Mon Sep 17 00:00:00 2001 From: abishek Date: Fri, 3 Oct 2025 09:17:53 +0530 Subject: [PATCH 21/28] udpated the controller serivce --- .../Models/CheckResult.cs | 53 +++++- .../Models/EndpointDetailDto.cs | 6 +- .../Models/HistoryDtos.cs | 12 -- .../Models/StatusDtos.cs | 4 +- .../Services/EndpointService.cs | 83 +++++---- .../Services/Monitoring/ProbeService.cs | 3 - .../Services/StatusService.cs | 163 +++++------------- 7 files changed, 141 insertions(+), 183 deletions(-) diff --git a/ThingConnect.Pulse.Server/Models/CheckResult.cs b/ThingConnect.Pulse.Server/Models/CheckResult.cs index e307f94..3614217 100644 --- a/ThingConnect.Pulse.Server/Models/CheckResult.cs +++ b/ThingConnect.Pulse.Server/Models/CheckResult.cs @@ -1,10 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; using ThingConnect.Pulse.Server.Data; namespace ThingConnect.Pulse.Server.Models; +public enum StatusType +{ + Up, + Down, + Service, + Flapping +} + /// /// Result of a single probe check (ICMP, TCP, or HTTP). /// @@ -128,20 +133,52 @@ public Classification DetermineClassification() return Data.Classification.Unknown; // No fallback info } - /// - /// 🔹 Common flapping detection utility (uses recent check list) - /// + // 🔹 StatusType logic (Up / Down / Service / Flapping) + public StatusType DetermineStatusType(List recentChecks, TimeSpan interval) + { + if (recentChecks == null || recentChecks.Count == 0) + { + return StatusType.Down; + } + + // Flapping overrides all + if (IsFlapping(recentChecks)) + { + return StatusType.Flapping; + } + + // Effective UP + if (GetEffectiveStatus() == UpDown.up) + { + if (Status == UpDown.down && FallbackAttempted && FallbackStatus == UpDown.up) + { + return StatusType.Service; + } + return StatusType.Up; + } + + return StatusType.Down; + } + + // 🔹 Flapping detection (>= 4 samples, >3 changes in 5 min window) public static bool IsFlapping(List recent) { - if (recent.Count < 4) return false; + if (recent == null || recent.Count < 4) return false; + var effectiveStatuses = recent .OrderBy(c => c.Timestamp) - .Select(c => c.GetEffectiveStatus().ToString()) + .Select(c => c.GetEffectiveStatus()) .ToList(); + int stateChanges = 0; for (int i = 1; i < effectiveStatuses.Count; i++) + { if (effectiveStatuses[i] != effectiveStatuses[i - 1]) + { stateChanges++; + } + } + return stateChanges > 3; } diff --git a/ThingConnect.Pulse.Server/Models/EndpointDetailDto.cs b/ThingConnect.Pulse.Server/Models/EndpointDetailDto.cs index 369224d..23336b9 100644 --- a/ThingConnect.Pulse.Server/Models/EndpointDetailDto.cs +++ b/ThingConnect.Pulse.Server/Models/EndpointDetailDto.cs @@ -4,17 +4,17 @@ public sealed class EndpointDetailDto { public required EndpointDto Endpoint { get; set; } - public CurrentStateDto CurrentState { get; set; } = default!; - public List Recent { get; set; } = []; + public List Recent { get; set; } = []; public List Outages { get; set; } = []; } -public sealed class CheckResultStructuredDto +public sealed class RawCheckDto { public DateTimeOffset Ts { get; set; } public Classification Classification { get; set; } public ProbeResultDto Primary { get; set; } = default!; public FallbackResultDto Fallback { get; set; } = default!; + public EffectiveStateDto CurrentState { get; set; } = default!; } public sealed class ProbeResultDto diff --git a/ThingConnect.Pulse.Server/Models/HistoryDtos.cs b/ThingConnect.Pulse.Server/Models/HistoryDtos.cs index 170e255..364dc07 100644 --- a/ThingConnect.Pulse.Server/Models/HistoryDtos.cs +++ b/ThingConnect.Pulse.Server/Models/HistoryDtos.cs @@ -2,18 +2,6 @@ namespace ThingConnect.Pulse.Server.Models; -public sealed class RawCheckDto -{ - public DateTimeOffset Ts { get; set; } - public string Status { get; set; } = default!; - public double? RttMs { get; set; } - public string? Error { get; set; } - public bool? FallbackAttempted { get; set; } - public string? FallbackStatus { get; set; } - public double? FallbackRttMs { get; set; } - public string? FallbackError { get; set; } - public Classification? Classification { get; set; } -} public sealed class RollupBucketDto { diff --git a/ThingConnect.Pulse.Server/Models/StatusDtos.cs b/ThingConnect.Pulse.Server/Models/StatusDtos.cs index 7c04179..1129f95 100644 --- a/ThingConnect.Pulse.Server/Models/StatusDtos.cs +++ b/ThingConnect.Pulse.Server/Models/StatusDtos.cs @@ -6,7 +6,7 @@ public sealed class LiveStatusItemDto // 🔹 LEGACY: Keep for backward compatibility (remove later) public string Status { get; set; } = default!; public double? RttMs { get; set; } - public CurrentStateDto CurrentState { get; set; } = default!; + public EffectiveStateDto CurrentState { get; set; } = default!; public DateTimeOffset LastChangeTs { get; set; } public List Sparkline { get; set; } = new(); } @@ -54,7 +54,7 @@ public sealed class PagedLiveDto public List Items { get; set; } = new(); } -public sealed class CurrentStateDto +public sealed class EffectiveStateDto { public string EffectiveStatus { get; set; } = default!; // "up" or "down" public double? EffectiveRtt { get; set; } // Priority-based RTT diff --git a/ThingConnect.Pulse.Server/Services/EndpointService.cs b/ThingConnect.Pulse.Server/Services/EndpointService.cs index 7b502e3..704c9d6 100644 --- a/ThingConnect.Pulse.Server/Services/EndpointService.cs +++ b/ThingConnect.Pulse.Server/Services/EndpointService.cs @@ -38,16 +38,56 @@ public EndpointService(PulseDbContext context) .Take(RecentFetchLimit) .ToListAsync(); - var recent = rawChecks - .Select(c => new RawCheckDto + // Map to CheckResult objects for easier processing + var checks = rawChecks + .Select(c => new CheckResult { - Ts = ConvertToDateTimeOffset(c.Ts), - Status = c.Status.ToString().ToLower(), + EndpointId = c.EndpointId, + Timestamp = ConvertToDateTimeOffset(c.Ts), + Status = c.Status, RttMs = c.RttMs, - Error = c.Error + Error = c.Error, + FallbackAttempted = (bool)c.FallbackAttempted, + FallbackStatus = c.FallbackStatus, + FallbackRttMs = c.FallbackRttMs, + FallbackError = c.FallbackError, + Classification = c.Classification + }).ToList(); + + // --- Map RawCheckDto including EffectiveState --- + var recent = checks + .Where(c => c.Timestamp >= windowStart) + .OrderByDescending(c => c.Timestamp) + .Select(c => new RawCheckDto + { + Ts = c.Timestamp, + Classification = c.DetermineClassification(), + Primary = new ProbeResultDto + { + Type = endpoint.Type.ToString().ToLower(), + Target = endpoint.Host, + Status = c.Status.ToString().ToLower(), + RttMs = c.RttMs, + Error = c.Error + }, + Fallback = new FallbackResultDto + { + Attempted = c.FallbackAttempted, + Type = "icmp", + Target = endpoint.Host, + Status = c.FallbackStatus?.ToString().ToLower(), + RttMs = c.FallbackRttMs, + Error = c.FallbackError + }, + CurrentState = new EffectiveStateDto + { + EffectiveStatus = c.GetEffectiveStatus().ToString().ToLower(), + EffectiveRtt = c.GetEffectiveRtt(), + Classification = (int)c.DetermineClassification(), + HostReachable = c.FallbackAttempted && c.FallbackStatus == UpDown.up, + LastCheck = c.Timestamp + } }) - .Where(r => r.Ts >= windowStart) - .OrderByDescending(r => r.Ts) .ToList(); // --- Fetch outages within window --- @@ -73,7 +113,7 @@ public EndpointService(PulseDbContext context) .ToList(); // --- Map endpoint DTO --- - var endpointDto = MapToEndpointDto(endpoint); + var endpointDto = CheckResult.MapToEndpointDto(endpoint); return new EndpointDetailDto { @@ -83,31 +123,6 @@ public EndpointService(PulseDbContext context) }; } - private EndpointDto MapToEndpointDto(Data.Endpoint endpoint) - { - return new EndpointDto - { - Id = endpoint.Id, - Name = endpoint.Name, - Group = new GroupDto - { - Id = endpoint.Group.Id, - Name = endpoint.Group.Name, - ParentId = endpoint.Group.ParentId, - Color = endpoint.Group.Color - }, - Type = endpoint.Type.ToString().ToLower(), - Host = endpoint.Host, - Port = endpoint.Port, - HttpPath = endpoint.HttpPath, - HttpMatch = endpoint.HttpMatch, - IntervalSeconds = endpoint.IntervalSeconds, - TimeoutMs = endpoint.TimeoutMs, - Retries = endpoint.Retries, - Enabled = endpoint.Enabled - }; - } - // --- Helper to convert timestamp to DateTimeOffset --- private static DateTimeOffset ConvertToDateTimeOffset(T value) { @@ -137,4 +152,4 @@ private static DateTimeOffset ConvertToDateTimeOffset(T value) _ => int.TryParse(value.ToString(), out var v) ? v : null }; } -} \ No newline at end of file +} diff --git a/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs b/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs index 9df3739..dec82d2 100644 --- a/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs +++ b/ThingConnect.Pulse.Server/Services/Monitoring/ProbeService.cs @@ -63,9 +63,6 @@ public async Task ProbeAsync(Data.Endpoint endpoint, CancellationTo } } - _logger.LogDebug("Probe completed for {EndpointId}: Status={Status}, Classification={Classification}, EffectiveStatus={EffectiveStatus}", - endpoint.Id, probeResult.Status, probeResult.Classification, probeResult.GetEffectiveStatus()); - return probeResult; } diff --git a/ThingConnect.Pulse.Server/Services/StatusService.cs b/ThingConnect.Pulse.Server/Services/StatusService.cs index 1139d2a..f8ab3e6 100644 --- a/ThingConnect.Pulse.Server/Services/StatusService.cs +++ b/ThingConnect.Pulse.Server/Services/StatusService.cs @@ -55,35 +55,37 @@ public async Task> GetLiveStatusAsync(string? group, str // Apply pagination List endpoints = await query - .OrderBy(e => e.GroupId) - .ThenBy(e => e.Name) - .ToListAsync(); - - // Get live status for each endpoint + .OrderBy(e => e.GroupId) + .ThenBy(e => e.Name) + .ToListAsync(); var items = new List(); var endpointIds = endpoints.Select(e => e.Id).ToList(); - // Get latest checks for all endpoints - optimized query using window functions in SQLite - var latestChecks = await _context.CheckResultsRaw - .Where(c => endpointIds.Contains(c.EndpointId)) + // Fetch recent checks for all endpoints (last 5 minutes) + long cutoffTime = UnixTimestamp.Subtract(UnixTimestamp.Now(), TimeSpan.FromMinutes(5)); + var recentChecks = await _context.CheckResultsRaw + .Where(c => endpointIds.Contains(c.EndpointId) && c.Ts >= cutoffTime) .AsNoTracking() - .GroupBy(c => c.EndpointId) - .Select(g => new + .Select(c => new CheckResult { - EndpointId = g.Key, - LatestCheck = g.OrderByDescending(c => c.Ts).FirstOrDefault() + EndpointId = c.EndpointId, + Timestamp = UnixTimestamp.FromUnixSeconds(c.Ts), + Status = c.Status }) .ToListAsync(); - var latestCheckDict = latestChecks.ToDictionary(x => x.EndpointId, x => x.LatestCheck); + var checksGrouped = recentChecks.GroupBy(c => c.EndpointId).ToDictionary(g => g.Key, g => g.ToList()); - // Get sparkline data (last 20 checks per endpoint for mini chart) Dictionary> sparklineData = await GetSparklineDataAsync(endpointIds); foreach (Data.Endpoint? endpoint in endpoints) { - StatusType status = DetermineStatus(endpoint, latestCheckDict); - List sparkline = sparklineData.ContainsKey(endpoint.Id) + var recent = checksGrouped.ContainsKey(endpoint.Id) ? checksGrouped[endpoint.Id] : new List(); + StatusType status = recent.Any() + ? recent.Last().DetermineStatusType(recent, TimeSpan.FromSeconds(endpoint.IntervalSeconds * 2)) + : StatusType.Down; + + List sparkline = sparklineData.ContainsKey(endpoint.Id) ? sparklineData[endpoint.Id] : new List(); @@ -93,7 +95,7 @@ public async Task> GetLiveStatusAsync(string? group, str items.Add(new LiveStatusItemDto { - Endpoint = MapToEndpointDto(endpoint), + Endpoint = CheckResult.MapToEndpointDto(endpoint), Status = status.ToString().ToLower(), RttMs = endpoint.LastRttMs, LastChangeTs = endpoint.LastChangeTs.HasValue ? UnixTimestamp.FromUnixSeconds(endpoint.LastChangeTs.Value) : DateTimeOffset.Now, @@ -152,120 +154,39 @@ private async Task>> GetSparklineDataAsync var recentChecks = await _context.CheckResultsRaw .Where(c => endpointIds.Contains(c.EndpointId) && c.Ts >= cutoffTime) .AsNoTracking() - .Select(c => new { c.EndpointId, c.Ts, c.Status }) + .Select(c => new CheckResult + { + EndpointId = c.EndpointId, + Timestamp = UnixTimestamp.FromUnixSeconds(c.Ts), + Status = c.Status, + FallbackAttempted = c.FallbackStatus.HasValue, + FallbackStatus = c.FallbackStatus, + }) .ToListAsync(); - recentChecks = recentChecks - .OrderBy(c => c.EndpointId) - .ThenByDescending(c => c.Ts) - .ToList(); - - var groupedChecks = recentChecks.GroupBy(c => c.EndpointId); - - foreach (var group in groupedChecks) + var groupedChecks = recentChecks + .GroupBy(c => c.EndpointId) + .ToDictionary(g => g.Key, g => g + .OrderByDescending(c => c.Timestamp) + .Take(20) + .OrderBy(c => c.Timestamp) // chronological order for sparkline + .ToList() + ); + + foreach (var kvp in groupedChecks) { - var points = group - .Take(20) // Maximum 20 points for sparkline - .OrderBy(c => c.Ts) // Order chronologically for display + var points = kvp.Value .Select(c => new SparklinePoint { - Ts = UnixTimestamp.FromUnixSeconds(c.Ts), - S = c.Status == UpDown.up ? "u" : "d" + Ts = c.Timestamp, + S = c.GetEffectiveStatus() == UpDown.up ? "u" : "d" // 🔹 use effective status }) .ToList(); - sparklineData[group.Key] = points; + sparklineData[kvp.Key] = points; } return sparklineData; } - private StatusType DetermineStatus(Data.Endpoint endpoint, Dictionary latestChecks) - { - // Check if we have recent check data - if (!latestChecks.TryGetValue(endpoint.Id, out CheckResultRaw? latestCheck) || latestCheck == null) - { - return StatusType.Down; // No data means down - } - - // Check if the latest check is recent enough (within 2x interval) - var expectedInterval = TimeSpan.FromSeconds(endpoint.IntervalSeconds * 2); - if (UnixTimestamp.Now() - latestCheck.Ts > (long)expectedInterval.TotalSeconds) - { - return StatusType.Down; // Stale data means down - } - - // Check for flapping (multiple state changes in short period) - // This is simplified - in production you'd want more sophisticated flap detection - if (IsFlapping(endpoint.Id).Result) - { - return StatusType.Flapping; - } - - return latestCheck.Status == UpDown.up ? StatusType.Up : StatusType.Down; - } - - private async Task IsFlapping(Guid endpointId) - { - // Simple flap detection: check if there were > 3 state changes in last 5 minutes - long cutoffTime = UnixTimestamp.Subtract(UnixTimestamp.Now(), TimeSpan.FromMinutes(5)); - var checks = await _context.CheckResultsRaw - .Where(c => c.EndpointId == endpointId && c.Ts >= cutoffTime) - .AsNoTracking() - .Select(c => new { c.Ts, c.Status }) - .ToListAsync(); - - var recentChecks = checks - .OrderBy(c => c.Ts) - .Select(c => c.Status) - .ToList(); - - if (recentChecks.Count < 4) - { - return false; - } - - int stateChanges = 0; - for (int i = 1; i < recentChecks.Count; i++) - { - if (recentChecks[i] != recentChecks[i - 1]) - { - stateChanges++; - } - } - - return stateChanges > 3; - } - - private EndpointDto MapToEndpointDto(Data.Endpoint endpoint) - { - return new EndpointDto - { - Id = endpoint.Id, - Name = endpoint.Name, - Group = new GroupDto - { - Id = endpoint.Group.Id, - Name = endpoint.Group.Name, - ParentId = endpoint.Group.ParentId, - Color = endpoint.Group.Color - }, - Type = endpoint.Type.ToString().ToLower(), - Host = endpoint.Host, - Port = endpoint.Port, - HttpPath = endpoint.HttpPath, - HttpMatch = endpoint.HttpMatch, - IntervalSeconds = endpoint.IntervalSeconds, - TimeoutMs = endpoint.TimeoutMs, - Retries = endpoint.Retries, - Enabled = endpoint.Enabled - }; - } - - private enum StatusType - { - Up, - Down, - Flapping - } -} \ No newline at end of file +} From 0b261d8b78171c585f0ae5d0b21216bad9ad92af Mon Sep 17 00:00:00 2001 From: abishek Date: Fri, 3 Oct 2025 10:50:11 +0530 Subject: [PATCH 22/28] history service udpated --- .../Models/CheckResult.cs | 9 +- .../Services/HistoryService.cs | 90 +++++++++++-------- 2 files changed, 59 insertions(+), 40 deletions(-) diff --git a/ThingConnect.Pulse.Server/Models/CheckResult.cs b/ThingConnect.Pulse.Server/Models/CheckResult.cs index 3614217..1c81674 100644 --- a/ThingConnect.Pulse.Server/Models/CheckResult.cs +++ b/ThingConnect.Pulse.Server/Models/CheckResult.cs @@ -122,14 +122,17 @@ public Classification DetermineClassification() { return Data.Classification.None; // Healthy } + if (FallbackAttempted) { if (FallbackStatus == UpDown.up) { return Data.Classification.Service; // Service down, host up } + return Data.Classification.Network; // Both down } + return Data.Classification.Unknown; // No fallback info } @@ -154,6 +157,7 @@ public StatusType DetermineStatusType(List recentChecks, TimeSpan i { return StatusType.Service; } + return StatusType.Up; } @@ -163,7 +167,10 @@ public StatusType DetermineStatusType(List recentChecks, TimeSpan i // 🔹 Flapping detection (>= 4 samples, >3 changes in 5 min window) public static bool IsFlapping(List recent) { - if (recent == null || recent.Count < 4) return false; + if (recent == null || recent.Count < 4) + { + return false; + } var effectiveStatuses = recent .OrderBy(c => c.Timestamp) diff --git a/ThingConnect.Pulse.Server/Services/HistoryService.cs b/ThingConnect.Pulse.Server/Services/HistoryService.cs index 6c1c224..c181f77 100644 --- a/ThingConnect.Pulse.Server/Services/HistoryService.cs +++ b/ThingConnect.Pulse.Server/Services/HistoryService.cs @@ -51,14 +51,14 @@ public HistoryService(PulseDbContext context, ILogger logger) var response = new HistoryResponseDto { - Endpoint = MapToEndpointDto(endpoint) + Endpoint = CheckResult.MapToEndpointDto(endpoint) }; // Fetch data based on bucket type switch (bucket.ToLower()) { case "raw": - response.Raw = await GetRawDataAsync(endpointId, from, to); + response.Raw = await GetRawDataAsync(endpoint, from, to); break; case "15m": @@ -73,32 +73,69 @@ public HistoryService(PulseDbContext context, ILogger logger) throw new ArgumentException($"Invalid bucket type: {bucket}. Valid values: raw, 15m, daily"); } - // Always include outages for the time range + // Always include outages response.Outages = await GetOutagesAsync(endpointId, from, to); return response; } - private async Task> GetRawDataAsync(Guid endpointId, DateTimeOffset from, DateTimeOffset to) + private async Task> GetRawDataAsync(Data.Endpoint endpoint, DateTimeOffset from, DateTimeOffset to) { long fromUnix = UnixTimestamp.ToUnixSeconds(from); long toUnix = UnixTimestamp.ToUnixSeconds(to); - // SQLite limitation: fetch all data and filter in memory var rawData = await _context.CheckResultsRaw - .Where(c => c.EndpointId == endpointId) - .Select(c => new { c.Ts, c.Status, c.RttMs, c.Error }) + .Where(c => c.EndpointId == endpoint.Id && c.Ts >= fromUnix && c.Ts <= toUnix) + .OrderBy(c => c.Ts) .ToListAsync(); - return rawData - .Where(c => c.Ts >= fromUnix && c.Ts <= toUnix) - .OrderBy(c => c.Ts) - .Select(c => new RawCheckDto + // Convert DB rows -> CheckResult -> RawCheckDto + var checks = rawData + .Select(c => new CheckResult { - Ts = UnixTimestamp.FromUnixSeconds(c.Ts), - Status = c.Status == UpDown.up ? "up" : "down", + EndpointId = c.EndpointId, + Timestamp = UnixTimestamp.FromUnixSeconds(c.Ts), + Status = c.Status, RttMs = c.RttMs, - Error = c.Error + Error = c.Error, + FallbackAttempted = (bool)c.FallbackAttempted, + FallbackStatus = c.FallbackStatus, + FallbackRttMs = c.FallbackRttMs, + FallbackError = c.FallbackError, + Classification = c.Classification + }) + .ToList(); + + return checks + .Select(c => new RawCheckDto + { + Ts = c.Timestamp, + Classification = c.DetermineClassification(), + Primary = new ProbeResultDto + { + Type = endpoint.Type.ToString().ToLower(), + Target = endpoint.Host, + Status = c.Status.ToString().ToLower(), + RttMs = c.RttMs, + Error = c.Error + }, + Fallback = new FallbackResultDto + { + Attempted = c.FallbackAttempted, + Type = "icmp", + Target = endpoint.Host, + Status = c.FallbackStatus?.ToString().ToLower(), + RttMs = c.FallbackRttMs, + Error = c.FallbackError + }, + CurrentState = new EffectiveStateDto + { + EffectiveStatus = c.GetEffectiveStatus().ToString().ToLower(), + EffectiveRtt = c.GetEffectiveRtt(), + Classification = (int)c.DetermineClassification(), + HostReachable = c.FallbackAttempted && c.FallbackStatus == UpDown.up, + LastCheck = c.Timestamp + } }) .ToList(); } @@ -175,29 +212,4 @@ private async Task> GetOutagesAsync(Guid endpointId, DateTimeOff }) .ToList(); } - - private EndpointDto MapToEndpointDto(Data.Endpoint endpoint) - { - return new EndpointDto - { - Id = endpoint.Id, - Name = endpoint.Name, - Group = new GroupDto - { - Id = endpoint.Group.Id, - Name = endpoint.Group.Name, - ParentId = endpoint.Group.ParentId, - Color = endpoint.Group.Color - }, - Type = endpoint.Type.ToString().ToLower(), - Host = endpoint.Host, - Port = endpoint.Port, - HttpPath = endpoint.HttpPath, - HttpMatch = endpoint.HttpMatch, - IntervalSeconds = endpoint.IntervalSeconds, - TimeoutMs = endpoint.TimeoutMs, - Retries = endpoint.Retries, - Enabled = endpoint.Enabled - }; - } } From 10f52a5837e03531171d990cb152c5c77c67ddb6 Mon Sep 17 00:00:00 2001 From: abishek Date: Fri, 3 Oct 2025 11:50:06 +0530 Subject: [PATCH 23/28] updated the frontend intefaces according to the backend --- ThingConnect.Pulse.Server/Data/Entities.cs | 2 +- .../Models/EndpointDetailDto.cs | 6 +-- .../Models/StatusDtos.cs | 15 +++----- .../Services/EndpointService.cs | 14 ++++--- .../Services/HistoryService.cs | 14 ++++--- .../Services/StatusService.cs | 20 +++++++--- thingconnect.pulse.client/src/api/types.ts | 37 +++++++++++++++---- 7 files changed, 69 insertions(+), 39 deletions(-) diff --git a/ThingConnect.Pulse.Server/Data/Entities.cs b/ThingConnect.Pulse.Server/Data/Entities.cs index 420faac..4e588a2 100644 --- a/ThingConnect.Pulse.Server/Data/Entities.cs +++ b/ThingConnect.Pulse.Server/Data/Entities.cs @@ -6,7 +6,7 @@ public enum ProbeType { icmp, tcp, http } public enum UpDown { up, down } /// -/// Outage classification for failed probe analysis. +/// Status classification for failed probe analysis. /// public enum Classification { diff --git a/ThingConnect.Pulse.Server/Models/EndpointDetailDto.cs b/ThingConnect.Pulse.Server/Models/EndpointDetailDto.cs index 23336b9..300cd56 100644 --- a/ThingConnect.Pulse.Server/Models/EndpointDetailDto.cs +++ b/ThingConnect.Pulse.Server/Models/EndpointDetailDto.cs @@ -12,12 +12,12 @@ public sealed class RawCheckDto { public DateTimeOffset Ts { get; set; } public Classification Classification { get; set; } - public ProbeResultDto Primary { get; set; } = default!; + public PrimaryResultDto Primary { get; set; } = default!; public FallbackResultDto Fallback { get; set; } = default!; - public EffectiveStateDto CurrentState { get; set; } = default!; + public CurrentStateDto CurrentState { get; set; } = default!; } -public sealed class ProbeResultDto +public sealed class PrimaryResultDto { public string Type { get; set; } = default!; public string Target { get; set; } = default!; diff --git a/ThingConnect.Pulse.Server/Models/StatusDtos.cs b/ThingConnect.Pulse.Server/Models/StatusDtos.cs index 1129f95..72fce72 100644 --- a/ThingConnect.Pulse.Server/Models/StatusDtos.cs +++ b/ThingConnect.Pulse.Server/Models/StatusDtos.cs @@ -3,10 +3,7 @@ namespace ThingConnect.Pulse.Server.Models; public sealed class LiveStatusItemDto { public EndpointDto Endpoint { get; set; } = default!; - // 🔹 LEGACY: Keep for backward compatibility (remove later) - public string Status { get; set; } = default!; - public double? RttMs { get; set; } - public EffectiveStateDto CurrentState { get; set; } = default!; + public CurrentStateDto CurrentState { get; set; } = default!; public DateTimeOffset LastChangeTs { get; set; } public List Sparkline { get; set; } = new(); } @@ -54,11 +51,11 @@ public sealed class PagedLiveDto public List Items { get; set; } = new(); } -public sealed class EffectiveStateDto +public sealed class CurrentStateDto { - public string EffectiveStatus { get; set; } = default!; // "up" or "down" - public double? EffectiveRtt { get; set; } // Priority-based RTT + public string Type { get; set; } = default!; + public string Target { get; set; } = default!; + public string Status { get; set; } = default!; // "up" or "down" + public double? RttMs { get; set; } // Priority-based RTT public int Classification { get; set; } // Classification enum value - public bool HostReachable { get; set; } // Quick connectivity check - public DateTimeOffset LastCheck { get; set; } } diff --git a/ThingConnect.Pulse.Server/Services/EndpointService.cs b/ThingConnect.Pulse.Server/Services/EndpointService.cs index 704c9d6..1f07798 100644 --- a/ThingConnect.Pulse.Server/Services/EndpointService.cs +++ b/ThingConnect.Pulse.Server/Services/EndpointService.cs @@ -62,7 +62,7 @@ public EndpointService(PulseDbContext context) { Ts = c.Timestamp, Classification = c.DetermineClassification(), - Primary = new ProbeResultDto + Primary = new PrimaryResultDto { Type = endpoint.Type.ToString().ToLower(), Target = endpoint.Host, @@ -79,13 +79,15 @@ public EndpointService(PulseDbContext context) RttMs = c.FallbackRttMs, Error = c.FallbackError }, - CurrentState = new EffectiveStateDto + CurrentState = new CurrentStateDto { - EffectiveStatus = c.GetEffectiveStatus().ToString().ToLower(), - EffectiveRtt = c.GetEffectiveRtt(), + Type = c.FallbackAttempted && c.FallbackStatus != null + ? "icmp" + : endpoint.Type.ToString().ToLower(), + Target = endpoint.Host, + Status = c.GetEffectiveStatus().ToString().ToLower(), + RttMs = c.GetEffectiveRtt(), Classification = (int)c.DetermineClassification(), - HostReachable = c.FallbackAttempted && c.FallbackStatus == UpDown.up, - LastCheck = c.Timestamp } }) .ToList(); diff --git a/ThingConnect.Pulse.Server/Services/HistoryService.cs b/ThingConnect.Pulse.Server/Services/HistoryService.cs index c181f77..73490cb 100644 --- a/ThingConnect.Pulse.Server/Services/HistoryService.cs +++ b/ThingConnect.Pulse.Server/Services/HistoryService.cs @@ -111,7 +111,7 @@ private async Task> GetRawDataAsync(Data.Endpoint endpoint, Da { Ts = c.Timestamp, Classification = c.DetermineClassification(), - Primary = new ProbeResultDto + Primary = new PrimaryResultDto { Type = endpoint.Type.ToString().ToLower(), Target = endpoint.Host, @@ -128,13 +128,15 @@ private async Task> GetRawDataAsync(Data.Endpoint endpoint, Da RttMs = c.FallbackRttMs, Error = c.FallbackError }, - CurrentState = new EffectiveStateDto + CurrentState = new CurrentStateDto { - EffectiveStatus = c.GetEffectiveStatus().ToString().ToLower(), - EffectiveRtt = c.GetEffectiveRtt(), + Type = c.FallbackAttempted && c.FallbackStatus != null + ? "icmp" + : endpoint.Type.ToString().ToLower(), + Target = endpoint.Host, + Status = c.GetEffectiveStatus().ToString().ToLower(), + RttMs = c.GetEffectiveRtt(), Classification = (int)c.DetermineClassification(), - HostReachable = c.FallbackAttempted && c.FallbackStatus == UpDown.up, - LastCheck = c.Timestamp } }) .ToList(); diff --git a/ThingConnect.Pulse.Server/Services/StatusService.cs b/ThingConnect.Pulse.Server/Services/StatusService.cs index f8ab3e6..1212dfc 100644 --- a/ThingConnect.Pulse.Server/Services/StatusService.cs +++ b/ThingConnect.Pulse.Server/Services/StatusService.cs @@ -85,9 +85,9 @@ public async Task> GetLiveStatusAsync(string? group, str ? recent.Last().DetermineStatusType(recent, TimeSpan.FromSeconds(endpoint.IntervalSeconds * 2)) : StatusType.Down; - List sparkline = sparklineData.ContainsKey(endpoint.Id) - ? sparklineData[endpoint.Id] - : new List(); + List sparkline = sparklineData.ContainsKey(endpoint.Id) + ? sparklineData[endpoint.Id] + : new List(); _logger.LogInformation( "Endpoint {EndpointName}: Status = {Status}, LastRttMs = {RttMs}, LastChangeTs = {LastChangeTs}", @@ -96,9 +96,17 @@ public async Task> GetLiveStatusAsync(string? group, str items.Add(new LiveStatusItemDto { Endpoint = CheckResult.MapToEndpointDto(endpoint), - Status = status.ToString().ToLower(), - RttMs = endpoint.LastRttMs, - LastChangeTs = endpoint.LastChangeTs.HasValue ? UnixTimestamp.FromUnixSeconds(endpoint.LastChangeTs.Value) : DateTimeOffset.Now, + CurrentState = new CurrentStateDto + { + Type = recent.Any() && recent.Last().FallbackAttempted ? "icmp" : endpoint.Type.ToString().ToLower(), + Target = endpoint.Host, + Status = recent.Any() ? recent.Last().GetEffectiveStatus().ToString().ToLower() : "down", + RttMs = recent.Any() ? recent.Last().GetEffectiveRtt() : null, + Classification = recent.Any() ? (int)recent.Last().DetermineClassification() : 0 + }, + LastChangeTs = endpoint.LastChangeTs.HasValue + ? UnixTimestamp.FromUnixSeconds(endpoint.LastChangeTs.Value) + : DateTimeOffset.Now, Sparkline = sparkline }); } diff --git a/thingconnect.pulse.client/src/api/types.ts b/thingconnect.pulse.client/src/api/types.ts index 3c6385c..fd49095 100644 --- a/thingconnect.pulse.client/src/api/types.ts +++ b/thingconnect.pulse.client/src/api/types.ts @@ -30,8 +30,7 @@ export interface SparklinePoint { export interface LiveStatusItem { endpoint: Endpoint; - status: 'up' | 'down' | 'flapping'; - rttMs?: number | null; + currentState: CurrentState; lastChangeTs: string; sparkline: SparklinePoint[]; } @@ -93,16 +92,38 @@ export type Classification = | 6 // DnsResolution | 7 // Congestion | 8; // Maintenance -export interface RawCheck { - ts: string; + +export interface PrimaryResult { + type: string; // "icmp" | "tcp" | "http" + target: string; // hostname or IP status: 'up' | 'down'; rttMs?: number | null; error?: string | null; - fallbackAttempted?: boolean; - fallbackSuccess?: boolean; - fallbackRttMs?: number | null; +} + +export interface FallbackResult { + attempted: boolean; + type?: 'icmp'| null; + target?: string | null; + status?: 'up' | 'down' | null; + rttMs?: number | null; + error?: string | null; +} + +export interface CurrentState { + type: 'icmp' | 'tcp' | 'http'; + target: string; + status: 'up' | 'down' | 'flapping' | 'serivce'; + rttMs?: number | null; classification?: Classification | null; - lastSeenViaIcmp?: string | null; // ISO timestamp when last reachable via ICMP +} + +export interface RawCheck { + ts: string; + classification: Classification; + primary: PrimaryResult; + fallback: FallbackResult; + currentState: CurrentState; } export interface Outage { From ef56238a9b903118c5f07c80f9eb1753dde796eb Mon Sep 17 00:00:00 2001 From: abishek Date: Fri, 3 Oct 2025 13:00:29 +0530 Subject: [PATCH 24/28] updated the ui for dashboard based on the api schema for status service. --- thingconnect.pulse.client/src/api/types.ts | 2 +- .../src/components/status/StatusAccordion.tsx | 39 +++++++-------- .../src/components/status/StatusCard.tsx | 12 +++-- .../status/StatusGroupAccordion.tsx | 37 +++++++------- .../src/components/status/StatusTable.tsx | 48 ++++++++++++++----- .../components/status/SystemOverviewStats.tsx | 14 +++++- .../src/pages/Dashboard.tsx | 37 +++++++++----- .../src/pages/EndpointDetail.tsx | 4 +- 8 files changed, 126 insertions(+), 67 deletions(-) diff --git a/thingconnect.pulse.client/src/api/types.ts b/thingconnect.pulse.client/src/api/types.ts index fd49095..4309b3e 100644 --- a/thingconnect.pulse.client/src/api/types.ts +++ b/thingconnect.pulse.client/src/api/types.ts @@ -113,7 +113,7 @@ export interface FallbackResult { export interface CurrentState { type: 'icmp' | 'tcp' | 'http'; target: string; - status: 'up' | 'down' | 'flapping' | 'serivce'; + status: 'up' | 'down' | 'flapping' | 'service'; rttMs?: number | null; classification?: Classification | null; } diff --git a/thingconnect.pulse.client/src/components/status/StatusAccordion.tsx b/thingconnect.pulse.client/src/components/status/StatusAccordion.tsx index a2036d1..3cdb9c5 100644 --- a/thingconnect.pulse.client/src/components/status/StatusAccordion.tsx +++ b/thingconnect.pulse.client/src/components/status/StatusAccordion.tsx @@ -3,11 +3,18 @@ import type { LiveStatusItem } from '@/api/types'; import { StatusTable } from './StatusTable'; type Props = { - groupedEndpoints: Record<'up' | 'down' | 'flapping', LiveStatusItem[]>; + groupedEndpoints: Record<'up' | 'down' | 'flapping' | 'service', LiveStatusItem[]>; isLoading: boolean; }; export function StatusAccordion({ groupedEndpoints, isLoading }: Props) { + const statusColorMap: Record<'up' | 'down' | 'flapping' | 'service', string> = { + up: 'green', + down: 'red', + flapping: 'yellow', + service: 'orange', + } as const; + return ( {Object.entries(groupedEndpoints).map(([status, items]) => { @@ -16,21 +23,15 @@ export function StatusAccordion({ groupedEndpoints, isLoading }: Props) { ? typedItems : Object.values(typedItems).flat(); - const statusColorMap: Record<'up' | 'down' | 'flapping', string> = { - up: 'green', - down: 'red', - flapping: 'yellow', - } as const; - return ( @@ -38,11 +39,11 @@ export function StatusAccordion({ groupedEndpoints, isLoading }: Props) { {status} @@ -62,11 +63,11 @@ export function StatusAccordion({ groupedEndpoints, isLoading }: Props) { {itemsArray?.length - ? itemsArray?.length > 1 - ? `${itemsArray?.length} Endpoints` + ? itemsArray.length > 1 + ? `${itemsArray.length} Endpoints` : '1 Endpoint' : 'No Endpoints'} diff --git a/thingconnect.pulse.client/src/components/status/StatusCard.tsx b/thingconnect.pulse.client/src/components/status/StatusCard.tsx index 210df71..53eb350 100644 --- a/thingconnect.pulse.client/src/components/status/StatusCard.tsx +++ b/thingconnect.pulse.client/src/components/status/StatusCard.tsx @@ -19,6 +19,8 @@ export function StatusCard({ item }: StatusCardProps) { return 'red'; case 'flapping': return 'yellow'; + case 'service': + return 'yellow'; default: return 'gray'; } @@ -77,15 +79,15 @@ export function StatusCard({ item }: StatusCardProps) { - {item.status} + {item.currentState.status} @@ -105,10 +107,10 @@ export function StatusCard({ item }: StatusCardProps) { - {formatRTT(item.rttMs)} + {formatRTT(item.currentState.rttMs)} diff --git a/thingconnect.pulse.client/src/components/status/StatusGroupAccordion.tsx b/thingconnect.pulse.client/src/components/status/StatusGroupAccordion.tsx index dac70d2..ab220f3 100644 --- a/thingconnect.pulse.client/src/components/status/StatusGroupAccordion.tsx +++ b/thingconnect.pulse.client/src/components/status/StatusGroupAccordion.tsx @@ -3,11 +3,21 @@ import type { LiveStatusItem } from '@/api/types'; import { StatusTable } from './StatusTable'; type Props = { - groupedEndpoints: Record<'up' | 'down' | 'flapping', Record>; + groupedEndpoints: Record< + 'up' | 'down' | 'flapping' | 'service', + Record + >; isLoading: boolean; }; export function StatusGroupAccordion({ groupedEndpoints, isLoading }: Props) { + const statusColorMap: Record<'up' | 'down' | 'flapping' | 'service', string> = { + up: 'green', + down: 'red', + flapping: 'yellow', + service: 'orange', + } as const; + return ( {Object.entries(groupedEndpoints).map(([status, groupItems]) => { @@ -15,11 +25,6 @@ export function StatusGroupAccordion({ groupedEndpoints, isLoading }: Props) { (sum, group) => sum + (group?.length || 0), 0 ); - const statusColorMap: Record<'up' | 'down' | 'flapping', string> = { - up: 'green', - down: 'red', - flapping: 'yellow', - } as const; // Narrow the type for TypeScript const typedGroupItems = groupItems || {}; @@ -27,12 +32,12 @@ export function StatusGroupAccordion({ groupedEndpoints, isLoading }: Props) { return ( @@ -40,15 +45,15 @@ export function StatusGroupAccordion({ groupedEndpoints, isLoading }: Props) { {totalEndpoints ? `${totalEndpoints} Endpoints` : 'No Endpoints'} diff --git a/thingconnect.pulse.client/src/components/status/StatusTable.tsx b/thingconnect.pulse.client/src/components/status/StatusTable.tsx index a1e9303..777977e 100644 --- a/thingconnect.pulse.client/src/components/status/StatusTable.tsx +++ b/thingconnect.pulse.client/src/components/status/StatusTable.tsx @@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom'; import { useAnalytics } from '@/hooks/useAnalytics'; import type { LiveStatusItem } from '@/api/types'; import TrendBlocks from './TrendBlocks'; +import { Tooltip } from '../ui/tooltip'; interface StatusTableProps { items: LiveStatusItem[] | null | undefined; @@ -22,6 +23,8 @@ export function StatusTable({ items, isLoading }: StatusTableProps) { case 'down': return 'red'; case 'flapping': + return 'orange'; + case 'service': return 'yellow'; default: return 'gray'; @@ -116,16 +119,35 @@ export function StatusTable({ items, isLoading }: StatusTableProps) { onClick={() => handleRowClick(item.endpoint.id)} > - - {item.status.toUpperCase()} - + {item.currentState.status === 'service' ? ( + + + {item.currentState.status.toUpperCase()} + + + ) : ( + + {item.currentState.status.toUpperCase()} + + )} {item.endpoint.name} @@ -145,10 +167,10 @@ export function StatusTable({ items, isLoading }: StatusTableProps) { - {formatRTT(item.rttMs)} + {formatRTT(item.currentState.rttMs)} diff --git a/thingconnect.pulse.client/src/components/status/SystemOverviewStats.tsx b/thingconnect.pulse.client/src/components/status/SystemOverviewStats.tsx index 4f67781..530f389 100644 --- a/thingconnect.pulse.client/src/components/status/SystemOverviewStats.tsx +++ b/thingconnect.pulse.client/src/components/status/SystemOverviewStats.tsx @@ -19,6 +19,7 @@ type SystemOverviewStatsProps = { up: number; down: number; flapping: number; + service: number; }; }; @@ -68,10 +69,21 @@ export function SystemOverviewStats({ statusCounts }: SystemOverviewStatsProps) darkColor: 'yellow.200', darkBg: 'yellow.800', }, + { + icon: Activity, + title: 'SERVICE', + subtitle: 'TCP/HTTP down, ICMP reachable', + value: statusCounts.service, + textColor: 'purple.500', + color: 'purple.600', + bg: 'purple.100', + darkColor: 'purple.200', + darkBg: 'purple.800', + }, ]; return ( - + {stats.map(stat => ( diff --git a/thingconnect.pulse.client/src/pages/Dashboard.tsx b/thingconnect.pulse.client/src/pages/Dashboard.tsx index c1d3950..dcb16e3 100644 --- a/thingconnect.pulse.client/src/pages/Dashboard.tsx +++ b/thingconnect.pulse.client/src/pages/Dashboard.tsx @@ -53,10 +53,10 @@ export default function Dashboard() { const statusCounts = data.items.reduce( (acc, item) => { acc.total++; - acc[item.status]++; + acc[item.currentState.status]++; return acc; }, - { total: 0, up: 0, down: 0, flapping: 0 } + { total: 0, up: 0, down: 0, flapping: 0, service: 0 } ); analytics.trackSystemMetrics({ @@ -99,17 +99,26 @@ export default function Dashboard() { if (isGroupByStatus && isGroupByGroup) { // Status → Group → Endpoints - const statusBuckets: Record<'up' | 'down' | 'flapping', Record> = { + const statusBuckets: Record< + 'up' | 'down' | 'flapping' | 'service', + Record + > = { up: {}, down: {}, flapping: {}, + service: {}, }; // Get unique groups from all endpoints const uniqueGroups = new Set(filteredItems.map(item => item.endpoint.group.name)); // Prepare status buckets with all groups, even if empty - const defaultStatuses: Array<'up' | 'down' | 'flapping'> = ['up', 'down', 'flapping']; + const defaultStatuses: Array<'up' | 'down' | 'flapping' | 'service'> = [ + 'up', + 'down', + 'flapping', + 'service', + ]; defaultStatuses.forEach(status => { statusBuckets[status] = {}; uniqueGroups.forEach(group => { @@ -119,7 +128,7 @@ export default function Dashboard() { // Populate the status buckets filteredItems.forEach(item => { - const status = item.status; + const status = item.currentState.status; const group = item.endpoint.group.name; statusBuckets[status][group].push(item); @@ -145,18 +154,24 @@ export default function Dashboard() { finalResult = groupBuckets; } else if (isGroupByStatus) { // Status → Endpoints - const statusBuckets: Record<'up' | 'down' | 'flapping', LiveStatusItem[]> = { + const statusBuckets: Record<'up' | 'down' | 'flapping' | 'service', LiveStatusItem[]> = { up: [], down: [], flapping: [], + service: [], }; filteredItems.forEach(item => { - statusBuckets[item.status].push(item); + statusBuckets[item.currentState.status].push(item); }); // Always include all statuses, even if empty - const defaultStatuses: Array<'up' | 'down' | 'flapping'> = ['up', 'down', 'flapping']; + const defaultStatuses: Array<'up' | 'down' | 'flapping' | 'service'> = [ + 'up', + 'down', + 'flapping', + 'service', + ]; defaultStatuses.forEach(status => { finalResult[status] = statusBuckets[status]; }); @@ -183,15 +198,15 @@ export default function Dashboard() { // Count status totals const statusCounts = useMemo(() => { - if (!data?.items) return { total: 0, up: 0, down: 0, flapping: 0 }; + if (!data?.items) return { total: 0, up: 0, down: 0, flapping: 0, service: 0 }; const counts = data.items.reduce( (acc, item) => { acc.total++; - acc[item.status]++; + acc[item.currentState.status]++; return acc; }, - { total: 0, up: 0, down: 0, flapping: 0 } + { total: 0, up: 0, down: 0, flapping: 0, service: 0 } ); return counts; diff --git a/thingconnect.pulse.client/src/pages/EndpointDetail.tsx b/thingconnect.pulse.client/src/pages/EndpointDetail.tsx index ab76fbf..6bfbc9f 100644 --- a/thingconnect.pulse.client/src/pages/EndpointDetail.tsx +++ b/thingconnect.pulse.client/src/pages/EndpointDetail.tsx @@ -46,6 +46,8 @@ function getStatusColor(status: string) { case 'down': return 'red'; case 'flapping': + return 'yellow'; + case 'service': return 'orange'; default: return 'gray'; @@ -169,7 +171,7 @@ export default function EndpointDetail() { const { endpoint, recent, outages } = endpointDetail; // Calculate uptime percentage from recent checks - const upChecks = recent.filter(check => check.status === 'up').length; + const upChecks = recent.filter(check => check..status === 'up').length; const uptimePercentage = recent.length > 0 ? Math.round((upChecks / recent.length) * 100) : 0; // Calculate average RTT From c59590e70286bfa2dbd9dd2b8d9bd333365ba9a5 Mon Sep 17 00:00:00 2001 From: abishek Date: Fri, 3 Oct 2025 15:20:38 +0530 Subject: [PATCH 25/28] udpated the enpoints details page. --- .../Services/EndpointService.cs | 6 +- .../Services/HistoryService.cs | 6 +- .../src/components/RecentChecksTable.tsx | 76 +++++++++++++++---- .../src/pages/EndpointDetail.tsx | 13 +++- 4 files changed, 76 insertions(+), 25 deletions(-) diff --git a/ThingConnect.Pulse.Server/Services/EndpointService.cs b/ThingConnect.Pulse.Server/Services/EndpointService.cs index 1f07798..ab08b46 100644 --- a/ThingConnect.Pulse.Server/Services/EndpointService.cs +++ b/ThingConnect.Pulse.Server/Services/EndpointService.cs @@ -81,11 +81,9 @@ public EndpointService(PulseDbContext context) }, CurrentState = new CurrentStateDto { - Type = c.FallbackAttempted && c.FallbackStatus != null - ? "icmp" - : endpoint.Type.ToString().ToLower(), + Type = c.FallbackAttempted && c.FallbackStatus != null ? "icmp" : endpoint.Type.ToString().ToLower(), Target = endpoint.Host, - Status = c.GetEffectiveStatus().ToString().ToLower(), + Status = c.DetermineStatusType(new List(), TimeSpan.FromSeconds(endpoint.IntervalSeconds * 2)).ToString().ToLower(), RttMs = c.GetEffectiveRtt(), Classification = (int)c.DetermineClassification(), } diff --git a/ThingConnect.Pulse.Server/Services/HistoryService.cs b/ThingConnect.Pulse.Server/Services/HistoryService.cs index 73490cb..71981e4 100644 --- a/ThingConnect.Pulse.Server/Services/HistoryService.cs +++ b/ThingConnect.Pulse.Server/Services/HistoryService.cs @@ -130,11 +130,9 @@ private async Task> GetRawDataAsync(Data.Endpoint endpoint, Da }, CurrentState = new CurrentStateDto { - Type = c.FallbackAttempted && c.FallbackStatus != null - ? "icmp" - : endpoint.Type.ToString().ToLower(), + Type = c.FallbackAttempted && c.FallbackStatus != null ? "icmp" : endpoint.Type.ToString().ToLower(), Target = endpoint.Host, - Status = c.GetEffectiveStatus().ToString().ToLower(), + Status = c.DetermineStatusType(new List(), TimeSpan.FromSeconds(endpoint.IntervalSeconds * 2)).ToString().ToLower(), RttMs = c.GetEffectiveRtt(), Classification = (int)c.DetermineClassification(), } diff --git a/thingconnect.pulse.client/src/components/RecentChecksTable.tsx b/thingconnect.pulse.client/src/components/RecentChecksTable.tsx index 4e78efa..02198e8 100644 --- a/thingconnect.pulse.client/src/components/RecentChecksTable.tsx +++ b/thingconnect.pulse.client/src/components/RecentChecksTable.tsx @@ -51,35 +51,85 @@ export function RecentChecksTable({ checks, pageSize = 10 }: RecentChecksTablePr - Time - Status - RTT - Error + Time + Primary Status + Primary RTT (ms) + Primary Error + Fallback Status + Fallback RTT (ms) + Fallback Error + - {pagedChecks.map((check, index) => ( - + {pagedChecks.map((check, idx) => ( + {formatDistanceToNow(new Date(check.ts), { addSuffix: true })} - - {check.status.toUpperCase()} + + {check.primary.status.toUpperCase()} - {check.rttMs ? `${check.rttMs}ms` : '-'} + + {check.primary.rttMs ? `${check.primary.rttMs}ms` : '-'} + - - - - {check.error || '-'} + + + + {check.primary.error || '-'} + + {check.fallback.attempted ? ( + + {check.fallback.status?.toUpperCase() ?? '-'} + + ) : ( + + Not attempted + + )} + + + + {check.fallback.attempted && check.fallback.rttMs != null + ? `${check.fallback.rttMs}ms` + : '-'} + + + + {check.fallback.attempted ? ( + + + {check.fallback.error || '-'} + + + ) : ( + + Not attempted + + )} + ))} diff --git a/thingconnect.pulse.client/src/pages/EndpointDetail.tsx b/thingconnect.pulse.client/src/pages/EndpointDetail.tsx index 6bfbc9f..12befa6 100644 --- a/thingconnect.pulse.client/src/pages/EndpointDetail.tsx +++ b/thingconnect.pulse.client/src/pages/EndpointDetail.tsx @@ -62,6 +62,8 @@ function getStatusIcon(status: string) { return ArrowDown; case 'flapping': return AlertTriangle; + case 'service': + return Activity; default: return Circle; } @@ -171,11 +173,13 @@ export default function EndpointDetail() { const { endpoint, recent, outages } = endpointDetail; // Calculate uptime percentage from recent checks - const upChecks = recent.filter(check => check..status === 'up').length; + const upChecks = recent.filter(check => check.currentState.status === 'up').length; const uptimePercentage = recent.length > 0 ? Math.round((upChecks / recent.length) * 100) : 0; // Calculate average RTT - const rttValues = recent.filter(check => check.rttMs != null).map(check => check.rttMs!); + const rttValues = recent + .filter(check => check.currentState.rttMs != null) + .map(check => check.currentState.rttMs!); const avgRtt = rttValues.length > 0 ? Math.round(rttValues.reduce((sum, rtt) => sum + rtt, 0) / rttValues.length) @@ -183,7 +187,8 @@ export default function EndpointDetail() { // Get current status from most recent check const latestCheck = recent.length > 0 ? recent[0] : null; - const currentStatus = latestCheck?.status || 'unknown'; + const currentStatus = latestCheck?.currentState.status || 'unknown'; + console.log(latestCheck?.currentState.rttMs); const stats = [ { @@ -219,7 +224,7 @@ export default function EndpointDetail() { value: latestCheck ? formatDistanceToNow(new Date(latestCheck.ts), { addSuffix: true }) : 'N/A', - help: latestCheck ? (latestCheck.rttMs ? `${latestCheck.rttMs}ms` : 'Failed') : '', + help: latestCheck ? `${latestCheck?.currentState.rttMs} ms` : '', icon: CircleCheckBig, color: 'purple', bg: 'purple', From ba8dcc6285996204248b54c154801c1cdabb4ab8 Mon Sep 17 00:00:00 2001 From: abishek Date: Fri, 3 Oct 2025 15:48:07 +0530 Subject: [PATCH 26/28] updated the ui for history page? --- .../Services/HistoryService.cs | 2 +- .../src/components/AvailabilityChart.tsx | 2 +- .../components/history/AvailabilityStats.tsx | 9 ++-- .../src/components/history/HistoryTable.tsx | 50 +++++++++++++------ 4 files changed, 44 insertions(+), 19 deletions(-) diff --git a/ThingConnect.Pulse.Server/Services/HistoryService.cs b/ThingConnect.Pulse.Server/Services/HistoryService.cs index 71981e4..365cc4a 100644 --- a/ThingConnect.Pulse.Server/Services/HistoryService.cs +++ b/ThingConnect.Pulse.Server/Services/HistoryService.cs @@ -132,7 +132,7 @@ private async Task> GetRawDataAsync(Data.Endpoint endpoint, Da { Type = c.FallbackAttempted && c.FallbackStatus != null ? "icmp" : endpoint.Type.ToString().ToLower(), Target = endpoint.Host, - Status = c.DetermineStatusType(new List(), TimeSpan.FromSeconds(endpoint.IntervalSeconds * 2)).ToString().ToLower(), + Status = c.GetEffectiveStatus().ToString().ToLower(), RttMs = c.GetEffectiveRtt(), Classification = (int)c.DetermineClassification(), } diff --git a/thingconnect.pulse.client/src/components/AvailabilityChart.tsx b/thingconnect.pulse.client/src/components/AvailabilityChart.tsx index 1e3c25b..2a1d75c 100644 --- a/thingconnect.pulse.client/src/components/AvailabilityChart.tsx +++ b/thingconnect.pulse.client/src/components/AvailabilityChart.tsx @@ -24,7 +24,7 @@ export function AvailabilityChart({ data, bucket, isLoading }: AvailabilityChart hour: '2-digit', minute: '2-digit', }), - uptime: check.status === 'up' ? 100 : 0, + uptime: check.currentState.status === 'up' ? 100 : 0, })); case '15m': return data.rollup15m.map(bucket => ({ diff --git a/thingconnect.pulse.client/src/components/history/AvailabilityStats.tsx b/thingconnect.pulse.client/src/components/history/AvailabilityStats.tsx index 5e27c3a..916dbb7 100644 --- a/thingconnect.pulse.client/src/components/history/AvailabilityStats.tsx +++ b/thingconnect.pulse.client/src/components/history/AvailabilityStats.tsx @@ -35,9 +35,12 @@ export function AvailabilityStats({ switch (bucket) { case 'raw': { totalPoints = data.raw.length; - upPoints = data.raw.filter(check => check.status === 'up').length; - const validRttChecks = data.raw.filter(check => check.rttMs != null); - totalResponseTime = validRttChecks.reduce((sum, check) => sum + (check.rttMs || 0), 0); + upPoints = data.raw.filter(check => check.currentState.status === 'up').length; + const validRttChecks = data.raw.filter(check => check.currentState.rttMs != null); + totalResponseTime = validRttChecks.reduce( + (sum, check) => sum + (check.currentState.rttMs || 0), + 0 + ); responseTimeCount = validRttChecks.length; break; } diff --git a/thingconnect.pulse.client/src/components/history/HistoryTable.tsx b/thingconnect.pulse.client/src/components/history/HistoryTable.tsx index 2371b67..1789729 100644 --- a/thingconnect.pulse.client/src/components/history/HistoryTable.tsx +++ b/thingconnect.pulse.client/src/components/history/HistoryTable.tsx @@ -41,9 +41,8 @@ export function HistoryTable({ data, bucket, pageSize = 20, isLoading }: History .map(check => ({ timestamp: check.ts, displayTime: new Date(check.ts).toLocaleString(), - status: check.status, - responseTime: check.rttMs, - error: check.error, + primary: check.primary, + fallback: check.fallback, type: 'raw' as const, })) .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); @@ -88,7 +87,7 @@ export function HistoryTable({ data, bucket, pageSize = 20, isLoading }: History return tableData.slice(startIndex, startIndex + pageSize); }, [tableData, currentPage, pageSize]); - const getStatusBadge = (status?: string) => { + const getStatusBadge = (status?: string | null) => { if (!status) return null; const config = { up: { color: 'green', icon: CheckCircle }, @@ -150,9 +149,12 @@ export function HistoryTable({ data, bucket, pageSize = 20, isLoading }: History Timestamp {bucket === 'raw' ? ( <> - Status - Response Time - Error + Primary Status + Primary RTT + Primary Error + Fallback Status + Fallback RTT + Fallback Error ) : ( <> @@ -168,7 +170,7 @@ export function HistoryTable({ data, bucket, pageSize = 20, isLoading }: History {isLoading ? Array.from({ length: 8 }).map((_, i) => ( - {Array.from({ length: bucket === 'raw' ? 4 : 4 }).map((_, j) => ( + {Array.from({ length: bucket === 'raw' ? 7 : 4 }).map((_, j) => ( @@ -185,23 +187,43 @@ export function HistoryTable({ data, bucket, pageSize = 20, isLoading }: History {row.type === 'raw' ? ( <> - {getStatusBadge(row.status)} + {getStatusBadge(row.primary?.status)} - {formatResponseTime(row.responseTime)} + {formatResponseTime(row.primary?.rttMs)} + + + + + + {row.primary?.error || '-'} + + + + {getStatusBadge(row.fallback?.status)} + + + {formatResponseTime(row.fallback?.rttMs)} - + - {row.error || '-'} + {row.fallback?.error || '-'} From 54efb2e39fc4824baff68f6e97b9cf5d3642dc73 Mon Sep 17 00:00:00 2001 From: abishek Date: Fri, 3 Oct 2025 16:05:42 +0530 Subject: [PATCH 27/28] build issue fixed --- .../src/api/services/endpoint.service.ts | 10 +--- .../src/api/services/history.service.ts | 13 +++-- .../src/utils/manufacturingAnalytics.ts | 52 +++++++++++-------- 3 files changed, 39 insertions(+), 36 deletions(-) diff --git a/thingconnect.pulse.client/src/api/services/endpoint.service.ts b/thingconnect.pulse.client/src/api/services/endpoint.service.ts index 77f921f..3d749ba 100644 --- a/thingconnect.pulse.client/src/api/services/endpoint.service.ts +++ b/thingconnect.pulse.client/src/api/services/endpoint.service.ts @@ -17,18 +17,10 @@ export class EndpointService { if (!endpointItem) { throw new Error(`Endpoint ${id} not found`); } - // Convert live status to endpoint detail format return { endpoint: endpointItem.endpoint, - recent: [ - { - ts: endpointItem.lastChangeTs, - status: endpointItem.status === 'flapping' ? 'down' : endpointItem.status, - rttMs: endpointItem.rttMs, - error: null, - }, - ], + recent: [], outages: [], }; } diff --git a/thingconnect.pulse.client/src/api/services/history.service.ts b/thingconnect.pulse.client/src/api/services/history.service.ts index db50fa7..795b3c8 100644 --- a/thingconnect.pulse.client/src/api/services/history.service.ts +++ b/thingconnect.pulse.client/src/api/services/history.service.ts @@ -73,14 +73,19 @@ export class HistoryService { // Determine which data to export based on bucket if (bucket === 'raw' && data.raw.length > 0) { - lines.push('Timestamp,Status,Response Time (ms),Error'); + lines.push( + 'Timestamp,Primary Status,Primary RTT (ms),Primary Error,Fallback Status,Fallback RTT (ms),Fallback Error' + ); data.raw.forEach(check => { lines.push( [ check.ts, - check.status, - check.rttMs || '', - check.error ? `"${check.error.replace(/"/g, '""')}"` : '', + check.primary.status, + check.primary.rttMs || '', + check.primary.error ? `"${check.primary.error.replace(/"/g, '""')}"` : '', + check.fallback.status, + check.fallback.rttMs || '', + check.fallback.error ? `"${check.fallback.error.replace(/"/g, '""')}"` : '', ].join(',') ); }); diff --git a/thingconnect.pulse.client/src/utils/manufacturingAnalytics.ts b/thingconnect.pulse.client/src/utils/manufacturingAnalytics.ts index 1fa473d..8995c06 100644 --- a/thingconnect.pulse.client/src/utils/manufacturingAnalytics.ts +++ b/thingconnect.pulse.client/src/utils/manufacturingAnalytics.ts @@ -41,7 +41,7 @@ export function calculateManufacturingKPIs( historicalData?: any[] ): ManufacturingKPIs { const totalEndpoints = statusItems.length; - const upEndpoints = statusItems.filter(item => item.status === 'up').length; + const upEndpoints = statusItems.filter(item => item.currentState.status === 'up').length; // Count down and flapping endpoints for potential future use // const downEndpoints = statusItems.filter(item => item.status === 'down').length; // const flappingEndpoints = statusItems.filter(item => item.status === 'flapping').length; @@ -59,11 +59,12 @@ export function calculateManufacturingKPIs( ).size; // Identify critical endpoints (assumed to be those with custom names or specific groups) - const criticalEndpoints = statusItems.filter(item => - item.endpoint.group?.name?.toLowerCase().includes('critical') || - item.endpoint.name?.toLowerCase().includes('critical') || - item.endpoint.host.includes('prod') || - item.endpoint.host.includes('main') + const criticalEndpoints = statusItems.filter( + item => + item.endpoint.group?.name?.toLowerCase().includes('critical') || + item.endpoint.name?.toLowerCase().includes('critical') || + item.endpoint.host.includes('prod') || + item.endpoint.host.includes('main') ).length; return { @@ -74,7 +75,7 @@ export function calculateManufacturingKPIs( networkSegments, criticalEndpoints, availabilityScore: totalEndpoints > 0 ? (upEndpoints / totalEndpoints) * 100 : 0, - alertResponseTime: calculateAverageAlertResponseTime(historicalData) + alertResponseTime: calculateAverageAlertResponseTime(historicalData), }; } @@ -83,7 +84,7 @@ export function calculateManufacturingKPIs( */ export function analyzeConfigurationComplexity(configData: any): ConfigurationComplexity { const config = typeof configData === 'string' ? parseYAMLSafely(configData) : configData; - + if (!config || typeof config !== 'object') { return { totalRules: 0, @@ -91,7 +92,7 @@ export function analyzeConfigurationComplexity(configData: any): ConfigurationCo customIntervals: 0, advancedFeatures: [], configSizeKb: 0, - validationErrors: 0 + validationErrors: 0, }; } @@ -99,13 +100,13 @@ export function analyzeConfigurationComplexity(configData: any): ConfigurationCo const probeTypes = new Set(); const customIntervals = new Set(); const advancedFeatures: string[] = []; - + endpoints.forEach((endpoint: any) => { if (endpoint.type) probeTypes.add(endpoint.type); if (endpoint.interval && endpoint.interval !== 30) { customIntervals.add(endpoint.interval); } - + // Detect advanced features if (endpoint.authentication) advancedFeatures.push('authentication'); if (endpoint.headers) advancedFeatures.push('custom_headers'); @@ -114,9 +115,10 @@ export function analyzeConfigurationComplexity(configData: any): ConfigurationCo if (endpoint.ssl_verify === false) advancedFeatures.push('ssl_bypass'); }); - const configSize = typeof configData === 'string' ? - new Blob([configData]).size / 1024 : - JSON.stringify(config).length / 1024; + const configSize = + typeof configData === 'string' + ? new Blob([configData]).size / 1024 + : JSON.stringify(config).length / 1024; return { totalRules: endpoints.length, @@ -124,7 +126,7 @@ export function analyzeConfigurationComplexity(configData: any): ConfigurationCo customIntervals: customIntervals.size, advancedFeatures: [...new Set(advancedFeatures)], configSizeKb: Math.round(configSize * 100) / 100, - validationErrors: 0 // Would be populated by validation logic + validationErrors: 0, // Would be populated by validation logic }; } @@ -135,16 +137,15 @@ export function trackUserEfficiency(): UserEfficiencyMetrics { const sessionStart = sessionStorage.getItem('session_start_time'); const tasksCompleted = parseInt(sessionStorage.getItem('tasks_completed') || '0', 10); const helpUsage = parseInt(sessionStorage.getItem('help_usage_count') || '0', 10); - - const sessionDuration = sessionStart ? - (Date.now() - parseInt(sessionStart, 10)) / 1000 : 0; + + const sessionDuration = sessionStart ? (Date.now() - parseInt(sessionStart, 10)) / 1000 : 0; return { tasksCompleted, avgTaskDuration: tasksCompleted > 0 ? sessionDuration / tasksCompleted : 0, navigationDepth: getNavigationDepth(), helpUsage, - keyboardShortcuts: 0 // Would track keyboard usage + keyboardShortcuts: 0, // Would track keyboard usage }; } @@ -154,7 +155,9 @@ export function trackUserEfficiency(): UserEfficiencyMetrics { function calculateMTTD(statusItems: LiveStatusItem[], _historicalData?: any[]): number { // This would analyze historical data to determine detection times // For now, return a reasonable estimate based on probe intervals - const avgInterval = statusItems.reduce((sum, item) => sum + (item.endpoint.intervalSeconds || 30), 0) / statusItems.length; + const avgInterval = + statusItems.reduce((sum, item) => sum + (item.endpoint.intervalSeconds || 30), 0) / + statusItems.length; return avgInterval || 30; } @@ -170,9 +173,12 @@ function calculateMTTR(_statusItems: LiveStatusItem[], _historicalData?: any[]): /** * Calculate false positive rate */ -function calculateFalsePositiveRate(statusItems: LiveStatusItem[], _historicalData?: any[]): number { +function calculateFalsePositiveRate( + statusItems: LiveStatusItem[], + _historicalData?: any[] +): number { // This would analyze flapping endpoints and quick state changes - const flappingCount = statusItems.filter(item => item.status === 'flapping').length; + const flappingCount = statusItems.filter(item => item.currentState.status === 'flapping').length; return statusItems.length > 0 ? (flappingCount / statusItems.length) * 100 : 0; } @@ -228,4 +234,4 @@ export function initializeSessionTracking(): void { if (!sessionStorage.getItem('session_start_time')) { sessionStorage.setItem('session_start_time', Date.now().toString()); } -} \ No newline at end of file +} From 24235390c7c7c999f3c69ceb7780f46397e68515 Mon Sep 17 00:00:00 2001 From: abishek Date: Fri, 3 Oct 2025 18:19:43 +0530 Subject: [PATCH 28/28] udpated the rollup service --- .../Services/EndpointService.cs | 7 +- .../Services/Rollup/RollupService.cs | 198 +++++++----------- .../Services/StatusService.cs | 11 +- 3 files changed, 94 insertions(+), 122 deletions(-) diff --git a/ThingConnect.Pulse.Server/Services/EndpointService.cs b/ThingConnect.Pulse.Server/Services/EndpointService.cs index ab08b46..9f7783d 100644 --- a/ThingConnect.Pulse.Server/Services/EndpointService.cs +++ b/ThingConnect.Pulse.Server/Services/EndpointService.cs @@ -54,6 +54,11 @@ public EndpointService(PulseDbContext context) Classification = c.Classification }).ToList(); + var recentForEndpoint = checks + .Where(x => x.Timestamp >= windowStart) + .OrderBy(x => x.Timestamp) + .ToList(); + // --- Map RawCheckDto including EffectiveState --- var recent = checks .Where(c => c.Timestamp >= windowStart) @@ -83,7 +88,7 @@ public EndpointService(PulseDbContext context) { Type = c.FallbackAttempted && c.FallbackStatus != null ? "icmp" : endpoint.Type.ToString().ToLower(), Target = endpoint.Host, - Status = c.DetermineStatusType(new List(), TimeSpan.FromSeconds(endpoint.IntervalSeconds * 2)).ToString().ToLower(), + Status = c.DetermineStatusType(recentForEndpoint, TimeSpan.FromSeconds(endpoint.IntervalSeconds * 2)).ToString().ToLower(), RttMs = c.GetEffectiveRtt(), Classification = (int)c.DetermineClassification(), } diff --git a/ThingConnect.Pulse.Server/Services/Rollup/RollupService.cs b/ThingConnect.Pulse.Server/Services/Rollup/RollupService.cs index c49e9b1..ed2287c 100644 --- a/ThingConnect.Pulse.Server/Services/Rollup/RollupService.cs +++ b/ThingConnect.Pulse.Server/Services/Rollup/RollupService.cs @@ -26,48 +26,33 @@ public async Task ProcessRollup15mAsync(CancellationToken cancellationToken = de try { - // Get last watermark DateTimeOffset? lastWatermark = await _settingsService.GetLastRollup15mTimestampAsync(); - long fromTs = lastWatermark.HasValue ? UnixTimestamp.ToUnixSeconds(lastWatermark.Value) : UnixTimestamp.Subtract(UnixTimestamp.Now(), TimeSpan.FromDays(7)); // Default: 7 days back + long fromTs = lastWatermark.HasValue + ? UnixTimestamp.ToUnixSeconds(lastWatermark.Value) + : UnixTimestamp.Subtract(UnixTimestamp.Now(), TimeSpan.FromDays(7)); long toTs = UnixTimestamp.Now(); - _logger.LogDebug("Processing 15m rollups from {FromTs} to {ToTs}", UnixTimestamp.FromUnixSeconds(fromTs), UnixTimestamp.FromUnixSeconds(toTs)); - - // Get all raw checks in the time window - // SQLite has issues with DateTimeOffset comparisons in LINQ, so fetch all and filter in memory List allChecks = await _context.CheckResultsRaw.ToListAsync(cancellationToken); var rawChecks = allChecks .Where(c => c.Ts > fromTs && c.Ts <= toTs) .OrderBy(c => c.EndpointId) .ThenBy(c => c.Ts) + .Select(c => new WrappedCheck(c)) .ToList(); - if (!rawChecks.Any()) - { - _logger.LogDebug("No raw checks found for rollup processing"); - return; - } - - _logger.LogInformation("Processing {Count} raw checks", rawChecks.Count); + if (!rawChecks.Any()) return; - // Group by endpoint and calculate rollups - IEnumerable> endpointGroups = rawChecks.GroupBy(c => c.EndpointId); + var endpointGroups = rawChecks.GroupBy(c => c.EndpointId); List rollupsToUpsert = new(); - foreach (IGrouping endpointGroup in endpointGroups) + foreach (var endpointGroup in endpointGroups) { var checks = endpointGroup.OrderBy(c => c.Ts).ToList(); - List endpointRollups = CalculateRollups15m(endpointGroup.Key, checks); - rollupsToUpsert.AddRange(endpointRollups); + rollupsToUpsert.AddRange(CalculateRollups15m(endpointGroup.Key, checks)); } - // Upsert rollups in batches await UpsertRollups15mAsync(rollupsToUpsert, cancellationToken); - - // Update watermark await _settingsService.SetLastRollup15mTimestampAsync(UnixTimestamp.FromUnixSeconds(toTs)); - - _logger.LogInformation("Completed 15m rollup processing. Generated {Count} rollup records", rollupsToUpsert.Count); } catch (Exception ex) { @@ -82,57 +67,36 @@ public async Task ProcessRollupDailyAsync(CancellationToken cancellationToken = try { - // Get last watermark DateOnly? lastWatermark = await _settingsService.GetLastRollupDailyDateAsync(); DateOnly fromDate = lastWatermark?.AddDays(1) ?? DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)); - var toDate = DateOnly.FromDateTime(DateTime.UtcNow.Date); + DateOnly toDate = DateOnly.FromDateTime(DateTime.UtcNow.Date); - if (fromDate >= toDate) - { - _logger.LogDebug("No new days to process for daily rollup"); - return; - } - - _logger.LogDebug("Processing daily rollups from {FromDate} to {ToDate}", fromDate, toDate); + if (fromDate >= toDate) return; - // Get all raw checks in the date range long fromTs = UnixTimestamp.ToUnixDate(fromDate); long toTs = UnixTimestamp.ToUnixDate(toDate); - // SQLite has issues with DateTimeOffset comparisons in LINQ, so fetch all and filter in memory List allChecks = await _context.CheckResultsRaw.ToListAsync(cancellationToken); var rawChecks = allChecks .Where(c => c.Ts >= fromTs && c.Ts < toTs) .OrderBy(c => c.EndpointId) .ThenBy(c => c.Ts) + .Select(c => new WrappedCheck(c)) .ToList(); - if (!rawChecks.Any()) - { - _logger.LogDebug("No raw checks found for daily rollup processing"); - return; - } + if (!rawChecks.Any()) return; - _logger.LogInformation("Processing {Count} raw checks for daily rollup", rawChecks.Count); - - // Group by endpoint and calculate rollups - IEnumerable> endpointGroups = rawChecks.GroupBy(c => c.EndpointId); + var endpointGroups = rawChecks.GroupBy(c => c.EndpointId); List rollupsToUpsert = new(); - foreach (IGrouping endpointGroup in endpointGroups) + foreach (var endpointGroup in endpointGroups) { var checks = endpointGroup.OrderBy(c => c.Ts).ToList(); - List endpointRollups = CalculateRollupsDaily(endpointGroup.Key, checks, fromDate, toDate); - rollupsToUpsert.AddRange(endpointRollups); + rollupsToUpsert.AddRange(CalculateRollupsDaily(endpointGroup.Key, checks, fromDate, toDate)); } - // Upsert rollups in batches await UpsertRollupsDailyAsync(rollupsToUpsert, cancellationToken); - - // Update watermark await _settingsService.SetLastRollupDailyDateAsync(toDate.AddDays(-1)); - - _logger.LogInformation("Completed daily rollup processing. Generated {Count} rollup records", rollupsToUpsert.Count); } catch (Exception ex) { @@ -141,44 +105,35 @@ public async Task ProcessRollupDailyAsync(CancellationToken cancellationToken = } } - private List CalculateRollups15m(Guid endpointId, List checks) + // --- Private rollup calculation helpers --- + private List CalculateRollups15m(Guid endpointId, List checks) { var rollups = new List(); - // Group by 15-minute bucket var bucketGroups = checks - .Select(c => new - { - Check = c, - Bucket = GetBucketTimestamp15m(c.Ts) - }) - .GroupBy(x => x.Bucket); + .GroupBy(c => GetBucketTimestamp15m(c.Ts)); foreach (var bucketGroup in bucketGroups) { - var bucketChecks = bucketGroup.Select(x => x.Check).OrderBy(c => c.Ts).ToList(); - - if (!bucketChecks.Any()) - { - continue; - } + var bucketChecks = bucketGroup.OrderBy(c => c.Ts).ToList(); + if (!bucketChecks.Any()) continue; - // Calculate metrics int totalChecks = bucketChecks.Count; - int upChecks = bucketChecks.Count(c => c.Status == UpDown.up); + int upChecks = bucketChecks.Count(c => c.GetEffectiveStatus() == UpDown.up); double upPct = totalChecks > 0 ? (double)upChecks / totalChecks * 100.0 : 0.0; var rttValues = bucketChecks - .Where(c => c.RttMs.HasValue && c.RttMs > 0) - .Select(c => c.RttMs!.Value) + .Select(c => c.GetEffectiveRtt()) + .Where(rtt => rtt.HasValue && rtt > 0) + .Select(rtt => rtt!.Value) .ToList(); double? avgRttMs = rttValues.Any() ? rttValues.Average() : null; - // Count down events (up→down transitions) int downEvents = 0; for (int i = 1; i < bucketChecks.Count; i++) { - if (bucketChecks[i - 1].Status == UpDown.up && bucketChecks[i].Status == UpDown.down) + if (bucketChecks[i - 1].GetEffectiveStatus() == UpDown.up && + bucketChecks[i].GetEffectiveStatus() == UpDown.down) { downEvents++; } @@ -187,7 +142,7 @@ public async Task ProcessRollupDailyAsync(CancellationToken cancellationToken = rollups.Add(new Data.Rollup15m { EndpointId = endpointId, - BucketTs = bucketGroup.Key, + BucketTs = GetBucketTimestamp15m(bucketChecks.First().Ts), UpPct = upPct, AvgRttMs = avgRttMs, DownEvents = downEvents @@ -197,45 +152,35 @@ public async Task ProcessRollupDailyAsync(CancellationToken cancellationToken = return rollups; } - private List CalculateRollupsDaily(Guid endpointId, List checks, DateOnly fromDate, DateOnly toDate) + private List CalculateRollupsDaily(Guid endpointId, List checks, DateOnly fromDate, DateOnly toDate) { var rollups = new List(); - // Group by date var dateGroups = checks - .Select(c => new - { - Check = c, - Date = DateOnly.FromDateTime(UnixTimestamp.FromUnixSeconds(c.Ts).Date) - }) - .Where(x => x.Date >= fromDate && x.Date < toDate) - .GroupBy(x => x.Date); + .GroupBy(c => DateOnly.FromDateTime(UnixTimestamp.FromUnixSeconds(c.Ts).Date)) + .Where(g => g.Key >= fromDate && g.Key < toDate); foreach (var dateGroup in dateGroups) { - var dayChecks = dateGroup.Select(x => x.Check).OrderBy(c => c.Ts).ToList(); - - if (!dayChecks.Any()) - { - continue; - } + var dayChecks = dateGroup.OrderBy(c => c.Ts).ToList(); + if (!dayChecks.Any()) continue; - // Calculate metrics int totalChecks = dayChecks.Count; - int upChecks = dayChecks.Count(c => c.Status == UpDown.up); + int upChecks = dayChecks.Count(c => c.GetEffectiveStatus() == UpDown.up); double upPct = totalChecks > 0 ? (double)upChecks / totalChecks * 100.0 : 0.0; var rttValues = dayChecks - .Where(c => c.RttMs.HasValue && c.RttMs > 0) - .Select(c => c.RttMs!.Value) + .Select(c => c.GetEffectiveRtt()) + .Where(rtt => rtt.HasValue && rtt > 0) + .Select(rtt => rtt!.Value) .ToList(); double? avgRttMs = rttValues.Any() ? rttValues.Average() : null; - // Count down events (up→down transitions) int downEvents = 0; for (int i = 1; i < dayChecks.Count; i++) { - if (dayChecks[i - 1].Status == UpDown.up && dayChecks[i].Status == UpDown.down) + if (dayChecks[i - 1].GetEffectiveStatus() == UpDown.up && + dayChecks[i].GetEffectiveStatus() == UpDown.down) { downEvents++; } @@ -256,74 +201,89 @@ public async Task ProcessRollupDailyAsync(CancellationToken cancellationToken = private static long GetBucketTimestamp15m(long unixTs) { - // Round down to nearest 15-minute boundary DateTimeOffset ts = UnixTimestamp.FromUnixSeconds(unixTs); - int minute = ts.Minute; - int bucketMinute = (minute / 15) * 15; - - var bucketTime = new DateTimeOffset(ts.Year, ts.Month, ts.Day, ts.Hour, bucketMinute, 0, ts.Offset); - return UnixTimestamp.ToUnixSeconds(bucketTime); + int bucketMinute = (ts.Minute / 15) * 15; + return UnixTimestamp.ToUnixSeconds(new DateTimeOffset(ts.Year, ts.Month, ts.Day, ts.Hour, bucketMinute, 0, ts.Offset)); } private async Task UpsertRollups15mAsync(List rollups, CancellationToken cancellationToken) { - if (!rollups.Any()) - { - return; - } + if (!rollups.Any()) return; - // SQLite doesn't support MERGE/UPSERT in EF Core, so we'll do it manually - foreach (Data.Rollup15m rollup in rollups) + foreach (var rollup in rollups) { - Rollup15m? existing = await _context.Rollups15m + var existing = await _context.Rollups15m .FirstOrDefaultAsync(r => r.EndpointId == rollup.EndpointId && r.BucketTs == rollup.BucketTs, cancellationToken); if (existing != null) { - // Update existing existing.UpPct = rollup.UpPct; existing.AvgRttMs = rollup.AvgRttMs; existing.DownEvents = rollup.DownEvents; } else { - // Add new _context.Rollups15m.Add(rollup); } } await _context.SaveChangesAsync(cancellationToken); - _logger.LogDebug("Upserted {Count} 15m rollup records", rollups.Count); } private async Task UpsertRollupsDailyAsync(List rollups, CancellationToken cancellationToken) { - if (!rollups.Any()) - { - return; - } + if (!rollups.Any()) return; - // SQLite doesn't support MERGE/UPSERT in EF Core, so we'll do it manually - foreach (Data.RollupDaily rollup in rollups) + foreach (var rollup in rollups) { - RollupDaily? existing = await _context.RollupsDaily + var existing = await _context.RollupsDaily .FirstOrDefaultAsync(r => r.EndpointId == rollup.EndpointId && r.BucketDate == rollup.BucketDate, cancellationToken); if (existing != null) { - // Update existing existing.UpPct = rollup.UpPct; existing.AvgRttMs = rollup.AvgRttMs; existing.DownEvents = rollup.DownEvents; } else { - // Add new _context.RollupsDaily.Add(rollup); } } await _context.SaveChangesAsync(cancellationToken); - _logger.LogDebug("Upserted {Count} daily rollup records", rollups.Count); + } + + // --- Private wrapper class for effective status & RTT --- + private class WrappedCheck + { + private readonly CheckResultRaw _check; + + public WrappedCheck(CheckResultRaw check) + { + _check = check; + Ts = check.Ts; + EndpointId = check.EndpointId; + } + + public long Ts { get; } + public Guid EndpointId { get; } + + public UpDown GetEffectiveStatus() + { + if (_check.Status == UpDown.down && _check.FallbackAttempted == true && _check.FallbackStatus == UpDown.up) + { + return UpDown.up; + } + return _check.Status; + } + + public double? GetEffectiveRtt() + { + if (_check.Status == UpDown.up && _check.RttMs.HasValue) return _check.RttMs; + if (_check.Status == UpDown.down && _check.FallbackAttempted == true && _check.FallbackStatus == UpDown.up && _check.FallbackRttMs.HasValue) + return _check.FallbackRttMs; + return null; + } } } diff --git a/ThingConnect.Pulse.Server/Services/StatusService.cs b/ThingConnect.Pulse.Server/Services/StatusService.cs index 1212dfc..b417a57 100644 --- a/ThingConnect.Pulse.Server/Services/StatusService.cs +++ b/ThingConnect.Pulse.Server/Services/StatusService.cs @@ -1,3 +1,4 @@ +using System.Net.NetworkInformation; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using ThingConnect.Pulse.Server.Data; @@ -70,7 +71,12 @@ public async Task> GetLiveStatusAsync(string? group, str { EndpointId = c.EndpointId, Timestamp = UnixTimestamp.FromUnixSeconds(c.Ts), - Status = c.Status + Status = c.Status, + RttMs = c.RttMs, + FallbackAttempted = c.FallbackStatus.HasValue, + FallbackStatus = c.FallbackStatus, + FallbackRttMs = c.FallbackRttMs, + Classification = c.Classification, }) .ToListAsync(); @@ -100,7 +106,7 @@ public async Task> GetLiveStatusAsync(string? group, str { Type = recent.Any() && recent.Last().FallbackAttempted ? "icmp" : endpoint.Type.ToString().ToLower(), Target = endpoint.Host, - Status = recent.Any() ? recent.Last().GetEffectiveStatus().ToString().ToLower() : "down", + Status = status.ToString().ToLower(), RttMs = recent.Any() ? recent.Last().GetEffectiveRtt() : null, Classification = recent.Any() ? (int)recent.Last().DetermineClassification() : 0 }, @@ -169,6 +175,7 @@ private async Task>> GetSparklineDataAsync Status = c.Status, FallbackAttempted = c.FallbackStatus.HasValue, FallbackStatus = c.FallbackStatus, + Classification = c.Classification, }) .ToListAsync();