Compare commits

..

2 Commits

Author SHA1 Message Date
community-scripts-pr-app[bot]
afa385d225 Update CHANGELOG.md (#9345)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-11-22 00:12:30 +00:00
community-scripts-pr-app[bot]
f43efc1431 Update versions.json (#9344)
Co-authored-by: GitHub Actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-11-22 01:12:09 +01:00
8 changed files with 480 additions and 1144 deletions

View File

@@ -10,6 +10,8 @@
> [!CAUTION]
Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit the project's popularity for potentially malicious purposes.
## 2025-11-22
## 2025-11-21
### 🚀 Updated Scripts

100
frontend/bun.lock generated
View File

@@ -11,7 +11,6 @@
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
@@ -27,7 +26,7 @@
"date-fns": "^4.1.0",
"framer-motion": "^11.18.2",
"fuse.js": "^7.1.0",
"lucide-react": "^0.554.0",
"lucide-react": "^0.542.0",
"mini-svg-data-uri": "^1.4.4",
"motion": "^12.23.12",
"next": "15.5.2",
@@ -44,7 +43,6 @@
"react-icons": "^5.5.0",
"react-simple-typewriter": "^5.0.1",
"react-use-measure": "^2.1.7",
"recharts": "2.15.4",
"sharp": "^0.33.5",
"simple-icons": "^13.21.0",
"sonner": "^1.7.4",
@@ -419,8 +417,6 @@
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q=="],
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.5", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA=="],
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
@@ -519,24 +515,6 @@
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
"@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
"@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
"@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="],
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
@@ -787,28 +765,6 @@
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
"d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
"d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="],
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
"d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
"d3-scale": ["d3-scale@4.0.2", "", { "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" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
"d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
"damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="],
"data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="],
@@ -827,8 +783,6 @@
"decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="],
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
"decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
@@ -853,8 +807,6 @@
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
@@ -987,16 +939,12 @@
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
"exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="],
"fast-equals": ["fast-equals@5.3.3", "", {}, "sha512-/boTcHZeIAQ2r/tL11voclBHDeP9WPxLt+tyAbVSyyXuUFyh0Tne7gJZTqGbxnvj79TjLdCXLOY7UIPhyG5MTw=="],
"fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
@@ -1111,8 +1059,6 @@
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
"is-alphabetical": ["is-alphabetical@1.0.4", "", {}, "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="],
"is-alphanumerical": ["is-alphanumerical@1.0.4", "", { "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" } }, "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A=="],
@@ -1245,7 +1191,7 @@
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lucide-react": ["lucide-react@0.554.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-St+z29uthEJVx0Is7ellNkgTEhaeSoA42I7JjOCBCrc5X6LYMGSv0P/2uS5HDLTExP5tpiqRD2PyUEOS6s9UXA=="],
"lucide-react": ["lucide-react@0.542.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw=="],
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
@@ -1509,7 +1455,7 @@
"react-icons": ["react-icons@5.5.0", "", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="],
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
@@ -1519,24 +1465,16 @@
"react-simple-typewriter": ["react-simple-typewriter@5.0.1", "", { "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-vA5HkABwJKL/DJ4RshSlY/igdr+FiVY4MLsSQYJX6FZG/f1/VwN4y1i3mPXRyfaswrvI8xii1kOVe1dYtO2Row=="],
"react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="],
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
"react-syntax-highlighter": ["react-syntax-highlighter@15.6.1", "", { "dependencies": { "@babel/runtime": "^7.3.1", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.27.0", "refractor": "^3.6.0" }, "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg=="],
"react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
"react-use-measure": ["react-use-measure@2.1.7", "", { "peerDependencies": { "react": ">=16.13", "react-dom": ">=16.13" } }, "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg=="],
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"recharts": ["recharts@2.15.4", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw=="],
"recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="],
"refa": ["refa@0.12.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.8.0" } }, "sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g=="],
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
@@ -1687,8 +1625,6 @@
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
@@ -1757,8 +1693,6 @@
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"victory-vendor": ["victory-vendor@36.9.2", "", { "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" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="],
"vite": ["vite@7.1.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx"], "bin": "bin/vite.js" }, "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ=="],
"vite-tsconfig-paths": ["vite-tsconfig-paths@5.1.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w=="],
@@ -1823,18 +1757,18 @@
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
"@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.15.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA=="],
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
"@radix-ui/react-scroll-area/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@radix-ui/react-scroll-area/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
"@types/hast/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"@typescript-eslint/typescript-estree/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
@@ -1863,18 +1797,24 @@
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"eslint-plugin-import/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"eslint-plugin-jsdoc/@es-joy/jsdoccomment": ["@es-joy/jsdoccomment@0.52.0", "", { "dependencies": { "@types/estree": "^1.0.8", "@typescript-eslint/types": "^8.34.1", "comment-parser": "1.4.1", "esquery": "^1.6.0", "jsdoc-type-pratt-parser": "~4.1.0" } }, "sha512-BXuN7BII+8AyNtn57euU2Yxo9yA/KUDNzrpXyi3pfqKmBhhysR6ZWOebFh3vyPoqA3/j1SOvGgucElMGwlXing=="],
"eslint-plugin-jsonc/synckit": ["synckit@0.11.8", "", { "dependencies": { "@pkgr/core": "^0.2.4" } }, "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A=="],
"eslint-plugin-jsx-a11y/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"eslint-plugin-n/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
"eslint-plugin-n/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"eslint-plugin-n/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"eslint-plugin-react/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"eslint-plugin-react/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="],
"eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@@ -1911,8 +1851,6 @@
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"refractor/prismjs": ["prismjs@1.27.0", "", {}, "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA=="],
@@ -1935,6 +1873,8 @@
"tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": "lib/cli.js" }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
"w3c-xmlserializer/xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@@ -1943,14 +1883,24 @@
"yaml-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"@typescript-eslint/typescript-estree/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"eslint-plugin-import/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"eslint-plugin-jsonc/synckit/@pkgr/core": ["@pkgr/core@0.2.7", "", {}, "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg=="],
"eslint-plugin-jsx-a11y/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"eslint-plugin-n/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"eslint-plugin-react/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"eslint-plugin-unicorn/@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.13.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw=="],
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],

4
frontend/package.json generated
View File

@@ -23,7 +23,6 @@
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
@@ -39,7 +38,7 @@
"date-fns": "^4.1.0",
"framer-motion": "^11.18.2",
"fuse.js": "^7.1.0",
"lucide-react": "^0.554.0",
"lucide-react": "^0.542.0",
"mini-svg-data-uri": "^1.4.4",
"motion": "^12.23.12",
"next": "15.5.2",
@@ -56,7 +55,6 @@
"react-icons": "^5.5.0",
"react-simple-typewriter": "^5.0.1",
"react-use-measure": "^2.1.7",
"recharts": "2.15.4",
"sharp": "^0.33.5",
"simple-icons": "^13.21.0",
"sonner": "^1.7.4",

View File

@@ -1,4 +1,89 @@
[
{
"name": "TwiN/gatus",
"version": "v5.33.0",
"date": "2025-11-21T22:54:49Z"
},
{
"name": "BerriAI/litellm",
"version": "v1.78.5-stable-patch-1",
"date": "2025-11-21T19:57:45Z"
},
{
"name": "laurent22/joplin",
"version": "server-v3.4.4",
"date": "2025-09-25T13:19:26Z"
},
{
"name": "homarr-labs/homarr",
"version": "v1.44.0",
"date": "2025-11-21T19:17:09Z"
},
{
"name": "gelbphoenix/autocaliweb",
"version": "v0.11.0",
"date": "2025-11-21T18:42:15Z"
},
{
"name": "rclone/rclone",
"version": "v1.72.0",
"date": "2025-11-21T18:20:58Z"
},
{
"name": "readeck/readeck",
"version": "0.21.1",
"date": "2025-11-21T17:17:52Z"
},
{
"name": "mattermost/mattermost",
"version": "v10.11.8",
"date": "2025-11-21T17:06:07Z"
},
{
"name": "home-assistant/core",
"version": "2025.11.3",
"date": "2025-11-21T17:03:22Z"
},
{
"name": "Bubka/2FAuth",
"version": "v5.6.1",
"date": "2025-11-21T16:51:21Z"
},
{
"name": "steveiliop56/tinyauth",
"version": "v4.0.1",
"date": "2025-10-15T16:53:55Z"
},
{
"name": "BookStackApp/BookStack",
"version": "v25.11.3",
"date": "2025-11-21T14:06:50Z"
},
{
"name": "pommee/goaway",
"version": "v0.62.19",
"date": "2025-11-21T13:24:09Z"
},
{
"name": "fuma-nama/fumadocs",
"version": "fumadocs-ui@16.1.0",
"date": "2025-11-21T13:02:57Z"
},
{
"name": "forgejo/forgejo",
"version": "v13.0.3",
"date": "2025-11-21T12:43:04Z"
},
{
"name": "bunkerity/bunkerweb",
"version": "v1.6.5",
"date": "2025-10-06T15:25:17Z"
},
{
"name": "chrisbenincasa/tunarr",
"version": "v0.23.0-alpha.24",
"date": "2025-11-21T12:16:39Z"
},
{
"name": "rcourtman/Pulse",
"version": "v4.32.3",
@@ -29,11 +114,6 @@
"version": "v1.11.0",
"date": "2025-11-21T08:09:03Z"
},
{
"name": "mattermost/mattermost",
"version": "v10.11.7",
"date": "2025-11-17T08:40:53Z"
},
{
"name": "MariaDB/server",
"version": "mariadb-12.1.2",
@@ -69,11 +149,6 @@
"version": "v2.1.11",
"date": "2025-11-20T20:14:44Z"
},
{
"name": "readeck/readeck",
"version": "0.21.0",
"date": "2025-11-20T18:35:59Z"
},
{
"name": "saltstack/salt",
"version": "v3007.9",
@@ -89,6 +164,11 @@
"version": "v1.30.12",
"date": "2025-11-20T16:13:19Z"
},
{
"name": "neo4j/neo4j",
"version": "5.26.17",
"date": "2025-11-20T16:09:28Z"
},
{
"name": "meilisearch/meilisearch",
"version": "prototype-v1.26.0.arm-native-docker-build-1",
@@ -124,11 +204,6 @@
"version": "v2.1.1",
"date": "2025-11-20T10:54:07Z"
},
{
"name": "fuma-nama/fumadocs",
"version": "fumadocs-ui@16.0.15",
"date": "2025-11-20T10:52:04Z"
},
{
"name": "Athou/commafeed",
"version": "5.12.0",
@@ -149,16 +224,6 @@
"version": "v1.56.0",
"date": "2025-11-20T02:28:35Z"
},
{
"name": "BerriAI/litellm",
"version": "v1.80.0.dev6",
"date": "2025-11-20T02:03:21Z"
},
{
"name": "steveiliop56/tinyauth",
"version": "v4.0.1",
"date": "2025-10-15T16:53:55Z"
},
{
"name": "coder/code-server",
"version": "v4.106.2",
@@ -199,11 +264,6 @@
"version": "0.51.3",
"date": "2025-11-19T15:45:28Z"
},
{
"name": "BookStackApp/BookStack",
"version": "v25.11.2",
"date": "2025-11-19T15:26:29Z"
},
{
"name": "grafana/grafana",
"version": "v12.3.0",
@@ -259,16 +319,6 @@
"version": "v1.0.1-stable",
"date": "2025-11-10T16:51:44Z"
},
{
"name": "bunkerity/bunkerweb",
"version": "testing",
"date": "2025-11-17T16:24:26Z"
},
{
"name": "chrisbenincasa/tunarr",
"version": "v0.23.0-alpha.23",
"date": "2025-11-18T21:36:15Z"
},
{
"name": "umami-software/umami",
"version": "v3.0.1",
@@ -369,11 +419,6 @@
"version": "v0.21.0",
"date": "2025-08-23T18:33:53Z"
},
{
"name": "TwiN/gatus",
"version": "v5.32.0",
"date": "2025-11-16T21:08:56Z"
},
{
"name": "binwiederhier/ntfy",
"version": "v2.15.0",
@@ -439,16 +484,6 @@
"version": "v0.16.1",
"date": "2025-11-14T22:50:06Z"
},
{
"name": "home-assistant/core",
"version": "2025.11.2",
"date": "2025-11-14T22:10:50Z"
},
{
"name": "homarr-labs/homarr",
"version": "v1.43.3",
"date": "2025-11-14T19:16:17Z"
},
{
"name": "mealie-recipes/mealie",
"version": "v3.5.0",
@@ -489,11 +524,6 @@
"version": "v1.7.7",
"date": "2025-11-13T21:28:44Z"
},
{
"name": "pommee/goaway",
"version": "v0.62.18",
"date": "2025-11-13T19:49:21Z"
},
{
"name": "pocketbase/pocketbase",
"version": "v0.33.0",
@@ -704,11 +734,6 @@
"version": "v0.9.1",
"date": "2025-11-06T02:26:53Z"
},
{
"name": "neo4j/neo4j",
"version": "5.26.16",
"date": "2025-11-05T20:41:40Z"
},
{
"name": "leiweibau/Pi.Alert",
"version": "v2025-11-05",
@@ -864,11 +889,6 @@
"version": "v1.11.1",
"date": "2025-10-29T22:09:26Z"
},
{
"name": "laurent22/joplin",
"version": "server-v3.4.4",
"date": "2025-09-25T13:19:26Z"
},
{
"name": "apache/cassandra",
"version": "cassandra-5.0.6",
@@ -899,11 +919,6 @@
"version": "v25.8.0",
"date": "2025-10-26T14:23:37Z"
},
{
"name": "forgejo/forgejo",
"version": "v13.0.2",
"date": "2025-10-26T06:33:05Z"
},
{
"name": "usememos/memos",
"version": "v0.25.2",
@@ -939,11 +954,6 @@
"version": "v3.2.5-beta",
"date": "2025-10-21T16:49:14Z"
},
{
"name": "rclone/rclone",
"version": "v1.71.2",
"date": "2025-10-20T15:25:52Z"
},
{
"name": "Part-DB/Part-DB-server",
"version": "v2.2.1",
@@ -1009,11 +1019,6 @@
"version": "v5.0.85",
"date": "2025-10-12T19:55:18Z"
},
{
"name": "gelbphoenix/autocaliweb",
"version": "v0.10.4",
"date": "2025-10-11T19:53:39Z"
},
{
"name": "projectsend/projectsend",
"version": "r1945",
@@ -1314,11 +1319,6 @@
"version": "v1.11.11",
"date": "2025-06-18T18:04:50Z"
},
{
"name": "Bubka/2FAuth",
"version": "v5.6.0",
"date": "2025-06-18T12:19:54Z"
},
{
"name": "TriliumNext/Notes",
"version": "v0.95.0",

View File

@@ -1,67 +1,9 @@
"use client";
import {
ArrowUpDown,
Box,
CheckCircle2,
ChevronLeft,
ChevronRight,
List,
Loader2,
Trophy,
XCircle,
} from "lucide-react";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
LabelList,
XAxis,
} from "recharts";
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, useState } from "react";
import "react-datepicker/dist/react-datepicker.css";
import type { ChartConfig } from "@/components/ui/chart";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { formattedBadge } from "@/components/command-menu";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import ApplicationChart from "../../components/application-chart";
type DataModel = {
id: number;
@@ -88,76 +30,42 @@ type SummaryData = {
nsapp_count: Record<string, number>;
};
// Chart colors optimized for both light and dark modes
// Medium-toned colors that are visible and not too flashy in both themes
const CHART_COLORS = [
"#5B8DEF", // blue - medium tone
"#4ECDC4", // teal - medium tone
"#FF8C42", // orange - medium tone
"#A78BFA", // purple - medium tone
"#F472B6", // pink - medium tone
"#38BDF8", // cyan - medium tone
"#4ADE80", // green - medium tone
"#FBBF24", // yellow - medium tone
"#818CF8", // indigo - medium tone
"#FB7185", // rose - medium tone
"#2DD4BF", // turquoise - medium tone
"#C084FC", // violet - medium tone
"#60A5FA", // sky blue - medium tone
"#84CC16", // lime - medium tone
"#F59E0B", // amber - medium tone
"#A855F7", // purple - medium tone
"#10B981", // emerald - medium tone
"#EAB308", // gold - medium tone
"#3B82F6", // royal blue - medium tone
"#EF4444", // red - medium tone
];
const chartConfigApps = {
count: {
label: "Installations",
color: "hsl(var(--chart-1))",
},
} satisfies ChartConfig;
export default function DataPage() {
const DataFetcher: React.FC = () => {
const [data, setData] = useState<DataModel[]>([]);
const [summary, setSummary] = useState<SummaryData | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(25);
const [sortConfig, setSortConfig] = useState<{
key: string;
direction: "ascending" | "descending";
} | null>(null);
const [sortConfig, setSortConfig] = useState<{ key: string; direction: "ascending" | "descending" } | null>(null);
const nf = new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 });
useEffect(() => {
const fetchData = async () => {
const fetchSummary = async () => {
try {
const response = await fetch("https://api.htl-braunau.at/data/summary");
if (!response.ok)
throw new Error(`Failed to fetch summary: ${response.statusText}`);
const result: SummaryData = await response.json();
setSummary(result);
}
catch (err) {
setError((err as Error).message);
}
};
fetchSummary();
}, []);
useEffect(() => {
const fetchPaginatedData = async () => {
setLoading(true);
try {
const [summaryRes, dataRes] = await Promise.all([
fetch("https://api.htl-braunau.at/data/summary"),
fetch(
`https://api.htl-braunau.at/data/paginated?page=${currentPage}&limit=${itemsPerPage === 0 ? "" : itemsPerPage
}`,
),
]);
if (!summaryRes.ok) {
throw new Error(`Failed to fetch summary: ${summaryRes.statusText}`);
}
if (!dataRes.ok) {
throw new Error(`Failed to fetch data: ${dataRes.statusText}`);
}
const summaryData: SummaryData = await summaryRes.json();
const pageData: DataModel[] = await dataRes.json();
setSummary(summaryData);
setData(pageData);
const response = await fetch(`https://api.htl-braunau.at/data/paginated?page=${currentPage}&limit=${itemsPerPage === 0 ? "" : itemsPerPage}`);
if (!response.ok)
throw new Error(`Failed to fetch data: ${response.statusText}`);
const result: DataModel[] = await response.json();
setData(result);
}
catch (err) {
setError((err as Error).message);
@@ -167,13 +75,13 @@ export default function DataPage() {
}
};
fetchData();
fetchPaginatedData();
}, [currentPage, itemsPerPage]);
const sortedData = useMemo(() => {
const sortedData = React.useMemo(() => {
if (!sortConfig)
return data;
return [...data].sort((a, b) => {
const sorted = [...data].sort((a, b) => {
if (a[sortConfig.key] < b[sortConfig.key]) {
return sortConfig.direction === "ascending" ? -1 : 1;
}
@@ -182,15 +90,23 @@ export default function DataPage() {
}
return 0;
});
return sorted;
}, [data, sortConfig]);
if (loading)
return <p>Loading...</p>;
if (error) {
return (
<p>
Error:
{error}
</p>
);
}
const requestSort = (key: string) => {
let direction: "ascending" | "descending" = "ascending";
if (
sortConfig
&& sortConfig.key === key
&& sortConfig.direction === "ascending"
) {
if (sortConfig && sortConfig.key === key && sortConfig.direction === "ascending") {
direction = "descending";
}
setSortConfig({ key, direction });
@@ -198,447 +114,135 @@ export default function DataPage() {
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
return new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
timeStyle: "short",
}).format(date);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const timezoneOffset = dateString.slice(-6);
return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`;
};
const getTypeBadge = (type: string) => {
if (type === "lxc")
return formattedBadge("ct");
if (type === "vm")
return formattedBadge("vm");
return null;
};
// Stats calculations
const successCount = summary?.status_count.done ?? 0;
const failureCount = summary?.status_count.failed ?? 0;
const totalCount = summary?.total_entries ?? 0;
const successRate = totalCount > 0 ? (successCount / totalCount) * 100 : 0;
const allApps = useMemo(() => {
if (!summary?.nsapp_count)
return [];
return Object.entries(summary.nsapp_count).sort(([, a], [, b]) => b - a);
}, [summary]);
const topApps = useMemo(() => {
return allApps.slice(0, 15);
}, [allApps]);
const mostPopularApp = topApps[0];
// Chart Data
const appsChartData = topApps.map(([name, count], index) => ({
app: name,
count,
fill: CHART_COLORS[index % CHART_COLORS.length],
}));
if (error) {
return (
<div className="p-6 text-center text-red-500">
<p>
Error loading data:
{error}
</p>
</div>
);
}
return (
<div className="mb-3">
<div className="mt-20 flex sm:px-4 xl:px-0">
<div className="mx-4 w-full sm:mx-0 space-y-8">
{/* Header */}
<div>
<h1 className="text-3xl font-bold tracking-tight">Analytics</h1>
<p className="text-muted-foreground">
Overview of container installations and system statistics.
</p>
</div>
{/* Widgets */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Created</CardTitle>
<Box className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{nf.format(totalCount)}</div>
<p className="text-xs text-muted-foreground">
Total LXC/VM entries found
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Success Rate</CardTitle>
<CheckCircle2 className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{successRate.toFixed(1)}
%
</div>
<p className="text-xs text-muted-foreground">
{nf.format(successCount)}
{" "}
successful installations
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Failures</CardTitle>
<XCircle className="h-4 w-4 text-red-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{nf.format(failureCount)}</div>
<p className="text-xs text-muted-foreground">
Installations encountered errors
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Most Popular</CardTitle>
<Trophy className="h-4 w-4 text-yellow-500" />
</CardHeader>
<CardContent>
<div className="truncate text-2xl font-bold">
{mostPopularApp ? mostPopularApp[0] : "N/A"}
</div>
<p className="text-xs text-muted-foreground">
{mostPopularApp ? nf.format(mostPopularApp[1]) : 0}
{" "}
installations
</p>
</CardContent>
</Card>
</div>
{/* Graphs */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div className="space-y-1.5">
<CardTitle>Top Applications</CardTitle>
<CardDescription>
The most frequently installed applications.
</CardDescription>
</div>
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="sm" className="ml-auto">
<List className="mr-2 h-4 w-4" />
View All
</Button>
</DialogTrigger>
<DialogContent className="max-h-[80vh] sm:max-w-md">
<DialogHeader>
<DialogTitle>Application Statistics</DialogTitle>
<DialogDescription>
Installation counts for all
{" "}
{allApps.length}
{" "}
applications.
</DialogDescription>
</DialogHeader>
<ScrollArea className="h-[60vh] w-full rounded-md border p-4">
<div className="space-y-4">
{allApps.map(([name, count], index) => (
<div
key={name}
className="flex items-center justify-between text-sm"
>
<div className="flex items-center gap-2">
<span className="w-8 font-mono text-muted-foreground">
{index + 1}
.
</span>
<span className="font-medium">{name}</span>
</div>
<span className="font-mono">{nf.format(count)}</span>
</div>
))}
</div>
</ScrollArea>
</DialogContent>
</Dialog>
</CardHeader>
<CardContent className="pl-2">
<div className="h-[300px] w-full">
{loading
? (
<div className="flex h-full w-full items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)
: (
<ChartContainer config={chartConfigApps} className="h-full w-full">
<BarChart
accessibilityLayer
data={appsChartData}
margin={{
top: 20,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="app"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={value => (value.length > 8 ? `${value.slice(0, 8)}...` : value)}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent nameKey="app" />}
/>
<Bar dataKey="count" radius={8}>
{appsChartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.fill} />
))}
<LabelList
position="top"
offset={12}
className="fill-foreground"
fontSize={12}
/>
</Bar>
</BarChart>
</ChartContainer>
)}
</div>
</CardContent>
</Card>
{/* Data Table */}
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle>Installation Log</CardTitle>
<CardDescription>
Detailed records of all container creation attempts.
</CardDescription>
</div>
<div className="flex items-center gap-2">
<Select
value={String(itemsPerPage)}
onValueChange={val => setItemsPerPage(Number(val))}
>
<SelectTrigger className="w-[80px]">
<SelectValue placeholder="Limit" />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10</SelectItem>
<SelectItem value="25">25</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
</SelectContent>
</Select>
</div>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead
className="w-[100px] cursor-pointer"
onClick={() => requestSort("status")}
>
Status
{sortConfig?.key === "status" && (
<ArrowUpDown className="ml-2 inline h-4 w-4" />
)}
</TableHead>
<TableHead
className="cursor-pointer"
onClick={() => requestSort("type")}
>
Type
{sortConfig?.key === "type" && (
<ArrowUpDown className="ml-2 inline h-4 w-4" />
)}
</TableHead>
<TableHead
className="cursor-pointer"
onClick={() => requestSort("nsapp")}
>
Application
{sortConfig?.key === "nsapp" && (
<ArrowUpDown className="ml-2 inline h-4 w-4" />
)}
</TableHead>
<TableHead
className="hidden cursor-pointer md:table-cell"
onClick={() => requestSort("os_type")}
>
OS
{sortConfig?.key === "os_type" && (
<ArrowUpDown className="ml-2 inline h-4 w-4" />
)}
</TableHead>
<TableHead
className="hidden cursor-pointer md:table-cell"
onClick={() => requestSort("disk_size")}
>
Disk Size
{sortConfig?.key === "disk_size" && (
<ArrowUpDown className="ml-2 inline h-4 w-4" />
)}
</TableHead>
<TableHead
className="hidden cursor-pointer lg:table-cell"
onClick={() => requestSort("core_count")}
>
Core Count
{sortConfig?.key === "core_count" && (
<ArrowUpDown className="ml-2 inline h-4 w-4" />
)}
</TableHead>
<TableHead
className="hidden cursor-pointer lg:table-cell"
onClick={() => requestSort("ram_size")}
>
RAM Size
{sortConfig?.key === "ram_size" && (
<ArrowUpDown className="ml-2 inline h-4 w-4" />
)}
</TableHead>
<TableHead
className="cursor-pointer text-right"
onClick={() => requestSort("created_at")}
>
Created At
{sortConfig?.key === "created_at" && (
<ArrowUpDown className="ml-2 inline h-4 w-4" />
)}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading
<div className="p-6 mt-20">
<h1 className="text-2xl font-bold mb-4 text-center">Created LXCs</h1>
<ApplicationChart data={summary} />
<p className="text-lg font-bold mt-4"> </p>
<div className="mb-4 flex justify-between items-center">
<p className="text-lg font-bold">
{nf.format(
summary?.total_entries ?? 0,
)}
{" "}
results found
</p>
<p className="text-lg font">
Status Legend: 🔄 installing
{" "}
{nf.format(summary?.status_count.installing ?? 0)}
{" "}
| completed
{" "}
{nf.format(summary?.status_count.done ?? 0)}
{" "}
| failed
{" "}
{nf.format(summary?.status_count.failed ?? 0)}
{" "}
| unknown
</p>
</div>
<div className="overflow-x-auto">
<div className="overflow-y-auto lg:overflow-y-visible">
<table className="min-w-full table-auto border-collapse">
<thead>
<tr>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("status")}>Status</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("type")}>Type</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("nsapp")}>Application</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("os_type")}>OS</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("os_version")}>OS Version</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("disk_size")}>Disk Size</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("core_count")}>Core Count</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("ram_size")}>RAM Size</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("method")}>Method</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("pve_version")}>PVE Version</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("error")}>Error Message</th>
<th className="px-4 py-2 border-b cursor-pointer" onClick={() => requestSort("created_at")}>Created At</th>
</tr>
</thead>
<tbody>
{sortedData.map((item, index) => (
<tr key={index}>
<td className="px-4 py-2 border-b">
{item.status === "done"
? (
<TableRow>
<TableCell colSpan={8} className="h-24 text-center">
<div className="flex items-center justify-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
{" "}
Loading data...
</div>
</TableCell>
</TableRow>
"✔️"
)
: sortedData.length > 0
: item.status === "failed"
? (
sortedData.map((item, idx) => (
<TableRow key={`${item.id}-${idx}`}>
<TableCell>
{item.status === "done"
? (
<Badge className="text-green-500/75 border-green-500/75">
Success
</Badge>
)
: item.status === "failed"
? (
<Badge className="text-red-500/75 border-red-500/75">
Failed
</Badge>
)
: item.status === "installing"
? (
<Badge className="text-blue-500/75 border-blue-500/75">
Installing
</Badge>
)
: (
<Badge variant="outline">
{item.status}
</Badge>
)}
</TableCell>
<TableCell>
{getTypeBadge(item.type) || (
<Badge variant="outline">
{item.type}
</Badge>
)}
</TableCell>
<TableCell className="font-medium">
{item.nsapp}
</TableCell>
<TableCell className="hidden md:table-cell">
{item.os_type}
{" "}
{item.os_version}
</TableCell>
<TableCell className="hidden md:table-cell">
{item.disk_size}
MB
</TableCell>
<TableCell className="hidden lg:table-cell">
{item.core_count}
</TableCell>
<TableCell className="hidden lg:table-cell">
{item.ram_size}
MB
</TableCell>
<TableCell className="text-right">
{formatDate(item.created_at)}
</TableCell>
</TableRow>
))
"❌"
)
: item.status === "installing"
? (
"🔄"
)
: (
item.status
)}
</td>
<td className="px-4 py-2 border-b">
{item.type === "lxc"
? (
"📦"
)
: item.type === "vm"
? (
"🖥️"
)
: (
<TableRow>
<TableCell colSpan={8} className="h-24 text-center">
No results found.
</TableCell>
</TableRow>
item.type
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1 || loading}
>
<ChevronLeft className="mr-2 h-4 w-4" />
Previous
</Button>
<div className="text-sm text-muted-foreground">
Page
{" "}
{currentPage}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => prev + 1)}
disabled={loading || sortedData.length < itemsPerPage}
>
Next
<ChevronRight className="ml-2 h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
</td>
<td className="px-4 py-2 border-b">{item.nsapp}</td>
<td className="px-4 py-2 border-b">{item.os_type}</td>
<td className="px-4 py-2 border-b">{item.os_version}</td>
<td className="px-4 py-2 border-b">{item.disk_size}</td>
<td className="px-4 py-2 border-b">{item.core_count}</td>
<td className="px-4 py-2 border-b">{item.ram_size}</td>
<td className="px-4 py-2 border-b">{item.method}</td>
<td className="px-4 py-2 border-b">{item.pve_version}</td>
<td className="px-4 py-2 border-b">{item.error}</td>
<td className="px-4 py-2 border-b">{formatDate(item.created_at)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div className="mt-4 flex justify-between items-center">
<button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage === 1} className="p-2 border">Previous</button>
<span>
Page
{currentPage}
</span>
<button onClick={() => setCurrentPage(prev => prev + 1)} className="p-2 border">Next</button>
<select
value={itemsPerPage}
onChange={e => setItemsPerPage(Number(e.target.value))}
className="p-2 border"
>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={50}>50</option>
<option value={100}>100</option>
<option value={250}>250</option>
<option value={500}>500</option>
<option value={5000}>5000</option>
</select>
</div>
</div>
);
}
};
export default DataFetcher;

View File

@@ -0,0 +1,199 @@
"use client";
import { ArcElement, Chart as ChartJS, Tooltip as ChartTooltip, Legend } from "chart.js";
import ChartDataLabels from "chartjs-plugin-datalabels";
import { BarChart3, PieChart } from "lucide-react";
import React, { useState } from "react";
import { Pie } from "react-chartjs-2";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
ChartJS.register(ArcElement, ChartTooltip, Legend, ChartDataLabels);
type SummaryData = {
nsapp_count: Record<string, number>;
};
type ApplicationChartProps = {
data: SummaryData | null;
};
const ITEMS_PER_PAGE = 20;
const CHART_COLORS = [
"#ff6384",
"#36a2eb",
"#ffce56",
"#4bc0c0",
"#9966ff",
"#ff9f40",
"#4dc9f6",
"#f67019",
"#537bc4",
"#acc236",
"#166a8f",
"#00a950",
"#58595b",
"#8549ba",
];
export default function ApplicationChart({ data }: ApplicationChartProps) {
const [isChartOpen, setIsChartOpen] = useState(false);
const [isTableOpen, setIsTableOpen] = useState(false);
const [chartStartIndex, setChartStartIndex] = useState(0);
const [tableLimit, setTableLimit] = useState(ITEMS_PER_PAGE);
if (!data)
return null;
const sortedApps = Object.entries(data.nsapp_count)
.sort(([, a], [, b]) => b - a);
const chartApps = sortedApps.slice(
chartStartIndex,
chartStartIndex + ITEMS_PER_PAGE,
);
const chartData = {
labels: chartApps.map(([name]) => name),
datasets: [
{
data: chartApps.map(([, count]) => count),
backgroundColor: CHART_COLORS,
},
],
};
const chartOptions = {
plugins: {
legend: { display: false },
datalabels: {
color: "white",
font: { weight: "bold" as const },
formatter: (value: number, context: any) => {
const label = context.chart.data.labels?.[context.dataIndex];
return `${label}\n(${value})`;
},
},
},
responsive: true,
maintainAspectRatio: false,
};
return (
<div className="mt-6 flex justify-center gap-4">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
onClick={() => setIsChartOpen(true)}
>
<PieChart className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>Open Chart View</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
onClick={() => setIsTableOpen(true)}
>
<BarChart3 className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent>Open Table View</TooltipContent>
</Tooltip>
</TooltipProvider>
<Dialog open={isChartOpen} onOpenChange={setIsChartOpen}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>Applications Distribution</DialogTitle>
</DialogHeader>
<div className="h-[60vh] w-full">
<Pie data={chartData} options={chartOptions} />
</div>
<div className="flex justify-center gap-4">
<Button
variant="outline"
onClick={() => setChartStartIndex(Math.max(0, chartStartIndex - ITEMS_PER_PAGE))}
disabled={chartStartIndex === 0}
>
Previous
{" "}
{ITEMS_PER_PAGE}
</Button>
<Button
variant="outline"
onClick={() => setChartStartIndex(chartStartIndex + ITEMS_PER_PAGE)}
disabled={chartStartIndex + ITEMS_PER_PAGE >= sortedApps.length}
>
Next
{" "}
{ITEMS_PER_PAGE}
</Button>
</div>
</DialogContent>
</Dialog>
<Dialog open={isTableOpen} onOpenChange={setIsTableOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Applications Count</DialogTitle>
</DialogHeader>
<div className="max-h-[60vh] overflow-y-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Application</TableHead>
<TableHead className="text-right">Count</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedApps.slice(0, tableLimit).map(([name, count]) => (
<TableRow key={name}>
<TableCell>{name}</TableCell>
<TableCell className="text-right">{count}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{tableLimit < sortedApps.length && (
<Button
variant="outline"
className="w-full"
onClick={() => setTableLimit(prev => prev + ITEMS_PER_PAGE)}
>
Load More
</Button>
)}
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -1,369 +0,0 @@
"use client"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
})
ChartContainer.displayName = "Chart"
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload
.filter((item) => item.type !== "none")
.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload
.filter((item) => item.type !== "none")
.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
)
ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}

View File

@@ -1,48 +0,0 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }