icebear0828 Claude Opus 4.6 commited on
Commit
bafbf4b
Β·
1 Parent(s): debd0e1

ui: redesign dashboard with Tailwind CSS + wire backend APIs

Browse files

Replace plain CSS dashboard with new Tailwind-based design while
connecting all backend endpoints (accounts, auth, models, config).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. public/dashboard1.html +704 -0
  2. src/routes/web.ts +1 -1
public/dashboard1.html ADDED
@@ -0,0 +1,704 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en"><head>
3
+ <meta charset="utf-8"/>
4
+ <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
5
+ <title>Codex Proxy Developer Dashboard</title>
6
+ <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=JetBrains+Mono:wght@400;500&amp;display=swap" rel="stylesheet"/>
8
+ <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
9
+ <script>
10
+ tailwind.config = {
11
+ darkMode: "class",
12
+ theme: {
13
+ extend: {
14
+ colors: {
15
+ "primary": "#10a235",
16
+ "primary-hover": "#0e8c2e",
17
+ "background-light": "#f6f8f6",
18
+ "background-dark": "#112115",
19
+ "card-border": "#e5e7eb",
20
+ },
21
+ fontFamily: {
22
+ "display": ["Inter", "sans-serif"],
23
+ "mono": ["JetBrains Mono", "monospace"],
24
+ },
25
+ borderRadius: {"DEFAULT": "0.25rem", "lg": "0.5rem", "xl": "0.75rem", "full": "9999px"},
26
+ },
27
+ },
28
+ }
29
+ </script>
30
+ </head>
31
+ <body class="bg-background-light dark:bg-background-dark font-display text-slate-900 dark:text-slate-100 antialiased min-h-screen flex flex-col">
32
+ <!-- Top Navigation -->
33
+ <header class="sticky top-0 z-50 w-full bg-white dark:bg-[#1a1a1a] border-b border-card-border shadow-sm">
34
+ <div class="px-4 md:px-8 lg:px-40 flex h-16 items-center justify-center">
35
+ <div class="flex w-full max-w-[960px] items-center justify-between">
36
+ <!-- Logo & Title -->
37
+ <div class="flex items-center gap-3">
38
+ <div class="flex items-center justify-center size-8 rounded-full bg-primary/10 text-primary">
39
+ <span class="material-symbols-outlined text-xl font-bold">check_circle</span>
40
+ </div>
41
+ <h1 class="text-lg font-bold tracking-tight text-slate-900 dark:text-white">Codex Proxy</h1>
42
+ </div>
43
+ <!-- Actions -->
44
+ <div class="flex items-center gap-3">
45
+ <div class="hidden sm:flex items-center gap-2 px-3 py-1.5 rounded-full bg-primary/10 border border-primary/20" id="serverBadge">
46
+ <span class="relative flex h-2.5 w-2.5">
47
+ <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
48
+ <span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-primary"></span>
49
+ </span>
50
+ <span class="text-xs font-semibold text-primary">Server Online</span>
51
+ </div>
52
+ <button id="addAccountBtn" onclick="startAddAccount()" class="flex items-center gap-2 px-4 py-2 bg-primary hover:bg-primary-hover text-white text-sm font-semibold rounded-lg transition-colors shadow-sm active:scale-95">
53
+ <span class="material-symbols-outlined text-sm">add</span>
54
+ <span>Add Account</span>
55
+ </button>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </header>
60
+ <!-- Main Content -->
61
+ <main class="flex-grow px-4 md:px-8 lg:px-40 py-8 flex justify-center">
62
+ <div class="flex flex-col w-full max-w-[960px] gap-8">
63
+
64
+ <!-- Add Account Section (hidden by default) -->
65
+ <section id="addSection" class="hidden bg-white dark:bg-[#1e1e1e] border border-card-border rounded-xl p-5 shadow-sm">
66
+ <p class="text-sm text-slate-500 mb-3">If the popup shows an error or you're on a different machine, copy the full callback URL and paste it below.</p>
67
+ <div class="flex gap-3">
68
+ <input id="addCallbackInput" type="text" placeholder="Paste callback URL" class="flex-1 px-3 py-2.5 bg-slate-50 border border-card-border rounded-lg text-sm font-mono text-slate-600 focus:ring-2 focus:ring-primary/50 focus:border-primary outline-none"/>
69
+ <button id="addRelayBtn" onclick="submitAddRelay()" class="px-4 py-2.5 bg-white border border-card-border rounded-lg text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors">Submit</button>
70
+ </div>
71
+ <p id="addInfo" class="text-sm text-primary mt-2 hidden"></p>
72
+ <p id="addError" class="text-sm text-red-500 mt-2 hidden"></p>
73
+ </section>
74
+
75
+ <!-- Section 1: Connected Accounts -->
76
+ <section class="flex flex-col gap-4">
77
+ <div class="flex items-end justify-between">
78
+ <div class="flex flex-col gap-1">
79
+ <h2 class="text-2xl font-bold text-slate-900 dark:text-white tracking-tight">Connected Accounts</h2>
80
+ <p class="text-slate-500 text-sm">Manage your AI model proxy services and usage limits.</p>
81
+ </div>
82
+ </div>
83
+ <div id="accountList" class="grid grid-cols-1 md:grid-cols-2 gap-4">
84
+ <div class="md:col-span-2 text-center py-8 text-slate-400 text-sm bg-white dark:bg-[#1e1e1e] border border-card-border rounded-xl">Loading accounts...</div>
85
+ </div>
86
+ </section>
87
+
88
+ <!-- Section 2: API Configuration -->
89
+ <section class="bg-white dark:bg-[#1e1e1e] border border-card-border rounded-xl p-6 shadow-sm">
90
+ <div class="flex items-center justify-between mb-6 border-b border-slate-100 pb-4">
91
+ <div class="flex items-center gap-2">
92
+ <span class="material-symbols-outlined text-primary">settings</span>
93
+ <h2 class="text-lg font-bold text-slate-900 dark:text-white">API Configuration</h2>
94
+ </div>
95
+ <button onclick="resetConfigDefaults()" class="text-xs text-slate-400 hover:text-slate-600 transition-colors">Reset to defaults</button>
96
+ </div>
97
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
98
+ <!-- Base URL -->
99
+ <div class="space-y-1.5">
100
+ <label class="text-sm font-semibold text-slate-700 dark:text-slate-300">Base Proxy URL</label>
101
+ <div class="relative flex items-center">
102
+ <input id="baseUrl" class="w-full pl-3 pr-10 py-2.5 bg-slate-50 border border-card-border rounded-lg text-sm font-mono text-slate-600 focus:ring-2 focus:ring-primary/50 focus:border-primary outline-none transition-all" type="text" value="Loading..."/>
103
+ <button onclick="copyField('baseUrl', this)" class="absolute right-2 p-1.5 text-slate-400 hover:text-primary transition-colors rounded-md hover:bg-slate-100" title="Copy URL">
104
+ <span class="material-symbols-outlined text-lg">content_copy</span>
105
+ </button>
106
+ </div>
107
+ </div>
108
+ <!-- Default Model -->
109
+ <div class="space-y-1.5">
110
+ <label class="text-sm font-semibold text-slate-700 dark:text-slate-300">Default Model</label>
111
+ <div class="relative">
112
+ <select id="defaultModel" class="w-full appearance-none pl-3 pr-10 py-2.5 bg-white border border-card-border rounded-lg text-sm text-slate-700 font-medium focus:ring-2 focus:ring-primary/50 focus:border-primary outline-none cursor-pointer">
113
+ <option value="codex">codex</option>
114
+ </select>
115
+ <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-slate-500">
116
+ <span class="material-symbols-outlined text-lg">expand_more</span>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ <!-- API Key -->
121
+ <div class="space-y-1.5 md:col-span-2">
122
+ <label class="text-sm font-semibold text-slate-700 dark:text-slate-300">Your API Key</label>
123
+ <div class="relative flex items-center">
124
+ <div class="absolute left-3 text-slate-400">
125
+ <span class="material-symbols-outlined text-lg">key</span>
126
+ </div>
127
+ <input id="apiKey" class="w-full pl-10 pr-10 py-2.5 bg-slate-50 border border-card-border rounded-lg text-sm font-mono text-slate-600 focus:ring-2 focus:ring-primary/50 focus:border-primary outline-none transition-all tracking-wider" type="text" value="Loading..."/>
128
+ <button onclick="copyField('apiKey', this)" class="absolute right-2 p-1.5 text-slate-400 hover:text-primary transition-colors rounded-md hover:bg-slate-100" title="Copy API Key">
129
+ <span class="material-symbols-outlined text-lg">content_copy</span>
130
+ </button>
131
+ </div>
132
+ <p class="text-xs text-slate-400 mt-1">Use this key to authenticate requests to the proxy. Do not share it.</p>
133
+ </div>
134
+ </div>
135
+ </section>
136
+
137
+ <!-- Section 3: Integration Examples -->
138
+ <section class="flex flex-col gap-4">
139
+ <h2 class="text-lg font-bold text-slate-900 dark:text-white">Integration Examples</h2>
140
+ <div class="bg-white dark:bg-[#1e1e1e] border border-card-border rounded-xl overflow-hidden shadow-sm">
141
+ <!-- Top Tabs: Providers -->
142
+ <div id="protocolTabs" class="flex border-b border-card-border bg-slate-50/50">
143
+ <button onclick="switchProtocol('openai', this)" class="proto-tab px-6 py-3 text-sm font-semibold text-primary border-b-2 border-primary bg-white transition-colors" data-proto="openai">OpenAI</button>
144
+ <button onclick="switchProtocol('anthropic', this)" class="proto-tab px-6 py-3 text-sm font-medium text-slate-500 hover:text-slate-700 hover:bg-slate-50 border-b-2 border-transparent transition-colors" data-proto="anthropic">Anthropic</button>
145
+ <button onclick="switchProtocol('gemini', this)" class="proto-tab px-6 py-3 text-sm font-medium text-slate-500 hover:text-slate-700 hover:bg-slate-50 border-b-2 border-transparent transition-colors" data-proto="gemini">Gemini</button>
146
+ </div>
147
+ <!-- Sub Tabs: Languages & Content -->
148
+ <div class="p-6">
149
+ <div class="flex items-center justify-between mb-4">
150
+ <div id="langTabs" class="flex gap-2 p-1 bg-slate-100 rounded-lg">
151
+ <button onclick="switchLang('python', this)" class="lang-tab px-3 py-1.5 text-xs font-semibold rounded bg-white text-slate-800 shadow-sm transition-all" data-lang="python">Python</button>
152
+ <button onclick="switchLang('node', this)" class="lang-tab px-3 py-1.5 text-xs font-medium rounded text-slate-500 hover:text-slate-700 hover:bg-white/50 transition-all" data-lang="node">Node.js</button>
153
+ <button onclick="switchLang('curl', this)" class="lang-tab px-3 py-1.5 text-xs font-medium rounded text-slate-500 hover:text-slate-700 hover:bg-white/50 transition-all" data-lang="curl">cURL</button>
154
+ </div>
155
+ </div>
156
+ <!-- Code Panels (one per protocol-lang combo) -->
157
+ <div id="codeContainer" class="relative group rounded-lg overflow-hidden bg-[#0d1117] text-slate-300 font-mono text-sm border border-slate-800">
158
+ <div class="absolute right-2 top-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity">
159
+ <button onclick="copyCurrentCode()" class="flex items-center gap-1.5 px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-white rounded text-xs font-medium transition-colors">
160
+ <span class="material-symbols-outlined text-sm">content_copy</span> Copy
161
+ </button>
162
+ </div>
163
+ <div class="p-4 overflow-x-auto">
164
+ <pre class="m-0"><code id="codeBlock">Loading...</code></pre>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ </section>
170
+ </div>
171
+ </main>
172
+ <!-- Footer -->
173
+ <footer class="mt-auto border-t border-card-border bg-white dark:bg-[#1a1a1a] py-6">
174
+ <div class="container mx-auto px-4 text-center">
175
+ <p class="text-sm text-slate-500">&copy; 2025 Codex Proxy. All rights reserved.</p>
176
+ </div>
177
+ </footer>
178
+
179
+ <script>
180
+ // ── State ──────────────────────────────────────────────────────
181
+ let authData = null;
182
+ let serverBaseUrl = '';
183
+ let serverApiKey = '';
184
+ let currentProtocol = 'openai';
185
+ let currentLang = 'python';
186
+ let addPollTimer = null;
187
+
188
+ const avatarColors = [
189
+ ['bg-purple-100', 'text-purple-600'],
190
+ ['bg-amber-100', 'text-amber-600'],
191
+ ['bg-blue-100', 'text-blue-600'],
192
+ ['bg-emerald-100', 'text-emerald-600'],
193
+ ['bg-red-100', 'text-red-600'],
194
+ ];
195
+
196
+ // ── Helpers ────────────────────────────────────────────────────
197
+ function escapeHtml(str) {
198
+ const div = document.createElement('div');
199
+ div.textContent = str;
200
+ return div.innerHTML;
201
+ }
202
+
203
+ function formatNumber(n) {
204
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
205
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
206
+ return String(n);
207
+ }
208
+
209
+ function statusBadge(status) {
210
+ const map = {
211
+ active: ['bg-green-100 text-green-700 border-green-200', 'Active'],
212
+ expired: ['bg-red-100 text-red-600 border-red-200', 'Expired'],
213
+ rate_limited: ['bg-amber-100 text-amber-700 border-amber-200', 'Rate Limited'],
214
+ refreshing: ['bg-blue-100 text-blue-600 border-blue-200', 'Refreshing'],
215
+ disabled: ['bg-slate-100 text-slate-500 border-slate-200', 'Disabled'],
216
+ };
217
+ const [cls, label] = map[status] || map.disabled;
218
+ return `<span class="px-2.5 py-1 rounded-full ${cls} text-xs font-medium border">${escapeHtml(label)}</span>`;
219
+ }
220
+
221
+ // ── OAuth message listener ─────────────────────────────────────
222
+ window.addEventListener('message', async (event) => {
223
+ if (event.data?.type === 'oauth-callback-success') {
224
+ if (addPollTimer) clearInterval(addPollTimer);
225
+ document.getElementById('addSection').classList.add('hidden');
226
+ showAddInfo('Account added successfully!');
227
+ await loadAccounts();
228
+ await loadStatus();
229
+ }
230
+ });
231
+
232
+ // ── Accounts ───────────────────────────────────────────────────
233
+ async function loadAccounts() {
234
+ try {
235
+ const resp = await fetch('/auth/accounts?quota=true');
236
+ const data = await resp.json();
237
+ renderAccounts(data.accounts || []);
238
+ } catch (err) {
239
+ document.getElementById('accountList').innerHTML =
240
+ `<div class="md:col-span-2 text-center py-8 text-red-400 text-sm bg-white dark:bg-[#1e1e1e] border border-card-border rounded-xl">Failed to load accounts: ${escapeHtml(err.message)}</div>`;
241
+ }
242
+ }
243
+
244
+ function renderAccounts(accounts) {
245
+ const container = document.getElementById('accountList');
246
+
247
+ if (accounts.length === 0) {
248
+ container.innerHTML = `<div class="md:col-span-2 text-center py-8 text-slate-400 text-sm bg-white dark:bg-[#1e1e1e] border border-card-border rounded-xl">
249
+ No accounts connected. Click "Add Account" to get started.
250
+ </div>`;
251
+ return;
252
+ }
253
+
254
+ container.innerHTML = accounts.map((acct, i) => {
255
+ const usage = acct.usage || {};
256
+ const email = acct.email || 'Unknown';
257
+ const initial = email.charAt(0).toUpperCase();
258
+ const [bgColor, textColor] = avatarColors[i % avatarColors.length];
259
+ const requests = usage.request_count ?? 0;
260
+ const tokens = (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
261
+ const plan = acct.planType || 'Free Tier';
262
+
263
+ // Quota bar
264
+ let quotaHtml = '';
265
+ const q = acct.quota;
266
+ if (q && q.rate_limit) {
267
+ const rl = q.rate_limit;
268
+ const pct = rl.used_percent != null ? Math.round(rl.used_percent) : null;
269
+ const barColor = pct == null ? 'bg-primary'
270
+ : pct >= 90 ? 'bg-red-500'
271
+ : pct >= 60 ? 'bg-amber-500'
272
+ : 'bg-primary';
273
+ const pctColor = pct == null ? 'text-primary'
274
+ : pct >= 90 ? 'text-red-500'
275
+ : pct >= 60 ? 'text-amber-600'
276
+ : 'text-primary';
277
+ const resetAt = rl.reset_at
278
+ ? new Date(rl.reset_at * 1000).toLocaleTimeString()
279
+ : null;
280
+
281
+ quotaHtml = `
282
+ <div class="pt-3 mt-3 border-t border-slate-100">
283
+ <div class="flex justify-between text-sm mb-1.5">
284
+ <span class="text-slate-500">Rate Limit</span>
285
+ ${rl.limit_reached
286
+ ? '<span class="px-2 py-0.5 rounded-full bg-red-100 text-red-600 text-xs font-medium">Limit Reached</span>'
287
+ : pct != null
288
+ ? `<span class="font-medium ${pctColor}">${pct}% Used</span>`
289
+ : '<span class="font-medium text-primary">OK</span>'}
290
+ </div>
291
+ ${pct != null ? `
292
+ <div class="w-full bg-slate-100 rounded-full h-2 overflow-hidden">
293
+ <div class="${barColor} h-2 rounded-full transition-all" style="width: ${pct}%"></div>
294
+ </div>` : ''}
295
+ ${resetAt ? `<p class="text-xs text-slate-400 mt-1">Resets at ${escapeHtml(resetAt)}</p>` : ''}
296
+ </div>`;
297
+ }
298
+
299
+ return `
300
+ <div class="group bg-white dark:bg-[#1e1e1e] border border-card-border rounded-xl p-5 shadow-sm hover:shadow-md transition-all hover:border-primary/30 relative">
301
+ <button onclick="deleteAccount('${escapeHtml(acct.id)}')" class="absolute top-3 right-3 opacity-0 group-hover:opacity-100 p-1.5 text-slate-300 hover:text-red-500 transition-all rounded-md hover:bg-red-50" title="Delete account">
302
+ <span class="material-symbols-outlined text-lg">delete</span>
303
+ </button>
304
+ <div class="flex justify-between items-start mb-4">
305
+ <div class="flex items-center gap-3">
306
+ <div class="size-10 rounded-full ${bgColor} ${textColor} flex items-center justify-center font-bold text-lg">${initial}</div>
307
+ <div>
308
+ <h3 class="font-semibold text-slate-900 dark:text-white leading-tight">${escapeHtml(email)}</h3>
309
+ <p class="text-xs text-slate-500">${escapeHtml(plan)}</p>
310
+ </div>
311
+ </div>
312
+ ${statusBadge(acct.status)}
313
+ </div>
314
+ <div class="space-y-2">
315
+ <div class="flex justify-between text-sm">
316
+ <span class="text-slate-500">Total Requests</span>
317
+ <span class="font-medium text-slate-900 dark:text-white">${formatNumber(requests)}</span>
318
+ </div>
319
+ <div class="flex justify-between text-sm">
320
+ <span class="text-slate-500">Tokens Used</span>
321
+ <span class="font-medium text-slate-900 dark:text-white">${formatNumber(tokens)}</span>
322
+ </div>
323
+ </div>
324
+ ${quotaHtml}
325
+ </div>`;
326
+ }).join('');
327
+ }
328
+
329
+ async function deleteAccount(id) {
330
+ if (!confirm('Remove this account?')) return;
331
+ try {
332
+ const resp = await fetch('/auth/accounts/' + encodeURIComponent(id), { method: 'DELETE' });
333
+ if (!resp.ok) {
334
+ const data = await resp.json();
335
+ alert(data.error || 'Failed to delete account.');
336
+ return;
337
+ }
338
+ await loadAccounts();
339
+ await loadStatus();
340
+ } catch (err) {
341
+ alert('Network error: ' + err.message);
342
+ }
343
+ }
344
+
345
+ // ── Add Account ────────────────────────────────────────────────
346
+ function showAddInfo(msg) {
347
+ const el = document.getElementById('addInfo');
348
+ el.textContent = msg;
349
+ el.classList.remove('hidden');
350
+ document.getElementById('addError').classList.add('hidden');
351
+ }
352
+
353
+ function showAddError(msg) {
354
+ const el = document.getElementById('addError');
355
+ el.textContent = msg;
356
+ el.classList.remove('hidden');
357
+ document.getElementById('addInfo').classList.add('hidden');
358
+ }
359
+
360
+ async function startAddAccount() {
361
+ const btn = document.getElementById('addAccountBtn');
362
+ const infoEl = document.getElementById('addInfo');
363
+ const errEl = document.getElementById('addError');
364
+ infoEl.classList.add('hidden');
365
+ errEl.classList.add('hidden');
366
+ btn.disabled = true;
367
+
368
+ try {
369
+ const resp = await fetch('/auth/login-start', { method: 'POST' });
370
+ const data = await resp.json();
371
+
372
+ if (!resp.ok || !data.authUrl) {
373
+ throw new Error(data.error || 'Failed to start login');
374
+ }
375
+
376
+ window.open(data.authUrl, 'oauth_add', 'width=600,height=700,scrollbars=yes');
377
+ document.getElementById('addSection').classList.remove('hidden');
378
+ btn.disabled = false;
379
+
380
+ // Poll for new account
381
+ if (addPollTimer) clearInterval(addPollTimer);
382
+ const prevCount = (await fetch('/auth/accounts').then(r => r.json())).accounts?.length || 0;
383
+ addPollTimer = setInterval(async () => {
384
+ try {
385
+ const r = await fetch('/auth/accounts');
386
+ const d = await r.json();
387
+ if ((d.accounts?.length || 0) > prevCount) {
388
+ clearInterval(addPollTimer);
389
+ document.getElementById('addSection').classList.add('hidden');
390
+ showAddInfo('Account added successfully!');
391
+ await loadAccounts();
392
+ await loadStatus();
393
+ }
394
+ } catch {}
395
+ }, 2000);
396
+ setTimeout(() => { if (addPollTimer) clearInterval(addPollTimer); }, 5 * 60 * 1000);
397
+
398
+ } catch (err) {
399
+ btn.disabled = false;
400
+ showAddError(err.message);
401
+ }
402
+ }
403
+
404
+ async function submitAddRelay() {
405
+ const callbackUrl = document.getElementById('addCallbackInput').value.trim();
406
+ const infoEl = document.getElementById('addInfo');
407
+ const errEl = document.getElementById('addError');
408
+ infoEl.classList.add('hidden');
409
+ errEl.classList.add('hidden');
410
+
411
+ if (!callbackUrl) {
412
+ showAddError('Please paste the callback URL');
413
+ return;
414
+ }
415
+
416
+ const btn = document.getElementById('addRelayBtn');
417
+ btn.disabled = true;
418
+ btn.textContent = 'Submitting...';
419
+
420
+ try {
421
+ const resp = await fetch('/auth/code-relay', {
422
+ method: 'POST',
423
+ headers: { 'Content-Type': 'application/json' },
424
+ body: JSON.stringify({ callbackUrl }),
425
+ });
426
+ const data = await resp.json();
427
+
428
+ if (resp.ok && data.success) {
429
+ if (addPollTimer) clearInterval(addPollTimer);
430
+ document.getElementById('addSection').classList.add('hidden');
431
+ document.getElementById('addCallbackInput').value = '';
432
+ showAddInfo('Account added successfully!');
433
+ await loadAccounts();
434
+ await loadStatus();
435
+ } else {
436
+ showAddError(data.error || 'Failed to exchange code');
437
+ }
438
+ } catch (err) {
439
+ showAddError('Network error: ' + err.message);
440
+ } finally {
441
+ btn.textContent = 'Submit';
442
+ btn.disabled = false;
443
+ }
444
+ }
445
+
446
+ // ── Status & Config ────────────────────────────────────────────
447
+ async function loadStatus() {
448
+ try {
449
+ const resp = await fetch('/auth/status');
450
+ authData = await resp.json();
451
+
452
+ if (!authData.authenticated) {
453
+ window.location.href = '/';
454
+ return;
455
+ }
456
+
457
+ serverBaseUrl = `${window.location.origin}/v1`;
458
+ serverApiKey = authData.proxy_api_key || 'any-string';
459
+ document.getElementById('baseUrl').value = serverBaseUrl;
460
+ document.getElementById('apiKey').value = serverApiKey;
461
+
462
+ await populateModelDropdown();
463
+ restoreOverrides();
464
+ updateCodeExamples();
465
+ } catch (err) {
466
+ console.error('Status load error:', err);
467
+ }
468
+ }
469
+
470
+ async function populateModelDropdown() {
471
+ const sel = document.getElementById('defaultModel');
472
+ try {
473
+ const resp = await fetch('/v1/models');
474
+ const data = await resp.json();
475
+ const models = data.data.map(m => m.id);
476
+ if (models.length > 0) {
477
+ sel.innerHTML = '';
478
+ models.forEach(id => {
479
+ const opt = document.createElement('option');
480
+ opt.value = id;
481
+ opt.textContent = id;
482
+ sel.appendChild(opt);
483
+ });
484
+ const preferred = models.find(n => n.includes('5.3-codex'));
485
+ if (preferred) sel.value = preferred;
486
+ }
487
+ } catch {
488
+ sel.innerHTML = '<option value="codex">codex</option>';
489
+ }
490
+ }
491
+
492
+ // ── Code Examples ──────────────────────────────────────────────
493
+ function updateCodeExamples() {
494
+ const baseUrl = document.getElementById('baseUrl').value;
495
+ const apiKey = document.getElementById('apiKey').value || 'any-string';
496
+ const model = document.getElementById('defaultModel').value;
497
+ const origin = window.location.origin;
498
+
499
+ window._codeExamples = {
500
+ 'openai-python': `from openai import OpenAI
501
+
502
+ client = OpenAI(
503
+ base_url="${baseUrl}",
504
+ api_key="${apiKey}",
505
+ )
506
+
507
+ response = client.chat.completions.create(
508
+ model="${model}",
509
+ messages=[{"role": "user", "content": "Hello"}],
510
+ )
511
+ print(response.choices[0].message.content)`,
512
+
513
+ 'openai-curl': `curl ${baseUrl}/chat/completions \\
514
+ -H "Content-Type: application/json" \\
515
+ -H "Authorization: Bearer ${apiKey}" \\
516
+ -d '{
517
+ "model": "${model}",
518
+ "messages": [{"role": "user", "content": "Hello"}]
519
+ }'`,
520
+
521
+ 'openai-node': `import OpenAI from "openai";
522
+
523
+ const client = new OpenAI({
524
+ baseURL: "${baseUrl}",
525
+ apiKey: "${apiKey}",
526
+ });
527
+
528
+ const stream = await client.chat.completions.create({
529
+ model: "${model}",
530
+ messages: [{ role: "user", content: "Hello" }],
531
+ stream: true,
532
+ });
533
+ for await (const chunk of stream) {
534
+ process.stdout.write(chunk.choices[0]?.delta?.content || "");
535
+ }`,
536
+
537
+ 'anthropic-python': `import anthropic
538
+
539
+ client = anthropic.Anthropic(
540
+ base_url="${origin}/v1",
541
+ api_key="${apiKey}",
542
+ )
543
+
544
+ message = client.messages.create(
545
+ model="claude-sonnet-4-20250514",
546
+ max_tokens=1024,
547
+ messages=[{"role": "user", "content": "Hello"}],
548
+ )
549
+ print(message.content[0].text)`,
550
+
551
+ 'anthropic-curl': `curl ${origin}/v1/messages \\
552
+ -H "Content-Type: application/json" \\
553
+ -H "x-api-key: ${apiKey}" \\
554
+ -H "anthropic-version: 2023-06-01" \\
555
+ -d '{
556
+ "model": "claude-sonnet-4-20250514",
557
+ "max_tokens": 1024,
558
+ "messages": [{"role": "user", "content": "Hello"}]
559
+ }'`,
560
+
561
+ 'anthropic-node': `import Anthropic from "@anthropic-ai/sdk";
562
+
563
+ const client = new Anthropic({
564
+ baseURL: "${origin}/v1",
565
+ apiKey: "${apiKey}",
566
+ });
567
+
568
+ const message = await client.messages.create({
569
+ model: "claude-sonnet-4-20250514",
570
+ max_tokens: 1024,
571
+ messages: [{ role: "user", content: "Hello" }],
572
+ });
573
+ console.log(message.content[0].text);`,
574
+
575
+ 'gemini-python': `from google import genai
576
+
577
+ client = genai.Client(
578
+ api_key="${apiKey}",
579
+ http_options={"base_url": "${origin}/v1beta"},
580
+ )
581
+
582
+ response = client.models.generate_content(
583
+ model="gemini-2.5-pro",
584
+ contents="Hello",
585
+ )
586
+ print(response.text)`,
587
+
588
+ 'gemini-curl': `curl "${origin}/v1beta/models/gemini-2.5-pro:generateContent?key=${apiKey}" \\
589
+ -H "Content-Type: application/json" \\
590
+ -d '{
591
+ "contents": [{"role": "user", "parts": [{"text": "Hello"}]}]
592
+ }'`,
593
+
594
+ 'gemini-node': `import { GoogleGenAI } from "@google/genai";
595
+
596
+ const ai = new GoogleGenAI({
597
+ apiKey: "${apiKey}",
598
+ httpOptions: { baseUrl: "${origin}/v1beta" },
599
+ });
600
+
601
+ const response = await ai.models.generateContent({
602
+ model: "gemini-2.5-pro",
603
+ contents: "Hello",
604
+ });
605
+ console.log(response.text);`,
606
+ };
607
+
608
+ showCurrentCode();
609
+ }
610
+
611
+ function showCurrentCode() {
612
+ const key = `${currentProtocol}-${currentLang}`;
613
+ const code = (window._codeExamples && window._codeExamples[key]) || 'Loading...';
614
+ document.getElementById('codeBlock').textContent = code;
615
+ }
616
+
617
+ // ── Tab Switching ──────────────────────────────────────────────
618
+ function switchProtocol(proto, el) {
619
+ currentProtocol = proto;
620
+ document.querySelectorAll('.proto-tab').forEach(t => {
621
+ t.className = 'proto-tab px-6 py-3 text-sm font-medium text-slate-500 hover:text-slate-700 hover:bg-slate-50 border-b-2 border-transparent transition-colors';
622
+ });
623
+ el.className = 'proto-tab px-6 py-3 text-sm font-semibold text-primary border-b-2 border-primary bg-white transition-colors';
624
+ showCurrentCode();
625
+ }
626
+
627
+ function switchLang(lang, el) {
628
+ currentLang = lang;
629
+ document.querySelectorAll('.lang-tab').forEach(t => {
630
+ t.className = 'lang-tab px-3 py-1.5 text-xs font-medium rounded text-slate-500 hover:text-slate-700 hover:bg-white/50 transition-all';
631
+ });
632
+ el.className = 'lang-tab px-3 py-1.5 text-xs font-semibold rounded bg-white text-slate-800 shadow-sm transition-all';
633
+ showCurrentCode();
634
+ }
635
+
636
+ // ── Copy ───────────────────────────────────────────────────────
637
+ function copyField(id, btn) {
638
+ const el = document.getElementById(id);
639
+ const text = el.value !== undefined ? el.value : el.textContent;
640
+ navigator.clipboard.writeText(text);
641
+ const icon = btn.querySelector('.material-symbols-outlined');
642
+ if (icon) {
643
+ icon.textContent = 'check';
644
+ btn.classList.add('text-primary');
645
+ setTimeout(() => { icon.textContent = 'content_copy'; btn.classList.remove('text-primary'); }, 2000);
646
+ }
647
+ }
648
+
649
+ function copyCurrentCode() {
650
+ const key = `${currentProtocol}-${currentLang}`;
651
+ const code = window._codeExamples?.[key] || '';
652
+ navigator.clipboard.writeText(code);
653
+ }
654
+
655
+ // ── Config Overrides (localStorage) ────────────────────────────
656
+ function saveOverrides() {
657
+ try {
658
+ localStorage.setItem('codex-proxy-config-overrides', JSON.stringify({
659
+ baseUrl: document.getElementById('baseUrl').value,
660
+ apiKey: document.getElementById('apiKey').value,
661
+ model: document.getElementById('defaultModel').value,
662
+ }));
663
+ } catch {}
664
+ }
665
+
666
+ function restoreOverrides() {
667
+ try {
668
+ const raw = localStorage.getItem('codex-proxy-config-overrides');
669
+ if (!raw) return;
670
+ const o = JSON.parse(raw);
671
+ if (o.baseUrl) document.getElementById('baseUrl').value = o.baseUrl;
672
+ if (o.apiKey) document.getElementById('apiKey').value = o.apiKey;
673
+ if (o.model) {
674
+ const sel = document.getElementById('defaultModel');
675
+ const opts = Array.from(sel.options).map(op => op.value);
676
+ if (opts.includes(o.model)) sel.value = o.model;
677
+ }
678
+ } catch {}
679
+ }
680
+
681
+ function resetConfigDefaults() {
682
+ try { localStorage.removeItem('codex-proxy-config-overrides'); } catch {}
683
+ document.getElementById('baseUrl').value = serverBaseUrl;
684
+ document.getElementById('apiKey').value = serverApiKey;
685
+ const sel = document.getElementById('defaultModel');
686
+ const preferred = Array.from(sel.options).find(o => o.value.includes('5.3-codex'));
687
+ if (preferred) sel.value = preferred.value;
688
+ else if (sel.options.length) sel.selectedIndex = 0;
689
+ updateCodeExamples();
690
+ }
691
+
692
+ // ── Reactive bindings ──────────────────────────────────────────
693
+ function setupReactiveBindings() {
694
+ document.getElementById('baseUrl').addEventListener('input', () => { updateCodeExamples(); saveOverrides(); });
695
+ document.getElementById('apiKey').addEventListener('input', () => { updateCodeExamples(); saveOverrides(); });
696
+ document.getElementById('defaultModel').addEventListener('change', () => { updateCodeExamples(); saveOverrides(); });
697
+ }
698
+
699
+ // ── Init ───────────────────────────────────────────────────────
700
+ loadStatus();
701
+ setupReactiveBindings();
702
+ loadAccounts();
703
+ </script>
704
+ </body></html>
src/routes/web.ts CHANGED
@@ -13,7 +13,7 @@ export function createWebRoutes(accountPool: AccountPool): Hono {
13
  app.get("/", (c) => {
14
  try {
15
  if (accountPool.isAuthenticated()) {
16
- const html = readFileSync(resolve(publicDir, "dashboard.html"), "utf-8");
17
  return c.html(html);
18
  }
19
  const html = readFileSync(resolve(publicDir, "login.html"), "utf-8");
 
13
  app.get("/", (c) => {
14
  try {
15
  if (accountPool.isAuthenticated()) {
16
+ const html = readFileSync(resolve(publicDir, "dashboard1.html"), "utf-8");
17
  return c.html(html);
18
  }
19
  const html = readFileSync(resolve(publicDir, "login.html"), "utf-8");