@pikoloo/codex-proxy 1.0.6 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/index.html CHANGED
@@ -57,13 +57,7 @@
57
57
 
58
58
  <div class="app-header h-14 border-b border-space-border flex items-center px-4 lg:px-6 justify-between bg-space-900/50 backdrop-blur-md z-50 relative">
59
59
  <div class="app-brand flex items-center gap-3">
60
- <button @click="sidebarOpen = !sidebarOpen" class="text-gray-400 hover:text-white focus:outline-none p-1 transition-colors lg:hidden">
61
- <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
62
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
63
- </svg>
64
- </button>
65
-
66
- <div class="w-8 h-8 rounded bg-gradient-to-br from-neon-purple to-blue-600 flex items-center justify-center text-white font-bold shadow-[0_0_15px_rgba(168,85,247,0.4)]">
60
+ <div class="app-brand-mark w-8 h-8 rounded bg-gradient-to-br from-neon-purple to-blue-600 flex items-center justify-center text-white font-bold shadow-[0_0_15px_rgba(168,85,247,0.4)]">
67
61
  CX
68
62
  </div>
69
63
  <div class="app-brand-copy flex items-center gap-2">
@@ -73,7 +67,7 @@
73
67
  </div>
74
68
 
75
69
  <div class="app-toolbar flex items-center gap-4">
76
- <div class="flex items-center gap-2 px-3 py-1 rounded-full text-xs font-mono border transition-all duration-300"
70
+ <div class="app-connection-status flex items-center gap-2 px-3 py-1 rounded-full text-xs font-mono border transition-all duration-300"
77
71
  :class="connectionStatus === 'connected'
78
72
  ? 'bg-neon-green/10 border-neon-green/20 text-neon-green'
79
73
  : 'bg-red-500/10 border-red-500/20 text-red-500'">
@@ -83,89 +77,36 @@
83
77
  <span x-text="connectionStatus === 'connected' ? 'Online' : 'Offline'"></span>
84
78
  </div>
85
79
 
86
- <div class="h-4 w-px bg-space-border"></div>
80
+ <div class="app-toolbar-divider h-4 w-px bg-space-border"></div>
87
81
 
88
82
  <a href="https://github.com/surajmandalcell/codex-proxy" target="_blank"
89
- class="app-toolbar-github btn btn-ghost btn-xs btn-square text-gray-400 hover:text-white hover:bg-white/5"
83
+ class="app-header-action app-toolbar-github btn btn-ghost btn-xs btn-square text-gray-400 hover:text-white hover:bg-white/5"
90
84
  title="Open GitHub repository" aria-label="Open GitHub repository">
91
85
  <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
92
86
  <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
93
87
  </svg>
94
88
  </a>
95
89
 
96
- <button type="button" class="btn btn-ghost btn-xs btn-square text-gray-400 hover:text-white hover:bg-white/5"
97
- @click="refreshAccounts()" :disabled="loading" title="Refresh data">
98
- <svg class="w-4 h-4" :class="{'animate-spin': loading}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
90
+ <button type="button" class="app-header-action btn btn-ghost btn-xs btn-square text-gray-400 hover:text-white hover:bg-white/5"
91
+ @click="refreshCurrentView()" :disabled="loading || metricsLoading" title="Refresh data">
92
+ <svg class="w-4 h-4" :class="{'animate-spin': loading || metricsLoading}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
99
93
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
100
94
  </svg>
101
95
  </button>
102
96
  </div>
103
97
  </div>
104
98
 
105
- <div class="flex h-[calc(100vh-56px)] relative">
106
- <div x-show="sidebarOpen" x-transition:enter="transition-opacity ease-linear duration-300"
107
- x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
108
- x-transition:leave="transition-opacity ease-linear duration-300"
109
- x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
110
- @click="sidebarOpen = false" class="fixed inset-0 bg-black/50 z-40 lg:hidden" style="display: none;"></div>
111
-
112
- <div class="fixed top-14 bottom-0 left-0 z-40 w-64 transform bg-space-900 border-r border-space-border transition-all duration-300 shadow-2xl overflow-hidden lg:static lg:h-auto lg:shadow-none lg:flex-shrink-0 lg:translate-x-0"
113
- :class="{
114
- 'translate-x-0': sidebarOpen,
115
- '-translate-x-full': !sidebarOpen
116
- }">
117
- <div class="w-64 flex flex-col h-full pt-6 pb-4 flex-shrink-0">
118
- <div class="px-4 mb-2 text-xs font-bold text-gray-600 uppercase tracking-widest hidden lg:block">Main</div>
119
-
120
- <nav class="flex flex-col gap-1">
121
- <button class="nav-item flex items-center gap-3 px-6 py-3 text-sm font-medium text-gray-400 hover:text-white hover:bg-white/5"
122
- :class="{'active': activeTab === 'dashboard'}" @click="setActiveTab('dashboard')">
123
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
124
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
125
- </svg>
126
- <span>Dashboard</span>
127
- </button>
128
- <button class="nav-item flex items-center gap-3 px-6 py-3 text-sm font-medium text-gray-400 hover:text-white hover:bg-white/5"
129
- :class="{'active': activeTab === 'accounts'}" @click="setActiveTab('accounts')">
130
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
131
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
132
- </svg>
133
- <span>Accounts</span>
134
- </button>
135
- </nav>
136
-
137
- <div class="px-4 mt-8 mb-2 text-xs font-bold text-gray-600 uppercase tracking-widest">System</div>
138
- <nav class="flex flex-col gap-1">
139
- <button class="nav-item flex items-center gap-3 px-6 py-3 text-sm font-medium text-gray-400 hover:text-white hover:bg-white/5"
140
- :class="{'active': activeTab === 'logs'}" @click="setActiveTab('logs')">
141
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
142
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
143
- </svg>
144
- <span>Logs</span>
145
- </button>
146
- <button class="nav-item flex items-center gap-3 px-6 py-3 text-sm font-medium text-gray-400 hover:text-white hover:bg-white/5"
147
- :class="{'active': activeTab === 'settings'}" @click="setActiveTab('settings')">
148
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
149
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
150
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
151
- </svg>
152
- <span>Settings</span>
153
- </button>
154
- </nav>
155
-
156
- </div>
157
- </div>
158
-
159
- <div class="flex-1 overflow-auto bg-space-950 relative custom-scrollbar">
99
+ <div class="app-shell">
100
+ <main class="app-main flex-1 overflow-auto bg-space-950 relative custom-scrollbar">
160
101
  <div x-show="activeTab === 'dashboard'" x-transition class="view-container">
161
- <div class="flex items-center justify-between gap-4 mb-6">
162
- <div class="flex flex-wrap items-center gap-4">
102
+ <div class="section-header flex items-center justify-between gap-4 mb-6">
103
+ <div class="section-heading flex flex-wrap items-center gap-4">
163
104
  <h1 class="text-2xl font-bold text-white tracking-tight">Dashboard</h1>
164
105
  <div class="flex items-center h-6 px-3 rounded-full bg-space-800/80 border border-space-border/50 shadow-sm backdrop-blur-sm">
165
106
  <span class="text-[10px] font-mono text-gray-400 uppercase tracking-wider">CHATGPT PROXY SYSTEM</span>
166
107
  </div>
167
108
  </div>
168
- <div class="flex items-center gap-2 px-2.5 py-1.5 rounded-lg bg-space-900/60 border border-space-border/40 whitespace-nowrap flex-shrink-0">
109
+ <div class="section-actions flex items-center gap-2 px-2.5 py-1.5 rounded-lg bg-space-900/60 border border-space-border/40 whitespace-nowrap flex-shrink-0">
169
110
  <div class="relative flex items-center justify-center">
170
111
  <span class="absolute w-1.5 h-1.5 bg-neon-green rounded-full animate-ping opacity-75"></span>
171
112
  <span class="relative w-1.5 h-1.5 bg-neon-green rounded-full"></span>
@@ -242,6 +183,12 @@
242
183
  <span>Test</span>
243
184
  </button>
244
185
  </div>
186
+ <div class="quick-test-meta" data-quick-test-status>
187
+ <span class="quick-test-dot" :class="'is-' + testStatus"></span>
188
+ <span x-text="testStatusText"></span>
189
+ <span x-show="testMeta?.durationMs" x-text="formatDuration(testMeta?.durationMs)"></span>
190
+ <span x-show="formatUsageSummary(testMeta?.usage)" x-text="formatUsageSummary(testMeta?.usage)"></span>
191
+ </div>
245
192
  <div x-show="testResponse" class="mt-4 p-3 rounded-lg bg-space-800/50 border border-space-border/30">
246
193
  <pre class="text-xs font-mono text-gray-300 whitespace-pre-wrap" x-text="testResponse"></pre>
247
194
  </div>
@@ -269,6 +216,12 @@
269
216
  <span>Test Haiku</span>
270
217
  </button>
271
218
  </div>
219
+ <div class="quick-test-meta" data-haiku-test-status>
220
+ <span class="quick-test-dot" :class="'is-' + haikuTestStatus"></span>
221
+ <span x-text="haikuTestStatusText"></span>
222
+ <span x-show="haikuTestMeta?.durationMs" x-text="formatDuration(haikuTestMeta?.durationMs)"></span>
223
+ <span x-show="formatUsageSummary(haikuTestMeta?.usage)" x-text="formatUsageSummary(haikuTestMeta?.usage)"></span>
224
+ </div>
272
225
  <div x-show="haikuTestResponse" class="mt-4 p-3 rounded-lg bg-space-800/50 border border-space-border/30">
273
226
  <pre class="text-xs font-mono text-gray-300 whitespace-pre-wrap" x-text="haikuTestResponse"></pre>
274
227
  </div>
@@ -285,32 +238,240 @@
285
238
  </div>
286
239
  <div class="bg-space-800/50 border border-space-border/30 rounded-lg p-4 font-mono text-sm text-gray-300">
287
240
  <div class="text-gray-500 mb-2"># Set environment variables</div>
288
- <div class="text-neon-cyan">export ANTHROPIC_BASE_URL=http://localhost:8081</div>
241
+ <div class="text-neon-cyan" x-text="'export ANTHROPIC_BASE_URL=' + serverUrl">export ANTHROPIC_BASE_URL=http://localhost:8081</div>
289
242
  <div class="text-neon-cyan">export ANTHROPIC_API_KEY=any-key</div>
290
243
  <div class="text-gray-500 mt-2 mb-2"># Run Claude</div>
291
244
  <div class="text-neon-green">claude</div>
292
245
  </div>
293
246
 
294
- <div class="mt-4 flex justify-end">
247
+ <div class="mt-4 flex flex-wrap items-center justify-between gap-3">
248
+ <label class="flex items-center gap-3 rounded-lg border border-space-border/30 bg-space-800/40 px-3 py-2">
249
+ <input type="checkbox"
250
+ class="checkbox checkbox-sm rounded-[3px]"
251
+ x-model="configureClaudeOnStartup"
252
+ @change="setConfigureClaudeOnStartup($event.target.checked)"
253
+ :disabled="claudeProxyStartupSaving">
254
+ <span class="text-sm text-gray-300">Configure on startup</span>
255
+ </label>
295
256
  <button class="btn btn-sm bg-neon-purple hover:bg-purple-600 border-none text-white font-sans"
296
- @click="setClaudeCodeProxyTestConfig()">
297
- Update Claude Code settings.json (localhost:8081 + key=test)
257
+ @click="configureClaudeProxy()"
258
+ :disabled="claudeProxyConfiguring">
259
+ <svg class="w-4 h-4" :class="{'animate-spin': claudeProxyConfiguring}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
260
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7h16M4 12h10M4 17h7m8-4l2 2 4-4"></path>
261
+ </svg>
262
+ <span x-text="claudeProxyConfiguring ? 'Configuring...' : 'Configure Claude Code'">Configure Claude Code</span>
298
263
  </button>
299
264
  </div>
300
265
  </div>
301
266
  </div>
302
267
 
268
+ <div x-show="activeTab === 'metrics'" x-transition class="view-container">
269
+ <div class="section-header flex items-center justify-between gap-4 mb-6">
270
+ <div class="section-heading flex flex-wrap items-center gap-4">
271
+ <h1 class="text-2xl font-bold text-white tracking-tight">Token Usage</h1>
272
+ <div class="flex items-center h-6 px-3 rounded-full bg-space-800/80 border border-space-border/50 shadow-sm backdrop-blur-sm">
273
+ <span class="text-[10px] font-mono text-gray-400 uppercase tracking-wider">Request metrics</span>
274
+ </div>
275
+ </div>
276
+ <div class="section-actions metrics-range-controls">
277
+ <button class="metrics-range-button" :class="{'active': metricsRange === '24h'}" @click="setMetricsRange('24h')">24h</button>
278
+ <button class="metrics-range-button" :class="{'active': metricsRange === '7d'}" @click="setMetricsRange('7d')">7d</button>
279
+ <button class="metrics-range-button" :class="{'active': metricsRange === '30d'}" @click="setMetricsRange('30d')">30d</button>
280
+ <button class="metrics-range-button" :class="{'active': metricsRange === 'all'}" @click="setMetricsRange('all')">All</button>
281
+ </div>
282
+ </div>
283
+
284
+ <div x-show="metricsError" class="alert alert-error mb-4">
285
+ <span x-text="metricsError"></span>
286
+ </div>
287
+
288
+ <div x-show="metricsStorage && metricsStorage.overLimit" class="metrics-storage-warning mb-4">
289
+ <div>
290
+ <div class="text-sm font-semibold text-red-300">Metrics database is above the configured cap</div>
291
+ <div class="text-xs text-red-200/80 font-mono">
292
+ <span x-text="formatBytes(metricsStorage?.sizeBytes)"></span>
293
+ <span>/</span>
294
+ <span x-text="formatBytes(metricsStorage?.maxBytes)"></span>
295
+ <span>after compact-only maintenance</span>
296
+ </div>
297
+ </div>
298
+ <button class="btn btn-xs btn-outline" @click="loadMetrics()" :disabled="metricsLoading">Refresh</button>
299
+ </div>
300
+
301
+ <div class="grid grid-cols-2 sm:grid-cols-4 gap-2 lg:gap-3 mb-6">
302
+ <div class="stat bg-space-900/40 border border-space-border/30 rounded-xl p-3 lg:p-4 min-w-0">
303
+ <div class="stat-value text-white font-mono text-2xl lg:text-3xl font-bold mb-1 truncate" x-text="formatTokenCount(metricsTotals.totalTokens)"></div>
304
+ <div class="stat-title text-gray-500 font-mono text-[10px] uppercase tracking-wider truncate">Total Tokens</div>
305
+ <div class="stat-desc text-cyan-400/60 text-[10px] truncate">Input + output</div>
306
+ </div>
307
+ <div class="stat bg-space-900/40 border border-space-border/30 rounded-xl p-3 lg:p-4 min-w-0">
308
+ <div class="stat-value text-white font-mono text-2xl lg:text-3xl font-bold mb-1 truncate" x-text="metricsTotals.requestCount"></div>
309
+ <div class="stat-title text-gray-500 font-mono text-[10px] uppercase tracking-wider truncate">Requests</div>
310
+ <div class="stat-desc text-green-400/60 text-[10px] truncate"><span x-text="metricsTotals.successCount"></span> successful</div>
311
+ </div>
312
+ <div class="stat bg-space-900/40 border border-space-border/30 rounded-xl p-3 lg:p-4 min-w-0">
313
+ <div class="stat-value text-white font-mono text-2xl lg:text-3xl font-bold mb-1 truncate" x-text="formatTokenCount(metricsTotals.outputTokens)"></div>
314
+ <div class="stat-title text-gray-500 font-mono text-[10px] uppercase tracking-wider truncate">Output Tokens</div>
315
+ <div class="stat-desc text-purple-400/60 text-[10px] truncate"><span x-text="formatTokenCount(metricsTotals.inputTokens)"></span> input</div>
316
+ </div>
317
+ <div class="stat bg-space-900/40 border border-space-border/30 rounded-xl p-3 lg:p-4 min-w-0">
318
+ <div class="stat-value text-white font-mono text-2xl lg:text-3xl font-bold mb-1 truncate" x-text="formatDuration(metricsTotals.averageDurationMs)"></div>
319
+ <div class="stat-title text-gray-500 font-mono text-[10px] uppercase tracking-wider truncate">Avg Latency</div>
320
+ <div class="stat-desc text-yellow-400/60 text-[10px] truncate"><span x-text="metricsTotals.errorCount"></span> errors</div>
321
+ </div>
322
+ </div>
323
+
324
+ <div class="metrics-grid mb-6">
325
+ <div class="view-card metrics-panel">
326
+ <div class="metrics-panel-header">
327
+ <h3>Token Trend</h3>
328
+ <span x-show="metricsLoading" class="metrics-muted">Loading</span>
329
+ </div>
330
+ <div class="metrics-timeline">
331
+ <template x-for="entry in metricsSummary.timeline" :key="entry.bucket">
332
+ <div class="metrics-timeline-row">
333
+ <span class="metrics-timeline-label" x-text="entry.bucket"></span>
334
+ <div class="metrics-bar-track">
335
+ <div class="metrics-bar-fill" :style="'width:' + metricBarWidth(entry.totalTokens, metricsTimelineMax) + '%'"></div>
336
+ </div>
337
+ <span class="metrics-value" x-text="formatTokenCount(entry.totalTokens)"></span>
338
+ </div>
339
+ </template>
340
+ <div x-show="metricsSummary.timeline.length === 0" class="metrics-empty">No usage recorded for this range</div>
341
+ </div>
342
+ </div>
343
+
344
+ <div class="view-card metrics-panel">
345
+ <div class="metrics-panel-header">
346
+ <h3>Storage</h3>
347
+ <span class="metrics-muted" x-text="formatBytes(metricsStorage?.sizeBytes)"></span>
348
+ </div>
349
+ <div class="space-y-3">
350
+ <div class="flex items-center justify-between py-2 border-b border-space-border/30">
351
+ <span class="text-sm text-gray-400">Database</span>
352
+ <span class="font-mono text-xs text-white truncate max-w-[260px]" x-text="metricsStorage?.dbPath || '-'"></span>
353
+ </div>
354
+ <div class="flex items-center justify-between py-2 border-b border-space-border/30">
355
+ <span class="text-sm text-gray-400">Cap</span>
356
+ <span class="font-mono text-sm text-white" x-text="formatBytes(metricsStorage?.maxBytes)"></span>
357
+ </div>
358
+ <div class="flex items-center justify-between py-2">
359
+ <span class="text-sm text-gray-400">Last compact</span>
360
+ <span class="font-mono text-xs text-white" x-text="formatMetricTime(metricsStorage?.lastCompactionAttemptAt)"></span>
361
+ </div>
362
+ </div>
363
+ </div>
364
+ </div>
365
+
366
+ <div class="metrics-grid mb-6">
367
+ <div class="view-card metrics-panel">
368
+ <div class="metrics-panel-header">
369
+ <h3>Models</h3>
370
+ <span class="metrics-muted" x-text="metricsSummary.byModel.length"></span>
371
+ </div>
372
+ <div class="metrics-list">
373
+ <template x-for="entry in metricsSummary.byModel" :key="entry.model">
374
+ <div class="metrics-breakdown-row">
375
+ <div class="min-w-0">
376
+ <div class="metrics-breakdown-title" x-text="entry.model"></div>
377
+ <div class="metrics-muted"><span x-text="entry.requestCount"></span> requests</div>
378
+ </div>
379
+ <div class="metrics-breakdown-meter">
380
+ <div class="metrics-bar-track">
381
+ <div class="metrics-bar-fill" :style="'width:' + metricBarWidth(entry.totalTokens, metricsModelMax) + '%'"></div>
382
+ </div>
383
+ <span class="metrics-value" x-text="formatTokenCount(entry.totalTokens)"></span>
384
+ </div>
385
+ </div>
386
+ </template>
387
+ <div x-show="metricsSummary.byModel.length === 0" class="metrics-empty">No model usage</div>
388
+ </div>
389
+ </div>
390
+
391
+ <div class="view-card metrics-panel">
392
+ <div class="metrics-panel-header">
393
+ <h3>Accounts</h3>
394
+ <span class="metrics-muted" x-text="metricsSummary.byAccount.length"></span>
395
+ </div>
396
+ <div class="metrics-list">
397
+ <template x-for="entry in metricsSummary.byAccount" :key="entry.accountLabel">
398
+ <div class="metrics-breakdown-row">
399
+ <div class="min-w-0">
400
+ <div class="metrics-breakdown-title" x-text="entry.accountLabel"></div>
401
+ <div class="metrics-muted"><span x-text="entry.requestCount"></span> requests</div>
402
+ </div>
403
+ <div class="metrics-breakdown-meter">
404
+ <div class="metrics-bar-track">
405
+ <div class="metrics-bar-fill" :style="'width:' + metricBarWidth(entry.totalTokens, metricsAccountMax) + '%'"></div>
406
+ </div>
407
+ <span class="metrics-value" x-text="formatTokenCount(entry.totalTokens)"></span>
408
+ </div>
409
+ </div>
410
+ </template>
411
+ <div x-show="metricsSummary.byAccount.length === 0" class="metrics-empty">No account usage</div>
412
+ </div>
413
+ </div>
414
+ </div>
415
+
416
+ <div class="view-card !p-0">
417
+ <div class="metrics-table-toolbar">
418
+ <h3>Recent Requests</h3>
419
+ <div class="metrics-range-controls">
420
+ <button class="metrics-range-button" :class="{'active': metricsStatusFilter === ''}" @click="setMetricsStatusFilter('')">All</button>
421
+ <button class="metrics-range-button" :class="{'active': metricsStatusFilter === 'success'}" @click="setMetricsStatusFilter('success')">Success</button>
422
+ <button class="metrics-range-button" :class="{'active': metricsStatusFilter === 'error'}" @click="setMetricsStatusFilter('error')">Error</button>
423
+ </div>
424
+ </div>
425
+ <div class="overflow-x-auto">
426
+ <table id="metrics-recent-events" class="w-full text-sm text-left">
427
+ <thead class="text-xs text-gray-500 uppercase bg-space-800/50 border-b border-space-border/30">
428
+ <tr>
429
+ <th class="px-4 py-3">Time</th>
430
+ <th class="px-4 py-3">Endpoint</th>
431
+ <th class="px-4 py-3">Model</th>
432
+ <th class="px-4 py-3">Account</th>
433
+ <th class="px-4 py-3 text-right">Tokens</th>
434
+ <th class="px-4 py-3">Status</th>
435
+ </tr>
436
+ </thead>
437
+ <tbody class="divide-y divide-space-border/30">
438
+ <template x-for="event in metricsRecent" :key="event.id">
439
+ <tr class="hover:bg-white/[0.03]">
440
+ <td class="px-4 py-3 font-mono text-xs text-gray-400" x-text="formatMetricTime(event.startedAt)"></td>
441
+ <td class="px-4 py-3 font-mono text-xs text-gray-300" x-text="event.endpoint"></td>
442
+ <td class="px-4 py-3 font-mono text-xs text-gray-300" x-text="event.upstreamModel"></td>
443
+ <td class="px-4 py-3 font-mono text-xs text-gray-400" x-text="event.accountLabel || '-'"></td>
444
+ <td class="px-4 py-3 font-mono text-xs text-right text-gray-300" x-text="formatTokenCount(event.totalTokens)"></td>
445
+ <td class="px-4 py-3">
446
+ <span class="metrics-status" :class="metricsStatusClass(event.status)" x-text="event.status"></span>
447
+ </td>
448
+ </tr>
449
+ </template>
450
+ <tr x-show="metricsRecent.length === 0">
451
+ <td colspan="6" class="px-4 py-10 text-center text-sm text-gray-500">No requests recorded</td>
452
+ </tr>
453
+ </tbody>
454
+ </table>
455
+ </div>
456
+ </div>
457
+ </div>
458
+
303
459
  <div x-show="activeTab === 'accounts'" x-transition class="view-container">
304
- <div class="flex items-center justify-between gap-4 mb-6">
305
- <div class="flex flex-wrap items-center gap-4">
306
- <h1 class="text-2xl font-bold text-white tracking-tight">Account Management</h1>
460
+ <div class="section-header flex items-center justify-between gap-4 mb-6">
461
+ <div class="section-heading flex flex-wrap items-center gap-4">
462
+ <h1 class="text-2xl font-bold text-white tracking-tight">Accounts</h1>
307
463
  <div class="flex items-center h-6 px-3 rounded-full bg-space-800/80 border border-space-border/50 shadow-sm backdrop-blur-sm">
308
- <span class="text-[10px] font-mono text-gray-400 uppercase tracking-wider">Manage ChatGPT accounts</span>
464
+ <span class="text-[10px] font-mono text-gray-400 uppercase tracking-wider">ChatGPT accounts</span>
465
+ </div>
466
+ <div class="account-count-pill flex items-center h-6 px-2 rounded bg-space-800/80 border border-space-border/50">
467
+ <span class="text-[11px] font-mono text-gray-400">
468
+ <span x-text="accounts.length"></span><span class="account-count-word" x-text="accounts.length === 1 ? ' account' : ' accounts'"></span>
469
+ </span>
309
470
  </div>
310
471
  </div>
311
472
 
312
- <div class="flex flex-wrap items-center justify-end gap-2 sm:flex-nowrap">
313
- <div class="relative" x-show="accounts.length > 0">
473
+ <div class="section-actions accounts-actions flex flex-wrap items-center justify-end gap-2 sm:flex-nowrap">
474
+ <div class="account-search relative" x-show="accounts.length > 0">
314
475
  <input type="text" x-model="searchQuery" placeholder="Search accounts..."
315
476
  class="input-search-sm w-48 pl-9 h-8" @keydown.escape="searchQuery = ''">
316
477
  <svg class="w-4 h-4 absolute left-3 top-2 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -319,23 +480,20 @@
319
480
  </div>
320
481
 
321
482
  <button class="btn btn-xs btn-outline border-space-border text-gray-400 hover:text-white transition-all gap-2 h-8"
322
- @click="refreshAllTokens()" x-show="accounts.length > 0">
483
+ @click="refreshAllTokens()" x-show="accounts.length > 0"
484
+ aria-label="Refresh all account tokens" title="Refresh all account tokens">
323
485
  <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
324
486
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path>
325
487
  </svg>
326
- <span>Refresh All</span>
488
+ <span class="action-label">Refresh All</span>
327
489
  </button>
328
490
 
329
- <div class="flex items-center h-6 px-2 rounded bg-space-800/80 border border-space-border/50 sm:ml-1">
330
- <span class="text-[11px] font-mono text-gray-400" x-text="accounts.length + ' accounts'"></span>
331
- </div>
332
-
333
491
  <button class="btn bg-neon-purple hover:bg-purple-600 border-none text-white btn-xs gap-2 shadow-lg shadow-neon-purple/20 h-8"
334
- @click="showAddModal = true">
492
+ @click="showAddModal = true" aria-label="Add account" title="Add account">
335
493
  <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
336
494
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
337
495
  </svg>
338
- <span>Add Account</span>
496
+ <span class="action-label">Add Account</span>
339
497
  </button>
340
498
  </div>
341
499
  </div>
@@ -433,7 +591,7 @@
433
591
  </template>
434
592
  </div>
435
593
  <template x-if="quotaResetSummary(acc)">
436
- <span class="text-[10px] font-mono text-gray-500" x-text="quotaResetSummary(acc)"></span>
594
+ <span class="quota-reset-summary text-[10px] font-mono text-gray-500" x-text="quotaResetSummary(acc)"></span>
437
595
  </template>
438
596
  </div>
439
597
  </template>
@@ -483,84 +641,68 @@
483
641
  </div>
484
642
  </div>
485
643
 
486
- <div x-show="activeTab === 'logs'" x-transition class="view-container h-full flex flex-col">
487
- <div class="flex items-center justify-between gap-4 mb-6">
488
- <div class="flex flex-wrap items-center gap-4">
644
+ <div x-show="activeTab === 'logs'" x-transition class="view-container logs-view h-full flex flex-col">
645
+ <div class="section-header flex items-center justify-between gap-4 mb-6">
646
+ <div class="section-heading flex flex-wrap items-center gap-4">
489
647
  <h1 class="text-2xl font-bold text-white tracking-tight">Server Logs</h1>
490
- <div class="flex items-center h-6 px-3 rounded-full bg-space-800/80 border border-space-border/50 shadow-sm backdrop-blur-sm">
491
- <span class="text-[10px] font-mono text-gray-400 uppercase tracking-wider">Real-time log stream</span>
648
+ <div class="log-stream-status" :class="'is-' + logStreamStatus">
649
+ <span class="log-stream-dot"></span>
650
+ <span x-text="logStreamStatusText()"></span>
492
651
  </div>
493
652
  </div>
494
- <div class="flex items-center gap-3">
495
- <div class="text-[10px] font-mono text-gray-600">
496
- <span x-text="logs.length"></span> entries
653
+ <div class="section-actions logs-count-strip">
654
+ <span><strong x-text="filteredLogs.length"></strong> shown</span>
655
+ <span><strong x-text="logs.length"></strong> total</span>
656
+ </div>
657
+ </div>
658
+
659
+ <div class="logs-shell view-card !p-0 flex flex-col min-h-0">
660
+ <div class="logs-toolbar">
661
+ <div class="logs-search">
662
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
663
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
664
+ </svg>
665
+ <input type="text" x-model="logSearchQuery" placeholder="Search logs, models, accounts..."
666
+ class="logs-search-input">
667
+ </div>
668
+
669
+ <div class="logs-filter-row">
670
+ <template x-for="level in ['INFO', 'SUCCESS', 'WARN', 'ERROR', 'DEBUG']" :key="level">
671
+ <label class="logs-filter-chip" :class="[logFilters[level] ? 'active' : '', 'is-' + level.toLowerCase()]">
672
+ <input type="checkbox" x-model="logFilters[level]">
673
+ <span x-text="level"></span>
674
+ <span class="logs-filter-count" x-text="logLevelCounts[level] || 0"></span>
675
+ </label>
676
+ </template>
497
677
  </div>
498
- <button class="btn btn-xs btn-ghost text-gray-400 hover:text-white" @click="clearLogs()" title="Clear logs">
678
+
679
+ <button class="btn btn-xs btn-ghost logs-clear-button" @click="clearLogs()" title="Clear logs">
499
680
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
500
681
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
501
682
  </svg>
683
+ <span>Clear</span>
502
684
  </button>
503
685
  </div>
504
- </div>
505
686
 
506
- <div class="view-card !p-0 flex flex-col flex-1 min-h-0">
507
- <div class="bg-space-900 flex items-center p-2 px-4 border-b border-space-border gap-4">
508
- <div class="flex gap-2">
509
- <div class="w-3 h-3 rounded-full bg-red-500/20 border border-red-500/50"></div>
510
- <div class="w-3 h-3 rounded-full bg-yellow-500/20 border border-yellow-500/50"></div>
511
- <div class="w-3 h-3 rounded-full bg-green-500/20 border border-green-500/50"></div>
687
+ <div id="logs-container" class="logs-grid">
688
+ <div class="logs-grid-head">
689
+ <span>Time</span>
690
+ <span>Level</span>
691
+ <span>Message</span>
692
+ <span>Details</span>
512
693
  </div>
513
- <span class="text-xs font-mono text-gray-500 hidden sm:inline-block">~/logs</span>
514
-
515
- <div class="flex-1 flex items-center justify-center gap-4">
516
- <div class="relative w-full max-w-xs">
517
- <input type="text" x-model="logSearchQuery" placeholder="Search logs..."
518
- class="w-full h-7 bg-space-950 border border-space-border rounded text-xs font-mono pl-7 pr-2 focus:border-neon-purple focus:outline-none transition-colors placeholder-gray-700 text-gray-300">
519
- <svg class="w-3 h-3 absolute left-2.5 top-2 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
520
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
521
- </svg>
522
- </div>
523
-
524
- <div class="hidden md:flex gap-3 text-[10px] font-mono font-bold uppercase select-none">
525
- <label class="flex items-center gap-1.5 cursor-pointer text-blue-400 opacity-50 hover:opacity-100 transition-opacity" :class="{'opacity-100': logFilters.INFO}">
526
- <input type="checkbox" class="checkbox checkbox-xs rounded-[2px] w-3 h-3" x-model="logFilters.INFO"> INFO
527
- </label>
528
- <label class="flex items-center gap-1.5 cursor-pointer text-green-400 opacity-50 hover:opacity-100 transition-opacity" :class="{'opacity-100': logFilters.SUCCESS}">
529
- <input type="checkbox" class="checkbox checkbox-xs rounded-[2px] w-3 h-3" x-model="logFilters.SUCCESS"> SUCCESS
530
- </label>
531
- <label class="flex items-center gap-1.5 cursor-pointer text-yellow-400 opacity-50 hover:opacity-100 transition-opacity" :class="{'opacity-100': logFilters.WARN}">
532
- <input type="checkbox" class="checkbox checkbox-xs rounded-[2px] w-3 h-3" x-model="logFilters.WARN"> WARN
533
- </label>
534
- <label class="flex items-center gap-1.5 cursor-pointer text-red-500 opacity-50 hover:opacity-100 transition-opacity" :class="{'opacity-100': logFilters.ERROR}">
535
- <input type="checkbox" class="checkbox checkbox-xs rounded-[2px] w-3 h-3" x-model="logFilters.ERROR"> ERROR
536
- </label>
537
- </div>
538
- </div>
539
- </div>
540
-
541
- <div id="logs-container" class="flex-1 overflow-auto p-4 font-mono text-[11px] leading-relaxed bg-space-950 custom-scrollbar">
542
- <template x-for="(log, idx) in filteredLogs" :key="idx">
543
- <div class="flex gap-3 px-2 py-1 -mx-2 hover:bg-white/[0.03] transition-colors group border-b border-space-border/10">
544
- <span class="text-zinc-600 w-16 shrink-0 select-none group-hover:text-zinc-500 transition-colors"
545
- x-text="new Date(log.timestamp).toLocaleTimeString([], {hour12:false})"></span>
546
- <div class="w-16 shrink-0 flex items-center">
547
- <span class="px-1.5 py-0.5 rounded-[2px] text-[10px] font-bold uppercase tracking-wider leading-none border"
548
- :class="{
549
- 'bg-blue-500/10 text-blue-400 border-blue-500/20': log.level === 'INFO',
550
- 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20': log.level === 'WARN',
551
- 'bg-red-500/10 text-red-500 border-red-500/20': log.level === 'ERROR',
552
- 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20': log.level === 'SUCCESS',
553
- 'bg-purple-500/10 text-purple-400 border-purple-500/20': log.level === 'DEBUG'
554
- }" x-text="log.level"></span>
555
- </div>
556
- <div class="flex-1 flex flex-col gap-0.5 min-w-0">
557
- <span class="text-zinc-200 break-all group-hover:text-white transition-colors"
558
- x-text="formatLogMessage(log.message)"></span>
694
+
695
+ <template x-for="(log, idx) in filteredLogs" :key="log.timestamp + '-' + idx">
696
+ <div class="logs-row" :class="'is-' + log.level.toLowerCase()">
697
+ <span class="logs-time" x-text="formatLogTime(log.timestamp)"></span>
698
+ <span class="logs-level" x-text="log.level"></span>
699
+ <span class="logs-message" x-text="formatLogMessage(log.message)"></span>
700
+ <div class="logs-details">
559
701
  <template x-if="getLogDetails(log.message)">
560
- <div class="flex flex-wrap gap-2 text-[10px] text-zinc-500 mt-0.5">
702
+ <div class="logs-detail-list">
561
703
  <template x-for="(detail, key) in getLogDetails(log.message)" :key="key">
562
- <span class="px-1.5 py-0.5 bg-space-800/50 rounded border border-space-border/30">
563
- <span class="text-zinc-600" x-text="key"></span>=<span class="text-zinc-400" x-text="detail"></span>
704
+ <span class="logs-detail-pill">
705
+ <span x-text="key"></span>=<strong x-text="detail"></strong>
564
706
  </span>
565
707
  </template>
566
708
  </div>
@@ -568,17 +710,20 @@
568
710
  </div>
569
711
  </div>
570
712
  </template>
571
- <div class="h-3 w-1.5 bg-zinc-600 animate-pulse mt-1 inline-block" x-show="filteredLogs.length === logs.length && !logSearchQuery"></div>
572
- <div x-show="filteredLogs.length === 0 && logs.length > 0" class="text-zinc-700 italic mt-8 text-center">
573
- No logs match filter
713
+
714
+ <div x-show="filteredLogs.length === 0 && logs.length === 0" class="logs-empty">
715
+ Waiting for log events
716
+ </div>
717
+ <div x-show="filteredLogs.length === 0 && logs.length > 0" class="logs-empty">
718
+ No logs match the current filters
574
719
  </div>
575
720
  </div>
576
721
  </div>
577
722
  </div>
578
723
 
579
724
  <div x-show="activeTab === 'settings'" x-transition class="view-container">
580
- <div class="flex items-center justify-between gap-4 mb-6">
581
- <div class="flex flex-wrap items-center gap-4">
725
+ <div class="section-header flex items-center justify-between gap-4 mb-6">
726
+ <div class="section-heading flex flex-wrap items-center gap-4">
582
727
  <h1 class="text-2xl font-bold text-white tracking-tight">Settings</h1>
583
728
  <div class="flex items-center h-6 px-3 rounded-full bg-space-800/80 border border-space-border/50 shadow-sm backdrop-blur-sm">
584
729
  <span class="text-[10px] font-mono text-gray-400 uppercase tracking-wider">Server configuration</span>
@@ -596,7 +741,7 @@
596
741
  <div class="space-y-3">
597
742
  <div class="flex items-center justify-between py-2 border-b border-space-border/30">
598
743
  <span class="text-sm text-gray-400">Server URL</span>
599
- <span class="font-mono text-sm text-white">http://localhost:8081</span>
744
+ <span class="font-mono text-sm text-white" x-text="serverUrl">http://localhost:8081</span>
600
745
  </div>
601
746
  <div class="flex items-center justify-between py-2 border-b border-space-border/30">
602
747
  <span class="text-sm text-gray-400">Config Path</span>
@@ -609,6 +754,89 @@
609
754
  </div>
610
755
  </div>
611
756
 
757
+ <div class="view-card mt-6">
758
+ <div class="flex items-center gap-2.5 mb-4">
759
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-4 h-4 text-neon-cyan">
760
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7h16M4 12h10M4 17h7m8-4l2 2 4-4"></path>
761
+ </svg>
762
+ <h3 class="text-xs font-mono text-gray-400 uppercase tracking-widest">Claude Model Mapping</h3>
763
+ </div>
764
+ <div class="model-mapping-list">
765
+ <div class="model-mapping-row" data-model-mapping-alias="opus">
766
+ <div class="model-mapping-copy">
767
+ <div class="text-sm text-gray-300">Opus</div>
768
+ <div class="text-xs text-gray-500 font-mono">Default <span x-text="modelOptionName(modelMappingDefaults.opus)"></span> / <span x-text="reasoningOptionName(reasoningMappingDefaults.opus)"></span></div>
769
+ </div>
770
+ <div class="model-mapping-controls">
771
+ <label class="model-mapping-field">
772
+ <span>Model</span>
773
+ <select class="model-mapping-select" :value="modelMappings.opus" @change="setModelMapping('opus', $event.target.value)" :disabled="Boolean(modelMappingSaving)">
774
+ <template x-for="model in openAiModelOptions" :key="model.id">
775
+ <option :value="model.id" x-text="model.name"></option>
776
+ </template>
777
+ </select>
778
+ </label>
779
+ <label class="model-mapping-field">
780
+ <span>Reasoning</span>
781
+ <select class="model-mapping-select" :value="reasoningMappings.opus" @change="setReasoningMapping('opus', $event.target.value)" :disabled="Boolean(reasoningMappingSaving)">
782
+ <template x-for="level in reasoningLevelOptions" :key="level.id">
783
+ <option :value="level.id" x-text="level.name"></option>
784
+ </template>
785
+ </select>
786
+ </label>
787
+ </div>
788
+ </div>
789
+ <div class="model-mapping-row" data-model-mapping-alias="sonnet">
790
+ <div class="model-mapping-copy">
791
+ <div class="text-sm text-gray-300">Sonnet</div>
792
+ <div class="text-xs text-gray-500 font-mono">Default <span x-text="modelOptionName(modelMappingDefaults.sonnet)"></span> / <span x-text="reasoningOptionName(reasoningMappingDefaults.sonnet)"></span></div>
793
+ </div>
794
+ <div class="model-mapping-controls">
795
+ <label class="model-mapping-field">
796
+ <span>Model</span>
797
+ <select class="model-mapping-select" :value="modelMappings.sonnet" @change="setModelMapping('sonnet', $event.target.value)" :disabled="Boolean(modelMappingSaving)">
798
+ <template x-for="model in openAiModelOptions" :key="model.id">
799
+ <option :value="model.id" x-text="model.name"></option>
800
+ </template>
801
+ </select>
802
+ </label>
803
+ <label class="model-mapping-field">
804
+ <span>Reasoning</span>
805
+ <select class="model-mapping-select" :value="reasoningMappings.sonnet" @change="setReasoningMapping('sonnet', $event.target.value)" :disabled="Boolean(reasoningMappingSaving)">
806
+ <template x-for="level in reasoningLevelOptions" :key="level.id">
807
+ <option :value="level.id" x-text="level.name"></option>
808
+ </template>
809
+ </select>
810
+ </label>
811
+ </div>
812
+ </div>
813
+ <div class="model-mapping-row" data-model-mapping-alias="haiku">
814
+ <div class="model-mapping-copy">
815
+ <div class="text-sm text-gray-300">Haiku</div>
816
+ <div class="text-xs text-gray-500 font-mono">Default <span x-text="modelOptionName(modelMappingDefaults.haiku)"></span> / <span x-text="reasoningOptionName(reasoningMappingDefaults.haiku)"></span></div>
817
+ </div>
818
+ <div class="model-mapping-controls">
819
+ <label class="model-mapping-field">
820
+ <span>Model</span>
821
+ <select class="model-mapping-select" :value="modelMappings.haiku" @change="setModelMapping('haiku', $event.target.value)" :disabled="Boolean(modelMappingSaving)">
822
+ <template x-for="model in openAiModelOptions" :key="model.id">
823
+ <option :value="model.id" x-text="model.name"></option>
824
+ </template>
825
+ </select>
826
+ </label>
827
+ <label class="model-mapping-field">
828
+ <span>Reasoning</span>
829
+ <select class="model-mapping-select" :value="reasoningMappings.haiku" @change="setReasoningMapping('haiku', $event.target.value)" :disabled="Boolean(reasoningMappingSaving)">
830
+ <template x-for="level in reasoningLevelOptions" :key="level.id">
831
+ <option :value="level.id" x-text="level.name"></option>
832
+ </template>
833
+ </select>
834
+ </label>
835
+ </div>
836
+ </div>
837
+ </div>
838
+ </div>
839
+
612
840
  <div class="view-card mt-6" x-show="kiloEnabled">
613
841
  <div class="flex items-center gap-2.5 mb-4">
614
842
  <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-4 h-4 text-neon-purple">
@@ -638,28 +866,55 @@
638
866
  </div>
639
867
  </div>
640
868
 
641
- <div class="view-card mt-6">
642
- <div class="flex items-center gap-2.5 mb-4">
643
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-4 h-4 text-neon-purple">
644
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"></path>
645
- </svg>
646
- <h3 class="text-xs font-mono text-gray-400 uppercase tracking-widest">Account Selection Strategy</h3>
647
- </div>
648
- <div class="flex flex-wrap items-center justify-between gap-4">
649
- <div>
650
- <div class="text-sm text-gray-300">Strategy</div>
651
- <div class="text-xs text-gray-500 font-mono" x-text="multiAccountRotationEnabled ? 'How accounts are selected when rotation is enabled' : 'Disabled for personal local mode'"></div>
652
- </div>
653
- <div class="inline-flex items-center gap-2">
654
- <button class="btn btn-xs" :class="accountStrategy === 'sticky' ? 'bg-neon-purple text-white' : 'btn-outline text-gray-400'"
655
- @click="setAccountStrategy('sticky')" :disabled="!multiAccountRotationEnabled || strategySaving">Sticky</button>
656
- <button class="btn btn-xs" :class="accountStrategy === 'round-robin' ? 'bg-neon-purple text-white' : 'btn-outline text-gray-400'"
657
- @click="setAccountStrategy('round-robin')" :disabled="!multiAccountRotationEnabled || strategySaving">Round-Robin</button>
658
- </div>
659
- </div>
660
- </div>
661
869
  </div>
662
- </div>
870
+ </main>
871
+
872
+ <nav class="bottom-nav" aria-label="Primary navigation">
873
+ <button class="bottom-nav-item" :class="{'active': activeTab === 'dashboard'}" @click="setActiveTab('dashboard')" type="button" aria-label="Dashboard">
874
+ <span class="bottom-nav-icon">
875
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
876
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
877
+ </svg>
878
+ </span>
879
+ <span class="bottom-nav-label">Dashboard</span>
880
+ </button>
881
+ <button class="bottom-nav-item" :class="{'active': activeTab === 'metrics'}" @click="setActiveTab('metrics')" type="button" aria-label="Metrics">
882
+ <span class="bottom-nav-icon">
883
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
884
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3v18h18M7 15l3-3 3 2 5-7" />
885
+ </svg>
886
+ </span>
887
+ <span class="bottom-nav-label">Metrics</span>
888
+ <span class="bottom-nav-badge" x-text="formatTokenCount(metricsTotals.totalTokens)"></span>
889
+ </button>
890
+ <button class="bottom-nav-item" :class="{'active': activeTab === 'logs'}" @click="setActiveTab('logs')" type="button" aria-label="Server Logs">
891
+ <span class="bottom-nav-icon">
892
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
893
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
894
+ </svg>
895
+ </span>
896
+ <span class="bottom-nav-label">Logs</span>
897
+ <span class="bottom-nav-badge" x-text="logs.length"></span>
898
+ </button>
899
+ <button class="bottom-nav-item" :class="{'active': activeTab === 'accounts'}" @click="setActiveTab('accounts')" type="button" aria-label="Accounts">
900
+ <span class="bottom-nav-icon">
901
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
902
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
903
+ </svg>
904
+ </span>
905
+ <span class="bottom-nav-label">Accounts</span>
906
+ <span class="bottom-nav-badge" x-text="stats.total"></span>
907
+ </button>
908
+ <button class="bottom-nav-item" :class="{'active': activeTab === 'settings'}" @click="setActiveTab('settings')" type="button" aria-label="Settings">
909
+ <span class="bottom-nav-icon">
910
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
911
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
912
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
913
+ </svg>
914
+ </span>
915
+ <span class="bottom-nav-label">Settings</span>
916
+ </button>
917
+ </nav>
663
918
  </div>
664
919
 
665
920
  <div x-show="showAddModal" x-transition class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;">
@@ -804,12 +1059,12 @@
804
1059
  <div class="mt-3 pt-3 border-t border-space-border/30 space-y-1">
805
1060
  <div class="flex items-center justify-between text-xs">
806
1061
  <span class="text-gray-500">Reset window</span>
807
- <span class="font-mono text-gray-300" x-text="quotaResetSummary(selectedAccount) || '-'"></span>
1062
+ <span class="quota-reset-summary font-mono text-gray-300" x-text="quotaResetSummary(selectedAccount) || '-'"></span>
808
1063
  </div>
809
1064
  <template x-if="quotaResetAtLabel(selectedAccount)">
810
1065
  <div class="flex items-center justify-between text-xs">
811
1066
  <span class="text-gray-500">Resets at</span>
812
- <span class="font-mono text-gray-300" x-text="quotaResetAtLabel(selectedAccount)"></span>
1067
+ <span class="quota-reset-summary font-mono text-gray-300" x-text="quotaResetAtLabel(selectedAccount)"></span>
813
1068
  </div>
814
1069
  </template>
815
1070
  </div>