diff --git a/bun.lock b/bun.lock index 0b40a78..1a60939 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "dependencies": { "@elysiajs/cors": "^1.4.1", "@elysiajs/html": "^1.4.0", + "@mantine/charts": "^9.0.0", "@mantine/core": "^8.3.18", "@mantine/hooks": "^8.3.18", "@prisma/client": "6", @@ -19,6 +20,7 @@ "react": "^19", "react-dom": "^19", "react-icons": "^5.6.0", + "recharts": "^3.8.1", }, "devDependencies": { "@biomejs/biome": "^2.4.10", @@ -181,6 +183,8 @@ "@kitajs/ts-html-plugin": ["@kitajs/ts-html-plugin@4.1.4", "", { "dependencies": { "chalk": "^5.6.2", "tslib": "^2.8.1", "yargs": "^18.0.0" }, "peerDependencies": { "@kitajs/html": "^4.2.10", "typescript": "^5.9.3" }, "bin": { "xss-scan": "dist/cli.js", "ts-html-plugin": "dist/cli.js" } }, "sha512-xK5mNrhnIy73kJFKx5yVGChJyWFRGmIaE0sjlVxVYllk5dyaEYVCrIh1N8AfnseEHka8gAqzPGW95HlkhDvnJA=="], + "@mantine/charts": ["@mantine/charts@9.0.0", "", { "peerDependencies": { "@mantine/core": "9.0.0", "@mantine/hooks": "9.0.0", "react": "^19.2.0", "react-dom": "^19.2.0", "recharts": ">=3.2.1" } }, "sha512-TnbjiT2tXZDAQWZrv/+Xu3JKYjPiTfO5jSIbcwnxZSVtLI+PIxA7zrSps+it/Nx3ch8GHpDizJ7UArC0UfmNkQ=="], + "@mantine/core": ["@mantine/core@8.3.18", "", { "dependencies": { "@floating-ui/react": "^0.27.16", "clsx": "^2.1.1", "react-number-format": "^5.4.4", "react-remove-scroll": "^2.7.1", "react-textarea-autosize": "8.5.9", "type-fest": "^4.41.0" }, "peerDependencies": { "@mantine/hooks": "8.3.18", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-9tph1lTVogKPjTx02eUxDUOdXacPzK62UuSqb4TdGliI54/Xgxftq0Dfqu6XuhCxn9J5MDJaNiLDvL/1KRkYqA=="], "@mantine/hooks": ["@mantine/hooks@8.3.18", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-QoWr9+S8gg5050TQ06aTSxtlpGjYOpIllRbjYYXlRvZeTsUqiTbVfvQROLexu4rEaK+yy9Wwriwl9PMRgbLqPw=="], @@ -205,6 +209,8 @@ "@puppeteer/browsers": ["@puppeteer/browsers@2.13.0", "", { "dependencies": { "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.4", "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA=="], + "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.12", "", { "os": "android", "cpu": "arm64" }, "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg=="], @@ -241,6 +247,8 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + "@tanstack/history": ["@tanstack/history@1.161.6", "", {}, "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg=="], "@tanstack/query-core": ["@tanstack/query-core@5.95.2", "", {}, "sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ=="], @@ -275,12 +283,32 @@ "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], + "@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.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], + + "@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/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], + "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], @@ -365,10 +393,34 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "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.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + + "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=="], + "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="], "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], @@ -399,6 +451,8 @@ "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="], + "esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -411,6 +465,8 @@ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], @@ -459,6 +515,10 @@ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], + + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], @@ -585,8 +645,12 @@ "react-icons": ["react-icons@5.6.0", "", { "peerDependencies": { "react": "*" } }, "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA=="], + "react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="], + "react-number-format": ["react-number-format@5.4.5", "", { "peerDependencies": { "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y8O2yHHj3w0aE9XO8d2BCcUOOdQTRSVq+WIuMlLVucAm5XNjJAy+BoOJiuQMldVYVOKTMyvVNfnbl2Oqp+YxGw=="], + "react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="], + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], @@ -599,8 +663,16 @@ "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], + "recharts": ["recharts@3.8.1", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg=="], + + "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], + + "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], "rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="], @@ -685,6 +757,8 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "victory-vendor": ["victory-vendor@37.3.6", "", { "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-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], + "vite": ["vite@8.0.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.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": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ=="], "webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.1", "", {}, "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw=="], @@ -717,6 +791,8 @@ "@kitajs/ts-html-plugin/yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], + "@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], + "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], diff --git a/package.json b/package.json index 6db5402..eddf26e 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "dependencies": { "@elysiajs/cors": "^1.4.1", "@elysiajs/html": "^1.4.0", + "@mantine/charts": "^9.0.0", "@mantine/core": "^8.3.18", "@mantine/hooks": "^8.3.18", "@prisma/client": "6", @@ -34,7 +35,8 @@ "postcss-simple-vars": "^7.0.1", "react": "^19", "react-dom": "^19", - "react-icons": "^5.6.0" + "react-icons": "^5.6.0", + "recharts": "^3.8.1" }, "devDependencies": { "@biomejs/biome": "^2.4.10", diff --git a/src/app.ts b/src/app.ts index 899b74f..d5b568e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -143,6 +143,27 @@ export function createApp() { set.status = 302; set.headers['location'] = user.role === 'SUPER_ADMIN' ? '/dashboard' : '/profile' }) + // ─── Monitoring API ──────────────────────────────── + .get('/api/dashboard/stats', () => ({ + totalApps: 3, + newErrors: 185, + activeUsers: '24.5k', + trends: { totalApps: 1, newErrors: 12, activeUsers: 5.2 } + })) + + .get('/api/apps', () => [ + { id: 'desa-plus', name: 'Desa+', status: 'active', users: 12450, errors: 12, version: '2.4.1' }, + { id: 'e-commerce', name: 'E-Commerce', status: 'warning', users: 8900, errors: 45, version: '1.8.0' }, + { id: 'fitness-app', name: 'Fitness App', status: 'error', users: 3200, errors: 128, version: '0.9.5' }, + ]) + + .get('/api/apps/:appId', ({ params: { appId } }) => { + const apps = { + 'desa-plus': { id: 'desa-plus', name: 'Desa+', status: 'active', users: 12450, errors: 12, version: '2.4.1' }, + } + return apps[appId as keyof typeof apps] || { id: appId, name: appId, status: 'active', users: 0, errors: 0, version: '1.0.0' } + }) + // ─── Example API ─────────────────────────────────── .get('/api/hello', () => ({ message: 'Hello, world!', diff --git a/src/frontend/App.tsx b/src/frontend/App.tsx index 6bf7800..09b029f 100644 --- a/src/frontend/App.tsx +++ b/src/frontend/App.tsx @@ -5,8 +5,38 @@ import { createRouter, RouterProvider } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' const theme = createTheme({ - primaryColor: 'blue', + primaryColor: 'brand-blue', + colors: { + 'brand-blue': [ + '#ebf2ff', + '#d6e4ff', + '#adc8ff', + '#85acff', + '#5c90ff', + '#2563eb', // Primary Blue + '#1e4fb8', + '#173b85', + '#102752', + '#09131f', + ], + 'brand-purple': [ + '#f3ebff', + '#e7d6ff', + '#cfadff', + '#b785ff', + '#9f5cff', + '#7c3aed', // Primary Purple + '#632eb8', + '#4a2285', + '#311652', + '#180b1f', + ], + }, fontFamily: 'Inter, system-ui, Avenir, Helvetica, Arial, sans-serif', + headings: { + fontFamily: 'Inter, system-ui, sans-serif', + fontWeight: '600', + }, }) const queryClient = new QueryClient({ diff --git a/src/frontend/components/AppCard.tsx b/src/frontend/components/AppCard.tsx new file mode 100644 index 0000000..4880e26 --- /dev/null +++ b/src/frontend/components/AppCard.tsx @@ -0,0 +1,102 @@ +import { Card, Group, Text, ThemeIcon, Badge, Avatar, Stack, Button, Progress, Box } from '@mantine/core' +import { Link } from '@tanstack/react-router' +import { TbDeviceMobile, TbActivity, TbAlertTriangle, TbChevronRight } from 'react-icons/tb' + +interface AppCardProps { + id: string + name: string + status: 'active' | 'warning' | 'error' + users: number + errors: number + version: string +} + +export function AppCard({ id, name, status, users, errors, version }: AppCardProps) { + const statusColor = status === 'active' ? 'teal' : status === 'warning' ? 'orange' : 'red' + + return ( + ({ + root: { + backgroundColor: 'rgba(30, 41, 59, 0.4)', + borderColor: 'rgba(255,255,255,0.08)', + transition: 'transform 0.2s ease, box-shadow 0.2s ease', + '&:hover': { + transform: 'translateY(-4px)', + boxShadow: '0 12px 24px -8px rgba(0, 0, 0, 0.4)', + borderColor: 'rgba(37, 99, 235, 0.3)', + }, + }, + })} + > + + + + + + + {name} + BUILD v{version} + + + + {status.toUpperCase()} + + + + + + + + + USER ADOPTION + + {users.toLocaleString()} + + + + + + + + 0 ? '#ef4444' : '#64748b'} /> + HEALTH INCIDENTS + + 0 ? 'red' : 'dimmed'}>{errors} + + 0 ? 30 : 0} size="sm" color="red" radius="xl" /> + + + + + + ) +} diff --git a/src/frontend/components/DashboardCharts.tsx b/src/frontend/components/DashboardCharts.tsx new file mode 100644 index 0000000..c2ddf93 --- /dev/null +++ b/src/frontend/components/DashboardCharts.tsx @@ -0,0 +1,127 @@ +import { + Paper, + Stack, + Text, + Group, + ThemeIcon, + Box, + Badge, + useMantineTheme +} from '@mantine/core' +import { LineChart, BarChart } from '@mantine/charts' +import { TbTimeline, TbChartBar, TbArrowUpRight } from 'react-icons/tb' + +const activityData = [ + { date: 'Mar 26', logs: 1200 }, + { date: 'Mar 27', logs: 1900 }, + { date: 'Mar 28', logs: 1540 }, + { date: 'Mar 29', logs: 2400 }, + { date: 'Mar 30', logs: 2100 }, + { date: 'Mar 31', logs: 3200 }, + { date: 'Apr 01', logs: 3800 }, +] + +const villageComparisonData = [ + { village: 'Sukatani', activity: 4500 }, + { village: 'Sukamaju', activity: 3800 }, + { village: 'Bojong Gede', activity: 3200 }, + { village: 'Beji', activity: 2800 }, + { village: 'Tapos', activity: 2400 }, +] + +export function VillageActivityLineChart() { + const theme = useMantineTheme() + + return ( + + + + + + + + + DAILY ACTIVITY - ALL VILLAGES + Trend over the last 7 days + + + }> + Growing + + + + + + + + + ) +} + +export function VillageComparisonBarChart() { + const theme = useMantineTheme() + + return ( + + + + + + + + + USAGE COMPARISON BETWEEN VILLAGES + Top 5 most active village deployments + + + + + + + {/* Custom SVG Gradient definitions for Premium SaaS look */} + + + + + + + + + + + ) +} diff --git a/src/frontend/components/DashboardLayout.tsx b/src/frontend/components/DashboardLayout.tsx new file mode 100644 index 0000000..1a119e7 --- /dev/null +++ b/src/frontend/components/DashboardLayout.tsx @@ -0,0 +1,228 @@ +import { APP_CONFIGS } from '@/frontend/config/appMenus' +import { + AppShell, + Avatar, + Box, + Burger, + Button, + Group, + Menu, + NavLink, + Select, + Stack, + Text, + ThemeIcon +} from '@mantine/core' +import { useDisclosure } from '@mantine/hooks' +import { Link, useLocation, useMatches, useNavigate, useParams } from '@tanstack/react-router' +import { + TbApps, + TbArrowLeft, + TbChevronRight, + TbDashboard, + TbDeviceMobile, + TbLogout, + TbSettings, + TbUserCircle +} from 'react-icons/tb' + +interface DashboardLayoutProps { + children: React.ReactNode +} + +export function DashboardLayout({ children }: DashboardLayoutProps) { + const [opened, { toggle }] = useDisclosure() + const location = useLocation() + const navigate = useNavigate() + const { appId } = useParams({ strict: false }) as { appId?: string } + + const matches = useMatches() + const currentPath = matches[matches.length - 1]?.pathname + + const globalNav = [ + { label: 'Dashboard', icon: TbDashboard, to: '/dashboard' }, + { label: 'Applications', icon: TbApps, to: '/apps' }, + { label: 'Settings', icon: TbSettings, to: '/settings' }, + ] + + const activeApp = appId ? APP_CONFIGS[appId] : null + const navLinks = activeApp ? activeApp.menus : globalNav + + return ( + ({ + main: { + backgroundColor: theme.colors.dark[7], // Dark mode background + }, + })} + > + + + + + + + + + + Monitoring System + + + + + + + + + + + + Application + }>Profile + }>Settings + + Danger Zone + }> + Logout + + + + + + + + + + {activeApp && ( + } + component={Link} + to="/dashboard" + styles={(theme) => ({ + root: { + borderRadius: theme.radius.md, + opacity: 0.7, + '&:hover': { opacity: 1 }, + }, + })} + /> + )} + + { + activeApp && + } + radius="md" + clearable + /> + + + + {mockErrors.map((error) => ( + + + + + + + + + {error.title} + + {error.severity.toUpperCase()} + + + + {error.time} • v{error.version} + + + {error.users} Users Affected + + + + + + + + + + MESSAGE + {error.message} + + + DEVICE METADATA + + {error.device.includes('PC') ? : } + {error.device} + + + + + + STACK TRACE + + + {error.stackTrace} + + + + + + + + + + + + ))} + + + + ) +} diff --git a/src/frontend/routes/apps.$appId.index.tsx b/src/frontend/routes/apps.$appId.index.tsx new file mode 100644 index 0000000..a206fd1 --- /dev/null +++ b/src/frontend/routes/apps.$appId.index.tsx @@ -0,0 +1,125 @@ +import { + Badge, + Button, + Card, + Group, + SimpleGrid, + Stack, + Text, + Title, + Paper, + Box, + ThemeIcon, + Select, + ActionIcon, + Container, + Divider, +} from '@mantine/core' +import { createFileRoute, Link, useParams } from '@tanstack/react-router' +import { + TbUsers, + TbActivity, + TbRefresh, + TbAlertTriangle, + TbCalendar, + TbFilter, + TbChevronRight, + TbArrowUpRight, + TbBuildingCommunity, + TbVersions +} from 'react-icons/tb' +import { SummaryCard } from '@/frontend/components/SummaryCard' +import { VillageActivityLineChart, VillageComparisonBarChart } from '@/frontend/components/DashboardCharts' +import { ErrorDataTable } from '@/frontend/components/ErrorDataTable' + +export const Route = createFileRoute('/apps/$appId/')({ + component: AppOverviewPage, +}) + +function AppOverviewPage() { + const { appId } = useParams({ from: '/apps/$appId/' }) + const isDesaPlus = appId === 'desa-plus' + + return ( + + {/* 🔝 HEADER SECTION */} + + + + Overview + + }> + APP: {isDesaPlus ? 'DESA+' : appId.toUpperCase()} + + LAST UPDATED: JUST NOW + + + + + } + radius="md" + clearable + /> + + + + + + Type + Village / Instance + Activity Name + Operator + Timestamp + Status + + + + {mockLogs.map((log) => ( + + + + {log.type} + + + + {log.village} + + + {log.activity} + + + + {log.operator[0]} + {log.operator} + + + + {log.time} + + + + {log.status} + + + + ))} + +
+
+
+ ) +} diff --git a/src/frontend/routes/apps.$appId.manage.tsx b/src/frontend/routes/apps.$appId.manage.tsx new file mode 100644 index 0000000..1dfb05c --- /dev/null +++ b/src/frontend/routes/apps.$appId.manage.tsx @@ -0,0 +1,278 @@ +import { useState } from 'react' +import { + Badge, + Container, + Group, + Stack, + Text, + Title, + Paper, + Table, + Button, + ActionIcon, + TextInput, + Select, + Tooltip, + SimpleGrid, + Modal, + Avatar, + Box, + NumberInput, +} from '@mantine/core' +import { useDisclosure } from '@mantine/hooks' +import { createFileRoute, useParams } from '@tanstack/react-router' +import { + TbPlus, + TbSearch, + TbPencil, + TbTrash, + TbUserPlus, + TbCircleCheck, + TbRefresh, + TbUser, + TbBuildingCommunity, +} from 'react-icons/tb' +import { StatsCard } from '@/frontend/components/StatsCard' + +export const Route = createFileRoute('/apps/$appId/manage')({ + component: AppManagePage, +}) + +const mockDevelopers = [ + { value: 'john-doe', label: 'John Doe', avatar: null }, + { value: 'amel', label: 'Amel', avatar: null }, + { value: 'jane-smith', label: 'Jane Smith', avatar: null }, + { value: 'rahmat', label: 'Rahmat Hidayat', avatar: null }, +] + +function AppManagePage() { + const { appId } = useParams({ from: '/apps/$appId' }) + const [initModalOpened, { open: openInit, close: closeInit }] = useDisclosure(false) + const [assignModalOpened, { open: openAssign, close: closeAssign }] = useDisclosure(false) + const [selectedVillage, setSelectedVillage] = useState(null) + + const isDesaPlus = appId === 'desa-plus' + + const mockVillages = [ + { id: 1, name: 'Sukatani', kecamatan: 'Tapos', population: 4500, status: 'fully integrated', developer: 'John Doe', lastUpdate: '2 mins ago' }, + { id: 2, name: 'Sukamaju', kecamatan: 'Cilodong', population: 3800, status: 'sync active', developer: 'Amel', lastUpdate: '15 mins ago' }, + { id: 3, name: 'Cikini', kecamatan: 'Menteng', population: 2100, status: 'sync pending', developer: 'Jane Smith', lastUpdate: '-' }, + { id: 4, name: 'Bojong Gede', kecamatan: 'Bojong Gede', population: 6700, status: 'fully integrated', developer: 'Rahmat', lastUpdate: '1 hour ago' }, + ] + + if (!isDesaPlus) { + return ( + + + + General Management + This feature is currently customized for Desa+. Other apps coming soon. + + + ) + } + + return ( + + {/* Metrics Row */} + + + + + + + + + + Village Deployment Center + Monitor and configure **Desa+** village instances across all districts. + + + + + + + } + style={{ flex: 1 }} + radius="md" + /> + + + + + + Village Profile + District + Integration Status + Lead Developer + Last Sync + Actions + + + + {mockVillages.map((village) => ( + + + + {village.name} + {village.population.toLocaleString()} Residents + + + + {village.kecamatan} + + + } + radius="sm" + style={{ textTransform: 'uppercase', fontVariant: 'small-caps' }} + > + {village.status} + + + + + + {village.developer} + { setSelectedVillage(village); openAssign(); }} + > + + + + + + + {village.lastUpdate} + + + + + {village.status === 'sync pending' && ( + + )} + + + + + + + + + + + + + + ))} + +
+
+ + {/* MODALS */} + Desa+ Instance Initialization} + radius="xl" + centered + padding="xl" + > + + + + + + + } + radius="md" + searchable + /> + + + + + + +
+ ) +} diff --git a/src/frontend/routes/apps.$appId.orders.tsx b/src/frontend/routes/apps.$appId.orders.tsx new file mode 100644 index 0000000..64eec5b --- /dev/null +++ b/src/frontend/routes/apps.$appId.orders.tsx @@ -0,0 +1,120 @@ +import { + Badge, + Container, + Group, + Stack, + Text, + Title, + Paper, + Table, + TextInput, + Button, + ActionIcon, + Tooltip, + SimpleGrid +} from '@mantine/core' +import { createFileRoute } from '@tanstack/react-router' +import { TbSearch, TbFilter, TbEye, TbReceipt, TbTruckDelivery, TbCreditCard, TbTrendingUp } from 'react-icons/tb' +import { StatsCard } from '@/frontend/components/StatsCard' + +export const Route = createFileRoute('/apps/$appId/orders')({ + component: OrdersPage, +}) + +const mockOrders = [ + { id: 'ORD-9921', customer: 'John Doe', amount: 'Rp 1.250.000', status: 'PAID', time: '2 mins ago', method: 'BCA Virtual Account' }, + { id: 'ORD-9922', customer: 'Jane Smith', amount: 'Rp 450.000', status: 'PENDING', time: '15 mins ago', method: 'OVO' }, + { id: 'ORD-9923', customer: 'Rahmat', amount: 'Rp 2.100.000', status: 'SHIPPING', time: '1 hour ago', method: 'Mandiri Transfer' }, + { id: 'ORD-9924', customer: 'Amel', amount: 'Rp 750.000', status: 'REFUNDED', time: '2 hours ago', method: 'GoPay' }, + { id: 'ORD-9925', customer: 'Siti', amount: 'Rp 1.100.000', status: 'PAID', time: '4 hours ago', method: 'Credit Card' }, +] + +function OrdersPage() { + return ( + + + + + + + + + + Orders Tracking + Detailed transaction and shipment tracking center. + + + + + + } + radius="md" + /> + + Filter by Status + + + + + + + Order ID + Customer + Amount + Payment Method + Status + Actions + + + + {mockOrders.map((order) => ( + + + + + {order.id} + + {order.time} + + + {order.customer} + + + {order.amount} + + + + + {order.method} + + + + + {order.status} + + + + + + + + + + + ))} + +
+
+
+ ) +} diff --git a/src/frontend/routes/apps.$appId.payments.tsx b/src/frontend/routes/apps.$appId.payments.tsx new file mode 100644 index 0000000..b56eca6 --- /dev/null +++ b/src/frontend/routes/apps.$appId.payments.tsx @@ -0,0 +1,124 @@ +import { + Stack, + Title, + Text, + Paper, + Group, + Badge, + Table, + ThemeIcon, + SimpleGrid, + Box, + Tooltip +} from '@mantine/core' +import { createFileRoute } from '@tanstack/react-router' +import { TbCreditCard, TbCheck, TbAlertCircle, TbWallet, TbHistory, TbArrowUpRight, TbCircleCheck, TbRefresh } from 'react-icons/tb' +import { StatsCard } from '@/frontend/components/StatsCard' + +export const Route = createFileRoute('/apps/$appId/payments')({ + component: PaymentsPage, +}) + +const mockPayments = [ + { id: 'TRX-8412', method: 'Credit Card (Visa)', amount: 'Rp 2.450.000', status: 'SUCCESS', date: '2026-04-01 14:30', gateway: 'Stripe' }, + { id: 'TRX-8413', method: 'BCA Virtual Account', amount: 'Rp 1.150.000', status: 'PENDING', date: '2026-04-01 14:28', gateway: 'Midtrans' }, + { id: 'TRX-8414', method: 'OVO / E-Wallet', amount: 'Rp 450.000', status: 'SUCCESS', date: '2026-04-01 14:25', gateway: 'Midtrans' }, + { id: 'TRX-8415', method: 'ShopeePay', amount: 'Rp 890.000', status: 'FAILED', date: '2026-04-01 14:20', gateway: 'Midtrans' }, + { id: 'TRX-8416', method: 'Mandiri Transfer', amount: 'Rp 3.200.000', status: 'SUCCESS', date: '2026-04-01 14:15', gateway: 'Bank Sync' }, +] + +const gateways = [ + { name: 'Stripe', status: 'Operational', color: 'teal' }, + { name: 'Midtrans', status: 'Operational', color: 'teal' }, + { name: 'Bank Sync', status: 'Slow Response', color: 'orange' }, +] + +function PaymentsPage() { + return ( + + + + + + + + + + Payment Gateway Health + Real-time status monitoring for integrated payment providers. + + + + + {gateways.map((gw) => ( + + + + + + + {gw.name} + + {gw.status} + + + ))} + + + + Recent Transactions + + + + + Transaction ID + Method + Gateway + Amount + Status + Timestamp + + + + {mockPayments.map((trx) => ( + + + + + {trx.id} + + + + + + {trx.method} + + + + {trx.gateway} + + + {trx.amount} + + + : } + radius="sm" + > + {trx.status} + + + + {trx.date} + + + ))} + +
+
+
+
+ ) +} diff --git a/src/frontend/routes/apps.$appId.products.tsx b/src/frontend/routes/apps.$appId.products.tsx new file mode 100644 index 0000000..1e8628e --- /dev/null +++ b/src/frontend/routes/apps.$appId.products.tsx @@ -0,0 +1,111 @@ +import { + Stack, + Title, + Text, + SimpleGrid, + Card, + Group, + Badge, + Button, + ThemeIcon, + Box, + Progress, + ActionIcon, + Tooltip +} from '@mantine/core' +import { createFileRoute } from '@tanstack/react-router' +import { TbPlus, TbPackage, TbAlertTriangle, TbTrendingUp, TbPencil, TbTrash, TbArchive } from 'react-icons/tb' +import { StatsCard } from '@/frontend/components/StatsCard' + +export const Route = createFileRoute('/apps/$appId/products')({ + component: ProductsPage, +}) + +const mockProducts = [ + { id: 1, name: 'Premium Wireless Headphones', price: 'Rp 2.450.000', stock: 12, sales: 145, status: 'IN_STOCK' }, + { id: 2, name: 'Mechanical Keyboard RGB', price: 'Rp 1.150.000', stock: 4, sales: 89, status: 'LOW_STOCK' }, + { id: 3, name: 'Ultra-wide Monitor 34"', price: 'Rp 8.900.000', stock: 2, sales: 34, status: 'LOW_STOCK' }, + { id: 4, name: 'Ergonomic Gaming Chair', price: 'Rp 3.200.000', stock: 0, sales: 56, status: 'OUT_OF_STOCK' }, + { id: 5, name: 'USB-C Hub Multiport', price: 'Rp 450.000', stock: 45, sales: 231, status: 'IN_STOCK' }, + { id: 6, name: 'Webcam 4K Ultra HD', price: 'Rp 1.750.000', stock: 18, sales: 67, status: 'IN_STOCK' }, +] + +function ProductsPage() { + return ( + + + + + + + + + + Product Catalog + Inventory management and performance monitoring center. + + + + + + {mockProducts.map((product) => ( + + + + + + + + + + + + {product.name} + + {product.status.replace('_', ' ')} + + + + + {product.price} + {product.sales} Sales + + + + + Stock Level + {product.stock}/50 + + 10 ? 'teal' : product.stock > 0 ? 'orange' : 'red'} + size="xs" + radius="xl" + /> + + + + + + + + + + + + + + + + + ))} + + + ) +} diff --git a/src/frontend/routes/apps.$appId.tsx b/src/frontend/routes/apps.$appId.tsx new file mode 100644 index 0000000..09300a1 --- /dev/null +++ b/src/frontend/routes/apps.$appId.tsx @@ -0,0 +1,45 @@ +import { DashboardLayout } from '@/frontend/components/DashboardLayout' +import { + Box, + Container, + Group, + Stack, + Text, + Title +} from '@mantine/core' +import { createFileRoute, Outlet, useNavigate, useParams } from '@tanstack/react-router' + +export const Route = createFileRoute('/apps/$appId')({ + component: AppDetailLayout, +}) + +function AppDetailLayout() { + const { appId } = useParams({ from: '/apps/$appId' }) + const navigate = useNavigate() + + // Format app ID for display (e.g., desa-plus -> Desa+) + const appName = appId + .split('-') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ') + .replace('Plus', '+') + + return ( + + + + + + {appName} + Application ID: {appId} + + + + + + + + + + ) +} diff --git a/src/frontend/routes/apps.index.tsx b/src/frontend/routes/apps.index.tsx new file mode 100644 index 0000000..770d3c5 --- /dev/null +++ b/src/frontend/routes/apps.index.tsx @@ -0,0 +1,59 @@ +import { useQuery } from '@tanstack/react-query' +import { Container, Stack, Title, Text, SimpleGrid, Group, Button, TextInput, Loader } from '@mantine/core' +import { createFileRoute } from '@tanstack/react-router' +import { TbPlus, TbSearch } from 'react-icons/tb' +import { DashboardLayout } from '@/frontend/components/DashboardLayout' +import { AppCard } from '@/frontend/components/AppCard' + +export const Route = createFileRoute('/apps/')({ + component: AppsPage, +}) + +function AppsPage() { + const { data: apps, isLoading } = useQuery({ + queryKey: ['apps'], + queryFn: () => fetch('/api/apps').then((r) => r.json()), + }) + + return ( + + + + + + Applications + Manage and monitor all your mobile applications from one place. + + + + + + } + style={{ flex: 1 }} + radius="md" + /> + + + {isLoading ? ( + + ) : ( + + {apps?.map((app: any) => ( + + ))} + + )} + + + + ) +} diff --git a/src/frontend/routes/apps.tsx b/src/frontend/routes/apps.tsx new file mode 100644 index 0000000..8efd756 --- /dev/null +++ b/src/frontend/routes/apps.tsx @@ -0,0 +1,5 @@ +import { createFileRoute, Outlet } from '@tanstack/react-router' + +export const Route = createFileRoute('/apps')({ + component: () => , +}) diff --git a/src/frontend/routes/dashboard.tsx b/src/frontend/routes/dashboard.tsx index 91bbb04..1b1c14f 100644 --- a/src/frontend/routes/dashboard.tsx +++ b/src/frontend/routes/dashboard.tsx @@ -1,20 +1,23 @@ +import { useQuery } from '@tanstack/react-query' import { - Avatar, Badge, Button, - Card, Container, Group, - Paper, SimpleGrid, Stack, Text, - ThemeIcon, Title, + Paper, + Table, + Loader, } from '@mantine/core' -import { createFileRoute, redirect } from '@tanstack/react-router' -import { TbChartBar, TbLogout, TbSettings, TbUsers } from 'react-icons/tb' +import { createFileRoute, redirect, Link } from '@tanstack/react-router' +import { TbActivity, TbApps, TbMessageReport, TbUsers, TbChevronRight } from 'react-icons/tb' import { useLogout, useSession } from '@/frontend/hooks/useAuth' +import { DashboardLayout } from '@/frontend/components/DashboardLayout' +import { StatsCard } from '@/frontend/components/StatsCard' +import { AppCard } from '@/frontend/components/AppCard' export const Route = createFileRoute('/dashboard')({ beforeLoad: async ({ context }) => { @@ -33,62 +36,140 @@ export const Route = createFileRoute('/dashboard')({ component: DashboardPage, }) -const stats = [ - { title: 'Users', value: '1,234', icon: TbUsers, color: 'blue' }, - { title: 'Revenue', value: '$12.4k', icon: TbChartBar, color: 'green' }, - { title: 'Settings', value: '3 active', icon: TbSettings, color: 'violet' }, +const recentErrors = [ + { id: 1, app: 'Desa+', message: 'NullPointerException at village_sync.dart:45', version: '2.4.1', time: '2 mins ago', severity: 'critical' }, + { id: 2, app: 'E-Commerce', message: 'Failed to load checkout session', version: '1.8.0', time: '15 mins ago', severity: 'high' }, + { id: 3, app: 'Fitness App', message: 'SocketException: Connection timed out', version: '0.9.5', time: '1 hour ago', severity: 'medium' }, ] function DashboardPage() { - const { data } = useSession() - const logout = useLogout() - const user = data?.user + const { data: sessionData } = useSession() + const user = sessionData?.user + + const { data: stats, isLoading: statsLoading } = useQuery({ + queryKey: ['dashboard', 'stats'], + queryFn: () => fetch('/api/dashboard/stats').then((r) => r.json()), + }) + + const { data: apps, isLoading: appsLoading } = useQuery({ + queryKey: ['apps'], + queryFn: () => fetch('/api/apps').then((r) => r.json()), + }) return ( - - - - Dashboard - - - - - - - {user?.name?.charAt(0).toUpperCase()} - -
- - {user?.name} - SUPER ADMIN - - {user?.email} -
+ + + + + + Overview Dashboard + Welcome back, {user?.name}. Here is what's happening today. + + -
- - {stats.map((stat) => ( - - - {stat.title} - - - - - {stat.value} - - ))} - -
-
+ {statsLoading ? ( + + ) : ( + + + + + + )} + + + Registered Applications + + + + {appsLoading ? ( + + ) : ( + + {apps?.map((app: any) => ( + + ))} + + )} + + + Recent Error Reports + + + + + + + + Application + Error Message + Version + Time + Severity + + + + {recentErrors.map((error) => ( + + + {error.app} + + + {error.message} + + + v{error.version} + + + {error.time} + + + + {error.severity.toUpperCase()} + + + + ))} + +
+
+ + + ) } diff --git a/src/frontend/routes/login.tsx b/src/frontend/routes/login.tsx index b804e78..cdb5836 100644 --- a/src/frontend/routes/login.tsx +++ b/src/frontend/routes/login.tsx @@ -17,7 +17,7 @@ import { TbAlertCircle, TbLogin, TbLock, TbMail } from 'react-icons/tb' import { useLogin } from '@/frontend/hooks/useAuth' export const Route = createFileRoute('/login')({ - validateSearch: (search: Record) => ({ + validateSearch: (search: Record): { error?: string } => ({ error: (search.error as string) || undefined, }), beforeLoad: async ({ context }) => { diff --git a/src/frontend/routes/settings.tsx b/src/frontend/routes/settings.tsx new file mode 100644 index 0000000..c4a3ada --- /dev/null +++ b/src/frontend/routes/settings.tsx @@ -0,0 +1,222 @@ +import { + ActionIcon, + Badge, + Button, + Card, + Container, + Group, + Stack, + Table, + Text, + TextInput, + Title, + Paper, + Tabs, + Avatar, + SimpleGrid, + ThemeIcon, + List, + Box, + Divider, +} from '@mantine/core' +import { createFileRoute } from '@tanstack/react-router' +import { + TbPlus, + TbSearch, + TbPencil, + TbTrash, + TbUserCheck, + TbShieldCheck, + TbAccessPoint, + TbCircleCheck, + TbClock, + TbApps, +} from 'react-icons/tb' +import { DashboardLayout } from '@/frontend/components/DashboardLayout' +import { StatsCard } from '@/frontend/components/StatsCard' + +export const Route = createFileRoute('/settings')({ + component: SettingsPage, +}) + +const mockUsers = [ + { id: 1, name: 'Amel', email: 'amel@company.com', role: 'SUPER_ADMIN', apps: 'All', status: 'Online', lastActive: 'Now' }, + { id: 2, name: 'John Doe', email: 'john@company.com', role: 'DEVELOPER', apps: 'Desa+, Fitness App', status: 'Offline', lastActive: '2h ago' }, + { id: 3, name: 'Jane Smith', email: 'jane@company.com', role: 'QA', apps: 'E-Commerce', status: 'Online', lastActive: '12m ago' }, + { id: 4, name: 'Rahmat Hidayat', email: 'rahmat@company.com', role: 'DEVELOPER', apps: 'Desa+', status: 'Online', lastActive: 'Now' }, +] + +const roles = [ + { + name: 'SUPER_ADMIN', + count: 2, + color: 'red', + permissions: ['Full Access', 'User Mgmt', 'Role Mgmt', 'App Config', 'Logs & Errors'] + }, + { + name: 'DEVELOPER', + count: 12, + color: 'brand-blue', + permissions: ['View All Apps', 'Manage Assigned App', 'View Logs', 'Resolve Errors', 'Village Setup'] + }, + { + name: 'QA', + count: 5, + color: 'orange', + permissions: ['View All Apps', 'View Logs', 'Report Errors', 'Test App Features'] + }, +] + +function SettingsPage() { + return ( + + + + + + Settings + Manage system users, security roles, and application access control. + + + + + + + + + + + + }>User Management + }>Role Management + + + + + + } + radius="md" + w={350} + variant="filled" + /> + + + + + + + + Name & Contact + Role + Status + App Access + Actions + + + + {mockUsers.map((user) => ( + + + + {user.name.charAt(0)} + + {user.name} + {user.email} + + + + + + {user.role} + + + + + + {user.status} + {user.lastActive} + + + + + + {user.apps} + + + + + + + + + + + + + + ))} + +
+
+
+
+ + + + {roles.map((role) => ( + + + + + + + {role.count} Users + + + + {role.name.replace('_', ' ')} + Core role for secure app management. + + + + + Key Permissions + + + + } + > + {role.permissions.map((p) => ( + {p} + ))} + + + + + + ))} + + +
+
+
+
+ ) +} + diff --git a/src/index.css b/src/index.css index 774eb83..774be0a 100644 --- a/src/index.css +++ b/src/index.css @@ -1,187 +1,113 @@ +@import '@mantine/core/styles.css'; + :root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; + --font-inter: 'Inter', system-ui, -apple-system, sans-serif; + + /* Monitoring System Colors */ + --blue-primary: #2563EB; + --purple-primary: #7C3AED; + --gradient-blue-purple: linear-gradient(135deg, var(--blue-primary) 0%, var(--purple-primary) 100%); + + /* Backgrounds & Cards (Light Mode) */ + --bg-light: #F8FAFC; + --card-light: #FFFFFF; + + /* Backgrounds & Cards (Dark Mode) */ + --bg-dark: #0F172A; + --card-dark: #1E293B; + + /* Transitions */ + --transition-smooth: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); } + +/* Base Resets */ +html, body { + margin: 0; + padding: 0; + height: 100%; + width: 100%; + font-family: var(--font-inter); + background-color: var(--bg-dark); /* Default to Dark Mode as per App.tsx */ + color: #F8FAFC; +} + body { - margin: 0; - display: grid; - place-items: center; - min-width: 320px; - min-height: 100vh; + overflow-x: hidden; +} + +/* Custom Scrollbars */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: rgba(124, 58, 237, 0.2); + border-radius: 10px; +} +::-webkit-scrollbar-thumb:hover { + background: rgba(124, 58, 237, 0.4); +} + +/* Premium Dashboard Utilities */ +.glass { + background: rgba(30, 41, 59, 0.7); + backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 24px; /* XL rounding for cards */ + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); +} + +.gradient-text { + background: var(--gradient-blue-purple); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} + +.gradient-bg { + background: var(--gradient-blue-purple); +} + +.premium-card { + transition: var(--transition-smooth); +} +.premium-card:hover { + transform: translateY(-4px); + box-shadow: 0 12px 24px -10px rgba(124, 58, 237, 0.3); +} + +.sidebar-nav-item { position: relative; + transition: var(--transition-smooth); } -body::before { - content: ""; - position: fixed; - inset: 0; - z-index: -1; - opacity: 0.05; - background: url("./logo.svg"); - background-size: 256px; - transform: rotate(-12deg) scale(1.35); - animation: slide 30s linear infinite; - pointer-events: none; + +.sidebar-nav-item.active { + background: var(--gradient-blue-purple); + color: white; } -@keyframes slide { - from { - background-position: 0 0; - } - to { - background-position: 256px 224px; - } + +/* Responsive Table */ +.data-table { + border-radius: 20px; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.05); } -.app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; - position: relative; - z-index: 1; + +.data-table thead th { + background: rgba(124, 58, 237, 0.05); + font-weight: 600; + text-transform: uppercase; + font-size: 11px; + letter-spacing: 0.05em; + padding: 12px 16px; } -.logo-container { - display: flex; - justify-content: center; - align-items: center; - gap: 2rem; - margin-bottom: 2rem; + +.data-table tbody tr { + transition: var(--transition-smooth); } -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 0.3s; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.bun-logo { - transform: scale(1.2); -} -.bun-logo:hover { - filter: drop-shadow(0 0 2em #fbf0dfaa); -} -.react-logo { - animation: spin 20s linear infinite; -} -.react-logo:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} -@keyframes spin { - from { - transform: rotate(0); - } - to { - transform: rotate(360deg); - } -} -h1 { - font-size: 3.2em; - line-height: 1.1; -} -code { - background-color: #1a1a1a; - padding: 0.2em 0.4em; - border-radius: 0.3em; - font-family: monospace; -} -.api-tester { - margin: 2rem auto 0; - width: 100%; - max-width: 600px; - text-align: left; - display: flex; - flex-direction: column; - gap: 1rem; -} -.endpoint-row { - display: flex; - align-items: center; - gap: 0.5rem; - background: #1a1a1a; - padding: 0.75rem; - border-radius: 12px; - font: monospace; - border: 2px solid #fbf0df; - transition: 0.3s; - width: 100%; - box-sizing: border-box; -} -.endpoint-row:focus-within { - border-color: #f3d5a3; -} -.method { - background: #fbf0df; - color: #1a1a1a; - padding: 0.3rem 0.7rem; - border-radius: 8px; - font-weight: 700; - font-size: 0.9em; - appearance: none; - margin: 0; - width: min-content; - display: block; - flex-shrink: 0; - border: none; -} -.method option { - text-align: left; -} -.url-input { - width: 100%; - flex: 1; - background: 0; - border: 0; - color: #fbf0df; - font: 1em monospace; - padding: 0.2rem; - outline: 0; -} -.url-input:focus { - color: #fff; -} -.url-input::placeholder { - color: rgba(251, 240, 223, 0.4); -} -.send-button { - background: #fbf0df; - color: #1a1a1a; - border: 0; - padding: 0.4rem 1.2rem; - border-radius: 8px; - font-weight: 700; - transition: 0.1s; - cursor: var(--bun-cursor); -} -.send-button:hover { - background: #f3d5a3; - transform: translateY(-1px); - cursor: pointer; -} -.response-area { - width: 100%; - min-height: 120px; - background: #1a1a1a; - border: 2px solid #fbf0df; - border-radius: 12px; - padding: 0.75rem; - color: #fbf0df; - font: monospace; - resize: vertical; - box-sizing: border-box; -} -.response-area:focus { - border-color: #f3d5a3; -} -.response-area::placeholder { - color: rgba(251, 240, 223, 0.4); -} -@media (prefers-reduced-motion) { - *, - ::before, - ::after { - animation: none !important; - } +.data-table tbody tr:hover { + background: rgba(124, 58, 237, 0.03); } diff --git a/tests/integration/dashboard.test.ts b/tests/integration/dashboard.test.ts new file mode 100644 index 0000000..3563711 --- /dev/null +++ b/tests/integration/dashboard.test.ts @@ -0,0 +1,30 @@ +import { test, expect, describe } from 'bun:test' +import { createTestApp } from '../helpers' + +const app = createTestApp() + +describe('Dashboard Routes (SPA)', () => { + test('GET /dashboard returns 200 HTML', async () => { + const res = await app.handle(new Request('http://localhost/dashboard')) + expect(res.status).toBe(200) + expect(res.headers.get('content-type')).toContain('text/html') + }) + + test('GET /apps returns 200 HTML', async () => { + const res = await app.handle(new Request('http://localhost/apps')) + expect(res.status).toBe(200) + expect(res.headers.get('content-type')).toContain('text/html') + }) + + test('GET /apps/desa-plus returns 200 HTML', async () => { + const res = await app.handle(new Request('http://localhost/apps/desa-plus')) + expect(res.status).toBe(200) + expect(res.headers.get('content-type')).toContain('text/html') + }) + + test('GET /settings returns 200 HTML', async () => { + const res = await app.handle(new Request('http://localhost/settings')) + expect(res.status).toBe(200) + expect(res.headers.get('content-type')).toContain('text/html') + }) +}) diff --git a/tests/integration/monitoring-api.test.ts b/tests/integration/monitoring-api.test.ts new file mode 100644 index 0000000..db5deb5 --- /dev/null +++ b/tests/integration/monitoring-api.test.ts @@ -0,0 +1,49 @@ +import { test, expect, describe } from 'bun:test' +import { createTestApp } from '../helpers' + +const app = createTestApp() + +describe('Monitoring API', () => { + describe('GET /api/dashboard/stats', () => { + test('returns 200 with dashboard stats', async () => { + const res = await app.handle(new Request('http://localhost/api/dashboard/stats')) + expect(res.status).toBe(200) + const body = await res.json() + expect(body).toHaveProperty('totalApps') + expect(body).toHaveProperty('newErrors') + expect(body).toHaveProperty('activeUsers') + expect(body).toHaveProperty('trends') + }) + }) + + describe('GET /api/apps', () => { + test('returns 200 with list of apps', async () => { + const res = await app.handle(new Request('http://localhost/api/apps')) + expect(res.status).toBe(200) + const body = await res.json() + expect(Array.isArray(body)).toBe(true) + expect(body.length).toBeGreaterThan(0) + expect(body[0]).toHaveProperty('id') + expect(body[0]).toHaveProperty('name') + expect(body[0]).toHaveProperty('status') + }) + }) + + describe('GET /api/apps/:appId', () => { + test('returns 200 with specific app details', async () => { + const res = await app.handle(new Request('http://localhost/api/apps/desa-plus')) + expect(res.status).toBe(200) + const body = await res.json() + expect(body.id).toBe('desa-plus') + expect(body.name).toBe('Desa+') + }) + + test('returns fallback for unknown app', async () => { + const res = await app.handle(new Request('http://localhost/api/apps/unknown-app')) + expect(res.status).toBe(200) + const body = await res.json() + expect(body.id).toBe('unknown-app') + expect(body.name).toBe('unknown-app') + }) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 05d8be4..501a12c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,6 @@ "sourceMap": true, "jsx": "react-jsx", "types": ["vite/client"], - "baseUrl": ".", "paths": { "@/*": ["./src/*"] }