rnilkyway commited on
Commit
19a497d
·
1 Parent(s): 06c8c6a

feat(admin): unify dashboard + login design with landing/docs theme

Browse files

Aligns the entire admin surface (auth gate, header, footer, cards,
buttons, /admin/login page) with the visual language already used by
landing.html and docs.html: pure black background with a subtle radial
grid pattern, gray-900 cards on gray-800 borders, Inter + JetBrains
Mono typography, amber accents instead of gradients.

dashboard.html:
- Drop the glassmorphism palette (rgba(17,17,24,.65) blur + radial
amber/blue/emerald glows) for flat gray-900 cards. Old .glass /
.glass-bright / .glass-hover class names are kept as aliases so all
existing JS-injected markup keeps working without changes.
- Rebuild auth gate: smaller logo, white primary button, mono v0.4.0
badge, 'Back to homepage' link.
- Rebuild header: sticky black/80 backdrop-blur (same as landing),
inline nav links instead of pill-group, ADMIN badge, Live indicator
pill, Docs link, sized-down logout button.
- Rebuild footer to match landing/docs: copyright + sync time +
Operational pulse + Home/Docs links.
- Replace gradient buttons with flat amber-500 (bg-amber-500 hover:
bg-amber-400).
- Drop the 4 decorative bg-*-500/5 rounded-full blur-2xl halos.
- text-3xl font-extrabold stats → text-2xl font-semibold.
- Soften rounded-2xl → rounded-xl on all glass cards.
- Map every text-/bg-/border-slate-* to gray-* (103 occurrences) so
the palette stays consistent with landing.html.
- Swap the missing lucide 'pocket' icon for 'inbox'.

index.py:
- /admin/login page rewritten to use the same theme tokens (black bg,
gray-900 card with gray-800 border, Inter font, white primary
button, grid background, back-to-home link). Turnstile widget and
master-key flow unchanged.

Files changed (2) hide show
  1. app/index.py +31 -10
  2. app/templates/dashboard.html +251 -312
app/index.py CHANGED
@@ -2098,24 +2098,45 @@ async def admin_login_page():
2098
  <meta charset="UTF-8">
2099
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
2100
  <title>APIarium | Admin Login</title>
 
 
 
2101
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
2102
  <script src="https://cdn.tailwindcss.com"></script>
2103
  <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
 
 
 
 
 
 
 
 
 
 
2104
  </head>
2105
- <body class="bg-slate-950 text-slate-100 min-h-screen flex items-center justify-center p-4">
2106
- <div class="w-full max-w-md">
 
 
2107
  <div class="text-center mb-8">
2108
- <h1 class="text-3xl font-bold">🐝 APIarium</h1>
2109
- <p class="text-slate-400 mt-2">Admin Login</p>
 
2110
  </div>
2111
- <div class="bg-slate-900/50 border border-white/5 rounded-xl p-8">
2112
- <label class="block text-xs font-mono text-slate-400 uppercase tracking-wider mb-2">Master Key</label>
2113
- <input type="password" id="master-key" placeholder="Enter master key" class="w-full bg-black/30 border border-white/10 rounded-lg px-4 py-3 text-sm font-mono focus:outline-none focus:border-amber-500/50 mb-4">
 
2114
  <div id="turnstile-widget" class="flex justify-center mb-4"></div>
2115
- <div id="login-error" class="text-red-400 text-sm mb-4 hidden"></div>
2116
- <button id="login-btn" class="w-full bg-gradient-to-r from-amber-500 to-amber-600 hover:from-amber-400 hover:to-amber-500 text-black font-semibold px-5 py-3 rounded-lg transition text-sm">
2117
- Sign in
 
2118
  </button>
 
 
 
2119
  </div>
2120
  </div>
2121
  <script>
 
2098
  <meta charset="UTF-8">
2099
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
2100
  <title>APIarium | Admin Login</title>
2101
+ <meta name="description" content="APIarium admin console — master key sign-in.">
2102
+ <link rel="preconnect" href="https://fonts.googleapis.com">
2103
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
2104
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
2105
  <script src="https://cdn.tailwindcss.com"></script>
2106
  <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
2107
+ <style>
2108
+ body { background-color: #000; color: #ededed; font-family: 'Inter', sans-serif; }
2109
+ .bg-grid-pattern {
2110
+ background-image: linear-gradient(to right, #ffffff05 1px, transparent 1px),
2111
+ linear-gradient(to bottom, #ffffff05 1px, transparent 1px);
2112
+ background-size: 40px 40px;
2113
+ mask-image: radial-gradient(circle at center, black, transparent 80%);
2114
+ -webkit-mask-image: radial-gradient(circle at center, black, transparent 80%);
2115
+ }
2116
+ </style>
2117
  </head>
2118
+ <body class="min-h-screen flex items-center justify-center p-4 relative">
2119
+ <div class="absolute inset-0 bg-grid-pattern pointer-events-none z-0"></div>
2120
+
2121
+ <div class="w-full max-w-md relative z-10">
2122
  <div class="text-center mb-8">
2123
+ <span class="text-5xl" aria-label="APIarium">🐝</span>
2124
+ <h1 class="text-2xl font-semibold tracking-tight mt-3">APIarium Admin</h1>
2125
+ <p class="text-xs font-mono text-gray-500 mt-2 px-2.5 py-0.5 rounded-full border border-gray-800 inline-block">v0.4.0 · Admin Console</p>
2126
  </div>
2127
+
2128
+ <div class="bg-gray-900 border border-gray-800 rounded-2xl p-8 shadow-2xl">
2129
+ <label class="block text-[10px] font-mono text-gray-500 uppercase tracking-wider mb-2">Master Key</label>
2130
+ <input type="password" id="master-key" placeholder="apiarium_master_..." class="w-full bg-black border border-gray-800 rounded-lg px-4 py-3 text-sm font-mono text-gray-100 focus:outline-none focus:border-amber-500/60 mb-4 placeholder:text-gray-700 transition-colors">
2131
  <div id="turnstile-widget" class="flex justify-center mb-4"></div>
2132
+ <div id="login-error" class="text-red-400 text-sm bg-red-500/10 border border-red-500/30 rounded-lg px-3 py-2 mb-4 hidden font-mono"></div>
2133
+ <button id="login-btn" class="w-full bg-white hover:bg-gray-100 active:scale-[0.99] text-black font-semibold px-5 py-3 rounded-lg transition-all text-sm flex items-center justify-center gap-2">
2134
+ <span>Sign in</span>
2135
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/></svg>
2136
  </button>
2137
+ <div class="mt-6 pt-5 border-t border-gray-800 text-center">
2138
+ <a href="/" class="text-[10px] font-mono text-gray-500 hover:text-gray-300 transition-colors">← Back to homepage</a>
2139
+ </div>
2140
  </div>
2141
  </div>
2142
  <script>
app/templates/dashboard.html CHANGED
@@ -3,19 +3,15 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>APIarium | Admin Gateway Console</title>
7
- <meta name="description" content="APIarium enterprise admin gateway dashboard — manage IP-bound client keys, dynamic providers, and models routing.">
8
 
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
12
 
13
- <!-- Tailwind CSS -->
14
  <script src="https://cdn.tailwindcss.com"></script>
15
- <!-- Chart.js for beautiful analytics -->
16
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
17
-
18
- <!-- Lucide Icons for high-fidelity UI -->
19
  <script src="https://unpkg.com/lucide@latest"></script>
20
 
21
  <script>
@@ -28,31 +24,12 @@
28
  mono: ['JetBrains Mono', 'monospace'],
29
  },
30
  colors: {
31
- primary: {
32
- 50: '#fffbeb',
33
- 100: '#fef3c7',
34
- 200: '#fde68a',
35
- 300: '#fcd34d',
36
- 400: '#fbbf24',
37
- 500: '#f59e0b',
38
- 600: '#d97706',
39
- 700: '#b45309',
40
- 800: '#92400e',
41
- 900: '#78350f'
42
- },
43
- dark: {
44
- 950: '#06060a',
45
- 900: '#0a0a0f',
46
- 800: '#111118',
47
- 700: '#1a1a24',
48
- 600: '#262636',
49
- 500: '#3c3c54'
50
- },
51
- },
52
- boxShadow: {
53
- 'amber-glow': '0 0 20px -3px rgba(245, 158, 11, 0.15)',
54
- 'emerald-glow': '0 0 20px -3px rgba(16, 185, 129, 0.15)',
55
- 'glass-inner': 'inset 0 1px 0 0 rgba(255, 255, 255, 0.05)',
56
  }
57
  }
58
  }
@@ -60,197 +37,158 @@
60
  </script>
61
  <style>
62
  body {
63
- background-color: #06060a;
64
- background-image:
65
- radial-gradient(at 10% 5%, rgba(245, 158, 11, 0.07) 0px, transparent 40%),
66
- radial-gradient(at 90% 95%, rgba(59, 130, 246, 0.05) 0px, transparent 45%),
67
- radial-gradient(at 50% 50%, rgba(16, 185, 129, 0.02) 0px, transparent 50%);
68
- background-attachment: fixed;
69
- min-height: 100vh;
70
- }
71
-
72
- /* Premium Glassmorphism */
73
- .glass {
74
- background: rgba(17, 17, 24, 0.65);
75
- backdrop-filter: blur(16px);
76
- -webkit-backdrop-filter: blur(16px);
77
- border: 1px solid rgba(255, 255, 255, 0.06);
78
- box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.03);
79
- }
80
-
81
- .glass-bright {
82
- background: rgba(255, 255, 255, 0.03);
83
- backdrop-filter: blur(20px);
84
- -webkit-backdrop-filter: blur(20px);
85
- border: 1px solid rgba(255, 255, 255, 0.1);
86
  }
87
 
88
- .glass-hover {
89
- transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
90
- }
91
- .glass-hover:hover {
92
- background: rgba(255, 255, 255, 0.05);
93
- border-color: rgba(245, 158, 11, 0.35);
94
- box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 0 15px -3px rgba(245, 158, 11, 0.08);
95
- transform: translateY(-1px);
96
  }
97
 
98
- /* Custom Scrollbar */
99
- ::-webkit-scrollbar {
100
- width: 6px;
101
- height: 6px;
102
  }
103
- ::-webkit-scrollbar-track {
104
- background: rgba(0, 0, 0, 0.2);
 
105
  }
106
- ::-webkit-scrollbar-thumb {
107
- background: rgba(255, 255, 255, 0.1);
108
- border-radius: 999px;
 
109
  }
110
- ::-webkit-scrollbar-thumb:hover {
111
- background: rgba(245, 158, 11, 0.3);
 
 
 
 
 
 
 
 
 
 
112
  }
113
 
114
- /* Toasts dynamic slider */
115
  .toast {
116
- position: fixed;
117
- bottom: 24px;
118
- right: 24px;
119
- padding: 14px 20px;
120
- border-radius: 12px;
121
- font-family: 'JetBrains Mono', monospace;
 
122
  font-size: 13px;
123
- z-index: 9999;
124
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
125
  display: flex;
126
  align-items: center;
127
  gap: 10px;
128
- animation: slideIn 0.35s cubic-bezier(0.16, 1, 0.3, 1);
129
- transition: all 0.3s;
130
- }
131
- @keyframes slideIn {
132
- from { transform: translateY(20px) scale(0.95); opacity: 0; }
133
- to { transform: translateY(0) scale(1); opacity: 1; }
134
  }
135
- .toast-success {
136
- background: rgba(10, 25, 20, 0.95);
137
- border: 1px solid rgba(16, 185, 129, 0.3);
138
- color: #34d399;
139
  }
140
- .toast-error {
141
- background: rgba(30, 15, 15, 0.95);
142
- border: 1px solid rgba(239, 68, 68, 0.3);
143
- color: #f87171;
144
  }
145
 
146
- /* Shimmer effect for status indicators */
147
- @keyframes pulse-slow {
148
- 0%, 100% { opacity: 1; }
149
- 50% { opacity: 0.4; }
150
- }
151
- .animate-pulse-slow {
152
- animation: pulse-slow 3s infinite;
153
- }
154
  </style>
155
  </head>
156
- <body class="font-sans text-slate-100 antialiased min-h-screen flex flex-col justify-between selection:bg-amber-500/30 selection:text-amber-200">
 
 
 
157
 
158
  <!-- Auth Gate -->
159
- <div id="auth-gate" class="hidden min-h-screen flex items-center justify-center px-4 py-12 relative z-50">
160
- <div class="absolute inset-0 bg-dark-950/40 backdrop-blur-md pointer-events-none"></div>
161
-
162
- <div class="glass rounded-2xl p-8 max-w-md w-full relative z-10 shadow-2xl border border-white/10">
163
- <div class="absolute -top-12 left-1/2 -translate-x-1/2 w-24 h-24 rounded-full bg-amber-500/10 border border-amber-500/20 blur-xl"></div>
164
-
165
  <div class="flex flex-col items-center text-center mb-8">
166
- <div class="w-14 h-14 rounded-xl bg-gradient-to-br from-amber-400 to-amber-600 flex items-center justify-center text-3xl shadow-lg shadow-amber-500/20 mb-4 ring-2 ring-amber-400/20">🐝</div>
167
- <div>
168
- <h1 class="text-2xl font-bold tracking-tight bg-gradient-to-r from-white via-slate-100 to-slate-400 bg-clip-text text-transparent">APIarium Admin</h1>
169
- <p class="text-xs text-slate-400 font-mono mt-1 bg-white/5 px-2 py-0.5 rounded-full inline-block">GATEWAY CONSOLE v0.4.0</p>
170
- </div>
171
  </div>
172
-
173
  <div class="space-y-4">
174
  <div>
175
- <label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 font-mono">Master Security Key</label>
176
  <div class="relative">
177
- <span class="absolute left-3.5 top-1/2 -translate-y-1/2 text-slate-500">
178
- <i data-lucide="key-round" class="w-4 h-4 text-amber-500/60"></i>
179
  </span>
180
  <input id="master-key-input" type="password" placeholder="apiarium_master_..."
181
- class="w-full bg-black/40 border border-white/10 rounded-xl pl-10 pr-4 py-3 text-slate-100 font-mono text-sm focus:outline-none focus:border-amber-500/60 focus:ring-1 focus:ring-amber-500/30 transition-all placeholder:text-slate-600">
182
  </div>
183
  </div>
184
-
185
- <button id="auth-btn" class="w-full bg-gradient-to-r from-amber-500 to-amber-600 hover:from-amber-400 hover:to-amber-500 active:scale-[0.98] text-black font-semibold py-3 rounded-xl transition-all shadow-lg shadow-amber-500/10 flex items-center justify-center gap-2 mt-2">
186
  <span>Unlock Dashboard</span>
187
  <i data-lucide="unlock" class="w-4 h-4"></i>
188
  </button>
189
-
190
- <p id="auth-error" class="text-red-400 text-xs mt-3 text-center bg-red-500/10 border border-red-500/20 py-2.5 px-3 rounded-lg hidden font-mono"></p>
191
  </div>
192
-
193
- <div class="mt-8 pt-6 border-t border-white/5 text-center">
194
- <span class="text-[10px] font-mono text-slate-500">Enterprise AI Reverse-Proxy Administration</span>
195
  </div>
196
  </div>
197
  </div>
198
 
199
  <!-- Dashboard Container -->
200
- <div id="dashboard" class="hidden flex-1 flex flex-col">
201
-
202
- <!-- Top Glow Line -->
203
- <div class="h-[2px] w-full bg-gradient-to-r from-amber-500/50 via-amber-400 to-indigo-500/30"></div>
204
 
205
  <!-- Header Navigation -->
206
- <header class="sticky top-0 z-40 glass border-b border-white/5">
207
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
208
- <div class="flex items-center justify-between h-16">
209
- <!-- Brand Signature -->
210
- <div class="flex items-center gap-3">
211
- <div class="w-10 h-10 rounded-xl bg-gradient-to-br from-amber-400 to-amber-600 flex items-center justify-center text-xl shadow-md shadow-amber-500/10">🐝</div>
212
- <div>
213
- <div class="flex items-center gap-2">
214
- <span class="font-bold text-slate-100 tracking-tight text-lg">APIarium</span>
215
- <span class="text-[10px] font-mono bg-amber-500/10 border border-amber-500/20 text-amber-400 px-1.5 py-0.2 rounded-md">GATEWAY</span>
216
- </div>
217
- <p class="text-[10px] text-slate-500 font-mono tracking-wider uppercase">Enterprise Admin panel</p>
218
- </div>
219
- </div>
220
-
221
- <!-- Live Router Status Indicator -->
222
- <div class="hidden sm:flex items-center gap-2 px-3 py-1.5 rounded-full bg-emerald-500/5 border border-emerald-500/15 text-emerald-400 text-xs font-mono">
223
- <span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse-slow"></span>
224
- <span>Router: Live</span>
 
 
 
 
 
 
 
 
225
  </div>
226
-
227
- <!-- Navigation Tabs & Actions -->
228
- <nav class="flex items-center gap-2 sm:gap-4">
229
- <div class="flex items-center gap-1 sm:gap-2 bg-black/25 p-1 rounded-xl border border-white/5">
230
- <a href="#stats" class="text-xs font-medium text-slate-300 hover:text-white px-2.5 sm:px-3 py-1.5 rounded-lg hover:bg-white/5 transition flex items-center gap-1.5">
231
- <i data-lucide="bar-chart-3" class="w-3.5 h-3.5 text-amber-500"></i>
232
- <span class="hidden md:inline">Analytics</span>
233
- </a>
234
- <a href="#providers" class="text-xs font-medium text-slate-300 hover:text-white px-2.5 sm:px-3 py-1.5 rounded-lg hover:bg-white/5 transition flex items-center gap-1.5">
235
- <i data-lucide="cpu" class="w-3.5 h-3.5 text-slate-400"></i>
236
- <span class="hidden md:inline">Providers</span>
237
- </a>
238
- <a href="#models" class="text-xs font-medium text-slate-300 hover:text-white px-2.5 sm:px-3 py-1.5 rounded-lg hover:bg-white/5 transition flex items-center gap-1.5">
239
- <i data-lucide="layers" class="w-3.5 h-3.5 text-slate-400"></i>
240
- <span class="hidden md:inline">Models</span>
241
- </a>
242
- <a href="#generator" class="text-xs font-medium text-slate-300 hover:text-white px-2.5 sm:px-3 py-1.5 rounded-lg hover:bg-white/5 transition flex items-center gap-1.5">
243
- <i data-lucide="key" class="w-3.5 h-3.5 text-slate-400"></i>
244
- <span class="hidden md:inline">API Keys</span>
245
- </a>
246
- </div>
247
-
248
- <!-- Power Logout Button -->
249
- <button id="logout-btn" class="flex items-center gap-1.5 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 px-3 py-1.5 rounded-xl text-xs font-mono transition">
250
- <i data-lucide="log-out" class="w-3.5 h-3.5"></i>
251
- <span class="hidden sm:inline">Logout</span>
252
- </button>
253
- </nav>
254
  </div>
255
  </div>
256
  </header>
@@ -266,9 +204,9 @@
266
  <i data-lucide="gauge" class="text-amber-500 w-5 h-5"></i>
267
  Gateway Overview
268
  </h2>
269
- <p class="text-sm text-slate-400">Real-time usage analytics, rate metrics, and authenticated keys.</p>
270
  </div>
271
- <div class="text-xs font-mono text-slate-500 flex items-center gap-2 bg-white/5 px-3 py-1.5 rounded-lg border border-white/5">
272
  <span class="w-2 h-2 rounded-full bg-amber-500 animate-pulse"></span>
273
  <span>Sync State: Active</span>
274
  </div>
@@ -277,50 +215,47 @@
277
  <!-- Stats KPI Cards -->
278
  <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
279
  <!-- Card 1: Unique Client IPs -->
280
- <div class="glass rounded-2xl p-5 relative overflow-hidden flex flex-col justify-between">
281
- <div class="absolute top-0 right-0 w-24 h-24 bg-amber-500/5 rounded-full blur-2xl"></div>
282
  <div class="flex items-center justify-between mb-4">
283
- <span class="text-xs font-mono uppercase tracking-wider text-slate-400">Unique Clients</span>
284
  <div class="p-2 bg-amber-500/10 text-amber-400 rounded-lg">
285
  <i data-lucide="users" class="w-4 h-4"></i>
286
  </div>
287
  </div>
288
  <div>
289
- <h3 id="stat-active" class="text-3xl font-extrabold text-white font-mono tracking-tight">—</h3>
290
- <p class="text-[11px] text-slate-500 mt-2 flex items-center gap-1">
291
  <i data-lucide="network" class="w-3.5 h-3.5 text-amber-500/60"></i> Active IP-bound keys in routing table
292
  </p>
293
  </div>
294
  </div>
295
 
296
  <!-- Card 2: Total API Keys -->
297
- <div class="glass rounded-2xl p-5 relative overflow-hidden flex flex-col justify-between">
298
- <div class="absolute top-0 right-0 w-24 h-24 bg-blue-500/5 rounded-full blur-2xl"></div>
299
  <div class="flex items-center justify-between mb-4">
300
- <span class="text-xs font-mono uppercase tracking-wider text-slate-400">Total Keycards</span>
301
  <div class="p-2 bg-blue-500/10 text-blue-400 rounded-lg">
302
  <i data-lucide="key-round" class="w-4 h-4"></i>
303
  </div>
304
  </div>
305
  <div>
306
- <h3 id="stat-total" class="text-3xl font-extrabold text-white font-mono tracking-tight">—</h3>
307
- <p class="text-[11px] text-slate-500 mt-2 flex items-center gap-1">
308
  <i data-lucide="check-circle" class="w-3.5 h-3.5 text-blue-500/60"></i> Provisioned reverse-proxy credentials
309
  </p>
310
  </div>
311
  </div>
312
 
313
  <!-- Card 3: Total Requests routed -->
314
- <div class="glass rounded-2xl p-5 relative overflow-hidden flex flex-col justify-between">
315
- <div class="absolute top-0 right-0 w-24 h-24 bg-emerald-500/5 rounded-full blur-2xl"></div>
316
  <div class="flex items-center justify-between mb-4">
317
- <span class="text-xs font-mono uppercase tracking-wider text-slate-400">Total API Requests</span>
318
  <div class="p-2 bg-emerald-500/10 text-emerald-400 rounded-lg">
319
  <i data-lucide="arrow-up-right" class="w-4 h-4"></i>
320
  </div>
321
  </div>
322
  <div>
323
- <h3 id="stat-requests" class="text-3xl font-extrabold text-white font-mono tracking-tight">0</h3>
324
  <p class="text-[11px] text-emerald-400/80 mt-2 flex items-center gap-1">
325
  <span class="inline-block w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
326
  Incoming calls intercepted by proxy
@@ -329,16 +264,15 @@
329
  </div>
330
 
331
  <!-- Card 4: Rate Limited attempts -->
332
- <div class="glass rounded-2xl p-5 relative overflow-hidden flex flex-col justify-between">
333
- <div class="absolute top-0 right-0 w-24 h-24 bg-red-500/5 rounded-full blur-2xl"></div>
334
  <div class="flex items-center justify-between mb-4">
335
- <span class="text-xs font-mono uppercase tracking-wider text-slate-400">Rate Limited</span>
336
  <div class="p-2 bg-red-500/10 text-red-400 rounded-lg">
337
  <i data-lucide="shield-alert" class="w-4 h-4"></i>
338
  </div>
339
  </div>
340
  <div>
341
- <h3 id="stat-ratelimited" class="text-3xl font-extrabold text-white font-mono tracking-tight">0</h3>
342
  <p class="text-[11px] text-red-400/80 mt-2 flex items-center gap-1">
343
  <i data-lucide="alert-octagon" class="w-3.5 h-3.5 text-red-400/60"></i> Unauthorized or rate-limited requests
344
  </p>
@@ -349,13 +283,13 @@
349
  <!-- ANALYTICS CHARTS PANEL -->
350
  <div id="charts" class="grid grid-cols-1 lg:grid-cols-3 gap-6">
351
  <!-- Chart 1: Request Load over 7 Days -->
352
- <div class="glass rounded-2xl p-6 lg:col-span-2 space-y-4">
353
  <div class="flex items-center justify-between">
354
- <h3 class="text-sm font-semibold text-slate-200 tracking-tight flex items-center gap-2">
355
  <i data-lucide="activity" class="w-4 h-4 text-amber-500"></i>
356
  Traffic Load Analysis
357
  </h3>
358
- <span class="text-[10px] font-mono text-slate-500 bg-white/5 px-2 py-0.5 rounded">7-day timeline</span>
359
  </div>
360
  <div class="relative min-h-[220px] flex items-center justify-center">
361
  <canvas id="chart-requests" class="max-h-[260px]"></canvas>
@@ -363,13 +297,13 @@
363
  </div>
364
 
365
  <!-- Chart 2: Popular Models Routing share -->
366
- <div class="glass rounded-2xl p-6 space-y-4">
367
  <div class="flex items-center justify-between">
368
- <h3 class="text-sm font-semibold text-slate-200 tracking-tight flex items-center gap-2">
369
  <i data-lucide="pie-chart" class="w-4 h-4 text-indigo-400"></i>
370
  Models Routing Share
371
  </h3>
372
- <span class="text-[10px] font-mono text-slate-500 bg-white/5 px-2 py-0.5 rounded font-mono">Real-time</span>
373
  </div>
374
  <div class="relative min-h-[220px] flex items-center justify-center">
375
  <canvas id="chart-models" class="max-h-[260px]"></canvas>
@@ -386,7 +320,7 @@
386
  <i data-lucide="cpu" class="text-amber-500 w-5 h-5"></i>
387
  Dynamic Providers
388
  </h2>
389
- <p class="text-sm text-slate-400">Plug external API end-points instantly. Automatically probe and extract upstream model paths.</p>
390
  </div>
391
  <div class="text-xs font-mono text-amber-500 flex items-center gap-1.5 bg-amber-500/10 border border-amber-500/20 px-3 py-1 rounded-lg">
392
  <i data-lucide="sparkles" class="w-3.5 h-3.5"></i>
@@ -396,23 +330,23 @@
396
 
397
  <div class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
398
  <!-- Add Provider form card -->
399
- <div class="glass rounded-2xl p-6 lg:col-span-5 space-y-4 border border-white/10 shadow-amber-glow">
400
  <div class="flex items-center gap-2 border-b border-white/5 pb-3">
401
  <div class="p-1.5 bg-amber-500/10 text-amber-400 rounded-lg">
402
  <i data-lucide="plus-circle" class="w-4 h-4"></i>
403
  </div>
404
- <h3 class="text-sm font-semibold text-slate-200">Register New Upstream AI Provider</h3>
405
  </div>
406
 
407
  <div class="space-y-3.5">
408
  <div class="grid grid-cols-1 sm:grid-cols-2 gap-3.5">
409
  <div>
410
- <label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1.5 font-mono">Provider Name</label>
411
  <input id="prov-name" type="text" placeholder="e.g. DeepBricks"
412
- class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-2.5 text-slate-100 font-sans text-sm focus:outline-none focus:border-amber-500/60 transition-all">
413
  </div>
414
  <div>
415
- <label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1.5 font-mono flex items-center gap-1.5">
416
  Prefix
417
  <span class="text-[9px] font-mono text-amber-400/80 bg-amber-500/10 border border-amber-500/20 px-1.5 py-0.5 rounded">{prefix}/model</span>
418
  </label>
@@ -421,23 +355,23 @@
421
  </div>
422
  </div>
423
  <div>
424
- <label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1.5 font-mono">Upstream Endpoint URL</label>
425
  <input id="prov-url" type="url" placeholder="https://api.yourprovider.com/v1"
426
- class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-2.5 text-slate-100 font-mono text-xs focus:outline-none focus:border-amber-500/60 transition-all">
427
  </div>
428
  <div>
429
- <label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1.5 font-mono">Bearer Access Keys (one per line)</label>
430
  <textarea id="prov-keys" rows="3" placeholder="sk-provider-..."
431
- class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-2.5 text-slate-100 font-mono text-xs focus:outline-none focus:border-amber-500/60 transition-all resize-none"></textarea>
432
  </div>
433
 
434
  <!-- Step 1: Discover Models -->
435
  <div class="flex gap-2 pt-2">
436
- <button id="prov-preview-btn" class="flex-1 bg-gradient-to-r from-amber-500 to-amber-600 hover:from-amber-400 hover:to-amber-500 text-black text-xs font-bold py-2.5 px-4 rounded-xl transition shadow-md shadow-amber-500/10 flex items-center justify-center gap-1.5">
437
  <i data-lucide="stethoscope" class="w-3.5 h-3.5"></i>
438
  <span>Discover Models</span>
439
  </button>
440
- <button id="prov-reset-btn" class="bg-white/5 hover:bg-white/10 text-slate-300 text-xs font-semibold py-2.5 px-3 rounded-xl transition border border-white/10 flex items-center justify-center" title="Reset form">
441
  <i data-lucide="rotate-ccw" class="w-3.5 h-3.5"></i>
442
  </button>
443
  </div>
@@ -448,14 +382,14 @@
448
  </div>
449
 
450
  <!-- Step 2: Model Selection + Alias Editor (full width when active) -->
451
- <div id="prov-selector" class="hidden lg:col-span-12 glass rounded-2xl p-6 space-y-4 border border-amber-500/20 shadow-amber-glow">
452
  <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b border-white/5 pb-3">
453
  <div>
454
  <h3 class="text-sm font-semibold text-amber-300 flex items-center gap-2">
455
  <i data-lucide="list-checks" class="w-4 h-4"></i>
456
  Step 2 — Choose & Alias Models
457
  </h3>
458
- <p class="text-[11px] text-slate-400 mt-1">Tick the models you want to expose. Edit the right column to set custom IDs. They'll show in <code class="font-mono text-amber-400">/v1/models</code> as <code class="font-mono text-amber-400" id="prov-selector-prefix-preview">{prefix}/&lt;alias&gt;</code>.</p>
459
  </div>
460
  <div class="flex items-center gap-2">
461
  <span id="prov-selected-count" class="text-xs font-mono text-amber-400 bg-amber-500/10 border border-amber-500/20 px-2.5 py-1 rounded-full">0 / 0 selected</span>
@@ -464,27 +398,27 @@
464
 
465
  <div class="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
466
  <div class="relative flex-1">
467
- <span class="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500">
468
  <i data-lucide="search" class="w-4 h-4"></i>
469
  </span>
470
  <input id="prov-selector-search" type="text" placeholder="Filter models..."
471
- class="w-full bg-black/40 border border-white/10 rounded-xl pl-9 pr-4 py-2 text-xs text-slate-100 font-mono focus:outline-none focus:border-amber-500/50 transition-all">
472
  </div>
473
  <div class="flex gap-2 text-[11px] font-mono">
474
- <button id="prov-select-all" class="bg-white/5 hover:bg-white/10 text-slate-200 px-3 py-1.5 rounded-lg border border-white/10 transition flex items-center gap-1">
475
  <i data-lucide="check-square" class="w-3 h-3"></i> All
476
  </button>
477
- <button id="prov-select-none" class="bg-white/5 hover:bg-white/10 text-slate-200 px-3 py-1.5 rounded-lg border border-white/10 transition flex items-center gap-1">
478
  <i data-lucide="square" class="w-3 h-3"></i> None
479
  </button>
480
- <button id="prov-reset-aliases" class="bg-white/5 hover:bg-white/10 text-slate-200 px-3 py-1.5 rounded-lg border border-white/10 transition flex items-center gap-1" title="Reset aliases to upstream id">
481
  <i data-lucide="undo-2" class="w-3 h-3"></i> Reset
482
  </button>
483
  </div>
484
  </div>
485
 
486
  <!-- Table header -->
487
- <div class="grid grid-cols-12 gap-3 text-[10px] uppercase font-mono text-slate-500 px-3">
488
  <div class="col-span-1"></div>
489
  <div class="col-span-5">Upstream Model ID</div>
490
  <div class="col-span-6">Exposed Alias <span class="text-amber-400/70">(editable)</span></div>
@@ -495,11 +429,11 @@
495
  </div>
496
 
497
  <div class="flex flex-col sm:flex-row gap-2 pt-3 border-t border-white/5">
498
- <button id="prov-cancel-import" class="flex-1 sm:flex-none bg-white/5 hover:bg-white/10 text-slate-300 text-xs font-semibold py-2.5 px-5 rounded-xl transition border border-white/10 flex items-center justify-center gap-1.5">
499
  <i data-lucide="x" class="w-3.5 h-3.5"></i>
500
  <span>Cancel</span>
501
  </button>
502
- <button id="prov-import-btn" class="flex-1 bg-gradient-to-r from-emerald-500 to-emerald-600 hover:from-emerald-400 hover:to-emerald-500 text-black text-xs font-bold py-2.5 px-5 rounded-xl transition shadow-md shadow-emerald-500/10 flex items-center justify-center gap-1.5">
503
  <i data-lucide="import" class="w-3.5 h-3.5"></i>
504
  <span>Import Selected Models</span>
505
  </button>
@@ -509,7 +443,7 @@
509
  <!-- Existing active dynamic upstreams -->
510
  <div class="lg:col-span-7 space-y-4">
511
  <div class="flex items-center justify-between border-b border-white/5 pb-3">
512
- <h3 class="text-sm font-semibold text-slate-200 flex items-center gap-2">
513
  <i data-lucide="server" class="w-4 h-4 text-emerald-400"></i>
514
  Configured Upstream Targets
515
  </h3>
@@ -517,8 +451,8 @@
517
 
518
  <div id="providers-list" class="space-y-3 max-h-[460px] overflow-y-auto pr-1">
519
  <!-- Dynamic providers list gets injected here -->
520
- <div class="flex flex-col items-center justify-center py-12 text-center text-slate-500 glass rounded-2xl border border-dashed border-white/10">
521
- <i data-lucide="database" class="w-8 h-8 mb-2 text-slate-600"></i>
522
  <span class="text-xs font-mono">No active dynamic provider targets</span>
523
  </div>
524
  </div>
@@ -534,29 +468,29 @@
534
  <i data-lucide="layers" class="text-indigo-400 w-5 h-5"></i>
535
  Unified Model Catalog
536
  </h2>
537
- <p class="text-sm text-slate-400">Click models to copy names instantly. Routes are automatically clustered by provider backends.</p>
538
  </div>
539
  <div class="relative w-full sm:w-64">
540
- <span class="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500">
541
- <i data-lucide="search" class="w-4 h-4 text-slate-500"></i>
542
  </span>
543
  <input id="models-search" type="text" placeholder="Filter unified models..."
544
- class="w-full bg-black/40 border border-white/10 rounded-xl pl-9 pr-4 py-2 text-xs text-slate-100 font-mono focus:outline-none focus:border-indigo-500/50 transition-all">
545
  </div>
546
  </div>
547
 
548
- <div class="glass rounded-2xl p-6">
549
  <div class="flex items-center justify-between mb-4 pb-3 border-b border-white/5">
550
  <div class="flex items-center gap-2">
551
  <span id="models-count" class="text-xs font-mono bg-indigo-500/15 border border-indigo-500/25 text-indigo-300 px-2.5 py-1 rounded-full">0 models</span>
552
- <span class="text-xs text-slate-500 font-mono">available through local proxy endpoints</span>
553
  </div>
554
  </div>
555
 
556
  <!-- Unified Grid of Route Models -->
557
  <div id="models-grid" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3.5 max-h-[380px] overflow-y-auto pr-1">
558
  <!-- Loaded dynamically -->
559
- <div class="col-span-full py-12 flex flex-col items-center justify-center text-center text-slate-500">
560
  <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-amber-500 mb-2"></div>
561
  <span class="text-xs font-mono">Indexing models routes...</span>
562
  </div>
@@ -572,35 +506,35 @@
572
  <i data-lucide="shield-check" class="text-amber-500 w-5 h-5"></i>
573
  IP-Bound API Keys
574
  </h2>
575
- <p class="text-sm text-slate-400">Strict IP validation logic on all reverse-proxy requests. Set labels to keep track of organizational nodes.</p>
576
  </div>
577
  </div>
578
 
579
  <div class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
580
 
581
  <!-- KEY GENERATION COMPONENT -->
582
- <div class="glass rounded-2xl p-6 lg:col-span-4 space-y-4 border border-white/10 shadow-amber-glow">
583
  <div class="flex items-center gap-2 border-b border-white/5 pb-3">
584
  <div class="p-1.5 bg-amber-500/10 text-amber-400 rounded-lg">
585
  <i data-lucide="key-round" class="w-4 h-4"></i>
586
  </div>
587
- <h3 class="text-sm font-semibold text-slate-200">Generate Client Key</h3>
588
  </div>
589
 
590
  <form id="key-form" class="space-y-4">
591
  <div>
592
- <label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1.5 font-mono">Bound IP Address</label>
593
  <input id="ip-input" type="text" required placeholder="e.g. 192.168.1.50 or ::1"
594
- class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-2.5 text-slate-100 font-mono text-xs focus:outline-none focus:border-amber-500/60 transition-all">
595
  <p id="ip-error" class="text-red-400 text-[11px] font-mono mt-1 hidden"></p>
596
  </div>
597
  <div>
598
- <label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1.5 font-mono">Descriptive Client Label</label>
599
  <input id="label-input" type="text" placeholder="e.g. Staging server, Analytics engine"
600
- class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-2.5 text-slate-100 font-sans text-sm focus:outline-none focus:border-amber-500/60 transition-all">
601
  </div>
602
 
603
- <button id="gen-btn" type="submit" class="w-full bg-gradient-to-r from-amber-500 to-amber-600 hover:from-amber-400 hover:to-amber-500 text-black font-bold py-2.5 rounded-xl transition-all shadow-md shadow-amber-500/10 flex items-center justify-center gap-1.5">
604
  <i data-lucide="plus-circle" class="w-4 h-4"></i>
605
  <span>Generate Route Key</span>
606
  </button>
@@ -614,16 +548,16 @@
614
  </div>
615
  <div class="space-y-1.5">
616
  <div class="flex items-center justify-between gap-2">
617
- <span class="text-[10px] text-slate-500 font-mono">KEY:</span>
618
  <span id="result-key" class="font-mono text-xs text-amber-200 select-all break-all text-right"></span>
619
  </div>
620
  <div class="flex items-center justify-between">
621
- <span class="text-[10px] text-slate-500 font-mono">BOUND TO:</span>
622
- <span id="result-ip" class="font-mono text-xs text-slate-300"></span>
623
  </div>
624
  <div class="flex items-center justify-between">
625
- <span class="text-[10px] text-slate-500 font-mono">LABEL:</span>
626
- <span id="result-label" class="font-sans text-xs text-slate-300"></span>
627
  </div>
628
  </div>
629
  <button id="copy-key-btn" class="w-full bg-white/5 hover:bg-white/10 text-white text-xs font-semibold py-1.5 rounded border border-white/5 transition flex items-center justify-center gap-1">
@@ -634,19 +568,19 @@
634
  </div>
635
 
636
  <!-- TABLE OF CURRENT KEYS -->
637
- <div class="lg:col-span-8 glass rounded-2xl p-6 space-y-4">
638
  <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-white/5 pb-3">
639
- <h3 class="text-sm font-semibold text-slate-200 flex items-center gap-2">
640
  <i data-lucide="table" class="w-4 h-4 text-amber-400"></i>
641
  IP Routing Table Keys
642
  </h3>
643
 
644
  <div class="relative w-full sm:w-56">
645
- <span class="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500">
646
- <i data-lucide="filter" class="w-3.5 h-3.5 text-slate-500"></i>
647
  </span>
648
  <input id="search-input" type="text" placeholder="Filter keys by IP/Label..."
649
- class="w-full bg-black/40 border border-white/10 rounded-xl pl-9 pr-4 py-1.5 text-xs text-slate-100 font-mono focus:outline-none focus:border-amber-500/50 transition-all">
650
  </div>
651
  </div>
652
 
@@ -654,7 +588,7 @@
654
  <div class="overflow-x-auto">
655
  <table class="w-full text-left text-xs min-w-[640px]">
656
  <thead>
657
- <tr class="text-slate-400 font-mono border-b border-white/5 uppercase text-[10px] tracking-wider">
658
  <th class="pb-3.5 pr-4 font-semibold">Client Key Auth</th>
659
  <th class="pb-3.5 pr-4 font-semibold">Ip Binding</th>
660
  <th class="pb-3.5 pr-4 font-semibold">Description Label</th>
@@ -670,10 +604,10 @@
670
  </div>
671
 
672
  <!-- Empty placeholder state -->
673
- <div id="empty-state" class="hidden flex flex-col items-center justify-center py-16 text-center text-slate-500">
674
- <i data-lucide="pocket" class="w-10 h-10 mb-2 text-slate-600 animate-bounce"></i>
675
- <h4 class="text-sm font-semibold text-slate-400">No Authorized IP-Bound Keys</h4>
676
- <p class="text-xs text-slate-500 max-w-xs mt-1">Generate your first IP-bound route configuration using the key generator panel on the left.</p>
677
  </div>
678
  </div>
679
 
@@ -683,16 +617,21 @@
683
  </main>
684
 
685
  <!-- Footer Area -->
686
- <footer class="border-t border-white/5 mt-auto bg-black/20">
687
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 flex flex-col md:flex-row items-center justify-between gap-4 text-xs text-slate-500 font-mono">
688
- <div class="flex items-center gap-2">
689
- <span class="w-2 h-2 rounded-full bg-amber-500 animate-pulse"></span>
690
- <span>APIarium Enterprise Reverse Proxy Server Engine</span>
691
  </div>
692
- <div class="flex items-center gap-4">
693
- <span id="sync-status">Last synchronized: <span id="last-sync" class="text-amber-400">—</span></span>
694
- <span>·</span>
695
- <span>System status: <span class="text-emerald-400">Operational</span></span>
 
 
 
 
 
696
  </div>
697
  </div>
698
  </footer>
@@ -794,7 +733,7 @@
794
 
795
  // Visual loading state
796
  authBtn.disabled = true;
797
- authBtn.innerHTML = `<span>Unlocking Console...</span> <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-slate-900"></div>`;
798
 
799
  try {
800
  const r = await fetch('/admin/keys', { headers: { 'Authorization': 'Bearer ' + val } });
@@ -964,22 +903,22 @@
964
  tbody.innerHTML = filtered.map(k => `
965
  <tr class="hover:bg-white/[0.02] transition border-b border-white/[0.03]">
966
  <td class="py-3.5 pr-4 font-mono text-xs text-amber-300 select-all font-semibold tracking-wide">${maskKey(k.key)}</td>
967
- <td class="py-3.5 pr-4 font-mono text-xs text-slate-300">
968
  <span class="bg-black/35 px-2 py-1 rounded border border-white/5">${k.ip || '—'}</span>
969
  </td>
970
- <td class="py-3.5 pr-4 text-slate-300 font-medium">${k.label || '<span class="text-slate-600 font-mono text-xs">NO_LABEL</span>'}</td>
971
- <td class="py-3.5 pr-4 text-[11px] font-mono text-slate-400">${formatDate(k.created_at)}</td>
972
  <td class="py-3.5 pr-4">
973
  <span class="text-[10px] font-mono px-2.5 py-1 rounded-full ${k.status === 'active' ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-red-500/10 text-red-400 border border-red-500/20'}">
974
  ${(k.status || 'active').toUpperCase()}
975
  </span>
976
  </td>
977
  <td class="py-3.5 text-right flex justify-end gap-2">
978
- <button onclick="window.__copyKey('${k.key}')" class="text-[11px] font-mono text-slate-300 hover:text-amber-400 px-2.5 py-1 rounded-lg bg-white/5 hover:bg-white/10 border border-white/5 transition flex items-center gap-1">
979
  <i data-lucide="clipboard" class="w-3 h-3"></i> COPY
980
  </button>
981
  ${k.status !== 'revoked' ? `
982
- <button onclick="window.__revokeKey('${k.key}')" class="text-[11px] font-mono text-red-400/80 hover:text-red-400 px-2.5 py-1 rounded-lg bg-red-500/5 hover:bg-red-500/15 border border-red-500/10 transition flex items-center gap-1">
983
  <i data-lucide="trash-2" class="w-3 h-3"></i> REVOKE
984
  </button>` : ''}
985
  </td>
@@ -1056,7 +995,7 @@
1056
 
1057
  const btn = $('gen-btn');
1058
  btn.disabled = true;
1059
- btn.innerHTML = `<span>Provisioning Key...</span> <div class="animate-spin rounded-full h-3.5 w-3.5 border-b-2 border-slate-900"></div>`;
1060
 
1061
  try {
1062
  const r = await api('/admin/keys', { method: 'POST', body: JSON.stringify({ ip, label }) });
@@ -1251,7 +1190,7 @@
1251
  const items = selectorState.available
1252
  .filter(id => !q || id.toLowerCase().includes(q));
1253
  if (items.length === 0) {
1254
- list.innerHTML = `<div class="text-center text-slate-500 text-xs font-mono py-8">No models match this filter.</div>`;
1255
  return;
1256
  }
1257
  const prefix = (($('prov-prefix') || {}).value || '').trim();
@@ -1270,7 +1209,7 @@
1270
  </label>
1271
  </div>
1272
  <div class="col-span-5 min-w-0">
1273
- <div class="font-mono text-[11px] text-slate-200 truncate" title="${escapeAttr(id)}">${escapeHtml(id)}</div>
1274
  </div>
1275
  <div class="col-span-6">
1276
  <div class="flex items-center gap-2">
@@ -1280,7 +1219,7 @@
1280
  data-upstream="${escapeAttr(id)}"
1281
  ${isSel ? '' : 'disabled'}
1282
  placeholder="alias">
1283
- <span class="text-[10px] font-mono text-slate-500 truncate flex-shrink-0 min-w-0" title="${escapeAttr(full)}">→ ${escapeHtml(full)}</span>
1284
  </div>
1285
  </div>
1286
  </div>`;
@@ -1356,8 +1295,8 @@
1356
  if (!list) return;
1357
  if (providers.length === 0) {
1358
  list.innerHTML = `
1359
- <div class="flex flex-col items-center justify-center py-10 text-center text-slate-500 bg-black/25 rounded-2xl border border-dashed border-white/5">
1360
- <i data-lucide="help-circle" class="w-8 h-8 mb-2 text-slate-600"></i>
1361
  <span class="text-xs font-mono">No dynamically registered upstreams</span>
1362
  </div>`;
1363
  lucide.createIcons();
@@ -1377,18 +1316,18 @@
1377
  const moreCount = Math.max(0, richModels.length - 6);
1378
  const prefixBadge = prefix
1379
  ? `<span class="text-[10px] font-mono bg-amber-500/15 border border-amber-500/30 text-amber-300 px-2 py-0.5 rounded-md tracking-wider">${escapeHtml(prefix)}/</span>`
1380
- : `<span class="text-[10px] font-mono bg-slate-500/10 border border-slate-500/20 text-slate-400 px-2 py-0.5 rounded-md">no prefix</span>`;
1381
  return `
1382
  <div class="bg-gradient-to-br from-black/45 to-black/25 rounded-2xl p-4 border border-white/5 hover:border-amber-500/25 transition-all group">
1383
  <div class="flex items-start justify-between gap-3">
1384
  <div class="flex-1 min-w-0">
1385
  <div class="flex items-center gap-2 flex-wrap">
1386
- <span class="font-bold text-slate-100 text-sm tracking-tight">${escapeHtml(p.name)}</span>
1387
  ${prefixBadge}
1388
- <span class="text-[9px] font-mono bg-white/5 border border-white/10 text-slate-400 px-1.5 py-0.5 rounded">${escapeHtml(p.id)}</span>
1389
  </div>
1390
- <div class="text-[11px] text-slate-400 font-mono mt-1.5 truncate flex items-center gap-1.5">
1391
- <i data-lucide="link-2" class="w-3 h-3 text-slate-500"></i>
1392
  <span class="truncate">${escapeHtml(p.base_url)}</span>
1393
  </div>
1394
  </div>
@@ -1396,20 +1335,20 @@
1396
  <button onclick="window.__editProvider('${escapeAttr(p.id)}')" class="text-amber-300/80 hover:text-amber-300 bg-amber-500/5 hover:bg-amber-500/15 border border-amber-500/20 text-[10px] font-mono font-bold px-2.5 py-1.5 rounded-lg transition flex items-center gap-1" title="Edit prefix & models">
1397
  <i data-lucide="settings-2" class="w-3 h-3"></i> EDIT
1398
  </button>
1399
- <button onclick="window.__deleteProvider('${escapeAttr(p.id)}')" class="text-red-400/80 hover:text-red-400 bg-red-500/5 hover:bg-red-500/15 border border-red-500/20 text-[10px] font-mono font-bold px-2.5 py-1.5 rounded-lg transition flex items-center gap-1">
1400
  <i data-lucide="trash-2" class="w-3 h-3"></i> DEL
1401
  </button>
1402
  </div>
1403
  </div>
1404
- <div class="flex flex-wrap gap-3 mt-3 text-[11px] text-slate-500 font-mono">
1405
  <span class="flex items-center gap-1"><i data-lucide="key-round" class="w-3 h-3"></i> ${p.key_count} keys</span>
1406
  <span class="flex items-center gap-1"><i data-lucide="box" class="w-3 h-3"></i> ${richModels.length} exposed</span>
1407
- <span class="flex items-center gap-1 text-slate-600"><i data-lucide="database" class="w-3 h-3"></i> ${(p.available_models || []).length} available upstream</span>
1408
  </div>
1409
  ${richModels.length > 0 ? `
1410
  <div class="mt-3 pt-3 border-t border-white/5 flex flex-wrap gap-1.5">
1411
  ${previewChips}
1412
- ${moreCount > 0 ? `<span class="text-[10px] font-mono text-slate-500 px-2 py-0.5">+${moreCount} more</span>` : ''}
1413
  </div>` : ''}
1414
  </div>`;
1415
  }).join('');
@@ -1455,7 +1394,7 @@
1455
  btn.innerHTML = `<div class="animate-spin rounded-full h-3.5 w-3.5 border-b-2 border-black"></div><span>Discovering...</span>`;
1456
  status.classList.remove('hidden');
1457
  status.textContent = 'Probing upstream /models endpoint...';
1458
- status.className = 'text-xs font-mono text-slate-400 bg-white/5 px-2.5 py-1.5 rounded-lg border border-white/10';
1459
 
1460
  let discovered = [];
1461
  try {
@@ -1612,7 +1551,7 @@
1612
  const q = (editState.filter || '').toLowerCase();
1613
  const items = editState.available.filter(id => !q || id.toLowerCase().includes(q));
1614
  if (items.length === 0) {
1615
- list.innerHTML = `<div class="text-center text-slate-500 text-xs font-mono py-8">No models match this filter.</div>`;
1616
  return;
1617
  }
1618
  list.innerHTML = items.map(id => {
@@ -1630,7 +1569,7 @@
1630
  </label>
1631
  </div>
1632
  <div class="col-span-5 min-w-0">
1633
- <div class="font-mono text-[11px] text-slate-200 truncate" title="${escapeAttr(id)}">${escapeHtml(id)}</div>
1634
  </div>
1635
  <div class="col-span-6">
1636
  <div class="flex items-center gap-2">
@@ -1639,7 +1578,7 @@
1639
  value="${escapeAttr(alias)}"
1640
  data-upstream="${escapeAttr(id)}"
1641
  ${isSel ? '' : 'disabled'}>
1642
- <span class="text-[10px] font-mono text-slate-500 truncate flex-shrink-0 min-w-0" title="${escapeAttr(full)}">→ ${escapeHtml(full)}</span>
1643
  </div>
1644
  </div>
1645
  </div>`;
@@ -1817,7 +1756,7 @@
1817
 
1818
  if (filtered.length === 0) {
1819
  grid.innerHTML = `
1820
- <div class="col-span-full py-10 text-center text-slate-500 bg-black/20 rounded-xl border border-dashed border-white/5 font-mono text-xs">
1821
  No registered proxy model routes matched query filters.
1822
  </div>`;
1823
  return;
@@ -1827,15 +1766,15 @@
1827
  <div class="bg-black/45 border border-white/5 hover:border-amber-500/30 rounded-xl p-3.5 transition-all duration-200 cursor-pointer group shadow-glass-inner"
1828
  onclick="window.__copyModelId('${escapeHtml(m.name)}')">
1829
  <div class="flex items-start justify-between gap-2">
1830
- <span class="font-mono text-xs text-slate-200 break-all select-all group-hover:text-amber-300 font-semibold transition-colors duration-150">${escapeHtml(m.name || m.id)}</span>
1831
  ${m.upstreams > 1 ? `<span class="text-[9px] font-mono font-bold text-amber-400 bg-amber-500/15 border border-amber-500/20 px-1.5 py-0.5 rounded flex-shrink-0">x${m.upstreams} nodes</span>` : ''}
1832
  </div>
1833
  <div class="mt-3 flex items-center justify-between">
1834
  <div class="flex items-center gap-1.5">
1835
  <span class="w-1.5 h-1.5 rounded-full ${m.provider === 'built-in' ? 'bg-amber-400' : m.provider === 'image-backend' ? 'bg-indigo-400' : 'bg-emerald-400'}"></span>
1836
- <span class="text-[10px] font-mono text-slate-400 truncate max-w-[120px]">${escapeHtml(m.provider)}</span>
1837
  </div>
1838
- <span class="text-[9px] font-mono text-slate-500 group-hover:text-amber-400 transition-colors flex items-center gap-1">
1839
  <i data-lucide="copy" class="w-3 h-3"></i> COPY
1840
  </span>
1841
  </div>
@@ -1872,11 +1811,11 @@
1872
  <i data-lucide="settings-2" class="w-4 h-4"></i>
1873
  </div>
1874
  <div class="min-w-0">
1875
- <h3 id="edit-prov-modal-title" class="font-bold text-slate-100 truncate">Edit Provider</h3>
1876
- <p class="text-[11px] text-slate-400 font-mono">Update prefix, model selection, and aliases</p>
1877
  </div>
1878
  </div>
1879
- <button id="edit-prov-close" class="text-slate-400 hover:text-slate-100 bg-white/5 hover:bg-white/10 rounded-lg p-1.5 transition">
1880
  <i data-lucide="x" class="w-4 h-4"></i>
1881
  </button>
1882
  </div>
@@ -1884,11 +1823,11 @@
1884
  <!-- Top: name + prefix -->
1885
  <div class="px-6 py-4 grid grid-cols-1 sm:grid-cols-2 gap-4 border-b border-white/5">
1886
  <div>
1887
- <label class="block text-[10px] font-semibold text-slate-400 uppercase tracking-wider mb-1.5 font-mono">Display Name</label>
1888
- <input id="edit-prov-name" type="text" class="w-full bg-black/40 border border-white/10 rounded-xl px-3 py-2 text-slate-100 text-sm focus:outline-none focus:border-amber-500/60 transition-all">
1889
  </div>
1890
  <div>
1891
- <label class="block text-[10px] font-semibold text-slate-400 uppercase tracking-wider mb-1.5 font-mono flex items-center gap-1.5">
1892
  Prefix <span class="text-[9px] text-amber-400/80 bg-amber-500/10 border border-amber-500/20 px-1.5 py-0.5 rounded">{prefix}/model</span>
1893
  </label>
1894
  <input id="edit-prov-prefix" type="text" class="w-full bg-black/40 border border-white/10 rounded-xl px-3 py-2 text-amber-200 font-mono text-xs focus:outline-none focus:border-amber-500/60 transition-all" placeholder="e.g. deb">
@@ -1898,17 +1837,17 @@
1898
  <!-- Filter & bulk -->
1899
  <div class="px-6 py-3 flex flex-col sm:flex-row gap-3 items-stretch sm:items-center border-b border-white/5">
1900
  <div class="relative flex-1">
1901
- <span class="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500">
1902
  <i data-lucide="search" class="w-4 h-4"></i>
1903
  </span>
1904
  <input id="edit-prov-search" type="text" placeholder="Filter models..."
1905
- class="w-full bg-black/40 border border-white/10 rounded-xl pl-9 pr-4 py-2 text-xs text-slate-100 font-mono focus:outline-none focus:border-amber-500/50 transition-all">
1906
  </div>
1907
  <div class="flex gap-2 text-[11px] font-mono">
1908
- <button id="edit-prov-select-all" class="bg-white/5 hover:bg-white/10 text-slate-200 px-3 py-1.5 rounded-lg border border-white/10 transition flex items-center gap-1">
1909
  <i data-lucide="check-square" class="w-3 h-3"></i> All
1910
  </button>
1911
- <button id="edit-prov-select-none" class="bg-white/5 hover:bg-white/10 text-slate-200 px-3 py-1.5 rounded-lg border border-white/10 transition flex items-center gap-1">
1912
  <i data-lucide="square" class="w-3 h-3"></i> None
1913
  </button>
1914
  <span id="edit-prov-count" class="text-xs font-mono text-amber-400 bg-amber-500/10 border border-amber-500/20 px-2.5 py-1.5 rounded-lg">0 / 0 selected</span>
@@ -1916,7 +1855,7 @@
1916
  </div>
1917
 
1918
  <!-- Table -->
1919
- <div class="px-6 pt-3 grid grid-cols-12 gap-3 text-[10px] uppercase font-mono text-slate-500">
1920
  <div class="col-span-1"></div>
1921
  <div class="col-span-5">Upstream Model ID</div>
1922
  <div class="col-span-6">Exposed Alias <span class="text-amber-400/70">(editable)</span></div>
@@ -1927,10 +1866,10 @@
1927
 
1928
  <!-- Footer -->
1929
  <div class="px-6 py-4 border-t border-white/10 flex flex-col sm:flex-row gap-2 justify-end">
1930
- <button id="edit-prov-cancel" class="bg-white/5 hover:bg-white/10 text-slate-300 text-xs font-semibold py-2.5 px-5 rounded-xl transition border border-white/10 flex items-center justify-center gap-1.5">
1931
  <i data-lucide="x" class="w-3.5 h-3.5"></i><span>Cancel</span>
1932
  </button>
1933
- <button id="edit-prov-save" class="bg-gradient-to-r from-amber-500 to-amber-600 hover:from-amber-400 hover:to-amber-500 text-black text-xs font-bold py-2.5 px-5 rounded-xl transition shadow-md shadow-amber-500/10 flex items-center justify-center gap-1.5">
1934
  <i data-lucide="save" class="w-3.5 h-3.5"></i><span>Save Changes</span>
1935
  </button>
1936
  </div>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>APIarium | Admin Console</title>
7
+ <meta name="description" content="APIarium admin console — manage IP-bound client keys, dynamic providers, and model routing.">
8
 
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
12
 
 
13
  <script src="https://cdn.tailwindcss.com"></script>
 
14
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
 
 
15
  <script src="https://unpkg.com/lucide@latest"></script>
16
 
17
  <script>
 
24
  mono: ['JetBrains Mono', 'monospace'],
25
  },
26
  colors: {
27
+ gray: {
28
+ 900: '#0A0A0A',
29
+ 800: '#171717',
30
+ 700: '#262626',
31
+ 600: '#404040',
32
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  }
34
  }
35
  }
 
37
  </script>
38
  <style>
39
  body {
40
+ background-color: #000000;
41
+ color: #ededed;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  }
43
 
44
+ /* Background grid pattern, matches landing.html */
45
+ .bg-grid-pattern {
46
+ background-image: linear-gradient(to right, #ffffff05 1px, transparent 1px),
47
+ linear-gradient(to bottom, #ffffff05 1px, transparent 1px);
48
+ background-size: 40px 40px;
49
+ mask-image: radial-gradient(circle at center, black, transparent 80%);
50
+ -webkit-mask-image: radial-gradient(circle at center, black, transparent 80%);
 
51
  }
52
 
53
+ /* Legacy aliases kept so existing JS classes keep working. */
54
+ .glass {
55
+ background-color: #0A0A0A; /* gray-900 */
56
+ border: 1px solid #171717; /* gray-800 */
57
  }
58
+ .glass-bright {
59
+ background-color: rgba(23, 23, 23, 0.6);
60
+ border: 1px solid #262626;
61
  }
62
+ .glass-hover { transition: border-color 0.2s ease, background-color 0.2s ease; }
63
+ .glass-hover:hover {
64
+ border-color: #404040;
65
+ background-color: rgba(38, 38, 38, 0.4);
66
  }
67
+ .shadow-amber-glow,
68
+ .shadow-emerald-glow,
69
+ .shadow-glass-inner { box-shadow: none; }
70
+
71
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
72
+ ::-webkit-scrollbar-track { background: transparent; }
73
+ ::-webkit-scrollbar-thumb { background: #262626; border-radius: 4px; }
74
+ ::-webkit-scrollbar-thumb:hover { background: #404040; }
75
+
76
+ @keyframes slideUp {
77
+ from { transform: translate(-50%, 100%); opacity: 0; }
78
+ to { transform: translate(-50%, 0); opacity: 1; }
79
  }
80
 
 
81
  .toast {
82
+ position: fixed;
83
+ bottom: 32px;
84
+ left: 50%;
85
+ transform: translateX(-50%);
86
+ padding: 12px 20px;
87
+ border-radius: 10px;
88
+ font-family: 'JetBrains Mono', monospace;
89
  font-size: 13px;
90
+ z-index: 9999;
91
+ box-shadow: 0 18px 36px -10px rgba(0, 0, 0, 0.6);
92
  display: flex;
93
  align-items: center;
94
  gap: 10px;
95
+ animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
 
 
 
 
 
96
  }
97
+ .toast-success {
98
+ background: #0A0A0A;
99
+ border: 1px solid rgba(16, 185, 129, 0.4);
100
+ color: #34d399;
101
  }
102
+ .toast-error {
103
+ background: #0A0A0A;
104
+ border: 1px solid rgba(239, 68, 68, 0.4);
105
+ color: #f87171;
106
  }
107
 
108
+ @keyframes pulse-slow { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
109
+ .animate-pulse-slow { animation: pulse-slow 3s infinite; }
110
+
111
+ /* Section anchor offset under sticky header */
112
+ section[id] { scroll-margin-top: 80px; }
 
 
 
113
  </style>
114
  </head>
115
+ <body class="font-sans text-gray-100 antialiased min-h-screen flex flex-col relative selection:bg-amber-500/30 selection:text-amber-100">
116
+
117
+ <!-- Background grid pattern (matches landing.html / docs.html) -->
118
+ <div class="absolute inset-0 bg-grid-pattern pointer-events-none z-0"></div>
119
 
120
  <!-- Auth Gate -->
121
+ <div id="auth-gate" class="hidden min-h-screen flex items-center justify-center px-4 py-12 relative z-10 w-full">
122
+ <div class="bg-gray-900 border border-gray-800 rounded-2xl p-8 max-w-md w-full shadow-2xl">
 
 
 
 
123
  <div class="flex flex-col items-center text-center mb-8">
124
+ <span class="text-5xl mb-3" aria-label="APIarium">🐝</span>
125
+ <h1 class="text-2xl font-semibold tracking-tight text-white">APIarium Admin</h1>
126
+ <p class="text-xs font-mono text-gray-500 mt-2 px-2.5 py-0.5 rounded-full border border-gray-800 inline-block">v0.4.0 · Admin Console</p>
 
 
127
  </div>
128
+
129
  <div class="space-y-4">
130
  <div>
131
+ <label class="block text-[10px] font-semibold text-gray-500 uppercase tracking-wider mb-2 font-mono">Master Security Key</label>
132
  <div class="relative">
133
+ <span class="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-500">
134
+ <i data-lucide="key-round" class="w-4 h-4"></i>
135
  </span>
136
  <input id="master-key-input" type="password" placeholder="apiarium_master_..."
137
+ class="w-full bg-black border border-gray-800 rounded-lg pl-10 pr-4 py-3 text-gray-100 font-mono text-sm focus:outline-none focus:border-amber-500/60 transition-colors placeholder:text-gray-700">
138
  </div>
139
  </div>
140
+
141
+ <button id="auth-btn" class="w-full bg-white hover:bg-gray-100 active:scale-[0.99] text-black font-semibold py-3 rounded-lg transition-all flex items-center justify-center gap-2">
142
  <span>Unlock Dashboard</span>
143
  <i data-lucide="unlock" class="w-4 h-4"></i>
144
  </button>
145
+
146
+ <p id="auth-error" class="text-red-400 text-xs mt-3 text-center bg-red-500/10 border border-red-500/30 py-2.5 px-3 rounded-lg hidden font-mono"></p>
147
  </div>
148
+
149
+ <div class="mt-8 pt-6 border-t border-gray-800 text-center">
150
+ <a href="/" class="text-[10px] font-mono text-gray-500 hover:text-gray-300 transition-colors"> Back to homepage</a>
151
  </div>
152
  </div>
153
  </div>
154
 
155
  <!-- Dashboard Container -->
156
+ <div id="dashboard" class="hidden flex-1 flex flex-col relative z-10 w-full">
 
 
 
157
 
158
  <!-- Header Navigation -->
159
+ <header class="sticky top-0 z-40 bg-black/80 backdrop-blur-md border-b border-white/10">
160
+ <div class="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
161
+ <a href="/" class="flex items-center gap-3 group">
162
+ <span class="text-2xl" aria-label="APIarium">🐝</span>
163
+ <span class="font-semibold text-lg tracking-tight group-hover:text-amber-300 transition-colors">APIarium</span>
164
+ <span class="ml-1 px-2 py-0.5 text-[10px] font-mono font-medium bg-amber-500/10 text-amber-300 border border-amber-500/30 rounded-full">ADMIN</span>
165
+ </a>
166
+
167
+ <nav class="hidden md:flex items-center gap-1 text-sm">
168
+ <a href="#stats" class="text-gray-400 hover:text-white px-3 py-1.5 rounded-md hover:bg-white/5 transition-colors flex items-center gap-1.5">
169
+ <i data-lucide="bar-chart-3" class="w-3.5 h-3.5"></i><span>Analytics</span>
170
+ </a>
171
+ <a href="#providers" class="text-gray-400 hover:text-white px-3 py-1.5 rounded-md hover:bg-white/5 transition-colors flex items-center gap-1.5">
172
+ <i data-lucide="cpu" class="w-3.5 h-3.5"></i><span>Providers</span>
173
+ </a>
174
+ <a href="#models" class="text-gray-400 hover:text-white px-3 py-1.5 rounded-md hover:bg-white/5 transition-colors flex items-center gap-1.5">
175
+ <i data-lucide="layers" class="w-3.5 h-3.5"></i><span>Models</span>
176
+ </a>
177
+ <a href="#generator" class="text-gray-400 hover:text-white px-3 py-1.5 rounded-md hover:bg-white/5 transition-colors flex items-center gap-1.5">
178
+ <i data-lucide="key" class="w-3.5 h-3.5"></i><span>Keys</span>
179
+ </a>
180
+ </nav>
181
+
182
+ <div class="flex items-center gap-3">
183
+ <div class="hidden sm:flex items-center gap-1.5 px-2.5 py-1 rounded-md bg-emerald-500/10 border border-emerald-500/30 text-emerald-300 text-xs font-mono">
184
+ <span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse-slow"></span>
185
+ <span>Live</span>
186
  </div>
187
+ <a href="/docs" class="hidden sm:inline-block text-xs text-gray-400 hover:text-white transition-colors">Docs</a>
188
+ <button id="logout-btn" class="flex items-center gap-1.5 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/30 px-2.5 py-1.5 rounded-md text-xs font-mono transition-colors">
189
+ <i data-lucide="log-out" class="w-3.5 h-3.5"></i>
190
+ <span class="hidden sm:inline">Logout</span>
191
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  </div>
193
  </div>
194
  </header>
 
204
  <i data-lucide="gauge" class="text-amber-500 w-5 h-5"></i>
205
  Gateway Overview
206
  </h2>
207
+ <p class="text-sm text-gray-400">Real-time usage analytics, rate metrics, and authenticated keys.</p>
208
  </div>
209
+ <div class="text-xs font-mono text-gray-500 flex items-center gap-2 bg-white/5 px-3 py-1.5 rounded-lg border border-white/5">
210
  <span class="w-2 h-2 rounded-full bg-amber-500 animate-pulse"></span>
211
  <span>Sync State: Active</span>
212
  </div>
 
215
  <!-- Stats KPI Cards -->
216
  <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
217
  <!-- Card 1: Unique Client IPs -->
218
+ <div class="glass rounded-xl p-5 relative overflow-hidden flex flex-col justify-between">
 
219
  <div class="flex items-center justify-between mb-4">
220
+ <span class="text-xs font-mono uppercase tracking-wider text-gray-400">Unique Clients</span>
221
  <div class="p-2 bg-amber-500/10 text-amber-400 rounded-lg">
222
  <i data-lucide="users" class="w-4 h-4"></i>
223
  </div>
224
  </div>
225
  <div>
226
+ <h3 id="stat-active" class="text-2xl font-semibold text-white font-mono tracking-tight">—</h3>
227
+ <p class="text-[11px] text-gray-500 mt-2 flex items-center gap-1">
228
  <i data-lucide="network" class="w-3.5 h-3.5 text-amber-500/60"></i> Active IP-bound keys in routing table
229
  </p>
230
  </div>
231
  </div>
232
 
233
  <!-- Card 2: Total API Keys -->
234
+ <div class="glass rounded-xl p-5 relative overflow-hidden flex flex-col justify-between">
 
235
  <div class="flex items-center justify-between mb-4">
236
+ <span class="text-xs font-mono uppercase tracking-wider text-gray-400">Total Keycards</span>
237
  <div class="p-2 bg-blue-500/10 text-blue-400 rounded-lg">
238
  <i data-lucide="key-round" class="w-4 h-4"></i>
239
  </div>
240
  </div>
241
  <div>
242
+ <h3 id="stat-total" class="text-2xl font-semibold text-white font-mono tracking-tight">—</h3>
243
+ <p class="text-[11px] text-gray-500 mt-2 flex items-center gap-1">
244
  <i data-lucide="check-circle" class="w-3.5 h-3.5 text-blue-500/60"></i> Provisioned reverse-proxy credentials
245
  </p>
246
  </div>
247
  </div>
248
 
249
  <!-- Card 3: Total Requests routed -->
250
+ <div class="glass rounded-xl p-5 relative overflow-hidden flex flex-col justify-between">
 
251
  <div class="flex items-center justify-between mb-4">
252
+ <span class="text-xs font-mono uppercase tracking-wider text-gray-400">Total API Requests</span>
253
  <div class="p-2 bg-emerald-500/10 text-emerald-400 rounded-lg">
254
  <i data-lucide="arrow-up-right" class="w-4 h-4"></i>
255
  </div>
256
  </div>
257
  <div>
258
+ <h3 id="stat-requests" class="text-2xl font-semibold text-white font-mono tracking-tight">0</h3>
259
  <p class="text-[11px] text-emerald-400/80 mt-2 flex items-center gap-1">
260
  <span class="inline-block w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
261
  Incoming calls intercepted by proxy
 
264
  </div>
265
 
266
  <!-- Card 4: Rate Limited attempts -->
267
+ <div class="glass rounded-xl p-5 relative overflow-hidden flex flex-col justify-between">
 
268
  <div class="flex items-center justify-between mb-4">
269
+ <span class="text-xs font-mono uppercase tracking-wider text-gray-400">Rate Limited</span>
270
  <div class="p-2 bg-red-500/10 text-red-400 rounded-lg">
271
  <i data-lucide="shield-alert" class="w-4 h-4"></i>
272
  </div>
273
  </div>
274
  <div>
275
+ <h3 id="stat-ratelimited" class="text-2xl font-semibold text-white font-mono tracking-tight">0</h3>
276
  <p class="text-[11px] text-red-400/80 mt-2 flex items-center gap-1">
277
  <i data-lucide="alert-octagon" class="w-3.5 h-3.5 text-red-400/60"></i> Unauthorized or rate-limited requests
278
  </p>
 
283
  <!-- ANALYTICS CHARTS PANEL -->
284
  <div id="charts" class="grid grid-cols-1 lg:grid-cols-3 gap-6">
285
  <!-- Chart 1: Request Load over 7 Days -->
286
+ <div class="glass rounded-xl p-6 lg:col-span-2 space-y-4">
287
  <div class="flex items-center justify-between">
288
+ <h3 class="text-sm font-semibold text-gray-200 tracking-tight flex items-center gap-2">
289
  <i data-lucide="activity" class="w-4 h-4 text-amber-500"></i>
290
  Traffic Load Analysis
291
  </h3>
292
+ <span class="text-[10px] font-mono text-gray-500 bg-white/5 px-2 py-0.5 rounded">7-day timeline</span>
293
  </div>
294
  <div class="relative min-h-[220px] flex items-center justify-center">
295
  <canvas id="chart-requests" class="max-h-[260px]"></canvas>
 
297
  </div>
298
 
299
  <!-- Chart 2: Popular Models Routing share -->
300
+ <div class="glass rounded-xl p-6 space-y-4">
301
  <div class="flex items-center justify-between">
302
+ <h3 class="text-sm font-semibold text-gray-200 tracking-tight flex items-center gap-2">
303
  <i data-lucide="pie-chart" class="w-4 h-4 text-indigo-400"></i>
304
  Models Routing Share
305
  </h3>
306
+ <span class="text-[10px] font-mono text-gray-500 bg-white/5 px-2 py-0.5 rounded font-mono">Real-time</span>
307
  </div>
308
  <div class="relative min-h-[220px] flex items-center justify-center">
309
  <canvas id="chart-models" class="max-h-[260px]"></canvas>
 
320
  <i data-lucide="cpu" class="text-amber-500 w-5 h-5"></i>
321
  Dynamic Providers
322
  </h2>
323
+ <p class="text-sm text-gray-400">Plug external API end-points instantly. Automatically probe and extract upstream model paths.</p>
324
  </div>
325
  <div class="text-xs font-mono text-amber-500 flex items-center gap-1.5 bg-amber-500/10 border border-amber-500/20 px-3 py-1 rounded-lg">
326
  <i data-lucide="sparkles" class="w-3.5 h-3.5"></i>
 
330
 
331
  <div class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
332
  <!-- Add Provider form card -->
333
+ <div class="glass rounded-xl p-6 lg:col-span-5 space-y-4 border border-white/10 shadow-amber-glow">
334
  <div class="flex items-center gap-2 border-b border-white/5 pb-3">
335
  <div class="p-1.5 bg-amber-500/10 text-amber-400 rounded-lg">
336
  <i data-lucide="plus-circle" class="w-4 h-4"></i>
337
  </div>
338
+ <h3 class="text-sm font-semibold text-gray-200">Register New Upstream AI Provider</h3>
339
  </div>
340
 
341
  <div class="space-y-3.5">
342
  <div class="grid grid-cols-1 sm:grid-cols-2 gap-3.5">
343
  <div>
344
+ <label class="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1.5 font-mono">Provider Name</label>
345
  <input id="prov-name" type="text" placeholder="e.g. DeepBricks"
346
+ class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-2.5 text-gray-100 font-sans text-sm focus:outline-none focus:border-amber-500/60 transition-all">
347
  </div>
348
  <div>
349
+ <label class="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1.5 font-mono flex items-center gap-1.5">
350
  Prefix
351
  <span class="text-[9px] font-mono text-amber-400/80 bg-amber-500/10 border border-amber-500/20 px-1.5 py-0.5 rounded">{prefix}/model</span>
352
  </label>
 
355
  </div>
356
  </div>
357
  <div>
358
+ <label class="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1.5 font-mono">Upstream Endpoint URL</label>
359
  <input id="prov-url" type="url" placeholder="https://api.yourprovider.com/v1"
360
+ class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-2.5 text-gray-100 font-mono text-xs focus:outline-none focus:border-amber-500/60 transition-all">
361
  </div>
362
  <div>
363
+ <label class="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1.5 font-mono">Bearer Access Keys (one per line)</label>
364
  <textarea id="prov-keys" rows="3" placeholder="sk-provider-..."
365
+ class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-2.5 text-gray-100 font-mono text-xs focus:outline-none focus:border-amber-500/60 transition-all resize-none"></textarea>
366
  </div>
367
 
368
  <!-- Step 1: Discover Models -->
369
  <div class="flex gap-2 pt-2">
370
+ <button id="prov-preview-btn" class="flex-1 bg-amber-500 hover:bg-amber-400 text-black text-xs font-bold py-2.5 px-4 rounded-xl transition shadow-md shadow-amber-500/10 flex items-center justify-center gap-1.5">
371
  <i data-lucide="stethoscope" class="w-3.5 h-3.5"></i>
372
  <span>Discover Models</span>
373
  </button>
374
+ <button id="prov-reset-btn" class="bg-white/5 hover:bg-white/10 text-gray-300 text-xs font-semibold py-2.5 px-3 rounded-xl transition border border-white/10 flex items-center justify-center" title="Reset form">
375
  <i data-lucide="rotate-ccw" class="w-3.5 h-3.5"></i>
376
  </button>
377
  </div>
 
382
  </div>
383
 
384
  <!-- Step 2: Model Selection + Alias Editor (full width when active) -->
385
+ <div id="prov-selector" class="hidden lg:col-span-12 glass rounded-xl p-6 space-y-4 border border-amber-500/20 shadow-amber-glow">
386
  <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b border-white/5 pb-3">
387
  <div>
388
  <h3 class="text-sm font-semibold text-amber-300 flex items-center gap-2">
389
  <i data-lucide="list-checks" class="w-4 h-4"></i>
390
  Step 2 — Choose & Alias Models
391
  </h3>
392
+ <p class="text-[11px] text-gray-400 mt-1">Tick the models you want to expose. Edit the right column to set custom IDs. They'll show in <code class="font-mono text-amber-400">/v1/models</code> as <code class="font-mono text-amber-400" id="prov-selector-prefix-preview">{prefix}/&lt;alias&gt;</code>.</p>
393
  </div>
394
  <div class="flex items-center gap-2">
395
  <span id="prov-selected-count" class="text-xs font-mono text-amber-400 bg-amber-500/10 border border-amber-500/20 px-2.5 py-1 rounded-full">0 / 0 selected</span>
 
398
 
399
  <div class="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
400
  <div class="relative flex-1">
401
+ <span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">
402
  <i data-lucide="search" class="w-4 h-4"></i>
403
  </span>
404
  <input id="prov-selector-search" type="text" placeholder="Filter models..."
405
+ class="w-full bg-black/40 border border-white/10 rounded-xl pl-9 pr-4 py-2 text-xs text-gray-100 font-mono focus:outline-none focus:border-amber-500/50 transition-all">
406
  </div>
407
  <div class="flex gap-2 text-[11px] font-mono">
408
+ <button id="prov-select-all" class="bg-white/5 hover:bg-white/10 text-gray-200 px-3 py-1.5 rounded-lg border border-white/10 transition flex items-center gap-1">
409
  <i data-lucide="check-square" class="w-3 h-3"></i> All
410
  </button>
411
+ <button id="prov-select-none" class="bg-white/5 hover:bg-white/10 text-gray-200 px-3 py-1.5 rounded-lg border border-white/10 transition flex items-center gap-1">
412
  <i data-lucide="square" class="w-3 h-3"></i> None
413
  </button>
414
+ <button id="prov-reset-aliases" class="bg-white/5 hover:bg-white/10 text-gray-200 px-3 py-1.5 rounded-lg border border-white/10 transition flex items-center gap-1" title="Reset aliases to upstream id">
415
  <i data-lucide="undo-2" class="w-3 h-3"></i> Reset
416
  </button>
417
  </div>
418
  </div>
419
 
420
  <!-- Table header -->
421
+ <div class="grid grid-cols-12 gap-3 text-[10px] uppercase font-mono text-gray-500 px-3">
422
  <div class="col-span-1"></div>
423
  <div class="col-span-5">Upstream Model ID</div>
424
  <div class="col-span-6">Exposed Alias <span class="text-amber-400/70">(editable)</span></div>
 
429
  </div>
430
 
431
  <div class="flex flex-col sm:flex-row gap-2 pt-3 border-t border-white/5">
432
+ <button id="prov-cancel-import" class="flex-1 sm:flex-none bg-white/5 hover:bg-white/10 text-gray-300 text-xs font-semibold py-2.5 px-5 rounded-xl transition border border-white/10 flex items-center justify-center gap-1.5">
433
  <i data-lucide="x" class="w-3.5 h-3.5"></i>
434
  <span>Cancel</span>
435
  </button>
436
+ <button id="prov-import-btn" class="flex-1 bg-emerald-500 hover:bg-emerald-400 text-black text-xs font-bold py-2.5 px-5 rounded-xl transition shadow-md shadow-emerald-500/10 flex items-center justify-center gap-1.5">
437
  <i data-lucide="import" class="w-3.5 h-3.5"></i>
438
  <span>Import Selected Models</span>
439
  </button>
 
443
  <!-- Existing active dynamic upstreams -->
444
  <div class="lg:col-span-7 space-y-4">
445
  <div class="flex items-center justify-between border-b border-white/5 pb-3">
446
+ <h3 class="text-sm font-semibold text-gray-200 flex items-center gap-2">
447
  <i data-lucide="server" class="w-4 h-4 text-emerald-400"></i>
448
  Configured Upstream Targets
449
  </h3>
 
451
 
452
  <div id="providers-list" class="space-y-3 max-h-[460px] overflow-y-auto pr-1">
453
  <!-- Dynamic providers list gets injected here -->
454
+ <div class="flex flex-col items-center justify-center py-12 text-center text-gray-500 glass rounded-xl border border-dashed border-white/10">
455
+ <i data-lucide="database" class="w-8 h-8 mb-2 text-gray-500"></i>
456
  <span class="text-xs font-mono">No active dynamic provider targets</span>
457
  </div>
458
  </div>
 
468
  <i data-lucide="layers" class="text-indigo-400 w-5 h-5"></i>
469
  Unified Model Catalog
470
  </h2>
471
+ <p class="text-sm text-gray-400">Click models to copy names instantly. Routes are automatically clustered by provider backends.</p>
472
  </div>
473
  <div class="relative w-full sm:w-64">
474
+ <span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">
475
+ <i data-lucide="search" class="w-4 h-4 text-gray-500"></i>
476
  </span>
477
  <input id="models-search" type="text" placeholder="Filter unified models..."
478
+ class="w-full bg-black/40 border border-white/10 rounded-xl pl-9 pr-4 py-2 text-xs text-gray-100 font-mono focus:outline-none focus:border-indigo-500/50 transition-all">
479
  </div>
480
  </div>
481
 
482
+ <div class="glass rounded-xl p-6">
483
  <div class="flex items-center justify-between mb-4 pb-3 border-b border-white/5">
484
  <div class="flex items-center gap-2">
485
  <span id="models-count" class="text-xs font-mono bg-indigo-500/15 border border-indigo-500/25 text-indigo-300 px-2.5 py-1 rounded-full">0 models</span>
486
+ <span class="text-xs text-gray-500 font-mono">available through local proxy endpoints</span>
487
  </div>
488
  </div>
489
 
490
  <!-- Unified Grid of Route Models -->
491
  <div id="models-grid" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3.5 max-h-[380px] overflow-y-auto pr-1">
492
  <!-- Loaded dynamically -->
493
+ <div class="col-span-full py-12 flex flex-col items-center justify-center text-center text-gray-500">
494
  <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-amber-500 mb-2"></div>
495
  <span class="text-xs font-mono">Indexing models routes...</span>
496
  </div>
 
506
  <i data-lucide="shield-check" class="text-amber-500 w-5 h-5"></i>
507
  IP-Bound API Keys
508
  </h2>
509
+ <p class="text-sm text-gray-400">Strict IP validation logic on all reverse-proxy requests. Set labels to keep track of organizational nodes.</p>
510
  </div>
511
  </div>
512
 
513
  <div class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
514
 
515
  <!-- KEY GENERATION COMPONENT -->
516
+ <div class="glass rounded-xl p-6 lg:col-span-4 space-y-4 border border-white/10 shadow-amber-glow">
517
  <div class="flex items-center gap-2 border-b border-white/5 pb-3">
518
  <div class="p-1.5 bg-amber-500/10 text-amber-400 rounded-lg">
519
  <i data-lucide="key-round" class="w-4 h-4"></i>
520
  </div>
521
+ <h3 class="text-sm font-semibold text-gray-200">Generate Client Key</h3>
522
  </div>
523
 
524
  <form id="key-form" class="space-y-4">
525
  <div>
526
+ <label class="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1.5 font-mono">Bound IP Address</label>
527
  <input id="ip-input" type="text" required placeholder="e.g. 192.168.1.50 or ::1"
528
+ class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-2.5 text-gray-100 font-mono text-xs focus:outline-none focus:border-amber-500/60 transition-all">
529
  <p id="ip-error" class="text-red-400 text-[11px] font-mono mt-1 hidden"></p>
530
  </div>
531
  <div>
532
+ <label class="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1.5 font-mono">Descriptive Client Label</label>
533
  <input id="label-input" type="text" placeholder="e.g. Staging server, Analytics engine"
534
+ class="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-2.5 text-gray-100 font-sans text-sm focus:outline-none focus:border-amber-500/60 transition-all">
535
  </div>
536
 
537
+ <button id="gen-btn" type="submit" class="w-full bg-amber-500 hover:bg-amber-400 text-black font-bold py-2.5 rounded-xl transition-all shadow-md shadow-amber-500/10 flex items-center justify-center gap-1.5">
538
  <i data-lucide="plus-circle" class="w-4 h-4"></i>
539
  <span>Generate Route Key</span>
540
  </button>
 
548
  </div>
549
  <div class="space-y-1.5">
550
  <div class="flex items-center justify-between gap-2">
551
+ <span class="text-[10px] text-gray-500 font-mono">KEY:</span>
552
  <span id="result-key" class="font-mono text-xs text-amber-200 select-all break-all text-right"></span>
553
  </div>
554
  <div class="flex items-center justify-between">
555
+ <span class="text-[10px] text-gray-500 font-mono">BOUND TO:</span>
556
+ <span id="result-ip" class="font-mono text-xs text-gray-300"></span>
557
  </div>
558
  <div class="flex items-center justify-between">
559
+ <span class="text-[10px] text-gray-500 font-mono">LABEL:</span>
560
+ <span id="result-label" class="font-sans text-xs text-gray-300"></span>
561
  </div>
562
  </div>
563
  <button id="copy-key-btn" class="w-full bg-white/5 hover:bg-white/10 text-white text-xs font-semibold py-1.5 rounded border border-white/5 transition flex items-center justify-center gap-1">
 
568
  </div>
569
 
570
  <!-- TABLE OF CURRENT KEYS -->
571
+ <div class="lg:col-span-8 glass rounded-xl p-6 space-y-4">
572
  <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-white/5 pb-3">
573
+ <h3 class="text-sm font-semibold text-gray-200 flex items-center gap-2">
574
  <i data-lucide="table" class="w-4 h-4 text-amber-400"></i>
575
  IP Routing Table Keys
576
  </h3>
577
 
578
  <div class="relative w-full sm:w-56">
579
+ <span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">
580
+ <i data-lucide="filter" class="w-3.5 h-3.5 text-gray-500"></i>
581
  </span>
582
  <input id="search-input" type="text" placeholder="Filter keys by IP/Label..."
583
+ class="w-full bg-black/40 border border-white/10 rounded-xl pl-9 pr-4 py-1.5 text-xs text-gray-100 font-mono focus:outline-none focus:border-amber-500/50 transition-all">
584
  </div>
585
  </div>
586
 
 
588
  <div class="overflow-x-auto">
589
  <table class="w-full text-left text-xs min-w-[640px]">
590
  <thead>
591
+ <tr class="text-gray-400 font-mono border-b border-white/5 uppercase text-[10px] tracking-wider">
592
  <th class="pb-3.5 pr-4 font-semibold">Client Key Auth</th>
593
  <th class="pb-3.5 pr-4 font-semibold">Ip Binding</th>
594
  <th class="pb-3.5 pr-4 font-semibold">Description Label</th>
 
604
  </div>
605
 
606
  <!-- Empty placeholder state -->
607
+ <div id="empty-state" class="hidden flex flex-col items-center justify-center py-16 text-center text-gray-500">
608
+ <i data-lucide="inbox" class="w-10 h-10 mb-2 text-gray-500 animate-bounce"></i>
609
+ <h4 class="text-sm font-semibold text-gray-400">No Authorized IP-Bound Keys</h4>
610
+ <p class="text-xs text-gray-500 max-w-xs mt-1">Generate your first IP-bound route configuration using the key generator panel on the left.</p>
611
  </div>
612
  </div>
613
 
 
617
  </main>
618
 
619
  <!-- Footer Area -->
620
+ <footer class="border-t border-gray-900 bg-black mt-auto">
621
+ <div class="max-w-7xl mx-auto px-6 py-6 flex flex-col sm:flex-row justify-between items-center gap-4 text-xs text-gray-500">
622
+ <div class="flex items-center gap-2 font-mono">
623
+ <span class="text-base">🐝</span>
624
+ <span>&copy; 2026 APIarium · Admin Console</span>
625
  </div>
626
+ <div class="flex items-center gap-4 font-mono">
627
+ <span id="sync-status">Last sync: <span id="last-sync" class="text-gray-300">—</span></span>
628
+ <span class="w-1 h-1 rounded-full bg-gray-700"></span>
629
+ <span class="flex items-center gap-1.5">
630
+ <span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse-slow"></span>
631
+ <span class="text-emerald-300">Operational</span>
632
+ </span>
633
+ <a href="/" class="hover:text-gray-300 transition-colors">Home</a>
634
+ <a href="/docs" class="hover:text-gray-300 transition-colors">Docs</a>
635
  </div>
636
  </div>
637
  </footer>
 
733
 
734
  // Visual loading state
735
  authBtn.disabled = true;
736
+ authBtn.innerHTML = `<span>Unlocking Console...</span> <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-900"></div>`;
737
 
738
  try {
739
  const r = await fetch('/admin/keys', { headers: { 'Authorization': 'Bearer ' + val } });
 
903
  tbody.innerHTML = filtered.map(k => `
904
  <tr class="hover:bg-white/[0.02] transition border-b border-white/[0.03]">
905
  <td class="py-3.5 pr-4 font-mono text-xs text-amber-300 select-all font-semibold tracking-wide">${maskKey(k.key)}</td>
906
+ <td class="py-3.5 pr-4 font-mono text-xs text-gray-300">
907
  <span class="bg-black/35 px-2 py-1 rounded border border-white/5">${k.ip || '—'}</span>
908
  </td>
909
+ <td class="py-3.5 pr-4 text-gray-300 font-medium">${k.label || '<span class="text-gray-500 font-mono text-xs">NO_LABEL</span>'}</td>
910
+ <td class="py-3.5 pr-4 text-[11px] font-mono text-gray-400">${formatDate(k.created_at)}</td>
911
  <td class="py-3.5 pr-4">
912
  <span class="text-[10px] font-mono px-2.5 py-1 rounded-full ${k.status === 'active' ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'bg-red-500/10 text-red-400 border border-red-500/20'}">
913
  ${(k.status || 'active').toUpperCase()}
914
  </span>
915
  </td>
916
  <td class="py-3.5 text-right flex justify-end gap-2">
917
+ <button onclick="window.__copyKey('${k.key}')" class="text-[11px] font-mono text-gray-300 hover:text-amber-400 px-2.5 py-1 rounded-lg bg-white/5 hover:bg-white/10 border border-white/5 transition flex items-center gap-1">
918
  <i data-lucide="clipboard" class="w-3 h-3"></i> COPY
919
  </button>
920
  ${k.status !== 'revoked' ? `
921
+ <button onclick="window.__revokeKey('${k.key}')" class="text-[11px] font-mono text-red-400/80 hover:text-red-400 px-2.5 py-1 rounded-lg bg-red-500/5 hover:bg-red-500/10 border border-red-500/10 transition flex items-center gap-1">
922
  <i data-lucide="trash-2" class="w-3 h-3"></i> REVOKE
923
  </button>` : ''}
924
  </td>
 
995
 
996
  const btn = $('gen-btn');
997
  btn.disabled = true;
998
+ btn.innerHTML = `<span>Provisioning Key...</span> <div class="animate-spin rounded-full h-3.5 w-3.5 border-b-2 border-gray-900"></div>`;
999
 
1000
  try {
1001
  const r = await api('/admin/keys', { method: 'POST', body: JSON.stringify({ ip, label }) });
 
1190
  const items = selectorState.available
1191
  .filter(id => !q || id.toLowerCase().includes(q));
1192
  if (items.length === 0) {
1193
+ list.innerHTML = `<div class="text-center text-gray-500 text-xs font-mono py-8">No models match this filter.</div>`;
1194
  return;
1195
  }
1196
  const prefix = (($('prov-prefix') || {}).value || '').trim();
 
1209
  </label>
1210
  </div>
1211
  <div class="col-span-5 min-w-0">
1212
+ <div class="font-mono text-[11px] text-gray-200 truncate" title="${escapeAttr(id)}">${escapeHtml(id)}</div>
1213
  </div>
1214
  <div class="col-span-6">
1215
  <div class="flex items-center gap-2">
 
1219
  data-upstream="${escapeAttr(id)}"
1220
  ${isSel ? '' : 'disabled'}
1221
  placeholder="alias">
1222
+ <span class="text-[10px] font-mono text-gray-500 truncate flex-shrink-0 min-w-0" title="${escapeAttr(full)}">→ ${escapeHtml(full)}</span>
1223
  </div>
1224
  </div>
1225
  </div>`;
 
1295
  if (!list) return;
1296
  if (providers.length === 0) {
1297
  list.innerHTML = `
1298
+ <div class="flex flex-col items-center justify-center py-10 text-center text-gray-500 bg-black/25 rounded-2xl border border-dashed border-white/5">
1299
+ <i data-lucide="help-circle" class="w-8 h-8 mb-2 text-gray-500"></i>
1300
  <span class="text-xs font-mono">No dynamically registered upstreams</span>
1301
  </div>`;
1302
  lucide.createIcons();
 
1316
  const moreCount = Math.max(0, richModels.length - 6);
1317
  const prefixBadge = prefix
1318
  ? `<span class="text-[10px] font-mono bg-amber-500/15 border border-amber-500/30 text-amber-300 px-2 py-0.5 rounded-md tracking-wider">${escapeHtml(prefix)}/</span>`
1319
+ : `<span class="text-[10px] font-mono bg-gray-500/10 border border-gray-500/20 text-gray-400 px-2 py-0.5 rounded-md">no prefix</span>`;
1320
  return `
1321
  <div class="bg-gradient-to-br from-black/45 to-black/25 rounded-2xl p-4 border border-white/5 hover:border-amber-500/25 transition-all group">
1322
  <div class="flex items-start justify-between gap-3">
1323
  <div class="flex-1 min-w-0">
1324
  <div class="flex items-center gap-2 flex-wrap">
1325
+ <span class="font-bold text-gray-100 text-sm tracking-tight">${escapeHtml(p.name)}</span>
1326
  ${prefixBadge}
1327
+ <span class="text-[9px] font-mono bg-white/5 border border-white/10 text-gray-400 px-1.5 py-0.5 rounded">${escapeHtml(p.id)}</span>
1328
  </div>
1329
+ <div class="text-[11px] text-gray-400 font-mono mt-1.5 truncate flex items-center gap-1.5">
1330
+ <i data-lucide="link-2" class="w-3 h-3 text-gray-500"></i>
1331
  <span class="truncate">${escapeHtml(p.base_url)}</span>
1332
  </div>
1333
  </div>
 
1335
  <button onclick="window.__editProvider('${escapeAttr(p.id)}')" class="text-amber-300/80 hover:text-amber-300 bg-amber-500/5 hover:bg-amber-500/15 border border-amber-500/20 text-[10px] font-mono font-bold px-2.5 py-1.5 rounded-lg transition flex items-center gap-1" title="Edit prefix & models">
1336
  <i data-lucide="settings-2" class="w-3 h-3"></i> EDIT
1337
  </button>
1338
+ <button onclick="window.__deleteProvider('${escapeAttr(p.id)}')" class="text-red-400/80 hover:text-red-400 bg-red-500/5 hover:bg-red-500/10 border border-red-500/20 text-[10px] font-mono font-bold px-2.5 py-1.5 rounded-lg transition flex items-center gap-1">
1339
  <i data-lucide="trash-2" class="w-3 h-3"></i> DEL
1340
  </button>
1341
  </div>
1342
  </div>
1343
+ <div class="flex flex-wrap gap-3 mt-3 text-[11px] text-gray-500 font-mono">
1344
  <span class="flex items-center gap-1"><i data-lucide="key-round" class="w-3 h-3"></i> ${p.key_count} keys</span>
1345
  <span class="flex items-center gap-1"><i data-lucide="box" class="w-3 h-3"></i> ${richModels.length} exposed</span>
1346
+ <span class="flex items-center gap-1 text-gray-500"><i data-lucide="database" class="w-3 h-3"></i> ${(p.available_models || []).length} available upstream</span>
1347
  </div>
1348
  ${richModels.length > 0 ? `
1349
  <div class="mt-3 pt-3 border-t border-white/5 flex flex-wrap gap-1.5">
1350
  ${previewChips}
1351
+ ${moreCount > 0 ? `<span class="text-[10px] font-mono text-gray-500 px-2 py-0.5">+${moreCount} more</span>` : ''}
1352
  </div>` : ''}
1353
  </div>`;
1354
  }).join('');
 
1394
  btn.innerHTML = `<div class="animate-spin rounded-full h-3.5 w-3.5 border-b-2 border-black"></div><span>Discovering...</span>`;
1395
  status.classList.remove('hidden');
1396
  status.textContent = 'Probing upstream /models endpoint...';
1397
+ status.className = 'text-xs font-mono text-gray-400 bg-white/5 px-2.5 py-1.5 rounded-lg border border-white/10';
1398
 
1399
  let discovered = [];
1400
  try {
 
1551
  const q = (editState.filter || '').toLowerCase();
1552
  const items = editState.available.filter(id => !q || id.toLowerCase().includes(q));
1553
  if (items.length === 0) {
1554
+ list.innerHTML = `<div class="text-center text-gray-500 text-xs font-mono py-8">No models match this filter.</div>`;
1555
  return;
1556
  }
1557
  list.innerHTML = items.map(id => {
 
1569
  </label>
1570
  </div>
1571
  <div class="col-span-5 min-w-0">
1572
+ <div class="font-mono text-[11px] text-gray-200 truncate" title="${escapeAttr(id)}">${escapeHtml(id)}</div>
1573
  </div>
1574
  <div class="col-span-6">
1575
  <div class="flex items-center gap-2">
 
1578
  value="${escapeAttr(alias)}"
1579
  data-upstream="${escapeAttr(id)}"
1580
  ${isSel ? '' : 'disabled'}>
1581
+ <span class="text-[10px] font-mono text-gray-500 truncate flex-shrink-0 min-w-0" title="${escapeAttr(full)}">→ ${escapeHtml(full)}</span>
1582
  </div>
1583
  </div>
1584
  </div>`;
 
1756
 
1757
  if (filtered.length === 0) {
1758
  grid.innerHTML = `
1759
+ <div class="col-span-full py-10 text-center text-gray-500 bg-black/20 rounded-xl border border-dashed border-white/5 font-mono text-xs">
1760
  No registered proxy model routes matched query filters.
1761
  </div>`;
1762
  return;
 
1766
  <div class="bg-black/45 border border-white/5 hover:border-amber-500/30 rounded-xl p-3.5 transition-all duration-200 cursor-pointer group shadow-glass-inner"
1767
  onclick="window.__copyModelId('${escapeHtml(m.name)}')">
1768
  <div class="flex items-start justify-between gap-2">
1769
+ <span class="font-mono text-xs text-gray-200 break-all select-all group-hover:text-amber-300 font-semibold transition-colors duration-150">${escapeHtml(m.name || m.id)}</span>
1770
  ${m.upstreams > 1 ? `<span class="text-[9px] font-mono font-bold text-amber-400 bg-amber-500/15 border border-amber-500/20 px-1.5 py-0.5 rounded flex-shrink-0">x${m.upstreams} nodes</span>` : ''}
1771
  </div>
1772
  <div class="mt-3 flex items-center justify-between">
1773
  <div class="flex items-center gap-1.5">
1774
  <span class="w-1.5 h-1.5 rounded-full ${m.provider === 'built-in' ? 'bg-amber-400' : m.provider === 'image-backend' ? 'bg-indigo-400' : 'bg-emerald-400'}"></span>
1775
+ <span class="text-[10px] font-mono text-gray-400 truncate max-w-[120px]">${escapeHtml(m.provider)}</span>
1776
  </div>
1777
+ <span class="text-[9px] font-mono text-gray-500 group-hover:text-amber-400 transition-colors flex items-center gap-1">
1778
  <i data-lucide="copy" class="w-3 h-3"></i> COPY
1779
  </span>
1780
  </div>
 
1811
  <i data-lucide="settings-2" class="w-4 h-4"></i>
1812
  </div>
1813
  <div class="min-w-0">
1814
+ <h3 id="edit-prov-modal-title" class="font-bold text-gray-100 truncate">Edit Provider</h3>
1815
+ <p class="text-[11px] text-gray-400 font-mono">Update prefix, model selection, and aliases</p>
1816
  </div>
1817
  </div>
1818
+ <button id="edit-prov-close" class="text-gray-400 hover:text-gray-100 bg-white/5 hover:bg-white/10 rounded-lg p-1.5 transition">
1819
  <i data-lucide="x" class="w-4 h-4"></i>
1820
  </button>
1821
  </div>
 
1823
  <!-- Top: name + prefix -->
1824
  <div class="px-6 py-4 grid grid-cols-1 sm:grid-cols-2 gap-4 border-b border-white/5">
1825
  <div>
1826
+ <label class="block text-[10px] font-semibold text-gray-400 uppercase tracking-wider mb-1.5 font-mono">Display Name</label>
1827
+ <input id="edit-prov-name" type="text" class="w-full bg-black/40 border border-white/10 rounded-xl px-3 py-2 text-gray-100 text-sm focus:outline-none focus:border-amber-500/60 transition-all">
1828
  </div>
1829
  <div>
1830
+ <label class="block text-[10px] font-semibold text-gray-400 uppercase tracking-wider mb-1.5 font-mono flex items-center gap-1.5">
1831
  Prefix <span class="text-[9px] text-amber-400/80 bg-amber-500/10 border border-amber-500/20 px-1.5 py-0.5 rounded">{prefix}/model</span>
1832
  </label>
1833
  <input id="edit-prov-prefix" type="text" class="w-full bg-black/40 border border-white/10 rounded-xl px-3 py-2 text-amber-200 font-mono text-xs focus:outline-none focus:border-amber-500/60 transition-all" placeholder="e.g. deb">
 
1837
  <!-- Filter & bulk -->
1838
  <div class="px-6 py-3 flex flex-col sm:flex-row gap-3 items-stretch sm:items-center border-b border-white/5">
1839
  <div class="relative flex-1">
1840
+ <span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">
1841
  <i data-lucide="search" class="w-4 h-4"></i>
1842
  </span>
1843
  <input id="edit-prov-search" type="text" placeholder="Filter models..."
1844
+ class="w-full bg-black/40 border border-white/10 rounded-xl pl-9 pr-4 py-2 text-xs text-gray-100 font-mono focus:outline-none focus:border-amber-500/50 transition-all">
1845
  </div>
1846
  <div class="flex gap-2 text-[11px] font-mono">
1847
+ <button id="edit-prov-select-all" class="bg-white/5 hover:bg-white/10 text-gray-200 px-3 py-1.5 rounded-lg border border-white/10 transition flex items-center gap-1">
1848
  <i data-lucide="check-square" class="w-3 h-3"></i> All
1849
  </button>
1850
+ <button id="edit-prov-select-none" class="bg-white/5 hover:bg-white/10 text-gray-200 px-3 py-1.5 rounded-lg border border-white/10 transition flex items-center gap-1">
1851
  <i data-lucide="square" class="w-3 h-3"></i> None
1852
  </button>
1853
  <span id="edit-prov-count" class="text-xs font-mono text-amber-400 bg-amber-500/10 border border-amber-500/20 px-2.5 py-1.5 rounded-lg">0 / 0 selected</span>
 
1855
  </div>
1856
 
1857
  <!-- Table -->
1858
+ <div class="px-6 pt-3 grid grid-cols-12 gap-3 text-[10px] uppercase font-mono text-gray-500">
1859
  <div class="col-span-1"></div>
1860
  <div class="col-span-5">Upstream Model ID</div>
1861
  <div class="col-span-6">Exposed Alias <span class="text-amber-400/70">(editable)</span></div>
 
1866
 
1867
  <!-- Footer -->
1868
  <div class="px-6 py-4 border-t border-white/10 flex flex-col sm:flex-row gap-2 justify-end">
1869
+ <button id="edit-prov-cancel" class="bg-white/5 hover:bg-white/10 text-gray-300 text-xs font-semibold py-2.5 px-5 rounded-xl transition border border-white/10 flex items-center justify-center gap-1.5">
1870
  <i data-lucide="x" class="w-3.5 h-3.5"></i><span>Cancel</span>
1871
  </button>
1872
+ <button id="edit-prov-save" class="bg-amber-500 hover:bg-amber-400 text-black text-xs font-bold py-2.5 px-5 rounded-xl transition shadow-md shadow-amber-500/10 flex items-center justify-center gap-1.5">
1873
  <i data-lucide="save" class="w-3.5 h-3.5"></i><span>Save Changes</span>
1874
  </button>
1875
  </div>