feat: wireview pin-imbalance warning, per-pin rating bars, pin_imbalance_a #18
No files matched your search
@@ -6,6 +6,7 @@ __pycache__/
|
|||||||
# Node
|
# Node
|
||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
.vitest/
|
||||||
|
|
||||||
# IDE/Misc
|
# IDE/Misc
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ NVCurve brings MSI Afterburner-style per-point voltage-frequency curve control t
|
|||||||
> **Blackwell GPU memory** — This is a specialized fork with extended memory offset support (up to +3000 MHz) for Blackwell GPUs (RTX 50-series). \
|
> **Blackwell GPU memory** — This is a specialized fork with extended memory offset support (up to +3000 MHz) for Blackwell GPUs (RTX 50-series). \
|
||||||
> **Fan Controls** — There is an additional "Fans" tab to setup a customized fan curve, controlling all fans or individual fans. \
|
> **Fan Controls** — There is an additional "Fans" tab to setup a customized fan curve, controlling all fans or individual fans. \
|
||||||
> **Dashboard** — The default tab is an Dashboard with additional information (PCIe link speed, VBIOS information, Max Core Clock, Throttle Reason and much much more.) \
|
> **Dashboard** — The default tab is an Dashboard with additional information (PCIe link speed, VBIOS information, Max Core Clock, Throttle Reason and much much more.) \
|
||||||
> **WireView Pro II** — Native monitoring of the Thermal Grizzly WireView Pro II 12VHPWR connector: a dedicated tab shows live temperature, power and current readings — no external exporter or kernel module required. The host needs a one-time setup (udev rule + port access, see [WireView Pro II Setup](docs/WireView.md)); after that the device is auto-detected over USB and the tab only appears while it is connected. \
|
> **WireView Pro II** — Native monitoring of the Thermal Grizzly WireView Pro II 12VHPWR connector: a dedicated tab shows live temperature, power and current readings — no external exporter or kernel module required. It also raises a big warning in the app header (visible on all tabs) when the current between two power pins diverges by more than 2 A — the signature of a bad pin contact (faulty cable/adapter) that can overheat and melt the connector. The host needs a one-time setup (udev rule + port access, see [WireView Pro II Setup](docs/WireView.md)); after that the device is auto-detected over USB and the tab only appears while it is connected. \
|
||||||
> **Authentification** — For production deplyoment, I added authentification with bcrypt hashing to allow only one or multiple people to have access. \
|
> **Authentication** — For production deployment, I added authentication with bcrypt hashing to allow only one or multiple people to have access. \
|
||||||
> Installing the pre-built PyPI package will NOT include these features. You must build from source.
|
> Installing the pre-built PyPI package will NOT include these features. You must build from source.
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
|
|||||||
@@ -74,6 +74,12 @@ ls -l /dev/wireview-pro2 # -> /dev/ttyACM0
|
|||||||
|
|
||||||
With the device plugged in, start (or restart) the NVCurve server. The **WireView** tab appears in the web UI and `GET /api/wireview` returns the device info and live samples. The tab disappears when the device is unplugged and reappears on re-plug — no restart needed.
|
With the device plugged in, start (or restart) the NVCurve server. The **WireView** tab appears in the web UI and `GET /api/wireview` returns the device info and live samples. The tab disappears when the device is unplugged and reappears on re-plug — no restart needed.
|
||||||
|
|
||||||
|
## Pin current imbalance warning
|
||||||
|
|
||||||
|
The 12 VHPWR connector is the most fragile part of a PC's power delivery: if one power pin has a bad contact (worn cable, faulty adapter, loose plug), the remaining pins are forced to carry the missing current. Under heavy load this can overheat and melt the connector — in rare cases even start a fire.
|
||||||
|
|
||||||
|
NVCurve watches for this: if the current between any two power pins diverges by more than **2 A**, a big warning appears in the app header (visible on every tab, not just WireView) naming the deviating pins (e.g. `Pin 5 (5.6 A) vs Pin 6 (3.5 A)`). It is debounced — the imbalance must persist for several seconds (~7 s) before the warning shows, so single-sample glitches and short load ramps (e.g. AI workloads) do not flash it. When you see it, reduce the load and replace the cable or adapter — do not keep using the connector.
|
||||||
|
|
||||||
## Alternative: wireview-hwmon kernel module
|
## Alternative: wireview-hwmon kernel module
|
||||||
|
|
||||||
If the official `wireview-hwmon` kernel module and its `wireviewd` daemon are installed, the daemon owns the serial port and NVCurve automatically reads the sysfs node instead. No udev rule is needed in that case.
|
If the official `wireview-hwmon` kernel module and its `wireviewd` daemon are installed, the daemon owns the serial port and NVCurve automatically reads the sysfs node instead. No udev rule is needed in that case.
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 151 KiB |
Generated
+524
-10
@@ -18,6 +18,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
"@tailwindcss/vite": "^4.2.1",
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
|
"@testing-library/react": "^16.3.3",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
@@ -27,10 +28,12 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
|
"happy-dom": "^20.14.5",
|
||||||
"tailwindcss": "^4.2.1",
|
"tailwindcss": "^4.2.1",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.48.0",
|
"typescript-eslint": "^8.48.0",
|
||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1",
|
||||||
|
"vitest": "^5.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
@@ -225,6 +228,16 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.28.6",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||||
@@ -971,9 +984,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.5",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
|
||||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
"integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -1879,6 +1892,74 @@
|
|||||||
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@testing-library/dom": {
|
||||||
|
"version": "10.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz",
|
||||||
|
"integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.10.4",
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@types/aria-query": "^5.0.1",
|
||||||
|
"aria-query": "5.3.0",
|
||||||
|
"dom-accessibility-api": "^0.5.9",
|
||||||
|
"lz-string": "^1.5.0",
|
||||||
|
"picocolors": "1.1.1",
|
||||||
|
"pretty-format": "^27.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@testing-library/react": {
|
||||||
|
"version": "16.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz",
|
||||||
|
"integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@testing-library/dom": "^10.0.0",
|
||||||
|
"@types/react": "^18.0.0 || ^19.0.0",
|
||||||
|
"@types/react-dom": "^18.0.0 || ^19.0.0",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/aria-query": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/chai": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/deep-eql": "*",
|
||||||
|
"assertion-error": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/d3": {
|
"node_modules/@types/d3": {
|
||||||
"version": "7.4.3",
|
"version": "7.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
|
||||||
@@ -2163,6 +2244,13 @@
|
|||||||
"@types/d3-selection": "*"
|
"@types/d3-selection": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/deep-eql": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||||
@@ -2214,6 +2302,23 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/whatwg-mimetype": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz",
|
||||||
|
"integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.59.2",
|
"version": "8.59.2",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz",
|
||||||
@@ -2526,6 +2631,54 @@
|
|||||||
"vite": "^4 || ^5 || ^6 || ^7 || ^8"
|
"vite": "^4 || ^5 || ^6 || ^7 || ^8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitest/mocker": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/trace-mapping": "0.3.31",
|
||||||
|
"@vitest/spy": "5.0.3",
|
||||||
|
"estree-walker": "^3.0.3",
|
||||||
|
"magic-string": "^1.2.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"msw": "^2.4.9",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"msw": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/mocker/node_modules/magic-string": {
|
||||||
|
"version": "1.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz",
|
||||||
|
"integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/spy": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.16.0",
|
"version": "8.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||||
@@ -2566,6 +2719,17 @@
|
|||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-styles": {
|
"node_modules/ansi-styles": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
@@ -2589,6 +2753,27 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/aria-query": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"dequal": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/assertion-error": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -2654,6 +2839,19 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-image-size": {
|
||||||
|
"version": "0.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz",
|
||||||
|
"integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
@@ -2685,6 +2883,16 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/chai": {
|
||||||
|
"version": "6.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz",
|
||||||
|
"integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
@@ -3202,6 +3410,17 @@
|
|||||||
"robust-predicates": "^3.0.2"
|
"robust-predicates": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dequal": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
@@ -3212,6 +3431,14 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-accessibility-api": {
|
||||||
|
"version": "0.5.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||||
|
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.353",
|
"version": "1.5.353",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz",
|
||||||
@@ -3233,6 +3460,26 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-module-lexer": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.27.7",
|
"version": "0.27.7",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
||||||
@@ -3472,6 +3719,16 @@
|
|||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/estree-walker": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esutils": {
|
"node_modules/esutils": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||||
@@ -3482,6 +3739,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expect-type": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -3630,6 +3897,25 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/happy-dom": {
|
||||||
|
"version": "20.14.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.14.5.tgz",
|
||||||
|
"integrity": "sha512-x/RzkpWO40bTjIoT30iQtt64FLLmH/iRcUCN2X//bLx7H3ifkdfPXyqsro/OYtqzIAhiLMMA7mmiOR9C3NOKjQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": ">=20.0.0",
|
||||||
|
"@types/whatwg-mimetype": "^3.0.2",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
|
"buffer-image-size": "^0.6.4",
|
||||||
|
"entities": "^7.0.1",
|
||||||
|
"whatwg-mimetype": "^3.0.0",
|
||||||
|
"ws": "^8.21.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has-flag": {
|
"node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
@@ -4149,6 +4435,17 @@
|
|||||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lz-string": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"bin": {
|
||||||
|
"lz-string": "bin/bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
@@ -4212,6 +4509,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/obug": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/sxzz",
|
||||||
|
"https://opencollective.com/debug"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@@ -4303,9 +4614,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
|
||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4354,6 +4665,36 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pretty-format": {
|
||||||
|
"version": "27.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||||
|
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1",
|
||||||
|
"ansi-styles": "^5.0.0",
|
||||||
|
"react-is": "^17.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pretty-format/node_modules/ansi-styles": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@@ -4385,6 +4726,14 @@
|
|||||||
"react": "^19.2.6"
|
"react": "^19.2.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-is": {
|
||||||
|
"version": "17.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/resolve-from": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
@@ -4524,6 +4873,13 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/std-env": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/strip-json-comments": {
|
"node_modules/strip-json-comments": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||||
@@ -4571,10 +4927,30 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tinybench": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinyexec": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.16",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4775,6 +5151,109 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vitest": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/chai": "^5.2.2",
|
||||||
|
"@vitest/mocker": "5.0.3",
|
||||||
|
"chai": "^6.2.2",
|
||||||
|
"es-module-lexer": "^2.3.2",
|
||||||
|
"expect-type": "^1.4.0",
|
||||||
|
"magic-string": "^1.2.3",
|
||||||
|
"obug": "^2.1.4",
|
||||||
|
"picomatch": "^4.0.7",
|
||||||
|
"std-env": "^4.2.0",
|
||||||
|
"tinybench": "^6.1.4",
|
||||||
|
"tinyexec": "^1.3.0",
|
||||||
|
"tinyglobby": "^0.2.17",
|
||||||
|
"why-is-node-running": "3.2.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vitest": "vitest.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^22.12.0 || ^24.0.0 || >=26.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@edge-runtime/vm": "*",
|
||||||
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"@types/node": "^22.0.0 || >=24.0.0",
|
||||||
|
"@vitest/browser-playwright": "5.0.3",
|
||||||
|
"@vitest/browser-preview": "5.0.3",
|
||||||
|
"@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
|
||||||
|
"@vitest/coverage-istanbul": "5.0.3",
|
||||||
|
"@vitest/coverage-v8": "5.0.3",
|
||||||
|
"@vitest/ui": "5.0.3",
|
||||||
|
"happy-dom": "*",
|
||||||
|
"jsdom": "*",
|
||||||
|
"vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@edge-runtime/vm": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@opentelemetry/api": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-playwright": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-preview": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-webdriverio": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/coverage-istanbul": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/coverage-v8": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/ui": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"happy-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jsdom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vitest/node_modules/magic-string": {
|
||||||
|
"version": "1.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz",
|
||||||
|
"integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-mimetype": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
@@ -4791,6 +5270,19 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/why-is-node-running": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"why-is-node-running": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
@@ -4801,6 +5293,28 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.22.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz",
|
||||||
|
"integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
"test": "vitest run",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
"@tailwindcss/vite": "^4.2.1",
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
|
"@testing-library/react": "^16.3.3",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
@@ -29,9 +31,11 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
|
"happy-dom": "^20.14.5",
|
||||||
"tailwindcss": "^4.2.1",
|
"tailwindcss": "^4.2.1",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.48.0",
|
"typescript-eslint": "^8.48.0",
|
||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1",
|
||||||
|
"vitest": "^5.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,7 @@ import { api, onUnauthorized } from "./api/client.js";
|
|||||||
import { LoginScreen } from "./components/Auth/LoginScreen.js";
|
import { LoginScreen } from "./components/Auth/LoginScreen.js";
|
||||||
import { useCurveStore } from "./store/curveStore.js";
|
import { useCurveStore } from "./store/curveStore.js";
|
||||||
import { useWireview } from "./hooks/useWireview.js";
|
import { useWireview } from "./hooks/useWireview.js";
|
||||||
|
import { useWireviewImbalance } from "./hooks/useWireviewImbalance.js";
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
import { Loader, ChevronDown } from "lucide-react";
|
import { Loader, ChevronDown } from "lucide-react";
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
@@ -98,6 +99,7 @@ function MainApp({
|
|||||||
const { setCurve, activeProfile, setActiveProfile, selectedGpuIndex } =
|
const { setCurve, activeProfile, setActiveProfile, selectedGpuIndex } =
|
||||||
useCurveStore();
|
useCurveStore();
|
||||||
const wireview = useWireview();
|
const wireview = useWireview();
|
||||||
|
const wireImbalance = useWireviewImbalance(wireview.sample);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<
|
const [activeTab, setActiveTab] = useState<
|
||||||
"dashboard" | "curve" | "performance" | "fans" | "wireview"
|
"dashboard" | "curve" | "performance" | "fans" | "wireview"
|
||||||
@@ -171,6 +173,26 @@ function MainApp({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{wireImbalance && (
|
||||||
|
<div className="bg-red-500/15 border-b-2 border-red-500 text-red-300 px-4 py-2.5 flex items-center justify-center gap-3 flex-wrap">
|
||||||
|
<span className="text-xl leading-none">⚠</span>
|
||||||
|
<span className="text-base font-bold">
|
||||||
|
WireView: current imbalance {wireImbalance.diff.toFixed(1)} A —
|
||||||
|
Pin {wireImbalance.hiPin} ({wireImbalance.maxI.toFixed(1)} A) vs
|
||||||
|
Pin {wireImbalance.loPin} ({wireImbalance.minI.toFixed(1)} A)
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-red-200/80">
|
||||||
|
Bad pin contact — reduce load, replace cable/adapter
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("wireview")}
|
||||||
|
className="text-sm font-semibold text-red-200 hover:text-white underline underline-offset-2"
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<main className="flex-1 p-4 flex flex-col gap-4 w-full max-w-[1600px] mx-auto">
|
<main className="flex-1 p-4 flex flex-col gap-4 w-full max-w-[1600px] mx-auto">
|
||||||
{/* Tab Header and Profile Selector */}
|
{/* Tab Header and Profile Selector */}
|
||||||
<div className="flex justify-between items-end border-b border-zinc-800 pb-2">
|
<div className="flex justify-between items-end border-b border-zinc-800 pb-2">
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ interface Props {
|
|||||||
history: WireViewSample[];
|
history: WireViewSample[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-pin over-current protection of the 12 VHPWR connector (A).
|
// Per-pin current rating of the 12 VHPWR connector (A). The bar's
|
||||||
const PIN_OCP_A = 55;
|
// full scale is 10 A (rating + headroom); red at/above the 9.2 A rating.
|
||||||
|
const PIN_RATED_A = 9.2;
|
||||||
|
const PIN_BAR_MAX_A = 10;
|
||||||
|
|
||||||
const FAULT_NAMES: Record<number, string> = {
|
const FAULT_NAMES: Record<number, string> = {
|
||||||
0: "Chip over-temperature",
|
0: "Chip over-temperature",
|
||||||
@@ -87,9 +89,13 @@ function PinCard({
|
|||||||
current: number;
|
current: number;
|
||||||
power: number;
|
power: number;
|
||||||
}) {
|
}) {
|
||||||
const loadPct = Math.min(100, (current / PIN_OCP_A) * 100);
|
const loadPct = Math.min(100, (current / PIN_BAR_MAX_A) * 100);
|
||||||
const barColor =
|
const barColor =
|
||||||
loadPct >= 90 ? "#f87171" : loadPct >= 75 ? "#fbbf24" : "#34d399";
|
current >= PIN_RATED_A
|
||||||
|
? "#f87171"
|
||||||
|
: loadPct >= 75
|
||||||
|
? "#fbbf24"
|
||||||
|
: "#34d399";
|
||||||
return (
|
return (
|
||||||
<div className="bg-zinc-900 rounded-lg p-3 flex flex-col gap-1.5 min-w-0">
|
<div className="bg-zinc-900 rounded-lg p-3 flex flex-col gap-1.5 min-w-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -97,7 +103,7 @@ function PinCard({
|
|||||||
Pin {index + 1}
|
Pin {index + 1}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] font-mono text-zinc-600">
|
<span className="text-[10px] font-mono text-zinc-600">
|
||||||
{current.toFixed(1)} / {PIN_OCP_A} A
|
{current.toFixed(1)} / {PIN_RATED_A} A
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-1 text-center">
|
<div className="grid grid-cols-3 gap-1 text-center">
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { renderHook, act } from "@testing-library/react";
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { useWireviewImbalance } from "./useWireviewImbalance.js";
|
||||||
|
import type { WireViewSample } from "../types.js";
|
||||||
|
|
||||||
|
/** Build a minimal sample with the given per-pin currents (A). */
|
||||||
|
function sample(pinCurrents: number[]): WireViewSample {
|
||||||
|
return {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
power_total_w: 0,
|
||||||
|
current_total_a: pinCurrents.reduce((a, b) => a + b, 0),
|
||||||
|
pin_imbalance_a: 0,
|
||||||
|
voltage_avg_v: 12,
|
||||||
|
pins: pinCurrents.map((a) => ({
|
||||||
|
voltage_v: 12,
|
||||||
|
current_a: a,
|
||||||
|
power_w: 12 * a,
|
||||||
|
})),
|
||||||
|
temp_in_c: null,
|
||||||
|
temp_out_c: null,
|
||||||
|
temp_ext1_c: null,
|
||||||
|
temp_ext2_c: null,
|
||||||
|
fan_duty_pct: 0,
|
||||||
|
fault_status: 0,
|
||||||
|
fault_log: 0,
|
||||||
|
psu_capability_w: 600,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const BALANCED = [4.5, 4.6, 4.4, 4.8, 5.0, 4.2]; // spread 0.8 A
|
||||||
|
const IMBALANCED = [4.5, 4.6, 4.4, 4.8, 5.6, 3.5]; // spread 2.1 A
|
||||||
|
|
||||||
|
function useTestHook() {
|
||||||
|
const [s, setS] = useState<WireViewSample | null>(null);
|
||||||
|
const alert = useWireviewImbalance(s);
|
||||||
|
return { alert, setSample: setS };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feed `times` fresh samples (new object identity, like the websocket).
|
||||||
|
* Each sample must be a separate state update — the hook counts per sample,
|
||||||
|
* so the loop is intentional (not batchable).
|
||||||
|
*/
|
||||||
|
function feed(
|
||||||
|
setSample: (s: WireViewSample) => void,
|
||||||
|
currents: number[],
|
||||||
|
times: number,
|
||||||
|
) {
|
||||||
|
for (let i = 0; i < times; i++) {
|
||||||
|
act(() => setSample(sample(currents)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("useWireviewImbalance", () => {
|
||||||
|
it("returns null for a null sample", () => {
|
||||||
|
const { result } = renderHook(() => useTestHook());
|
||||||
|
expect(result.current.alert).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stays hidden while pins are balanced", () => {
|
||||||
|
const { result } = renderHook(() => useTestHook());
|
||||||
|
feed(result.current.setSample, BALANCED, 10);
|
||||||
|
expect(result.current.alert).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows only after 7 consecutive imbalanced samples", () => {
|
||||||
|
const { result } = renderHook(() => useTestHook());
|
||||||
|
feed(result.current.setSample, IMBALANCED, 6);
|
||||||
|
expect(result.current.alert).toBeNull();
|
||||||
|
feed(result.current.setSample, IMBALANCED, 1);
|
||||||
|
expect(result.current.alert).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports the correct pins and spread", () => {
|
||||||
|
const { result } = renderHook(() => useTestHook());
|
||||||
|
feed(result.current.setSample, IMBALANCED, 7);
|
||||||
|
const alert = result.current.alert!;
|
||||||
|
expect(alert.diff).toBeCloseTo(2.1, 5);
|
||||||
|
expect(alert.hiPin).toBe(5); // 5.6 A
|
||||||
|
expect(alert.loPin).toBe(6); // 3.5 A
|
||||||
|
expect(alert.maxI).toBeCloseTo(5.6, 5);
|
||||||
|
expect(alert.minI).toBeCloseTo(3.5, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("a single balanced sample in the streak resets the counter", () => {
|
||||||
|
const { result } = renderHook(() => useTestHook());
|
||||||
|
feed(result.current.setSample, IMBALANCED, 6);
|
||||||
|
feed(result.current.setSample, BALANCED, 1);
|
||||||
|
feed(result.current.setSample, IMBALANCED, 6);
|
||||||
|
expect(result.current.alert).toBeNull();
|
||||||
|
feed(result.current.setSample, IMBALANCED, 1);
|
||||||
|
expect(result.current.alert).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps showing for 5 samples after the imbalance clears, then hides", () => {
|
||||||
|
const { result } = renderHook(() => useTestHook());
|
||||||
|
feed(result.current.setSample, IMBALANCED, 7);
|
||||||
|
expect(result.current.alert).not.toBeNull();
|
||||||
|
feed(result.current.setSample, BALANCED, 4);
|
||||||
|
expect(result.current.alert).not.toBeNull(); // grace period
|
||||||
|
feed(result.current.setSample, BALANCED, 1);
|
||||||
|
expect(result.current.alert).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stays shown (with updated values) while the imbalance persists", () => {
|
||||||
|
const { result } = renderHook(() => useTestHook());
|
||||||
|
feed(result.current.setSample, IMBALANCED, 7);
|
||||||
|
feed(result.current.setSample, [4.5, 4.6, 4.4, 4.8, 6.1, 3.2], 1);
|
||||||
|
const alert = result.current.alert!;
|
||||||
|
expect(alert.diff).toBeCloseTo(2.9, 5);
|
||||||
|
expect(alert.hiPin).toBe(5);
|
||||||
|
expect(alert.loPin).toBe(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears immediately when the sample disappears (device unplugged)", () => {
|
||||||
|
const { result } = renderHook(() => useTestHook());
|
||||||
|
feed(result.current.setSample, IMBALANCED, 7);
|
||||||
|
expect(result.current.alert).not.toBeNull();
|
||||||
|
act(() => result.current.setSample(null));
|
||||||
|
expect(result.current.alert).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores samples with fewer than 2 pins", () => {
|
||||||
|
const { result } = renderHook(() => useTestHook());
|
||||||
|
feed(result.current.setSample, [10], 5);
|
||||||
|
expect(result.current.alert).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import type { WireViewSample } from "../types.js";
|
||||||
|
|
||||||
|
// Current difference between any two pins that indicates a bad contact.
|
||||||
|
const PIN_IMBALANCE_A = 2;
|
||||||
|
// Show only after the imbalance holds for this many consecutive samples (~1 s each).
|
||||||
|
// AI workloads produce frequent short load ramps that skew one pin transiently,
|
||||||
|
// so the threshold is deliberately high: a bad contact stays imbalanced, a ramp does not.
|
||||||
|
const SHOW_AFTER = 7;
|
||||||
|
// Keep showing this many samples after the imbalance clears (grace period).
|
||||||
|
const HIDE_AFTER = 5;
|
||||||
|
|
||||||
|
export interface ImbalanceAlert {
|
||||||
|
diff: number;
|
||||||
|
maxI: number;
|
||||||
|
minI: number;
|
||||||
|
/** 1-based pin index carrying the most current */
|
||||||
|
hiPin: number;
|
||||||
|
/** 1-based pin index carrying the least current */
|
||||||
|
loPin: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounced pin-current imbalance detection.
|
||||||
|
*
|
||||||
|
* A single sample can transiently skew (load change, measurement glitch), so
|
||||||
|
* the alert only appears after the imbalance persists for a few samples, and
|
||||||
|
* stays visible briefly after it clears instead of flickering.
|
||||||
|
*/
|
||||||
|
export function useWireviewImbalance(
|
||||||
|
sample: WireViewSample | null,
|
||||||
|
): ImbalanceAlert | null {
|
||||||
|
const [alert, setAlert] = useState<ImbalanceAlert | null>(null);
|
||||||
|
const alertRef = useRef<ImbalanceAlert | null>(null);
|
||||||
|
const streak = useRef(0);
|
||||||
|
const grace = useRef(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function update(next: ImbalanceAlert | null) {
|
||||||
|
alertRef.current = next;
|
||||||
|
setAlert(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sample || sample.pins.length < 2) {
|
||||||
|
streak.current = 0;
|
||||||
|
grace.current = 0;
|
||||||
|
if (alertRef.current) update(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currents = sample.pins.map((p) => p.current_a);
|
||||||
|
const maxI = Math.max(...currents);
|
||||||
|
const minI = Math.min(...currents);
|
||||||
|
const diff = maxI - minI;
|
||||||
|
|
||||||
|
if (diff > PIN_IMBALANCE_A) {
|
||||||
|
streak.current += 1;
|
||||||
|
grace.current = 0;
|
||||||
|
if (streak.current >= SHOW_AFTER) {
|
||||||
|
update({
|
||||||
|
diff,
|
||||||
|
maxI,
|
||||||
|
minI,
|
||||||
|
hiPin: currents.indexOf(maxI) + 1,
|
||||||
|
loPin: currents.indexOf(minI) + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
streak.current = 0;
|
||||||
|
if (alertRef.current) {
|
||||||
|
grace.current += 1;
|
||||||
|
if (grace.current >= HIDE_AFTER) update(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [sample]);
|
||||||
|
|
||||||
|
return alert;
|
||||||
|
}
|
||||||
@@ -178,6 +178,8 @@ export interface WireViewSample {
|
|||||||
timestamp: number;
|
timestamp: number;
|
||||||
power_total_w: number;
|
power_total_w: number;
|
||||||
current_total_a: number;
|
current_total_a: number;
|
||||||
|
/** Spread (A) between the highest- and lowest-loaded pin */
|
||||||
|
pin_imbalance_a: number;
|
||||||
voltage_avg_v: number;
|
voltage_avg_v: number;
|
||||||
pins: WireViewPin[];
|
pins: WireViewPin[];
|
||||||
// Temperatures are null when the source has no channel for them
|
// Temperatures are null when the source has no channel for them
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: "happy-dom",
|
||||||
|
include: ["src/**/*.test.ts"],
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -152,10 +152,16 @@ def parse_sensor_struct(buf: bytes) -> dict:
|
|||||||
power_total += voltage_v * current_a
|
power_total += voltage_v * current_a
|
||||||
current_total += current_a
|
current_total += current_a
|
||||||
|
|
||||||
|
# Spread between the highest- and lowest-loaded pin; a growing spread
|
||||||
|
# indicates a bad pin contact (see the frontend imbalance warning).
|
||||||
|
pin_currents = [p["current_a"] for p in pins]
|
||||||
|
pin_imbalance = round(max(pin_currents) - min(pin_currents), 3)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"timestamp": time.time(),
|
"timestamp": time.time(),
|
||||||
"power_total_w": round(power_total, 3),
|
"power_total_w": round(power_total, 3),
|
||||||
"current_total_a": round(current_total, 3),
|
"current_total_a": round(current_total, 3),
|
||||||
|
"pin_imbalance_a": pin_imbalance,
|
||||||
"voltage_avg_v": (
|
"voltage_avg_v": (
|
||||||
round(power_total / current_total, 3) if current_total > 0 else 0.0
|
round(power_total / current_total, 3) if current_total > 0 else 0.0
|
||||||
),
|
),
|
||||||
@@ -608,11 +614,14 @@ class WireViewHwmonDevice:
|
|||||||
for v, i in zip(pin_voltage, pin_current, strict=True)
|
for v, i in zip(pin_voltage, pin_current, strict=True)
|
||||||
)
|
)
|
||||||
current_total = sum(pin_current)
|
current_total = sum(pin_current)
|
||||||
|
pin_currents = [round(i, 3) for i in pin_current]
|
||||||
|
pin_imbalance = round(max(pin_currents) - min(pin_currents), 3)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"timestamp": time.time(),
|
"timestamp": time.time(),
|
||||||
"power_total_w": round(power_total, 3),
|
"power_total_w": round(power_total, 3),
|
||||||
"current_total_a": round(current_total, 3),
|
"current_total_a": round(current_total, 3),
|
||||||
|
"pin_imbalance_a": pin_imbalance,
|
||||||
"voltage_avg_v": (
|
"voltage_avg_v": (
|
||||||
round(power_total / current_total, 3)
|
round(power_total / current_total, 3)
|
||||||
if current_total > 0
|
if current_total > 0
|
||||||
|
|||||||
@@ -89,6 +89,18 @@ def test_parse_sensor_struct():
|
|||||||
"total power",
|
"total power",
|
||||||
abs(s["power_total_w"] - round(6 * 11.904 * 6.221, 3)) < 1e-9,
|
abs(s["power_total_w"] - round(6 * 11.904 * 6.221, 3)) < 1e-9,
|
||||||
)
|
)
|
||||||
|
check(
|
||||||
|
"pin imbalance (uniform)",
|
||||||
|
s["pin_imbalance_a"] == 0.0,
|
||||||
|
)
|
||||||
|
# Imbalance is the spread between the highest- and lowest-loaded pin.
|
||||||
|
uneven = wv.parse_sensor_struct(
|
||||||
|
make_frame(pins=((11904, 6221, 74055),) * 5 + ((11900, 1100, 13090),))
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
"pin imbalance (uneven)",
|
||||||
|
abs(uneven["pin_imbalance_a"] - 5.121) < 1e-9,
|
||||||
|
)
|
||||||
check(
|
check(
|
||||||
"avg voltage",
|
"avg voltage",
|
||||||
abs(s["voltage_avg_v"] - 11.904) < 1e-3,
|
abs(s["voltage_avg_v"] - 11.904) < 1e-3,
|
||||||
@@ -296,6 +308,7 @@ def test_hwmon_device():
|
|||||||
"hwmon total current",
|
"hwmon total current",
|
||||||
abs(s["current_total_a"] - round(6 * 6.221, 3)) < 1e-9,
|
abs(s["current_total_a"] - round(6 * 6.221, 3)) < 1e-9,
|
||||||
)
|
)
|
||||||
|
check("hwmon pin imbalance (uniform)", s["pin_imbalance_a"] == 0.0)
|
||||||
dev.close()
|
dev.close()
|
||||||
check("hwmon closed", not dev.connected)
|
check("hwmon closed", not dev.connected)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user