a cache for slack profile pictures and emojis

feat: add dashboard and update deps

dunkirk.sh fae6bcd8 995d03ab

verified
+318
bun.lock
···
+
{
+
"lockfileVersion": 1,
+
"workspaces": {
+
"": {
+
"name": "cachet",
+
"dependencies": {
+
"@elysiajs/cors": "^1.1.1",
+
"@elysiajs/cron": "^1.2.0",
+
"@elysiajs/html": "^1.3.0",
+
"@elysiajs/swagger": "^1.1.6",
+
"@sentry/bun": "^8.45.0",
+
"@tqman/nice-logger": "^1.0.7",
+
"@types/node-cron": "^3.0.11",
+
"bottleneck": "^2.19.5",
+
"elysia": "latest",
+
"node-cron": "^3.0.3",
+
},
+
"devDependencies": {
+
"@types/bun": "latest",
+
"bun-types": "latest",
+
},
+
"peerDependencies": {
+
"typescript": "^5",
+
},
+
},
+
},
+
"packages": {
+
"@elysiajs/cors": ["@elysiajs/cors@1.3.3", "", { "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-mYIU6PyMM6xIJuj7d27Vt0/wuzVKIEnFPjcvlkyd7t/m9xspAG37cwNjFxVOnyvY43oOd2I/oW2DB85utXpA2Q=="],
+
+
"@elysiajs/cron": ["@elysiajs/cron@1.3.0", "", { "dependencies": { "croner": "^6.0.3" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-8MaL9ypxo+6r9ISYGx1uabDJhdJxCHGc5IUjpNSOGnDT6koFW2xAKtrFtECrkWt4IhwWmew6GMLXvYZ3aOcHGQ=="],
+
+
"@elysiajs/html": ["@elysiajs/html@1.3.0", "", { "dependencies": { "@kitajs/html": "^4.1.0", "@kitajs/ts-html-plugin": "^4.0.1" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-NpujllWwiEXdsX8GJhbBppOv7+aJr+OU7Gn3K8fVXpwieutwau0/B/M6vzjYXsh9OaoGByUTpL8U9rA/tVSn7w=="],
+
+
"@elysiajs/swagger": ["@elysiajs/swagger@1.3.1", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ=="],
+
+
"@kitajs/html": ["@kitajs/html@4.2.9", "", { "dependencies": { "csstype": "^3.1.3" } }, "sha512-FDHHf5Mi5nR0D+Btq86IV1O9XfsePVCiC5rwU4PXjw2aHja16FmIiwLZBO0CS16rJxKkibjMldyRLAW2ni2mzA=="],
+
+
"@kitajs/ts-html-plugin": ["@kitajs/ts-html-plugin@4.1.1", "", { "dependencies": { "chalk": "^4.1.2", "tslib": "^2.8.1", "yargs": "^17.7.2" }, "peerDependencies": { "@kitajs/html": "^4.2.5", "typescript": "^5.6.2" }, "bin": { "ts-html-plugin": "dist/cli.js", "xss-scan": "dist/cli.js" } }, "sha512-wmjyV8hmJmDOnUM/ZyPkc0UBYgUYmf32/93rkW8wr8h+HiHVMU0tEKFnmRdBjTcy9jwoC9Bnt2NuzS9l67lq5g=="],
+
+
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
+
+
"@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.57.2", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A=="],
+
+
"@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@1.30.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA=="],
+
+
"@opentelemetry/core": ["@opentelemetry/core@1.30.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ=="],
+
+
"@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.57.2", "", { "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "semver": "^7.5.2", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg=="],
+
+
"@opentelemetry/instrumentation-amqplib": ["@opentelemetry/instrumentation-amqplib@0.46.1", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ=="],
+
+
"@opentelemetry/instrumentation-connect": ["@opentelemetry/instrumentation-connect@0.43.0", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.57.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/connect": "3.4.36" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Q57JGpH6T4dkYHo9tKXONgLtxzsh1ZEW5M9A/OwKrZFyEpLqWgjhcZ3hIuVvDlhb426iDF1f9FPToV/mi5rpeA=="],
+
+
"@opentelemetry/instrumentation-dataloader": ["@opentelemetry/instrumentation-dataloader@0.16.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.57.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-88+qCHZC02up8PwKHk0UQKLLqGGURzS3hFQBZC7PnGwReuoKjHXS1o29H58S+QkXJpkTr2GACbx8j6mUoGjNPA=="],
+
+
"@opentelemetry/instrumentation-express": ["@opentelemetry/instrumentation-express@0.47.0", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.57.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-XFWVx6k0XlU8lu6cBlCa29ONtVt6ADEjmxtyAyeF2+rifk8uBJbk1La0yIVfI0DoKURGbaEDTNelaXG9l/lNNQ=="],
+
+
"@opentelemetry/instrumentation-fastify": ["@opentelemetry/instrumentation-fastify@0.44.1", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.57.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-RoVeMGKcNttNfXMSl6W4fsYoCAYP1vi6ZAWIGhBY+o7R9Y0afA7f9JJL0j8LHbyb0P0QhSYk+6O56OwI2k4iRQ=="],
+
+
"@opentelemetry/instrumentation-fs": ["@opentelemetry/instrumentation-fs@0.19.0", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.57.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-JGwmHhBkRT2G/BYNV1aGI+bBjJu4fJUD/5/Jat0EWZa2ftrLV3YE8z84Fiij/wK32oMZ88eS8DI4ecLGZhpqsQ=="],
+
+
"@opentelemetry/instrumentation-generic-pool": ["@opentelemetry/instrumentation-generic-pool@0.43.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.57.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-at8GceTtNxD1NfFKGAuwtqM41ot/TpcLh+YsGe4dhf7gvv1HW/ZWdq6nfRtS6UjIvZJOokViqLPJ3GVtZItAnQ=="],
+
+
"@opentelemetry/instrumentation-graphql": ["@opentelemetry/instrumentation-graphql@0.47.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.57.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Cc8SMf+nLqp0fi8oAnooNEfwZWFnzMiBHCGmDFYqmgjPylyLmi83b+NiTns/rKGwlErpW0AGPt0sMpkbNlzn8w=="],
+
+
"@opentelemetry/instrumentation-hapi": ["@opentelemetry/instrumentation-hapi@0.45.1", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.57.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-VH6mU3YqAKTePPfUPwfq4/xr049774qWtfTuJqVHoVspCLiT3bW+fCQ1toZxt6cxRPYASoYaBsMA3CWo8B8rcw=="],
+
+
"@opentelemetry/instrumentation-http": ["@opentelemetry/instrumentation-http@0.57.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/instrumentation": "0.57.1", "@opentelemetry/semantic-conventions": "1.28.0", "forwarded-parse": "2.1.2", "semver": "^7.5.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ThLmzAQDs7b/tdKI3BV2+yawuF09jF111OFsovqT1Qj3D8vjwKBwhi/rDE5xethwn4tSXtZcJ9hBsVAlWFQZ7g=="],
+
+
"@opentelemetry/instrumentation-ioredis": ["@opentelemetry/instrumentation-ioredis@0.47.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.57.0", "@opentelemetry/redis-common": "^0.36.2", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4HqP9IBC8e7pW9p90P3q4ox0XlbLGme65YTrA3UTLvqvo4Z6b0puqZQP203YFu8m9rE/luLfaG7/xrwwqMUpJw=="],
+
+
"@opentelemetry/instrumentation-kafkajs": ["@opentelemetry/instrumentation-kafkajs@0.7.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.57.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-LB+3xiNzc034zHfCtgs4ITWhq6Xvdo8bsq7amR058jZlf2aXXDrN9SV4si4z2ya9QX4tz6r4eZJwDkXOp14/AQ=="],
+
+
"@opentelemetry/instrumentation-knex": ["@opentelemetry/instrumentation-knex@0.44.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.57.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-SlT0+bLA0Lg3VthGje+bSZatlGHw/vwgQywx0R/5u9QC59FddTQSPJeWNw29M6f8ScORMeUOOTwihlQAn4GkJQ=="],
+
+
"@opentelemetry/instrumentation-koa": ["@opentelemetry/instrumentation-koa@0.47.0", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.57.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HFdvqf2+w8sWOuwtEXayGzdZ2vWpCKEQv5F7+2DSA74Te/Cv4rvb2E5So5/lh+ok4/RAIPuvCbCb/SHQFzMmbw=="],
+
+
"@opentelemetry/instrumentation-lru-memoizer": ["@opentelemetry/instrumentation-lru-memoizer@0.44.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.57.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Tn7emHAlvYDFik3vGU0mdwvWJDwtITtkJ+5eT2cUquct6nIs+H8M47sqMJkCpyPe5QIBJoTOHxmc6mj9lz6zDw=="],
+
+
"@opentelemetry/instrumentation-mongodb": ["@opentelemetry/instrumentation-mongodb@0.51.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.57.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-cMKASxCX4aFxesoj3WK8uoQ0YUrRvnfxaO72QWI2xLu5ZtgX/QvdGBlU3Ehdond5eb74c2s1cqRQUIptBnKz1g=="],
+
+
"@opentelemetry/instrumentation-mongoose": ["@opentelemetry/instrumentation-mongoose@0.46.0", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.57.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-mtVv6UeaaSaWTeZtLo4cx4P5/ING2obSqfWGItIFSunQBrYROfhuVe7wdIrFUs2RH1tn2YYpAJyMaRe/bnTTIQ=="],
+
+
"@opentelemetry/instrumentation-mysql": ["@opentelemetry/instrumentation-mysql@0.45.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.57.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/mysql": "2.15.26" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-tWWyymgwYcTwZ4t8/rLDfPYbOTF3oYB8SxnYMtIQ1zEf5uDm90Ku3i6U/vhaMyfHNlIHvDhvJh+qx5Nc4Z3Acg=="],
+
+
"@opentelemetry/instrumentation-mysql2": ["@opentelemetry/instrumentation-mysql2@0.45.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.57.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@opentelemetry/sql-common": "^0.40.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-qLslv/EPuLj0IXFvcE3b0EqhWI8LKmrgRPIa4gUd8DllbBpqJAvLNJSv3cC6vWwovpbSI3bagNO/3Q2SuXv2xA=="],
+
+
"@opentelemetry/instrumentation-nestjs-core": ["@opentelemetry/instrumentation-nestjs-core@0.44.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.57.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-t16pQ7A4WYu1yyQJZhRKIfUNvl5PAaF2pEteLvgJb/BWdd1oNuU1rOYt4S825kMy+0q4ngiX281Ss9qiwHfxFQ=="],
+
+
"@opentelemetry/instrumentation-pg": ["@opentelemetry/instrumentation-pg@0.50.0", "", { "dependencies": { "@opentelemetry/core": "^1.26.0", "@opentelemetry/instrumentation": "^0.57.0", "@opentelemetry/semantic-conventions": "1.27.0", "@opentelemetry/sql-common": "^0.40.1", "@types/pg": "8.6.1", "@types/pg-pool": "2.0.6" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-TtLxDdYZmBhFswm8UIsrDjh/HFBeDXd4BLmE8h2MxirNHewLJ0VS9UUddKKEverb5Sm2qFVjqRjcU+8Iw4FJ3w=="],
+
+
"@opentelemetry/instrumentation-redis-4": ["@opentelemetry/instrumentation-redis-4@0.46.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.57.0", "@opentelemetry/redis-common": "^0.36.2", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aTUWbzbFMFeRODn3720TZO0tsh/49T8H3h8vVnVKJ+yE36AeW38Uj/8zykQ/9nO8Vrtjr5yKuX3uMiG/W8FKNw=="],
+
+
"@opentelemetry/instrumentation-tedious": ["@opentelemetry/instrumentation-tedious@0.18.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.57.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/tedious": "^4.0.14" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-9zhjDpUDOtD+coeADnYEJQ0IeLVCj7w/hqzIutdp5NqS1VqTAanaEfsEcSypyvYv5DX3YOsTUoF+nr2wDXPETA=="],
+
+
"@opentelemetry/instrumentation-undici": ["@opentelemetry/instrumentation-undici@0.10.0", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.57.0" }, "peerDependencies": { "@opentelemetry/api": "^1.7.0" } }, "sha512-vm+V255NGw9gaSsPD6CP0oGo8L55BffBc8KnxqsMuc6XiAD1L8SFNzsW0RHhxJFqy9CJaJh+YiJ5EHXuZ5rZBw=="],
+
+
"@opentelemetry/redis-common": ["@opentelemetry/redis-common@0.36.2", "", {}, "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g=="],
+
+
"@opentelemetry/resources": ["@opentelemetry/resources@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA=="],
+
+
"@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg=="],
+
+
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.36.0", "", {}, "sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ=="],
+
+
"@opentelemetry/sql-common": ["@opentelemetry/sql-common@0.40.1", "", { "dependencies": { "@opentelemetry/core": "^1.1.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0" } }, "sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg=="],
+
+
"@prisma/instrumentation": ["@prisma/instrumentation@5.22.0", "", { "dependencies": { "@opentelemetry/api": "^1.8", "@opentelemetry/instrumentation": "^0.49 || ^0.50 || ^0.51 || ^0.52.0 || ^0.53.0", "@opentelemetry/sdk-trace-base": "^1.22" } }, "sha512-LxccF392NN37ISGxIurUljZSh1YWnphO34V5a0+T7FVQG2u9bhAXRTJpgmQ3483woVhkraQZFF7cbRrpbw/F4Q=="],
+
+
"@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="],
+
+
"@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="],
+
+
"@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="],
+
+
"@sentry/bun": ["@sentry/bun@8.55.0", "", { "dependencies": { "@sentry/core": "8.55.0", "@sentry/node": "8.55.0", "@sentry/opentelemetry": "8.55.0" } }, "sha512-qG2FXesp1OiXieMY5ePMzCkfN20UqwV1NJOI8yUCkQjNKEXqcZRt19Qfi4NZk4stDQn8AI6GfZltwQGs51wKbQ=="],
+
+
"@sentry/core": ["@sentry/core@8.55.0", "", {}, "sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA=="],
+
+
"@sentry/node": ["@sentry/node@8.55.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1", "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/instrumentation-amqplib": "^0.46.0", "@opentelemetry/instrumentation-connect": "0.43.0", "@opentelemetry/instrumentation-dataloader": "0.16.0", "@opentelemetry/instrumentation-express": "0.47.0", "@opentelemetry/instrumentation-fastify": "0.44.1", "@opentelemetry/instrumentation-fs": "0.19.0", "@opentelemetry/instrumentation-generic-pool": "0.43.0", "@opentelemetry/instrumentation-graphql": "0.47.0", "@opentelemetry/instrumentation-hapi": "0.45.1", "@opentelemetry/instrumentation-http": "0.57.1", "@opentelemetry/instrumentation-ioredis": "0.47.0", "@opentelemetry/instrumentation-kafkajs": "0.7.0", "@opentelemetry/instrumentation-knex": "0.44.0", "@opentelemetry/instrumentation-koa": "0.47.0", "@opentelemetry/instrumentation-lru-memoizer": "0.44.0", "@opentelemetry/instrumentation-mongodb": "0.51.0", "@opentelemetry/instrumentation-mongoose": "0.46.0", "@opentelemetry/instrumentation-mysql": "0.45.0", "@opentelemetry/instrumentation-mysql2": "0.45.0", "@opentelemetry/instrumentation-nestjs-core": "0.44.0", "@opentelemetry/instrumentation-pg": "0.50.0", "@opentelemetry/instrumentation-redis-4": "0.46.0", "@opentelemetry/instrumentation-tedious": "0.18.0", "@opentelemetry/instrumentation-undici": "0.10.0", "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.28.0", "@prisma/instrumentation": "5.22.0", "@sentry/core": "8.55.0", "@sentry/opentelemetry": "8.55.0", "import-in-the-middle": "^1.11.2" } }, "sha512-h10LJLDTRAzYgay60Oy7moMookqqSZSviCWkkmHZyaDn+4WURnPp5SKhhfrzPRQcXKrweiOwDSHBgn1tweDssg=="],
+
+
"@sentry/opentelemetry": ["@sentry/opentelemetry@8.55.0", "", { "dependencies": { "@sentry/core": "8.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1", "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.28.0" } }, "sha512-UvatdmSr3Xf+4PLBzJNLZ2JjG1yAPWGe/VrJlJAqyTJ2gKeTzgXJJw8rp4pbvNZO8NaTGEYhhO+scLUj0UtLAQ=="],
+
+
"@sinclair/typebox": ["@sinclair/typebox@0.34.38", "", {}, "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA=="],
+
+
"@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
+
+
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
+
+
"@tqman/nice-logger": ["@tqman/nice-logger@1.1.1", "", { "dependencies": { "picocolors": "^1.1.1" }, "peerDependencies": { "elysia": ">=1.2.0" } }, "sha512-/0PWFNXaUE3Lx9IQ8O0aurH+PW7x9GrCK/WyY4vVVWPz+fShijmt9vPCHLI226AZvwVfvU4BOIrQdCqzsBgeVg=="],
+
+
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
+
+
"@types/connect": ["@types/connect@3.4.36", "", { "dependencies": { "@types/node": "*" } }, "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w=="],
+
+
"@types/mysql": ["@types/mysql@2.15.26", "", { "dependencies": { "@types/node": "*" } }, "sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ=="],
+
+
"@types/node": ["@types/node@24.0.15", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA=="],
+
+
"@types/node-cron": ["@types/node-cron@3.0.11", "", {}, "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg=="],
+
+
"@types/pg": ["@types/pg@8.6.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w=="],
+
+
"@types/pg-pool": ["@types/pg-pool@2.0.6", "", { "dependencies": { "@types/pg": "*" } }, "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ=="],
+
+
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
+
+
"@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="],
+
+
"@types/tedious": ["@types/tedious@4.0.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw=="],
+
+
"@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="],
+
+
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
+
+
"acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="],
+
+
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+
"bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="],
+
+
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
+
+
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
+
+
"cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="],
+
+
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
+
+
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
+
+
"croner": ["croner@6.0.7", "", {}, "sha512-k3Xx3Rcclfr60Yx4TmvsF3Yscuiql8LSvYLaphTsaq5Hk8La4Z/udmUANMOTKpgGGroI2F6/XOr9cU9OFkYluQ=="],
+
+
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+
+
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
+
+
"elysia": ["elysia@1.3.5", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-XVIKXlKFwUT7Sta8GY+wO5reD9I0rqAEtaz1Z71UgJb61csYt8Q3W9al8rtL5RgumuRR8e3DNdzlUN9GkC4KDw=="],
+
+
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+
"exact-mirror": ["exact-mirror@0.1.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw=="],
+
+
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
+
+
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
+
+
"file-type": ["file-type@21.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.7", "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg=="],
+
+
"forwarded-parse": ["forwarded-parse@2.1.2", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="],
+
+
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
+
+
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+
+
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+
+
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
+
+
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
+
+
"import-in-the-middle": ["import-in-the-middle@1.14.2", "", { "dependencies": { "acorn": "^8.14.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^1.2.2", "module-details-from-path": "^1.0.3" } }, "sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw=="],
+
+
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
+
+
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
+
"module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="],
+
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+
"nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="],
+
+
"node-cron": ["node-cron@3.0.3", "", { "dependencies": { "uuid": "8.3.2" } }, "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A=="],
+
+
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
+
+
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
+
+
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
+
+
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
+
+
"pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="],
+
+
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
+
+
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
+
+
"postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
+
+
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
+
+
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
+
+
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
+
+
"require-in-the-middle": ["require-in-the-middle@7.5.2", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3", "resolve": "^1.22.8" } }, "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ=="],
+
+
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
+
+
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
+
+
"shimmer": ["shimmer@1.2.1", "", {}, "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="],
+
+
"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=="],
+
+
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+
"strtok3": ["strtok3@10.3.2", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-or9w505RhhY66+uoe5YOC5QO/bRuATaoim3XTh+pGKx5VMWi/HDhMKuCjDLsLJouU2zg9Hf1nLPcNW7IHv80kQ=="],
+
+
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+
+
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
+
+
"token-types": ["token-types@6.0.3", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-IKJ6EzuPPWtKtEIEPpIdXv9j5j2LGJEYk0CKY2efgKoYKLBiZdh6iQkLVBow/CB3phyWAWCyk+bZeaimJn6uRQ=="],
+
+
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
+
+
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
+
+
"uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
+
+
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
+
+
"uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
+
+
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+
+
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
+
+
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
+
+
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
+
+
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
+
+
"zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="],
+
+
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+
+
"@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
+
+
"@opentelemetry/instrumentation-http/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.57.1", "", { "dependencies": { "@opentelemetry/api-logs": "0.57.1", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "semver": "^7.5.2", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-SgHEKXoVxOjc20ZYusPG3Fh+RLIZTSa4x8QtD3NfgAUDyqdFFS9W1F2ZVbZkqDCdyMcQG02Ok4duUGLHJXHgbA=="],
+
+
"@opentelemetry/instrumentation-http/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
+
+
"@opentelemetry/instrumentation-pg/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.27.0", "", {}, "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="],
+
+
"@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
+
+
"@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
+
+
"@prisma/instrumentation/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.53.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.53.0", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "semver": "^7.5.2", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A=="],
+
+
"@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="],
+
+
"@opentelemetry/instrumentation-http/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.57.1", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-I4PHczeujhQAQv6ZBzqHYEUiggZL4IdSMixtVD3EYqbdrjujE7kRfI5QohjlPoJm8BvenoW5YaTMWRrbpot6tg=="],
+
+
"@prisma/instrumentation/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.53.0", "", { "dependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw=="],
+
+
"@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
+
}
+
}
bun.lockb

This is a binary file and will not be displayed.

+6 -2
package.json
···
"dependencies": {
"@elysiajs/cors": "^1.1.1",
"@elysiajs/cron": "^1.2.0",
+
"@elysiajs/html": "^1.3.0",
"@elysiajs/swagger": "^1.1.6",
"@sentry/bun": "^8.45.0",
"@tqman/nice-logger": "^1.0.7",
···
"node-cron": "^3.0.3"
},
"devDependencies": {
-
"bun-types": "latest"
+
"@types/bun": "latest"
},
-
"module": "src/index.js"
+
"private": true,
+
"peerDependencies": {
+
"typescript": "^5"
+
}
}
+729 -44
src/cache.ts
···
alias: string | null;
}
-
type CacheTypes = User | Emoji;
-
/**
* Cache class for storing user and emoji data with automatic expiration
*/
···
}>;
averageResponseTime: number | null;
topUserAgents: Array<{ userAgent: string; count: number }>;
+
latencyAnalytics: {
+
percentiles: {
+
p50: number | null;
+
p75: number | null;
+
p90: number | null;
+
p95: number | null;
+
p99: number | null;
+
};
+
distribution: Array<{
+
range: string;
+
count: number;
+
percentage: number;
+
}>;
+
slowestEndpoints: Array<{
+
endpoint: string;
+
averageResponseTime: number;
+
count: number;
+
}>;
+
latencyOverTime: Array<{
+
time: string;
+
averageResponseTime: number;
+
p95: number | null;
+
count: number;
+
}>;
+
};
+
performanceMetrics: {
+
uptime: number;
+
errorRate: number;
+
throughput: number;
+
apdex: number;
+
cachehitRate: number;
+
};
+
peakTraffic: {
+
peakHour: string;
+
peakRequests: number;
+
peakDay: string;
+
peakDayRequests: number;
+
};
+
dashboardMetrics: {
+
statsRequests: number;
+
totalWithStats: number;
+
};
+
trafficOverview: Array<{
+
time: string;
+
routes: Record<string, number>;
+
total: number;
+
}>;
}> {
const cutoffTime = Date.now() - days * 24 * 60 * 60 * 1000;
-
// Total requests
+
// Total requests (excluding stats endpoint)
const totalResult = this.db
.query(
-
"SELECT COUNT(*) as count FROM request_analytics WHERE timestamp > ?",
+
"SELECT COUNT(*) as count FROM request_analytics WHERE timestamp > ? AND endpoint != '/stats'",
)
.get(cutoffTime) as { count: number };
-
// Requests by endpoint with average response time
-
const endpointResultsRaw = this.db
+
// Stats endpoint requests (tracked separately)
+
const statsResult = this.db
+
.query(
+
"SELECT COUNT(*) as count FROM request_analytics WHERE timestamp > ? AND endpoint = '/stats'",
+
)
+
.get(cutoffTime) as { count: number };
+
+
// Get raw endpoint data and group them intelligently (excluding stats)
+
const rawEndpointResults = this.db
.query(
`
SELECT endpoint, COUNT(*) as count, AVG(response_time) as averageResponseTime
FROM request_analytics
-
WHERE timestamp > ?
+
WHERE timestamp > ? AND endpoint != '/stats'
GROUP BY endpoint
ORDER BY count DESC
`,
···
averageResponseTime: number | null;
}>;
-
const endpointResults = endpointResultsRaw.map((e) => ({
-
endpoint: e.endpoint,
-
count: e.count,
-
averageResponseTime: e.averageResponseTime ?? 0,
-
}));
+
// Group endpoints intelligently
+
const endpointGroups: Record<
+
string,
+
{ count: number; totalResponseTime: number; requestCount: number }
+
> = {};
+
+
for (const result of rawEndpointResults) {
+
const endpoint = result.endpoint;
+
let groupKey: string;
+
+
if (endpoint === "/" || endpoint === "/dashboard") {
+
groupKey = "Dashboard";
+
} else if (endpoint === "/health") {
+
groupKey = "Health Check";
+
} else if (endpoint === "/swagger" || endpoint.startsWith("/swagger")) {
+
groupKey = "API Documentation";
+
} else if (endpoint === "/emojis") {
+
groupKey = "Emoji List";
+
} else if (endpoint.match(/^\/emojis\/[^\/]+$/)) {
+
groupKey = "Emoji Data";
+
} else if (endpoint.match(/^\/emojis\/[^\/]+\/r$/)) {
+
groupKey = "Emoji Redirects";
+
} else if (endpoint.match(/^\/users\/[^\/]+$/)) {
+
groupKey = "User Data";
+
} else if (endpoint.match(/^\/users\/[^\/]+\/r$/)) {
+
groupKey = "User Redirects";
+
} else if (endpoint.match(/^\/users\/[^\/]+\/purge$/)) {
+
groupKey = "Cache Management";
+
} else if (endpoint === "/reset") {
+
groupKey = "Cache Management";
+
} else {
+
groupKey = endpoint; // Keep as-is for unknown endpoints
+
}
-
// Requests by status code with average response time
+
if (!endpointGroups[groupKey]) {
+
endpointGroups[groupKey] = {
+
count: 0,
+
totalResponseTime: 0,
+
requestCount: 0,
+
};
+
}
+
+
// Defensive: Only update if groupKey exists (should always exist due to initialization above)
+
const group = endpointGroups[groupKey];
+
if (group) {
+
group.count += result.count;
+
if (
+
result.averageResponseTime !== null &&
+
result.averageResponseTime !== undefined
+
) {
+
group.totalResponseTime += result.averageResponseTime * result.count;
+
group.requestCount += result.count;
+
}
+
}
+
}
+
+
// Convert back to array format with calculated averages
+
const requestsByEndpoint = Object.entries(endpointGroups)
+
.map(([endpoint, data]) => ({
+
endpoint,
+
count: data.count,
+
averageResponseTime:
+
data.requestCount > 0
+
? data.totalResponseTime / data.requestCount
+
: 0,
+
}))
+
.sort((a, b) => b.count - a.count);
+
+
// Requests by status code with average response time (excluding stats)
const statusResultsRaw = this.db
.query(
`
SELECT status_code as status, COUNT(*) as count, AVG(response_time) as averageResponseTime
FROM request_analytics
-
WHERE timestamp > ?
+
WHERE timestamp > ? AND endpoint != '/stats'
GROUP BY status_code
ORDER BY count DESC
`,
···
averageResponseTime: s.averageResponseTime ?? 0,
}));
-
// Requests by day with average response time
-
const dayResultsRaw = this.db
-
.query(
-
`
-
SELECT
-
DATE(timestamp / 1000, 'unixepoch') as date,
-
COUNT(*) as count,
-
AVG(response_time) as averageResponseTime
-
FROM request_analytics
-
WHERE timestamp > ?
-
GROUP BY DATE(timestamp / 1000, 'unixepoch')
-
ORDER BY date DESC
-
`,
-
)
-
.all(cutoffTime) as Array<{
+
// Requests over time - hourly for 1 day, daily for longer periods
+
let timeResults: Array<{
date: string;
count: number;
-
averageResponseTime: number | null;
+
averageResponseTime: number;
}>;
-
const dayResults = dayResultsRaw.map((d) => ({
-
date: d.date,
-
count: d.count,
-
averageResponseTime: d.averageResponseTime ?? 0,
-
}));
+
if (days === 1) {
+
// Hourly data for last 24 hours (excluding stats)
+
const hourResultsRaw = this.db
+
.query(
+
`
+
SELECT
+
strftime('%Y-%m-%d %H:00', datetime(timestamp / 1000, 'unixepoch')) as date,
+
COUNT(*) as count,
+
AVG(response_time) as averageResponseTime
+
FROM request_analytics
+
WHERE timestamp > ? AND endpoint != '/stats'
+
GROUP BY strftime('%Y-%m-%d %H:00', datetime(timestamp / 1000, 'unixepoch'))
+
ORDER BY date ASC
+
`,
+
)
+
.all(cutoffTime) as Array<{
+
date: string;
+
count: number;
+
averageResponseTime: number | null;
+
}>;
+
+
timeResults = hourResultsRaw.map((h) => ({
+
date: h.date,
+
count: h.count,
+
averageResponseTime: h.averageResponseTime ?? 0,
+
}));
+
} else {
+
// Daily data for longer periods (excluding stats)
+
const dayResultsRaw = this.db
+
.query(
+
`
+
SELECT
+
DATE(timestamp / 1000, 'unixepoch') as date,
+
COUNT(*) as count,
+
AVG(response_time) as averageResponseTime
+
FROM request_analytics
+
WHERE timestamp > ? AND endpoint != '/stats'
+
GROUP BY DATE(timestamp / 1000, 'unixepoch')
+
ORDER BY date ASC
+
`,
+
)
+
.all(cutoffTime) as Array<{
+
date: string;
+
count: number;
+
averageResponseTime: number | null;
+
}>;
-
// Average response time
+
timeResults = dayResultsRaw.map((d) => ({
+
date: d.date,
+
count: d.count,
+
averageResponseTime: d.averageResponseTime ?? 0,
+
}));
+
}
+
+
// Average response time (excluding stats)
const avgResponseResult = this.db
.query(
`
SELECT AVG(response_time) as avg
FROM request_analytics
-
WHERE timestamp > ? AND response_time IS NOT NULL
+
WHERE timestamp > ? AND response_time IS NOT NULL AND endpoint != '/stats'
`,
)
.get(cutoffTime) as { avg: number | null };
-
// Top user agents
-
const userAgentResults = this.db
+
// Top user agents (simplified and grouped, excluding stats)
+
const rawUserAgentResults = this.db
.query(
`
SELECT user_agent as userAgent, COUNT(*) as count
FROM request_analytics
-
WHERE timestamp > ? AND user_agent IS NOT NULL
+
WHERE timestamp > ? AND user_agent IS NOT NULL AND endpoint != '/stats'
GROUP BY user_agent
ORDER BY count DESC
-
LIMIT 10
+
LIMIT 20
`,
)
.all(cutoffTime) as Array<{ userAgent: string; count: number }>;
+
// Group user agents intelligently
+
const userAgentGroups: Record<string, number> = {};
+
+
for (const result of rawUserAgentResults) {
+
const ua = result.userAgent.toLowerCase();
+
let groupKey: string;
+
+
if (ua.includes("chrome") && !ua.includes("edg")) {
+
groupKey = "Chrome";
+
} else if (ua.includes("firefox")) {
+
groupKey = "Firefox";
+
} else if (ua.includes("safari") && !ua.includes("chrome")) {
+
groupKey = "Safari";
+
} else if (ua.includes("edg")) {
+
groupKey = "Edge";
+
} else if (ua.includes("curl")) {
+
groupKey = "curl";
+
} else if (ua.includes("wget")) {
+
groupKey = "wget";
+
} else if (ua.includes("postman")) {
+
groupKey = "Postman";
+
} else if (
+
ua.includes("bot") ||
+
ua.includes("crawler") ||
+
ua.includes("spider")
+
) {
+
groupKey = "Bots/Crawlers";
+
} else if (ua.includes("python")) {
+
groupKey = "Python Scripts";
+
} else if (
+
ua.includes("node") ||
+
ua.includes("axios") ||
+
ua.includes("fetch")
+
) {
+
groupKey = "API Clients";
+
} else {
+
groupKey = "Other";
+
}
+
+
userAgentGroups[groupKey] =
+
(userAgentGroups[groupKey] || 0) + result.count;
+
}
+
+
// Convert back to array format, sorted by count
+
const topUserAgents = Object.entries(userAgentGroups)
+
.map(([userAgent, count]) => ({ userAgent, count }))
+
.sort((a, b) => b.count - a.count)
+
.slice(0, 10);
+
+
// Enhanced Latency Analytics
+
+
// Get all response times for percentile calculations (excluding stats)
+
const responseTimes = this.db
+
.query(
+
`
+
SELECT response_time
+
FROM request_analytics
+
WHERE timestamp > ? AND response_time IS NOT NULL AND endpoint != '/stats'
+
ORDER BY response_time
+
`,
+
)
+
.all(cutoffTime) as Array<{ response_time: number }>;
+
+
// Calculate percentiles
+
const calculatePercentile = (
+
arr: number[],
+
percentile: number,
+
): number | null => {
+
if (arr.length === 0) return null;
+
const index = Math.ceil((percentile / 100) * arr.length) - 1;
+
return arr[Math.max(0, index)] ?? 0;
+
};
+
+
const sortedTimes = responseTimes
+
.map((r) => r.response_time)
+
.sort((a, b) => a - b);
+
const percentiles = {
+
p50: calculatePercentile(sortedTimes, 50),
+
p75: calculatePercentile(sortedTimes, 75),
+
p90: calculatePercentile(sortedTimes, 90),
+
p95: calculatePercentile(sortedTimes, 95),
+
p99: calculatePercentile(sortedTimes, 99),
+
};
+
+
// Response time distribution
+
const totalWithResponseTime = responseTimes.length;
+
const distributionRanges = [
+
{ min: 0, max: 50, label: "0-50ms" },
+
{ min: 50, max: 100, label: "50-100ms" },
+
{ min: 100, max: 200, label: "100-200ms" },
+
{ min: 200, max: 500, label: "200-500ms" },
+
{ min: 500, max: 1000, label: "500ms-1s" },
+
{ min: 1000, max: 2000, label: "1-2s" },
+
{ min: 2000, max: 5000, label: "2-5s" },
+
{ min: 5000, max: Infinity, label: "5s+" },
+
];
+
+
const distribution = distributionRanges.map((range) => {
+
const count = sortedTimes.filter(
+
(time) => time >= range.min && time < range.max,
+
).length;
+
return {
+
range: range.label,
+
count,
+
percentage:
+
totalWithResponseTime > 0 ? (count / totalWithResponseTime) * 100 : 0,
+
};
+
});
+
+
// Slowest endpoints (grouped)
+
const slowestEndpoints = requestsByEndpoint
+
.filter((e) => e.averageResponseTime > 0)
+
.sort((a, b) => b.averageResponseTime - a.averageResponseTime)
+
.slice(0, 10);
+
+
// Latency over time - hourly for 1 day, daily for longer periods
+
let latencyOverTime: Array<{
+
time: string;
+
averageResponseTime: number;
+
p95: number | null;
+
count: number;
+
}>;
+
+
if (days === 1) {
+
// Hourly latency data for last 24 hours (excluding stats)
+
const latencyOverTimeRaw = this.db
+
.query(
+
`
+
SELECT
+
strftime('%Y-%m-%d %H:00', datetime(timestamp / 1000, 'unixepoch')) as time,
+
AVG(response_time) as averageResponseTime,
+
COUNT(*) as count
+
FROM request_analytics
+
WHERE timestamp > ? AND response_time IS NOT NULL AND endpoint != '/stats'
+
GROUP BY strftime('%Y-%m-%d %H:00', datetime(timestamp / 1000, 'unixepoch'))
+
ORDER BY time ASC
+
`,
+
)
+
.all(cutoffTime) as Array<{
+
time: string;
+
averageResponseTime: number;
+
count: number;
+
}>;
+
+
// Calculate P95 for each hour
+
latencyOverTime = latencyOverTimeRaw.map((hourData) => {
+
const hourStart = new Date(hourData.time).getTime();
+
const hourEnd = hourStart + 60 * 60 * 1000; // 1 hour later
+
+
const hourResponseTimes = this.db
+
.query(
+
`
+
SELECT response_time
+
FROM request_analytics
+
WHERE timestamp >= ? AND timestamp < ? AND response_time IS NOT NULL AND endpoint != '/stats'
+
ORDER BY response_time
+
`,
+
)
+
.all(hourStart, hourEnd) as Array<{ response_time: number }>;
+
+
const hourTimes = hourResponseTimes
+
.map((r) => r.response_time)
+
.sort((a, b) => a - b);
+
const p95 = calculatePercentile(hourTimes, 95);
+
+
return {
+
time: hourData.time,
+
averageResponseTime: hourData.averageResponseTime,
+
p95,
+
count: hourData.count,
+
};
+
});
+
} else {
+
// Daily latency data for longer periods (excluding stats)
+
const latencyOverTimeRaw = this.db
+
.query(
+
`
+
SELECT
+
DATE(timestamp / 1000, 'unixepoch') as time,
+
AVG(response_time) as averageResponseTime,
+
COUNT(*) as count
+
FROM request_analytics
+
WHERE timestamp > ? AND response_time IS NOT NULL AND endpoint != '/stats'
+
GROUP BY DATE(timestamp / 1000, 'unixepoch')
+
ORDER BY time ASC
+
`,
+
)
+
.all(cutoffTime) as Array<{
+
time: string;
+
averageResponseTime: number;
+
count: number;
+
}>;
+
+
// Calculate P95 for each day
+
latencyOverTime = latencyOverTimeRaw.map((dayData) => {
+
const dayStart = new Date(dayData.time + " 00:00:00").getTime();
+
const dayEnd = dayStart + 24 * 60 * 60 * 1000; // 1 day later
+
+
const dayResponseTimes = this.db
+
.query(
+
`
+
SELECT response_time
+
FROM request_analytics
+
WHERE timestamp >= ? AND timestamp < ? AND response_time IS NOT NULL AND endpoint != '/stats'
+
ORDER BY response_time
+
`,
+
)
+
.all(dayStart, dayEnd) as Array<{ response_time: number }>;
+
+
const dayTimes = dayResponseTimes
+
.map((r) => r.response_time)
+
.sort((a, b) => a - b);
+
const p95 = calculatePercentile(dayTimes, 95);
+
+
return {
+
time: dayData.time,
+
averageResponseTime: dayData.averageResponseTime,
+
p95,
+
count: dayData.count,
+
};
+
});
+
}
+
+
// Performance Metrics
+
const errorRequests = statusResults
+
.filter((s) => s.status >= 400)
+
.reduce((sum, s) => sum + s.count, 0);
+
const errorRate =
+
totalResult.count > 0 ? (errorRequests / totalResult.count) * 100 : 0;
+
+
// Calculate throughput (requests per hour)
+
const timeSpanHours = days * 24;
+
const throughput = totalResult.count / timeSpanHours;
+
+
// Calculate APDEX score (Application Performance Index)
+
// Satisfied: <= 100ms, Tolerating: <= 400ms, Frustrated: > 400ms
+
const satisfiedCount = sortedTimes.filter((t) => t <= 100).length;
+
const toleratingCount = sortedTimes.filter(
+
(t) => t > 100 && t <= 400,
+
).length;
+
const apdex =
+
totalWithResponseTime > 0
+
? (satisfiedCount + toleratingCount * 0.5) / totalWithResponseTime
+
: 0;
+
+
// Calculate cache hit rate (redirects vs data endpoints)
+
const redirectRequests = requestsByEndpoint
+
.filter((e) => e.endpoint.includes("Redirects"))
+
.reduce((sum, e) => sum + e.count, 0);
+
const dataRequests = requestsByEndpoint
+
.filter((e) => e.endpoint.includes("Data"))
+
.reduce((sum, e) => sum + e.count, 0);
+
const cachehitRate =
+
redirectRequests + dataRequests > 0
+
? (redirectRequests / (redirectRequests + dataRequests)) * 100
+
: 0;
+
+
// Simulate uptime (would need actual monitoring data)
+
const uptime = Math.max(0, 100 - errorRate * 2); // Simple approximation
+
+
// Peak traffic analysis (excluding stats)
+
const peakHourData = this.db
+
.query(
+
`
+
SELECT
+
strftime('%H:00', datetime(timestamp / 1000, 'unixepoch')) as hour,
+
COUNT(*) as count
+
FROM request_analytics
+
WHERE timestamp > ? AND endpoint != '/stats'
+
GROUP BY strftime('%H:00', datetime(timestamp / 1000, 'unixepoch'))
+
ORDER BY count DESC
+
LIMIT 1
+
`,
+
)
+
.get(cutoffTime) as { hour: string; count: number } | null;
+
+
const peakDayData = this.db
+
.query(
+
`
+
SELECT
+
DATE(timestamp / 1000, 'unixepoch') as day,
+
COUNT(*) as count
+
FROM request_analytics
+
WHERE timestamp > ? AND endpoint != '/stats'
+
GROUP BY DATE(timestamp / 1000, 'unixepoch')
+
ORDER BY count DESC
+
LIMIT 1
+
`,
+
)
+
.get(cutoffTime) as { day: string; count: number } | null;
+
+
// Traffic Overview - detailed route breakdown over time
+
let trafficOverview: Array<{
+
time: string;
+
routes: Record<string, number>;
+
total: number;
+
}>;
+
+
if (days === 1) {
+
// Hourly route breakdown for last 24 hours
+
const trafficRaw = this.db
+
.query(
+
`
+
SELECT
+
strftime('%Y-%m-%d %H:00', datetime(timestamp / 1000, 'unixepoch')) as time,
+
endpoint,
+
COUNT(*) as count
+
FROM request_analytics
+
WHERE timestamp > ? AND endpoint != '/stats'
+
GROUP BY strftime('%Y-%m-%d %H:00', datetime(timestamp / 1000, 'unixepoch')), endpoint
+
ORDER BY time ASC
+
`,
+
)
+
.all(cutoffTime) as Array<{
+
time: string;
+
endpoint: string;
+
count: number;
+
}>;
+
+
// Group by time and create route breakdown
+
const timeGroups: Record<string, Record<string, number>> = {};
+
for (const row of trafficRaw) {
+
if (!timeGroups[row.time]) {
+
timeGroups[row.time] = {};
+
}
+
+
// Apply same grouping logic as endpoints
+
let groupKey: string;
+
const endpoint = row.endpoint;
+
+
if (endpoint === "/" || endpoint === "/dashboard") {
+
groupKey = "Dashboard";
+
} else if (endpoint === "/health") {
+
groupKey = "Health Check";
+
} else if (endpoint === "/swagger" || endpoint.startsWith("/swagger")) {
+
groupKey = "API Documentation";
+
} else if (endpoint === "/emojis") {
+
groupKey = "Emoji List";
+
} else if (endpoint.match(/^\/emojis\/[^\/]+$/)) {
+
groupKey = "Emoji Data";
+
} else if (endpoint.match(/^\/emojis\/[^\/]+\/r$/)) {
+
groupKey = "Emoji Redirects";
+
} else if (endpoint.match(/^\/users\/[^\/]+$/)) {
+
groupKey = "User Data";
+
} else if (endpoint.match(/^\/users\/[^\/]+\/r$/)) {
+
groupKey = "User Redirects";
+
} else if (endpoint.match(/^\/users\/[^\/]+\/purge$/)) {
+
groupKey = "Cache Management";
+
} else if (endpoint === "/reset") {
+
groupKey = "Cache Management";
+
} else {
+
groupKey = endpoint;
+
}
+
+
const group = timeGroups[row.time];
+
+
if (group) {
+
group[groupKey] = (group[groupKey] || 0) + row.count;
+
}
+
}
+
+
trafficOverview = Object.entries(timeGroups)
+
.map(([time, routes]) => ({
+
time,
+
routes,
+
total: Object.values(routes).reduce((sum, count) => sum + count, 0),
+
}))
+
.sort((a, b) => a.time.localeCompare(b.time));
+
} else if (days <= 7) {
+
// 4-hour intervals for 7 days
+
const trafficRaw = this.db
+
.query(
+
`
+
SELECT
+
strftime('%Y-%m-%d %H:00', datetime(timestamp / 1000, 'unixepoch')) as hour,
+
endpoint,
+
COUNT(*) as count
+
FROM request_analytics
+
WHERE timestamp > ? AND endpoint != '/stats'
+
GROUP BY strftime('%Y-%m-%d %H:00', datetime(timestamp / 1000, 'unixepoch')), endpoint
+
ORDER BY hour ASC
+
`,
+
)
+
.all(cutoffTime) as Array<{
+
hour: string;
+
endpoint: string;
+
count: number;
+
}>;
+
+
// Group into 4-hour intervals
+
const intervalGroups: Record<string, Record<string, number>> = {};
+
for (const row of trafficRaw) {
+
const hourStr = row.hour?.split(" ")[1]?.split(":")[0];
+
const hour = hourStr ? parseInt(hourStr) : 0;
+
const intervalHour = Math.floor(hour / 4) * 4;
+
const intervalTime =
+
row.hour.split(" ")[0] +
+
` ${intervalHour.toString().padStart(2, "0")}:00`;
+
+
if (!intervalGroups[intervalTime]) {
+
intervalGroups[intervalTime] = {};
+
}
+
+
// Apply same grouping logic
+
let groupKey: string;
+
const endpoint = row.endpoint;
+
+
if (endpoint === "/" || endpoint === "/dashboard") {
+
groupKey = "Dashboard";
+
} else if (endpoint === "/health") {
+
groupKey = "Health Check";
+
} else if (endpoint === "/swagger" || endpoint.startsWith("/swagger")) {
+
groupKey = "API Documentation";
+
} else if (endpoint === "/emojis") {
+
groupKey = "Emoji List";
+
} else if (endpoint.match(/^\/emojis\/[^\/]+$/)) {
+
groupKey = "Emoji Data";
+
} else if (endpoint.match(/^\/emojis\/[^\/]+\/r$/)) {
+
groupKey = "Emoji Redirects";
+
} else if (endpoint.match(/^\/users\/[^\/]+$/)) {
+
groupKey = "User Data";
+
} else if (endpoint.match(/^\/users\/[^\/]+\/r$/)) {
+
groupKey = "User Redirects";
+
} else if (endpoint.match(/^\/users\/[^\/]+\/purge$/)) {
+
groupKey = "Cache Management";
+
} else if (endpoint === "/reset") {
+
groupKey = "Cache Management";
+
} else {
+
groupKey = endpoint;
+
}
+
+
intervalGroups[intervalTime][groupKey] =
+
(intervalGroups[intervalTime][groupKey] || 0) + row.count;
+
}
+
+
trafficOverview = Object.entries(intervalGroups)
+
.map(([time, routes]) => ({
+
time,
+
routes,
+
total: Object.values(routes).reduce((sum, count) => sum + count, 0),
+
}))
+
.sort((a, b) => a.time.localeCompare(b.time));
+
} else {
+
// Daily breakdown for longer periods
+
const trafficRaw = this.db
+
.query(
+
`
+
SELECT
+
DATE(timestamp / 1000, 'unixepoch') as time,
+
endpoint,
+
COUNT(*) as count
+
FROM request_analytics
+
WHERE timestamp > ? AND endpoint != '/stats'
+
GROUP BY DATE(timestamp / 1000, 'unixepoch'), endpoint
+
ORDER BY time ASC
+
`,
+
)
+
.all(cutoffTime) as Array<{
+
time: string;
+
endpoint: string;
+
count: number;
+
}>;
+
+
// Group by day
+
const dayGroups: Record<string, Record<string, number>> = {};
+
for (const row of trafficRaw) {
+
if (!dayGroups[row.time]) {
+
dayGroups[row.time] = {};
+
}
+
+
// Apply same grouping logic
+
let groupKey: string;
+
const endpoint = row.endpoint;
+
+
if (endpoint === "/" || endpoint === "/dashboard") {
+
groupKey = "Dashboard";
+
} else if (endpoint === "/health") {
+
groupKey = "Health Check";
+
} else if (endpoint === "/swagger" || endpoint.startsWith("/swagger")) {
+
groupKey = "API Documentation";
+
} else if (endpoint === "/emojis") {
+
groupKey = "Emoji List";
+
} else if (endpoint.match(/^\/emojis\/[^\/]+$/)) {
+
groupKey = "Emoji Data";
+
} else if (endpoint.match(/^\/emojis\/[^\/]+\/r$/)) {
+
groupKey = "Emoji Redirects";
+
} else if (endpoint.match(/^\/users\/[^\/]+$/)) {
+
groupKey = "User Data";
+
} else if (endpoint.match(/^\/users\/[^\/]+\/r$/)) {
+
groupKey = "User Redirects";
+
} else if (endpoint.match(/^\/users\/[^\/]+\/purge$/)) {
+
groupKey = "Cache Management";
+
} else if (endpoint === "/reset") {
+
groupKey = "Cache Management";
+
} else {
+
groupKey = endpoint;
+
}
+
const group = dayGroups[row.time];
+
if (group) {
+
group[groupKey] = (group[groupKey] || 0) + row.count;
+
}
+
}
+
+
trafficOverview = Object.entries(dayGroups)
+
.map(([time, routes]) => ({
+
time,
+
routes,
+
total: Object.values(routes).reduce((sum, count) => sum + count, 0),
+
}))
+
.sort((a, b) => a.time.localeCompare(b.time));
+
}
+
return {
totalRequests: totalResult.count,
-
requestsByEndpoint: endpointResults,
+
requestsByEndpoint: requestsByEndpoint,
requestsByStatus: statusResults,
-
requestsByDay: dayResults,
+
requestsByDay: timeResults,
averageResponseTime: avgResponseResult.avg,
-
topUserAgents: userAgentResults,
+
topUserAgents: topUserAgents,
+
latencyAnalytics: {
+
percentiles,
+
distribution,
+
slowestEndpoints,
+
latencyOverTime,
+
},
+
performanceMetrics: {
+
uptime,
+
errorRate,
+
throughput,
+
apdex,
+
cachehitRate,
+
},
+
peakTraffic: {
+
peakHour: peakHourData?.hour || "N/A",
+
peakRequests: peakHourData?.count || 0,
+
peakDay: peakDayData?.day || "N/A",
+
peakDayRequests: peakDayData?.count || 0,
+
},
+
dashboardMetrics: {
+
statsRequests: statsResult.count,
+
totalWithStats: totalResult.count + statsResult.count,
+
},
+
trafficOverview,
};
}
}
+904
src/dashboard.html
···
+
<!doctype html>
+
<html lang="en">
+
<head>
+
<meta charset="UTF-8" />
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+
<title>Cachet Analytics Dashboard</title>
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
+
<style>
+
* {
+
margin: 0;
+
padding: 0;
+
box-sizing: border-box;
+
}
+
+
body {
+
font-family:
+
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
+
sans-serif;
+
background: #f5f5f5;
+
color: #333;
+
}
+
+
.header {
+
background: #fff;
+
padding: 1rem 2rem;
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+
margin-bottom: 2rem;
+
display: flex;
+
justify-content: space-between;
+
align-items: center;
+
}
+
+
.header h1 {
+
color: #2c3e50;
+
}
+
+
.header-links a {
+
margin-left: 1rem;
+
color: #3498db;
+
text-decoration: none;
+
}
+
+
.header-links a:hover {
+
text-decoration: underline;
+
}
+
+
.controls {
+
margin-bottom: 2rem;
+
text-align: center;
+
}
+
+
.controls select,
+
.controls button {
+
padding: 0.5rem 1rem;
+
margin: 0 0.5rem;
+
border: 1px solid #ddd;
+
border-radius: 4px;
+
background: white;
+
cursor: pointer;
+
}
+
+
.controls button {
+
background: #3498db;
+
color: white;
+
border: none;
+
}
+
+
.controls button:hover {
+
background: #2980b9;
+
}
+
+
.dashboard {
+
max-width: 1200px;
+
margin: 0 auto;
+
padding: 0 2rem;
+
}
+
+
.stats-grid {
+
display: grid;
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+
gap: 1rem;
+
margin-bottom: 2rem;
+
}
+
+
.stat-card {
+
background: white;
+
padding: 1.5rem;
+
border-radius: 8px;
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+
text-align: center;
+
}
+
+
.stat-number {
+
font-size: 2rem;
+
font-weight: bold;
+
color: #3498db;
+
}
+
+
.stat-label {
+
color: #666;
+
margin-top: 0.5rem;
+
}
+
+
.charts-grid {
+
display: grid;
+
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
+
gap: 2rem;
+
margin-bottom: 2rem;
+
}
+
+
.chart-container {
+
background: white;
+
padding: 1.5rem;
+
border-radius: 8px;
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+
}
+
+
.chart-title {
+
font-size: 1.2rem;
+
margin-bottom: 1rem;
+
color: #2c3e50;
+
}
+
+
.loading {
+
text-align: center;
+
padding: 2rem;
+
color: #666;
+
}
+
+
.error {
+
background: #e74c3c;
+
color: white;
+
padding: 1rem;
+
border-radius: 4px;
+
margin: 1rem 0;
+
}
+
+
.auto-refresh {
+
display: flex;
+
align-items: center;
+
gap: 0.5rem;
+
justify-content: center;
+
margin-top: 1rem;
+
}
+
+
.auto-refresh input[type="checkbox"] {
+
transform: scale(1.2);
+
}
+
+
@media (max-width: 768px) {
+
.charts-grid {
+
grid-template-columns: 1fr;
+
}
+
+
.dashboard {
+
padding: 0 1rem;
+
}
+
+
.header {
+
flex-direction: column;
+
gap: 1rem;
+
text-align: center;
+
}
+
}
+
</style>
+
</head>
+
<body>
+
<div class="header">
+
<h1>📊 Cachet Analytics Dashboard</h1>
+
<div class="header-links">
+
<a href="/swagger">API Docs</a>
+
<a href="/stats">Raw Stats</a>
+
</div>
+
</div>
+
+
<div class="dashboard">
+
<div class="controls">
+
<select id="daysSelect">
+
<option value="1">Last 24 hours</option>
+
<option value="7" selected>Last 7 days</option>
+
<option value="30">Last 30 days</option>
+
</select>
+
<button onclick="loadData()">Refresh</button>
+
<div class="auto-refresh">
+
<input type="checkbox" id="autoRefresh" />
+
<label for="autoRefresh">Auto-refresh (30s)</label>
+
</div>
+
</div>
+
+
<div id="loading" class="loading">Loading analytics data...</div>
+
<div id="error" class="error" style="display: none"></div>
+
+
<div id="content" style="display: none">
+
<div
+
class="chart-container"
+
style="margin-bottom: 2rem; height: 450px"
+
>
+
<div class="chart-title">
+
Traffic Overview - All Routes Over Time
+
</div>
+
<canvas
+
id="trafficOverviewChart"
+
style="padding-bottom: 2rem"
+
></canvas>
+
</div>
+
+
<div class="stats-grid">
+
<div class="stat-card">
+
<div class="stat-number" id="totalRequests">-</div>
+
<div class="stat-label">Total Requests</div>
+
</div>
+
<div class="stat-card">
+
<div class="stat-number" id="avgResponseTime">-</div>
+
<div class="stat-label">Avg Response Time (ms)</div>
+
</div>
+
<div class="stat-card">
+
<div class="stat-number" id="p95ResponseTime">-</div>
+
<div class="stat-label">P95 Response Time (ms)</div>
+
</div>
+
<div class="stat-card">
+
<div class="stat-number" id="uniqueEndpoints">-</div>
+
<div class="stat-label">Unique Endpoints</div>
+
</div>
+
<div class="stat-card">
+
<div class="stat-number" id="errorRate">-</div>
+
<div class="stat-label">Error Rate (%)</div>
+
</div>
+
<div class="stat-card">
+
<div class="stat-number" id="fastRequests">-</div>
+
<div class="stat-label">Fast Requests (&lt;100ms)</div>
+
</div>
+
<div class="stat-card">
+
<div class="stat-number" id="uptime">-</div>
+
<div class="stat-label">Uptime (%)</div>
+
</div>
+
<div class="stat-card">
+
<div class="stat-number" id="throughput">-</div>
+
<div class="stat-label">Throughput (req/hr)</div>
+
</div>
+
<div class="stat-card">
+
<div class="stat-number" id="apdex">-</div>
+
<div class="stat-label">APDEX Score</div>
+
</div>
+
<div class="stat-card">
+
<div class="stat-number" id="cacheHitRate">-</div>
+
<div class="stat-label">Cache Hit Rate (%)</div>
+
</div>
+
</div>
+
+
<div class="stats-grid">
+
<div class="stat-card">
+
<div class="stat-number" id="peakHour">-</div>
+
<div class="stat-label">Peak Hour</div>
+
</div>
+
<div class="stat-card">
+
<div class="stat-number" id="peakHourRequests">-</div>
+
<div class="stat-label">Peak Hour Requests</div>
+
</div>
+
<div class="stat-card">
+
<div class="stat-number" id="peakDay">-</div>
+
<div class="stat-label">Peak Day</div>
+
</div>
+
<div class="stat-card">
+
<div class="stat-number" id="peakDayRequests">-</div>
+
<div class="stat-label">Peak Day Requests</div>
+
</div>
+
<div class="stat-card">
+
<div class="stat-number" id="dashboardRequests">-</div>
+
<div class="stat-label">Dashboard Requests</div>
+
</div>
+
</div>
+
+
<div class="charts-grid">
+
<div class="chart-container">
+
<div class="chart-title">Requests Over Time</div>
+
<canvas id="timeChart"></canvas>
+
</div>
+
+
<div class="chart-container">
+
<div class="chart-title">
+
Latency Over Time (Hourly)
+
</div>
+
<canvas id="latencyTimeChart"></canvas>
+
</div>
+
+
<div class="chart-container">
+
<div class="chart-title">
+
Response Time Distribution
+
</div>
+
<canvas id="latencyDistributionChart"></canvas>
+
</div>
+
+
<div class="chart-container">
+
<div class="chart-title">Latency Percentiles</div>
+
<canvas id="percentilesChart"></canvas>
+
</div>
+
+
<div class="chart-container">
+
<div class="chart-title">Top Endpoints</div>
+
<canvas id="endpointChart"></canvas>
+
</div>
+
+
<div class="chart-container">
+
<div class="chart-title">Slowest Endpoints</div>
+
<canvas id="slowestEndpointsChart"></canvas>
+
</div>
+
+
<div class="chart-container">
+
<div class="chart-title">Status Codes</div>
+
<canvas id="statusChart"></canvas>
+
</div>
+
+
<div class="chart-container">
+
<div class="chart-title">Top User Agents</div>
+
<canvas id="userAgentChart"></canvas>
+
</div>
+
</div>
+
</div>
+
</div>
+
+
<script>
+
let charts = {};
+
let autoRefreshInterval;
+
+
async function loadData() {
+
const days = document.getElementById("daysSelect").value;
+
const loading = document.getElementById("loading");
+
const error = document.getElementById("error");
+
const content = document.getElementById("content");
+
+
loading.style.display = "block";
+
error.style.display = "none";
+
content.style.display = "none";
+
+
try {
+
const response = await fetch(`/stats?days=${days}`);
+
if (!response.ok)
+
throw new Error(`HTTP ${response.status}`);
+
+
const data = await response.json();
+
updateDashboard(data);
+
+
loading.style.display = "none";
+
content.style.display = "block";
+
} catch (err) {
+
loading.style.display = "none";
+
error.style.display = "block";
+
error.textContent = `Failed to load data: ${err.message}`;
+
}
+
}
+
+
function updateDashboard(data) {
+
// Main metrics
+
document.getElementById("totalRequests").textContent =
+
data.totalRequests.toLocaleString();
+
document.getElementById("avgResponseTime").textContent =
+
data.averageResponseTime
+
? Math.round(data.averageResponseTime)
+
: "N/A";
+
document.getElementById("p95ResponseTime").textContent = data
+
.latencyAnalytics.percentiles.p95
+
? Math.round(data.latencyAnalytics.percentiles.p95)
+
: "N/A";
+
document.getElementById("uniqueEndpoints").textContent =
+
data.requestsByEndpoint.length;
+
+
const errorRequests = data.requestsByStatus
+
.filter((s) => s.status >= 400)
+
.reduce((sum, s) => sum + s.count, 0);
+
const errorRate =
+
data.totalRequests > 0
+
? ((errorRequests / data.totalRequests) * 100).toFixed(
+
1,
+
)
+
: "0.0";
+
document.getElementById("errorRate").textContent = errorRate;
+
+
// Calculate fast requests percentage
+
const fastRequestsData = data.latencyAnalytics.distribution
+
.filter(
+
(d) => d.range === "0-50ms" || d.range === "50-100ms",
+
)
+
.reduce((sum, d) => sum + d.percentage, 0);
+
document.getElementById("fastRequests").textContent =
+
fastRequestsData.toFixed(1) + "%";
+
+
// Performance metrics
+
document.getElementById("uptime").textContent =
+
data.performanceMetrics.uptime.toFixed(1);
+
document.getElementById("throughput").textContent = Math.round(
+
data.performanceMetrics.throughput,
+
);
+
document.getElementById("apdex").textContent =
+
data.performanceMetrics.apdex.toFixed(2);
+
document.getElementById("cacheHitRate").textContent =
+
data.performanceMetrics.cachehitRate.toFixed(1);
+
+
// Peak traffic
+
document.getElementById("peakHour").textContent =
+
data.peakTraffic.peakHour;
+
document.getElementById("peakHourRequests").textContent =
+
data.peakTraffic.peakRequests.toLocaleString();
+
document.getElementById("peakDay").textContent =
+
data.peakTraffic.peakDay;
+
document.getElementById("peakDayRequests").textContent =
+
data.peakTraffic.peakDayRequests.toLocaleString();
+
+
// Dashboard metrics
+
document.getElementById("dashboardRequests").textContent =
+
data.dashboardMetrics.statsRequests.toLocaleString();
+
+
// Determine if we're showing hourly or daily data
+
const days = parseInt(
+
document.getElementById("daysSelect").value,
+
);
+
const isHourly = days === 1;
+
+
updateTrafficOverviewChart(data.trafficOverview, days);
+
updateTimeChart(data.requestsByDay, isHourly);
+
updateLatencyTimeChart(
+
data.latencyAnalytics.latencyOverTime,
+
isHourly,
+
);
+
updateLatencyDistributionChart(
+
data.latencyAnalytics.distribution,
+
);
+
updatePercentilesChart(data.latencyAnalytics.percentiles);
+
updateEndpointChart(data.requestsByEndpoint.slice(0, 10));
+
updateSlowestEndpointsChart(
+
data.latencyAnalytics.slowestEndpoints,
+
);
+
updateStatusChart(data.requestsByStatus);
+
updateUserAgentChart(data.topUserAgents.slice(0, 5));
+
}
+
+
function updateTrafficOverviewChart(data, days) {
+
const ctx = document
+
.getElementById("trafficOverviewChart")
+
.getContext("2d");
+
+
if (charts.trafficOverview) charts.trafficOverview.destroy();
+
+
// Update chart title based on granularity
+
const chartTitle = document
+
.querySelector("#trafficOverviewChart")
+
.parentElement.querySelector(".chart-title");
+
let titleText = "Traffic Overview - All Routes Over Time";
+
if (days === 1) {
+
titleText += " (Hourly)";
+
} else if (days <= 7) {
+
titleText += " (4-Hour Intervals)";
+
} else {
+
titleText += " (Daily)";
+
}
+
chartTitle.textContent = titleText;
+
+
// Get all unique routes across all time periods
+
const allRoutes = new Set();
+
data.forEach((timePoint) => {
+
Object.keys(timePoint.routes).forEach((route) =>
+
allRoutes.add(route),
+
);
+
});
+
+
// Define colors for different route types
+
const routeColors = {
+
Dashboard: "#3498db",
+
"User Data": "#2ecc71",
+
"User Redirects": "#27ae60",
+
"Emoji Data": "#e74c3c",
+
"Emoji Redirects": "#c0392b",
+
"Emoji List": "#e67e22",
+
"Health Check": "#f39c12",
+
"API Documentation": "#9b59b6",
+
"Cache Management": "#34495e",
+
};
+
+
// Create datasets for each route
+
const datasets = Array.from(allRoutes).map((route) => {
+
const color = routeColors[route] || "#95a5a6";
+
return {
+
label: route,
+
data: data.map(
+
(timePoint) => timePoint.routes[route] || 0,
+
),
+
borderColor: color,
+
backgroundColor: color + "20", // Add transparency
+
tension: 0.4,
+
fill: false,
+
pointRadius: 2,
+
pointHoverRadius: 4,
+
};
+
});
+
+
// Format labels based on time granularity
+
const labels = data.map((timePoint) => {
+
if (days === 1) {
+
// Show just hour for 24h view
+
return timePoint.time.split(" ")[1] || timePoint.time;
+
} else if (days <= 7) {
+
// Show day and hour for 7-day view
+
const parts = timePoint.time.split(" ");
+
const date = parts[0].split("-")[2]; // Get day
+
const hour = parts[1] || "00:00";
+
return `${date} ${hour}`;
+
} else {
+
// Show full date for longer periods
+
return timePoint.time;
+
}
+
});
+
+
charts.trafficOverview = new Chart(ctx, {
+
type: "line",
+
data: {
+
labels: labels,
+
datasets: datasets,
+
},
+
options: {
+
responsive: true,
+
maintainAspectRatio: false,
+
interaction: {
+
mode: "index",
+
intersect: false,
+
},
+
plugins: {
+
legend: {
+
position: "top",
+
labels: {
+
usePointStyle: true,
+
padding: 15,
+
font: {
+
size: 11,
+
},
+
},
+
},
+
tooltip: {
+
mode: "index",
+
intersect: false,
+
callbacks: {
+
afterLabel: function (context) {
+
const timePoint =
+
data[context.dataIndex];
+
return `Total: ${timePoint.total} requests`;
+
},
+
},
+
},
+
},
+
scales: {
+
x: {
+
display: true,
+
title: {
+
display: true,
+
text:
+
days === 1
+
? "Hour"
+
: days <= 7
+
? "Day & Hour"
+
: "Date",
+
},
+
ticks: {
+
maxTicksLimit: 20,
+
},
+
},
+
y: {
+
display: true,
+
title: {
+
display: true,
+
text: "Requests",
+
},
+
beginAtZero: true,
+
},
+
},
+
elements: {
+
line: {
+
tension: 0.4,
+
},
+
},
+
},
+
});
+
}
+
+
function updateTimeChart(data, isHourly) {
+
const ctx = document
+
.getElementById("timeChart")
+
.getContext("2d");
+
+
if (charts.time) charts.time.destroy();
+
+
// Update chart title
+
const chartTitle = document
+
.querySelector("#timeChart")
+
.parentElement.querySelector(".chart-title");
+
chartTitle.textContent = isHourly
+
? "Requests Over Time (Hourly)"
+
: "Requests Over Time (Daily)";
+
+
charts.time = new Chart(ctx, {
+
type: "line",
+
data: {
+
labels: data.map((d) =>
+
isHourly ? d.date.split(" ")[1] : d.date,
+
),
+
datasets: [
+
{
+
label: "Requests",
+
data: data.map((d) => d.count),
+
borderColor: "#3498db",
+
backgroundColor: "rgba(52, 152, 219, 0.1)",
+
tension: 0.4,
+
fill: true,
+
},
+
],
+
},
+
options: {
+
responsive: true,
+
scales: {
+
y: {
+
beginAtZero: true,
+
},
+
},
+
},
+
});
+
}
+
+
function updateEndpointChart(data) {
+
const ctx = document
+
.getElementById("endpointChart")
+
.getContext("2d");
+
+
if (charts.endpoint) charts.endpoint.destroy();
+
+
charts.endpoint = new Chart(ctx, {
+
type: "bar",
+
data: {
+
labels: data.map((d) => d.endpoint),
+
datasets: [
+
{
+
label: "Requests",
+
data: data.map((d) => d.count),
+
backgroundColor: "#2ecc71",
+
},
+
],
+
},
+
options: {
+
responsive: true,
+
indexAxis: "y",
+
scales: {
+
x: {
+
beginAtZero: true,
+
},
+
},
+
},
+
});
+
}
+
+
function updateStatusChart(data) {
+
const ctx = document
+
.getElementById("statusChart")
+
.getContext("2d");
+
+
if (charts.status) charts.status.destroy();
+
+
const colors = data.map((d) => {
+
if (d.status >= 200 && d.status < 300) return "#2ecc71";
+
if (d.status >= 300 && d.status < 400) return "#f39c12";
+
if (d.status >= 400 && d.status < 500) return "#e74c3c";
+
return "#9b59b6";
+
});
+
+
charts.status = new Chart(ctx, {
+
type: "doughnut",
+
data: {
+
labels: data.map((d) => `${d.status}`),
+
datasets: [
+
{
+
data: data.map((d) => d.count),
+
backgroundColor: colors,
+
},
+
],
+
},
+
options: {
+
responsive: true,
+
},
+
});
+
}
+
+
function updateUserAgentChart(data) {
+
const ctx = document
+
.getElementById("userAgentChart")
+
.getContext("2d");
+
+
if (charts.userAgent) charts.userAgent.destroy();
+
+
const labels = data.map((d) => {
+
const ua = d.userAgent;
+
if (ua.includes("Chrome")) return "Chrome";
+
if (ua.includes("Firefox")) return "Firefox";
+
if (ua.includes("Safari")) return "Safari";
+
if (ua.includes("curl")) return "curl";
+
if (ua.includes("bot")) return "Bot";
+
return "Other";
+
});
+
+
charts.userAgent = new Chart(ctx, {
+
type: "pie",
+
data: {
+
labels: labels,
+
datasets: [
+
{
+
data: data.map((d) => d.count),
+
backgroundColor: [
+
"#3498db",
+
"#e74c3c",
+
"#2ecc71",
+
"#f39c12",
+
"#9b59b6",
+
],
+
},
+
],
+
},
+
options: {
+
responsive: true,
+
},
+
});
+
}
+
+
function updateLatencyTimeChart(data, isHourly) {
+
const ctx = document
+
.getElementById("latencyTimeChart")
+
.getContext("2d");
+
+
if (charts.latencyTime) charts.latencyTime.destroy();
+
+
// Update chart title
+
const chartTitle = document
+
.querySelector("#latencyTimeChart")
+
.parentElement.querySelector(".chart-title");
+
chartTitle.textContent = isHourly
+
? "Latency Over Time (Hourly)"
+
: "Latency Over Time (Daily)";
+
+
charts.latencyTime = new Chart(ctx, {
+
type: "line",
+
data: {
+
labels: data.map((d) =>
+
isHourly ? d.time.split(" ")[1] : d.time,
+
),
+
datasets: [
+
{
+
label: "Average Response Time",
+
data: data.map((d) => d.averageResponseTime),
+
borderColor: "#3498db",
+
backgroundColor: "rgba(52, 152, 219, 0.1)",
+
tension: 0.4,
+
yAxisID: "y",
+
},
+
{
+
label: "P95 Response Time",
+
data: data.map((d) => d.p95),
+
borderColor: "#e74c3c",
+
backgroundColor: "rgba(231, 76, 60, 0.1)",
+
tension: 0.4,
+
yAxisID: "y",
+
},
+
],
+
},
+
options: {
+
responsive: true,
+
scales: {
+
y: {
+
beginAtZero: true,
+
title: {
+
display: true,
+
text: "Response Time (ms)",
+
},
+
},
+
},
+
},
+
});
+
}
+
+
function updateLatencyDistributionChart(data) {
+
const ctx = document
+
.getElementById("latencyDistributionChart")
+
.getContext("2d");
+
+
if (charts.latencyDistribution)
+
charts.latencyDistribution.destroy();
+
+
charts.latencyDistribution = new Chart(ctx, {
+
type: "bar",
+
data: {
+
labels: data.map((d) => d.range),
+
datasets: [
+
{
+
label: "Requests",
+
data: data.map((d) => d.count),
+
backgroundColor: "#2ecc71",
+
},
+
],
+
},
+
options: {
+
responsive: true,
+
scales: {
+
y: {
+
beginAtZero: true,
+
},
+
},
+
},
+
});
+
}
+
+
function updatePercentilesChart(percentiles) {
+
const ctx = document
+
.getElementById("percentilesChart")
+
.getContext("2d");
+
+
if (charts.percentiles) charts.percentiles.destroy();
+
+
const data = [
+
{ label: "P50", value: percentiles.p50 },
+
{ label: "P75", value: percentiles.p75 },
+
{ label: "P90", value: percentiles.p90 },
+
{ label: "P95", value: percentiles.p95 },
+
{ label: "P99", value: percentiles.p99 },
+
].filter((d) => d.value !== null);
+
+
charts.percentiles = new Chart(ctx, {
+
type: "bar",
+
data: {
+
labels: data.map((d) => d.label),
+
datasets: [
+
{
+
label: "Response Time (ms)",
+
data: data.map((d) => d.value),
+
backgroundColor: [
+
"#3498db",
+
"#2ecc71",
+
"#f39c12",
+
"#e74c3c",
+
"#9b59b6",
+
],
+
},
+
],
+
},
+
options: {
+
responsive: true,
+
scales: {
+
y: {
+
beginAtZero: true,
+
},
+
},
+
},
+
});
+
}
+
+
function updateSlowestEndpointsChart(data) {
+
const ctx = document
+
.getElementById("slowestEndpointsChart")
+
.getContext("2d");
+
+
if (charts.slowestEndpoints) charts.slowestEndpoints.destroy();
+
+
charts.slowestEndpoints = new Chart(ctx, {
+
type: "bar",
+
data: {
+
labels: data.map((d) => d.endpoint),
+
datasets: [
+
{
+
label: "Avg Response Time (ms)",
+
data: data.map((d) => d.averageResponseTime),
+
backgroundColor: "#e74c3c",
+
},
+
],
+
},
+
options: {
+
responsive: true,
+
indexAxis: "y",
+
scales: {
+
x: {
+
beginAtZero: true,
+
},
+
},
+
},
+
});
+
}
+
+
document
+
.getElementById("autoRefresh")
+
.addEventListener("change", function () {
+
if (this.checked) {
+
autoRefreshInterval = setInterval(loadData, 30000);
+
} else {
+
clearInterval(autoRefreshInterval);
+
}
+
});
+
+
loadData();
+
document
+
.getElementById("daysSelect")
+
.addEventListener("change", loadData);
+
</script>
+
</body>
+
</html>
+61 -2
src/index.ts
···
import { cors } from "@elysiajs/cors";
import { cron } from "@elysiajs/cron";
+
import { html } from "@elysiajs/html";
import { swagger } from "@elysiajs/swagger";
import * as Sentry from "@sentry/bun";
import { logger } from "@tqman/nice-logger";
···
import { version } from "../package.json";
import { getEmojiUrl } from "../utils/emojiHelper";
import { SlackCache } from "./cache";
+
import dashboard from "./dashboard.html" with { type: "text" };
import type { SlackUser } from "./slack";
import { SlackWrapper } from "./slackWrapper";
···
);
const app = new Elysia()
+
.use(html())
.use(
logger({
mode: "combined",
···
headers["user-agent"]?.toLowerCase().includes("chrome") ||
headers["user-agent"]?.toLowerCase().includes("safari")
) {
-
return redirect("/swagger", 302);
+
return redirect("/dashboard", 302);
}
-
return "Hello World from Cachet 😊\n\n---\nSee /swagger for docs\n---";
+
return "Hello World from Cachet 😊\n\n---\nSee /swagger for docs\nSee /dashboard for analytics\n---";
})
.get("/favicon.ico", Bun.file("./favicon.ico"))
+
.get("/dashboard", () => dashboard)
.get(
"/health",
async ({ error }) => {
···
t.Object({
userAgent: t.String(),
count: t.Number(),
+
}),
+
),
+
latencyAnalytics: t.Object({
+
percentiles: t.Object({
+
p50: t.Nullable(t.Number()),
+
p75: t.Nullable(t.Number()),
+
p90: t.Nullable(t.Number()),
+
p95: t.Nullable(t.Number()),
+
p99: t.Nullable(t.Number()),
+
}),
+
distribution: t.Array(
+
t.Object({
+
range: t.String(),
+
count: t.Number(),
+
percentage: t.Number(),
+
}),
+
),
+
slowestEndpoints: t.Array(
+
t.Object({
+
endpoint: t.String(),
+
averageResponseTime: t.Number(),
+
count: t.Number(),
+
}),
+
),
+
latencyOverTime: t.Array(
+
t.Object({
+
time: t.String(),
+
averageResponseTime: t.Number(),
+
p95: t.Nullable(t.Number()),
+
count: t.Number(),
+
}),
+
),
+
}),
+
performanceMetrics: t.Object({
+
uptime: t.Number(),
+
errorRate: t.Number(),
+
throughput: t.Number(),
+
apdex: t.Number(),
+
cachehitRate: t.Number(),
+
}),
+
peakTraffic: t.Object({
+
peakHour: t.String(),
+
peakRequests: t.Number(),
+
peakDay: t.String(),
+
peakDayRequests: t.Number(),
+
}),
+
dashboardMetrics: t.Object({
+
statsRequests: t.Number(),
+
totalWithStats: t.Number(),
+
}),
+
trafficOverview: t.Array(
+
t.Object({
+
time: t.String(),
+
routes: t.Record(t.String(), t.Number()),
+
total: t.Number(),
}),
),
}),