feat(admin): unify dashboard + login design with landing/docs theme
Browse filesAligns 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.
- app/index.py +31 -10
- app/templates/dashboard.html +251 -312
|
@@ -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="
|
| 2106 |
-
<div class="
|
|
|
|
|
|
|
| 2107 |
<div class="text-center mb-8">
|
| 2108 |
-
<
|
| 2109 |
-
<
|
|
|
|
| 2110 |
</div>
|
| 2111 |
-
|
| 2112 |
-
|
| 2113 |
-
<
|
|
|
|
| 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-
|
| 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>
|
|
@@ -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
|
| 7 |
-
<meta name="description" content="APIarium
|
| 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@
|
| 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 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 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: #
|
| 64 |
-
|
| 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 |
-
.
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
background:
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
transform: translateY(-1px);
|
| 96 |
}
|
| 97 |
|
| 98 |
-
/*
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
}
|
| 103 |
-
|
| 104 |
-
background: rgba(
|
|
|
|
| 105 |
}
|
| 106 |
-
:
|
| 107 |
-
|
| 108 |
-
border-
|
|
|
|
| 109 |
}
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
}
|
| 113 |
|
| 114 |
-
/* Toasts dynamic slider */
|
| 115 |
.toast {
|
| 116 |
-
position: fixed;
|
| 117 |
-
bottom:
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
|
|
|
| 122 |
font-size: 13px;
|
| 123 |
-
z-index: 9999;
|
| 124 |
-
box-shadow: 0
|
| 125 |
display: flex;
|
| 126 |
align-items: center;
|
| 127 |
gap: 10px;
|
| 128 |
-
animation:
|
| 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:
|
| 137 |
-
border: 1px solid rgba(16, 185, 129, 0.
|
| 138 |
-
color: #34d399;
|
| 139 |
}
|
| 140 |
-
.toast-error {
|
| 141 |
-
background:
|
| 142 |
-
border: 1px solid rgba(239, 68, 68, 0.
|
| 143 |
-
color: #f87171;
|
| 144 |
}
|
| 145 |
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
}
|
| 151 |
-
.animate-pulse-slow {
|
| 152 |
-
animation: pulse-slow 3s infinite;
|
| 153 |
-
}
|
| 154 |
</style>
|
| 155 |
</head>
|
| 156 |
-
<body class="font-sans text-
|
|
|
|
|
|
|
|
|
|
| 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-
|
| 160 |
-
<div class="
|
| 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 |
-
<
|
| 167 |
-
<
|
| 168 |
-
|
| 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-
|
| 176 |
<div class="relative">
|
| 177 |
-
<span class="absolute left-3.5 top-1/2 -translate-y-1/2 text-
|
| 178 |
-
<i data-lucide="key-round" class="w-4 h-4
|
| 179 |
</span>
|
| 180 |
<input id="master-key-input" type="password" placeholder="apiarium_master_..."
|
| 181 |
-
class="w-full bg-black
|
| 182 |
</div>
|
| 183 |
</div>
|
| 184 |
-
|
| 185 |
-
<button id="auth-btn" class="w-full bg-
|
| 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/
|
| 191 |
</div>
|
| 192 |
-
|
| 193 |
-
<div class="mt-8 pt-6 border-t border-
|
| 194 |
-
<
|
| 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
|
| 207 |
-
<div class="max-w-7xl mx-auto px-
|
| 208 |
-
<
|
| 209 |
-
<
|
| 210 |
-
<
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
<
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
</div>
|
| 226 |
-
|
| 227 |
-
<
|
| 228 |
-
|
| 229 |
-
<
|
| 230 |
-
|
| 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-
|
| 270 |
</div>
|
| 271 |
-
<div class="text-xs font-mono text-
|
| 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-
|
| 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-
|
| 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-
|
| 290 |
-
<p class="text-[11px] text-
|
| 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-
|
| 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-
|
| 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-
|
| 307 |
-
<p class="text-[11px] text-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 353 |
<div class="flex items-center justify-between">
|
| 354 |
-
<h3 class="text-sm font-semibold text-
|
| 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-
|
| 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-
|
| 367 |
<div class="flex items-center justify-between">
|
| 368 |
-
<h3 class="text-sm font-semibold text-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 413 |
</div>
|
| 414 |
<div>
|
| 415 |
-
<label class="block text-xs font-semibold text-
|
| 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-
|
| 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-
|
| 427 |
</div>
|
| 428 |
<div>
|
| 429 |
-
<label class="block text-xs font-semibold text-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 521 |
-
<i data-lucide="database" class="w-8 h-8 mb-2 text-
|
| 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-
|
| 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-
|
| 541 |
-
<i data-lucide="search" class="w-4 h-4 text-
|
| 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-
|
| 545 |
</div>
|
| 546 |
</div>
|
| 547 |
|
| 548 |
-
<div class="glass rounded-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 588 |
</div>
|
| 589 |
|
| 590 |
<form id="key-form" class="space-y-4">
|
| 591 |
<div>
|
| 592 |
-
<label class="block text-xs font-semibold text-
|
| 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-
|
| 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-
|
| 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-
|
| 601 |
</div>
|
| 602 |
|
| 603 |
-
<button id="gen-btn" type="submit" class="w-full bg-
|
| 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-
|
| 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-
|
| 622 |
-
<span id="result-ip" class="font-mono text-xs text-
|
| 623 |
</div>
|
| 624 |
<div class="flex items-center justify-between">
|
| 625 |
-
<span class="text-[10px] text-
|
| 626 |
-
<span id="result-label" class="font-sans text-xs text-
|
| 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-
|
| 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-
|
| 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-
|
| 646 |
-
<i data-lucide="filter" class="w-3.5 h-3.5 text-
|
| 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-
|
| 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-
|
| 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-
|
| 674 |
-
<i data-lucide="
|
| 675 |
-
<h4 class="text-sm font-semibold text-
|
| 676 |
-
<p class="text-xs text-
|
| 677 |
</div>
|
| 678 |
</div>
|
| 679 |
|
|
@@ -683,16 +617,21 @@
|
|
| 683 |
</main>
|
| 684 |
|
| 685 |
<!-- Footer Area -->
|
| 686 |
-
<footer class="border-t border-
|
| 687 |
-
<div class="max-w-7xl mx-auto px-
|
| 688 |
-
<div class="flex items-center gap-2">
|
| 689 |
-
<span class="
|
| 690 |
-
<span>
|
| 691 |
</div>
|
| 692 |
-
<div class="flex items-center gap-4">
|
| 693 |
-
<span id="sync-status">Last
|
| 694 |
-
<span>
|
| 695 |
-
<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-
|
| 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-
|
| 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-
|
| 971 |
-
<td class="py-3.5 pr-4 text-[11px] font-mono text-
|
| 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-
|
| 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/
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 1360 |
-
<i data-lucide="help-circle" class="w-8 h-8 mb-2 text-
|
| 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-
|
| 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-
|
| 1387 |
${prefixBadge}
|
| 1388 |
-
<span class="text-[9px] font-mono bg-white/5 border border-white/10 text-
|
| 1389 |
</div>
|
| 1390 |
-
<div class="text-[11px] text-
|
| 1391 |
-
<i data-lucide="link-2" class="w-3 h-3 text-
|
| 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/
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 1837 |
</div>
|
| 1838 |
-
<span class="text-[9px] font-mono text-
|
| 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-
|
| 1876 |
-
<p class="text-[11px] text-
|
| 1877 |
</div>
|
| 1878 |
</div>
|
| 1879 |
-
<button id="edit-prov-close" class="text-
|
| 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-
|
| 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-
|
| 1889 |
</div>
|
| 1890 |
<div>
|
| 1891 |
-
<label class="block text-[10px] font-semibold text-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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}/<alias></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>© 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>
|