From 6227a49992d83e16c3e34cbf806d84d792c61638 Mon Sep 17 00:00:00 2001 From: LovroG05 Date: Sun, 28 Jun 2026 23:15:28 +0200 Subject: [PATCH] feat: add React admin dashboard Tailwind v4 dark design (DM Sans/Mono, violet accent). Pages: Overview, Logs, API Keys, Providers, Models, Analytics, Routing, Settings. Auth gate uses localStorage token validated against /api/admin/providers. Vite builds to internal/web/dist which is embedded into the binary. --- backend/dashboard/index.html | 3 + backend/dashboard/package-lock.json | 725 ++++++++++++++++++-- backend/dashboard/package.json | 5 +- backend/dashboard/src/App.css | 38 - backend/dashboard/src/App.tsx | 59 +- backend/dashboard/src/components/Layout.tsx | 182 +++++ backend/dashboard/src/index.css | 53 +- backend/dashboard/src/lib/api.ts | 67 ++ backend/dashboard/src/lib/types.ts | 83 +++ backend/dashboard/src/logo.svg | 1 - backend/dashboard/src/pages/Analytics.tsx | 136 ++++ backend/dashboard/src/pages/ApiKeys.tsx | 161 +++++ backend/dashboard/src/pages/Login.tsx | 104 +++ backend/dashboard/src/pages/Logs.tsx | 89 +++ backend/dashboard/src/pages/Models.tsx | 139 ++++ backend/dashboard/src/pages/Overview.tsx | 118 ++++ backend/dashboard/src/pages/Providers.tsx | 146 ++++ backend/dashboard/src/pages/Routing.tsx | 89 +++ backend/dashboard/src/pages/Settings.tsx | 119 ++++ backend/dashboard/vite.config.ts | 3 +- 20 files changed, 2178 insertions(+), 142 deletions(-) delete mode 100644 backend/dashboard/src/App.css create mode 100644 backend/dashboard/src/components/Layout.tsx create mode 100644 backend/dashboard/src/lib/api.ts create mode 100644 backend/dashboard/src/lib/types.ts delete mode 100644 backend/dashboard/src/logo.svg create mode 100644 backend/dashboard/src/pages/Analytics.tsx create mode 100644 backend/dashboard/src/pages/ApiKeys.tsx create mode 100644 backend/dashboard/src/pages/Login.tsx create mode 100644 backend/dashboard/src/pages/Logs.tsx create mode 100644 backend/dashboard/src/pages/Models.tsx create mode 100644 backend/dashboard/src/pages/Overview.tsx create mode 100644 backend/dashboard/src/pages/Providers.tsx create mode 100644 backend/dashboard/src/pages/Routing.tsx create mode 100644 backend/dashboard/src/pages/Settings.tsx diff --git a/backend/dashboard/index.html b/backend/dashboard/index.html index 7dd35d0..330c8da 100644 --- a/backend/dashboard/index.html +++ b/backend/dashboard/index.html @@ -5,6 +5,9 @@ LMR Dashboard + + +
diff --git a/backend/dashboard/package-lock.json b/backend/dashboard/package-lock.json index 3906ea6..0a9079c 100644 --- a/backend/dashboard/package-lock.json +++ b/backend/dashboard/package-lock.json @@ -8,8 +8,11 @@ "name": "dashboard", "version": "0.1.0", "dependencies": { + "@tailwindcss/vite": "^4.3.1", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-router-dom": "^7.18.0", + "tailwindcss": "^4.3.1" }, "devDependencies": { "@types/react": "^19.2.0", @@ -308,7 +311,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -325,7 +327,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -342,7 +343,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -359,7 +359,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -376,7 +375,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -393,7 +391,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -410,7 +407,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -427,7 +423,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -444,7 +439,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -461,7 +455,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -478,7 +471,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -495,7 +487,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -512,7 +503,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -529,7 +519,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -546,7 +535,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -563,7 +551,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -580,7 +567,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -597,7 +583,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -614,7 +599,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -631,7 +615,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -648,7 +631,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -665,7 +647,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -682,7 +663,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -699,7 +679,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -716,7 +695,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -733,7 +711,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -747,7 +724,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -758,7 +734,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -769,7 +744,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -779,14 +753,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -807,7 +779,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -821,7 +792,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -835,7 +805,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -849,7 +818,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -863,7 +831,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -877,7 +844,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -891,7 +857,6 @@ "cpu": [ "arm" ], - "dev": true, "libc": [ "glibc" ], @@ -908,7 +873,6 @@ "cpu": [ "arm" ], - "dev": true, "libc": [ "musl" ], @@ -925,7 +889,6 @@ "cpu": [ "arm64" ], - "dev": true, "libc": [ "glibc" ], @@ -942,7 +905,6 @@ "cpu": [ "arm64" ], - "dev": true, "libc": [ "musl" ], @@ -959,7 +921,6 @@ "cpu": [ "loong64" ], - "dev": true, "libc": [ "glibc" ], @@ -976,7 +937,6 @@ "cpu": [ "loong64" ], - "dev": true, "libc": [ "musl" ], @@ -993,7 +953,6 @@ "cpu": [ "ppc64" ], - "dev": true, "libc": [ "glibc" ], @@ -1010,7 +969,6 @@ "cpu": [ "ppc64" ], - "dev": true, "libc": [ "musl" ], @@ -1027,7 +985,6 @@ "cpu": [ "riscv64" ], - "dev": true, "libc": [ "glibc" ], @@ -1044,7 +1001,6 @@ "cpu": [ "riscv64" ], - "dev": true, "libc": [ "musl" ], @@ -1061,7 +1017,6 @@ "cpu": [ "s390x" ], - "dev": true, "libc": [ "glibc" ], @@ -1078,7 +1033,6 @@ "cpu": [ "x64" ], - "dev": true, "libc": [ "glibc" ], @@ -1095,7 +1049,6 @@ "cpu": [ "x64" ], - "dev": true, "libc": [ "musl" ], @@ -1112,7 +1065,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1126,7 +1078,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1140,7 +1091,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1154,7 +1104,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1168,7 +1117,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1182,13 +1130,281 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ] }, + "node_modules/@tailwindcss/node": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz", + "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz", + "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-arm64": "4.3.1", + "@tailwindcss/oxide-darwin-x64": "4.3.1", + "@tailwindcss/oxide-freebsd-x64": "4.3.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", + "@tailwindcss/oxide-linux-x64-musl": "4.3.1", + "@tailwindcss/oxide-wasm32-wasi": "4.3.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz", + "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz", + "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz", + "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz", + "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz", + "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz", + "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz", + "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz", + "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz", + "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz", + "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz", + "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz", + "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz", + "integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.1", + "@tailwindcss/oxide": "4.3.1", + "tailwindcss": "4.3.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1238,7 +1454,6 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { @@ -1357,6 +1572,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1382,6 +1610,15 @@ } } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.380", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.380.tgz", @@ -1389,11 +1626,23 @@ "dev": true, "license": "ISC" }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -1445,7 +1694,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -1463,7 +1711,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -1484,6 +1731,21 @@ "node": ">=6.9.0" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1517,6 +1779,267 @@ "node": ">=6" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -1527,6 +2050,15 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1538,7 +2070,6 @@ "version": "3.3.15", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", - "dev": true, "funding": [ { "type": "github", @@ -1567,14 +2098,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1587,7 +2116,6 @@ "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", - "dev": true, "funding": [ { "type": "opencollective", @@ -1643,11 +2171,48 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz", + "integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz", + "integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/rollup": { "version": "4.62.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.9" @@ -1704,21 +2269,44 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/tailwindcss": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz", + "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -1780,7 +2368,6 @@ "version": "7.3.6", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", diff --git a/backend/dashboard/package.json b/backend/dashboard/package.json index 08c9d0e..265e961 100644 --- a/backend/dashboard/package.json +++ b/backend/dashboard/package.json @@ -9,8 +9,11 @@ "preview": "vite preview" }, "dependencies": { + "@tailwindcss/vite": "^4.3.1", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-router-dom": "^7.18.0", + "tailwindcss": "^4.3.1" }, "devDependencies": { "@types/react": "^19.2.0", diff --git a/backend/dashboard/src/App.css b/backend/dashboard/src/App.css deleted file mode 100644 index 74b5e05..0000000 --- a/backend/dashboard/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/backend/dashboard/src/App.tsx b/backend/dashboard/src/App.tsx index e5fcd63..79c9c3d 100644 --- a/backend/dashboard/src/App.tsx +++ b/backend/dashboard/src/App.tsx @@ -1,25 +1,40 @@ -import logo from './logo.svg'; -import './App.css'; +import { useState } from 'react' +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' +import { getToken } from './lib/api' +import Login from './pages/Login' +import Layout from './components/Layout' +import Overview from './pages/Overview' +import Logs from './pages/Logs' +import ApiKeys from './pages/ApiKeys' +import Providers from './pages/Providers' +import Routing from './pages/Routing' +import Analytics from './pages/Analytics' +import Models from './pages/Models' +import Settings from './pages/Settings' + +export default function App() { + const [authed, setAuthed] = useState(() => !!getToken()) + + if (!authed) { + return setAuthed(true)} /> + } -function App() { return ( -
-
- logo -

- Edit src/App.tsx and save to reload. -

- - Learn React - -
-
- ); + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + ) } - -export default App; diff --git a/backend/dashboard/src/components/Layout.tsx b/backend/dashboard/src/components/Layout.tsx new file mode 100644 index 0000000..d104e3f --- /dev/null +++ b/backend/dashboard/src/components/Layout.tsx @@ -0,0 +1,182 @@ +import { NavLink, Outlet, useLocation } from 'react-router-dom' +import { clearToken } from '../lib/api' + +const NAV = [ + { to: '/overview', label: 'Overview', icon: }, + { to: '/logs', label: 'Request Logs', icon: }, + { to: '/keys', label: 'API Keys', icon: }, + { to: '/providers',label: 'Providers', icon: }, + { to: '/routing', label: 'Routing Rules', icon: }, + { to: '/analytics',label: 'Analytics', icon: }, + { to: '/models', label: 'Model Catalog', icon: }, +] + +function navStyle(active: boolean) { + return { + display: 'flex', alignItems: 'center', gap: '8px', + padding: '7px 10px', borderRadius: '7px', cursor: 'pointer', + fontSize: '13px', fontWeight: 500, + color: active ? '#f0eeff' : 'rgba(237,237,240,0.5)', + background: active ? 'rgba(124,58,237,0.18)' : 'transparent', + transition: 'all 0.12s', + } +} + +export default function Layout() { + const loc = useLocation() + const section = NAV.find(n => loc.pathname.startsWith(n.to))?.label ?? 'Settings' + + return ( +
+ {/* Sidebar */} + + + {/* Main */} +
+ {/* Topbar */} +
+

+ {section} +

+
+ + + /v1 + +
+
+ + {/* Content */} +
+ +
+
+
+ ) +} + +/* ── Icons ── */ +function IconGrid() { + return + + + +} +function IconLogs() { + return + + +} +function IconKey() { + return + + +} +function IconServer() { + return + + + +} +function IconRoute() { + return + + +} +function IconChart() { + return + + +} +function IconBox() { + return + + + +} +function IconSettings() { + return + + + +} diff --git a/backend/dashboard/src/index.css b/backend/dashboard/src/index.css index ec2585e..18dca7a 100644 --- a/backend/dashboard/src/index.css +++ b/backend/dashboard/src/index.css @@ -1,13 +1,46 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; +@import "tailwindcss"; + +@theme { + --color-bg: #09090b; + --color-sidebar: #111115; + --color-card: #17171c; + --color-primary: #7c3aed; + --color-primary-h: #6d28d9; + --color-text: #f0eeff; + --color-green: #22c55e; + --color-red: #ef4444; + --color-v3: #c4b5fd; + --color-v4: #a78bfa; + + --font-sans: 'DM Sans', system-ui, sans-serif; + --font-mono: 'DM Mono', ui-monospace, monospace; } -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; +* { box-sizing: border-box; margin: 0; padding: 0; } + +body { + background: #09090b; + color: #f0eeff; + font-family: 'DM Sans', system-ui, sans-serif; + font-size: 14px; + -webkit-font-smoothing: antialiased; } + +input, select, button, textarea { font-family: inherit; } + +@keyframes fadeUp { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +.animate-fade-up { animation: fadeUp 0.18s ease; } +.dot-pulse { animation: pulse 2.4s ease infinite; } + +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.18); } diff --git a/backend/dashboard/src/lib/api.ts b/backend/dashboard/src/lib/api.ts new file mode 100644 index 0000000..aba0d97 --- /dev/null +++ b/backend/dashboard/src/lib/api.ts @@ -0,0 +1,67 @@ +import type { + Provider, VirtualKey, AdminModel, RequestLog, + CreateProviderParams, CreateKeyParams, ModelOverride, +} from './types' + +const BASE = '/api/admin' + +export function getToken() { return localStorage.getItem('admin_token') ?? '' } +export function setToken(t: string) { localStorage.setItem('admin_token', t) } +export function clearToken() { localStorage.removeItem('admin_token') } + +async function req(path: string, opts?: RequestInit): Promise { + const res = await fetch(BASE + path, { + ...opts, + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${getToken()}`, + ...opts?.headers, + }, + }) + if (res.status === 204) return undefined as T + if (!res.ok) { + if (res.status === 401) { clearToken(); window.location.reload() } + const body = await res.text().catch(() => res.statusText) + throw new Error(body || res.statusText) + } + return res.json() +} + +export const api = { + providers: { + list: () => req('/providers'), + create: (d: CreateProviderParams) => + req<{ id: string }>('/providers', { method: 'POST', body: JSON.stringify(d) }), + delete: (id: string) => req(`/providers/${id}`, { method: 'DELETE' }), + }, + keys: { + list: () => req('/keys'), + create: (d: CreateKeyParams) => + req<{ id: string; key: string; prefix: string }>('/keys', { method: 'POST', body: JSON.stringify(d) }), + revoke: (id: string) => req(`/keys/${id}`, { method: 'DELETE' }), + }, + models: { + list: () => req('/models'), + update: (id: string, d: ModelOverride) => + req(`/models/${id}`, { method: 'PATCH', body: JSON.stringify(d) }), + }, + settings: { + get: () => req>('/settings'), + put: (key: string, value: string) => + req(`/settings/${encodeURIComponent(key)}`, { method: 'PUT', body: JSON.stringify(value) }), + }, + catalog: { + refresh: () => + req<{ status: string; models: number }>('/catalog/refresh', { method: 'POST' }), + }, + logs: { + list: (params: { provider?: string; status?: string; since?: string } = {}) => { + const qs = new URLSearchParams() + if (params.provider) qs.set('provider', params.provider) + if (params.status) qs.set('status', params.status) + if (params.since) qs.set('since', params.since) + const s = qs.toString() + return req('/logs' + (s ? '?' + s : '')) + }, + }, +} diff --git a/backend/dashboard/src/lib/types.ts b/backend/dashboard/src/lib/types.ts new file mode 100644 index 0000000..3178198 --- /dev/null +++ b/backend/dashboard/src/lib/types.ts @@ -0,0 +1,83 @@ +export interface Provider { + id: string + name: string + type: string + base_url: string + enabled: boolean + has_credentials: boolean +} + +export interface VirtualKey { + id: string + name: string + prefix: string + type: string + allow_all_models: boolean + expires_at: string | null + revoked_at: string | null + last_used_at: string | null +} + +export interface AdminModel { + id: string + provider_name: string + slug: string + display_name: string + source: string + available: boolean + enabled: boolean + blocked: boolean + context_length: number | null + disc_input_per_mtok: number | null + disc_output_per_mtok: number | null + price_input_per_mtok: number | null + price_output_per_mtok: number | null +} + +export interface RequestLog { + ID: string + CreatedAt: string + RequestID: string + KeyPrefix: string + ModelName: string + ProviderName: string + StatusCode: number + Streamed: boolean + Error: string + PromptTokens: number + CompletionTokens: number + TotalTokens: number + Cost: number + LatencyMS: number + TTFTMs: number | null +} + +export interface CatalogEntry { + id: string + object: string + owned_by: string +} + +export interface CreateProviderParams { + name: string + type: string + base_url: string + credentials: Record + enabled: boolean +} + +export interface CreateKeyParams { + name: string + allow_all_models: boolean + rules: string[] + expires_in_days: number +} + +export interface ModelOverride { + enabled?: boolean + blocked?: boolean + input_price_per_mtok?: number + output_price_per_mtok?: number + rpm_limit?: number + tpm_limit?: number +} diff --git a/backend/dashboard/src/logo.svg b/backend/dashboard/src/logo.svg deleted file mode 100644 index 9dfc1c0..0000000 --- a/backend/dashboard/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/backend/dashboard/src/pages/Analytics.tsx b/backend/dashboard/src/pages/Analytics.tsx new file mode 100644 index 0000000..6ab8569 --- /dev/null +++ b/backend/dashboard/src/pages/Analytics.tsx @@ -0,0 +1,136 @@ +import { useEffect, useState } from 'react' +import { api } from '../lib/api' +import type { RequestLog } from '../lib/types' + +const D = 'rgba(237,237,240,' + +const RANGES = [ + { label: '24h', ms: 86400000 }, + { label: '7d', ms: 604800000 }, + { label: '30d', ms: 2592000000 }, +] + +function SparkLine({ data, color = '#7c3aed' }: { data: number[]; color?: string }) { + if (data.length < 2) return
+ const max = Math.max(...data, 1) + const w = 560, h = 110, step = w / (data.length - 1) + const y = (v: number) => h - (v / max) * (h - 8) - 4 + const pts = data.map((v, i) => `${i * step},${y(v)}`).join(' ') + const area = `M0,${h} L${pts.split(' ').map(p => `L${p}`).join('')} L${w},${h} Z` + const line = `M${pts.split(' ').join(' L')}` + return ( + + + + + + + + + + + ) +} + +export default function Analytics() { + const [logs, setLogs] = useState([]) + const [loading, setLoading] = useState(true) + const [rangeIdx, setRangeIdx] = useState(0) + + useEffect(() => { + setLoading(true) + api.logs.list({ since: new Date(Date.now() - RANGES[rangeIdx].ms).toISOString() }) + .then(r => setLogs(r ?? [])) + .finally(() => setLoading(false)) + }, [rangeIdx]) + + // Build hourly buckets + const buckets = 24 + const bucketMs = RANGES[rangeIdx].ms / buckets + const now = Date.now() + const reqByBucket = Array(buckets).fill(0) + logs.forEach(l => { + const age = now - new Date(l.CreatedAt).getTime() + const idx = Math.floor(age / bucketMs) + if (idx >= 0 && idx < buckets) reqByBucket[buckets - 1 - idx]++ + }) + + const totalReqs = logs.length + const totalTokens = logs.reduce((s, l) => s + l.TotalTokens, 0) + const totalCost = logs.reduce((s, l) => s + l.Cost, 0) + const avgLatency = logs.length ? Math.round(logs.reduce((s, l) => s + l.LatencyMS, 0) / logs.length) : 0 + + // Top models by token usage + const byModel: Record = {} + logs.forEach(l => { byModel[l.ModelName] = (byModel[l.ModelName] ?? 0) + l.TotalTokens }) + const topModels = Object.entries(byModel).sort((a, b) => b[1] - a[1]).slice(0, 6) + const maxTokens = topModels[0]?.[1] ?? 1 + + const stats = [ + { label: 'Total requests', value: totalReqs.toLocaleString() }, + { label: 'Total tokens', value: totalTokens > 1e6 ? `${(totalTokens/1e6).toFixed(1)}M` : totalTokens.toLocaleString() }, + { label: 'Total cost', value: `$${totalCost.toFixed(4)}` }, + { label: 'Avg latency', value: `${avgLatency}ms` }, + ] + + return ( +
+ {/* Time range picker */} +
+ {RANGES.map((r, i) => ( + + ))} +
+ + {/* Stat cards */} +
+ {stats.map(s => ( +
+
{s.label}
+
+ {loading ? '—' : s.value} +
+
+ ))} +
+ +
+ {/* Line chart */} +
+
Request Volume
+
+ {RANGES[rangeIdx].label === '24h' ? 'Hourly' : 'Periodic'}, last {RANGES[rangeIdx].label} +
+ +
+ {['start','','','','now'].map((t, i) => ( + {t} + ))} +
+
+ + {/* Top models */} +
+
Top Models
+
By token usage
+ {topModels.length === 0 &&
No data yet
} + {topModels.map(([name, tokens]) => ( +
+
+ {name} + {tokens > 1e6 ? `${(tokens/1e6).toFixed(1)}M` : tokens.toLocaleString()} +
+
+
+
+
+ ))} +
+
+
+ ) +} diff --git a/backend/dashboard/src/pages/ApiKeys.tsx b/backend/dashboard/src/pages/ApiKeys.tsx new file mode 100644 index 0000000..4ca2f86 --- /dev/null +++ b/backend/dashboard/src/pages/ApiKeys.tsx @@ -0,0 +1,161 @@ +import { useEffect, useState } from 'react' +import { api } from '../lib/api' +import type { VirtualKey } from '../lib/types' + +const D = 'rgba(237,237,240,' + +function fmtDate(s: string | null) { + if (!s) return '—' + return new Date(s).toLocaleDateString() +} + +function Modal({ onClose, onCreate }: { onClose: () => void; onCreate: (key: string) => void }) { + const [name, setName] = useState('') + const [allModels, setAllModels] = useState(true) + const [rules, setRules] = useState('') + const [expiry, setExpiry] = useState(0) + const [loading, setLoading] = useState(false) + const [newKey, setNewKey] = useState('') + + async function submit(e: React.FormEvent) { + e.preventDefault() + setLoading(true) + try { + const r = await api.keys.create({ + name, allow_all_models: allModels, + rules: rules.split('\n').map(s => s.trim()).filter(Boolean), + expires_in_days: expiry, + }) + setNewKey(r.key) + onCreate(r.key) + } finally { + setLoading(false) + } + } + + return ( +
e.target === e.currentTarget && !newKey && onClose()}> +
+

+ {newKey ? 'Key created' : 'Create API key'} +

+ + {newKey ? ( + <> +

Copy this key now — it won't be shown again.

+
{newKey}
+ + + ) : ( +
+ + setName(e.target.value)} required + style={{ width: '100%', background: 'rgba(0,0,0,0.28)', border: '1px solid rgba(255,255,255,0.09)', borderRadius: 6, padding: '8px 12px', fontSize: 13, color: '#ededf0', outline: 'none', marginBottom: 14 }}/> + + + + {!allModels && ( + <> + +