@pikoloo/codex-proxy 1.0.7 → 1.2.2

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +28 -11
  3. package/bin/cli.js +15 -15
  4. package/docs/ACCOUNT.md +104 -0
  5. package/docs/API.md +21 -29
  6. package/docs/ARCHITECTURE.md +9 -9
  7. package/docs/CLAUDE_INTEGRATION.md +3 -3
  8. package/docs/OAUTH.md +13 -13
  9. package/docs/OPENCLAW.md +1 -1
  10. package/docs/legal.md +6 -0
  11. package/images/dashboard-screenshot.png +0 -0
  12. package/images/readme-cover.png +0 -0
  13. package/images/settings-screenshot.png +0 -0
  14. package/package.json +19 -10
  15. package/public/css/style.css +802 -22
  16. package/public/index.html +236 -338
  17. package/public/js/app.js +140 -118
  18. package/src/account-manager.js +210 -292
  19. package/src/cli/account.js +236 -0
  20. package/src/direct-api.js +7 -9
  21. package/src/index.js +7 -7
  22. package/src/middleware/credentials.js +6 -47
  23. package/src/oauth.js +2 -1
  24. package/src/routes/{accounts-route.js → account-route.js} +25 -109
  25. package/src/routes/api-routes.js +18 -30
  26. package/src/routes/chat-route.js +3 -3
  27. package/src/routes/messages-route.js +37 -199
  28. package/src/routes/models-route.js +11 -21
  29. package/src/routes/settings-route.js +1 -41
  30. package/src/security.js +1 -1
  31. package/src/server-settings.js +30 -38
  32. package/src/utils/logger.js +14 -1
  33. package/docs/ACCOUNTS.md +0 -202
  34. package/images/demo-screenshot.png +0 -0
  35. package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
  36. package/src/account-rotation/index.js +0 -93
  37. package/src/account-rotation/rate-limits.js +0 -293
  38. package/src/account-rotation/strategies/base-strategy.js +0 -48
  39. package/src/account-rotation/strategies/index.js +0 -31
  40. package/src/account-rotation/strategies/round-robin-strategy.js +0 -42
  41. package/src/account-rotation/strategies/sticky-strategy.js +0 -97
  42. package/src/cli/accounts.js +0 -557
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,17 +77,17 @@
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"
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"
97
91
  @click="refreshCurrentView()" :disabled="loading || metricsLoading" title="Refresh data">
98
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" />
@@ -102,80 +96,17 @@
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">Monitor</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 === 'metrics'}" @click="setActiveTab('metrics')">
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="M3 3v18h18M7 15l3-3 3 2 5-7" />
132
- </svg>
133
- <span>Metrics</span>
134
- <span class="nav-count ml-auto" x-text="formatTokenCount(metricsTotals.totalTokens)"></span>
135
- </button>
136
- <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"
137
- :class="{'active': activeTab === 'logs'}" @click="setActiveTab('logs')">
138
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
139
- <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" />
140
- </svg>
141
- <span>Logs</span>
142
- <span class="nav-count ml-auto" x-text="logs.length"></span>
143
- </button>
144
- </nav>
145
-
146
- <div class="px-4 mt-8 mb-2 text-xs font-bold text-gray-600 uppercase tracking-widest">Manage</div>
147
- <nav class="flex flex-col gap-1">
148
- <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"
149
- :class="{'active': activeTab === 'accounts'}" @click="setActiveTab('accounts')">
150
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
151
- <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" />
152
- </svg>
153
- <span>Accounts</span>
154
- <span class="nav-count ml-auto" x-text="stats.total"></span>
155
- </button>
156
- <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"
157
- :class="{'active': activeTab === 'settings'}" @click="setActiveTab('settings')">
158
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
159
- <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" />
160
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
161
- </svg>
162
- <span>Settings</span>
163
- </button>
164
- </nav>
165
-
166
- </div>
167
- </div>
168
-
169
- <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">
170
101
  <div x-show="activeTab === 'dashboard'" x-transition class="view-container">
171
- <div class="flex items-center justify-between gap-4 mb-6">
172
- <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">
173
104
  <h1 class="text-2xl font-bold text-white tracking-tight">Dashboard</h1>
174
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">
175
106
  <span class="text-[10px] font-mono text-gray-400 uppercase tracking-wider">CHATGPT PROXY SYSTEM</span>
176
107
  </div>
177
108
  </div>
178
- <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">
179
110
  <div class="relative flex items-center justify-center">
180
111
  <span class="absolute w-1.5 h-1.5 bg-neon-green rounded-full animate-ping opacity-75"></span>
181
112
  <span class="relative w-1.5 h-1.5 bg-neon-green rounded-full"></span>
@@ -194,8 +125,8 @@
194
125
  </svg>
195
126
  </div>
196
127
  <div class="stat-value text-white font-mono text-2xl lg:text-3xl font-bold mb-1 truncate" x-text="stats.total"></div>
197
- <div class="stat-title text-gray-500 font-mono text-[10px] uppercase tracking-wider truncate">Total Accounts</div>
198
- <div class="stat-desc text-cyan-400/60 text-[10px] truncate">Linked accounts</div>
128
+ <div class="stat-title text-gray-500 font-mono text-[10px] uppercase tracking-wider truncate">Account</div>
129
+ <div class="stat-desc text-cyan-400/60 text-[10px] truncate">Single local profile</div>
199
130
  </div>
200
131
 
201
132
  <div class="stat bg-space-900/40 border border-space-border/30 rounded-xl p-3 lg:p-4 hover:border-green-500/30 hover:bg-green-500/5 transition-all duration-300 group relative cursor-pointer min-w-0">
@@ -252,6 +183,12 @@
252
183
  <span>Test</span>
253
184
  </button>
254
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>
255
192
  <div x-show="testResponse" class="mt-4 p-3 rounded-lg bg-space-800/50 border border-space-border/30">
256
193
  <pre class="text-xs font-mono text-gray-300 whitespace-pre-wrap" x-text="testResponse"></pre>
257
194
  </div>
@@ -279,6 +216,12 @@
279
216
  <span>Test Haiku</span>
280
217
  </button>
281
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>
282
225
  <div x-show="haikuTestResponse" class="mt-4 p-3 rounded-lg bg-space-800/50 border border-space-border/30">
283
226
  <pre class="text-xs font-mono text-gray-300 whitespace-pre-wrap" x-text="haikuTestResponse"></pre>
284
227
  </div>
@@ -323,14 +266,14 @@
323
266
  </div>
324
267
 
325
268
  <div x-show="activeTab === 'metrics'" x-transition class="view-container">
326
- <div class="flex items-center justify-between gap-4 mb-6">
327
- <div class="flex flex-wrap items-center gap-4">
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">
328
271
  <h1 class="text-2xl font-bold text-white tracking-tight">Token Usage</h1>
329
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">
330
273
  <span class="text-[10px] font-mono text-gray-400 uppercase tracking-wider">Request metrics</span>
331
274
  </div>
332
275
  </div>
333
- <div class="metrics-range-controls">
276
+ <div class="section-actions metrics-range-controls">
334
277
  <button class="metrics-range-button" :class="{'active': metricsRange === '24h'}" @click="setMetricsRange('24h')">24h</button>
335
278
  <button class="metrics-range-button" :class="{'active': metricsRange === '7d'}" @click="setMetricsRange('7d')">7d</button>
336
279
  <button class="metrics-range-button" :class="{'active': metricsRange === '30d'}" @click="setMetricsRange('30d')">30d</button>
@@ -513,267 +456,192 @@
513
456
  </div>
514
457
  </div>
515
458
 
516
- <div x-show="activeTab === 'accounts'" x-transition class="view-container">
517
- <div class="flex items-center justify-between gap-4 mb-6">
518
- <div class="flex flex-wrap items-center gap-4">
519
- <h1 class="text-2xl font-bold text-white tracking-tight">Account Management</h1>
459
+ <div x-show="activeTab === 'account'" x-transition class="view-container">
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">Account</h1>
520
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">
521
- <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">Single ChatGPT account</span>
522
465
  </div>
523
466
  </div>
524
467
 
525
- <div class="flex flex-wrap items-center justify-end gap-2 sm:flex-nowrap">
526
- <div class="relative" x-show="accounts.length > 0">
527
- <input type="text" x-model="searchQuery" placeholder="Search accounts..."
528
- class="input-search-sm w-48 pl-9 h-8" @keydown.escape="searchQuery = ''">
529
- <svg class="w-4 h-4 absolute left-3 top-2 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
530
- <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" />
531
- </svg>
532
- </div>
533
-
468
+ <div class="section-actions account-actions flex flex-wrap items-center justify-end gap-2 sm:flex-nowrap">
534
469
  <button class="btn btn-xs btn-outline border-space-border text-gray-400 hover:text-white transition-all gap-2 h-8"
535
- @click="refreshAllTokens()" x-show="accounts.length > 0">
470
+ @click="refreshToken(accounts[0]?.email)" x-show="accounts.length > 0"
471
+ aria-label="Refresh account token" title="Refresh account token">
536
472
  <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
537
473
  <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>
538
474
  </svg>
539
- <span>Refresh All</span>
475
+ <span class="action-label">Refresh</span>
540
476
  </button>
541
477
 
542
- <div class="flex items-center h-6 px-2 rounded bg-space-800/80 border border-space-border/50 sm:ml-1">
543
- <span class="text-[11px] font-mono text-gray-400" x-text="accounts.length + ' accounts'"></span>
544
- </div>
545
-
546
478
  <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"
547
- @click="showAddModal = true">
479
+ @click="showAddModal = true" aria-label="Configure account" title="Configure account">
548
480
  <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
549
481
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
550
482
  </svg>
551
- <span>Add Account</span>
483
+ <span class="action-label" x-text="accounts.length ? 'Replace Account' : 'Add Account'"></span>
552
484
  </button>
553
485
  </div>
554
486
  </div>
555
487
 
556
- <div class="view-card !p-0 overflow-x-auto">
557
- <table class="w-full min-w-[860px]">
558
- <thead x-show="filteredAccounts.length > 0">
559
- <tr class="bg-space-900/50 border-b border-space-border/50">
560
- <th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-20">Status</th>
561
- <th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider flex-1 min-w-[200px]">Account (Email)</th>
562
- <th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-20">Plan</th>
563
- <th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-32">Token</th>
564
- <th class="py-3 text-left text-[10px] font-bold text-gray-500 uppercase tracking-wider w-24">Quota</th>
565
- <th class="py-3 pr-6 text-right text-[10px] font-bold text-gray-500 uppercase tracking-wider w-40">Operations</th>
566
- </tr>
567
- </thead>
568
- <tbody>
569
- <template x-if="filteredAccounts.length === 0 && accounts.length === 0">
570
- <tr>
571
- <td colspan="6" class="py-16 text-center">
572
- <div class="flex flex-col items-center gap-4 max-w-lg mx-auto">
573
- <svg class="w-20 h-20 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
574
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
575
- </svg>
576
- <h3 class="text-xl font-semibold text-gray-400">No Accounts Yet</h3>
577
- <p class="text-sm text-gray-600 max-w-md leading-relaxed">Get started by adding a ChatGPT account via OAuth, or import from the Codex app.</p>
578
- <div class="flex items-center gap-4 mt-2">
579
- <button class="btn bg-neon-purple hover:bg-purple-600 border-none text-white btn-sm gap-2 shadow-lg shadow-neon-purple/20"
580
- @click="showAddModal = true">
581
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
582
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
583
- </svg>
584
- <span>Add Your First Account</span>
585
- </button>
586
- <span class="text-xs text-gray-600">or</span>
587
- <button class="btn btn-outline btn-sm text-gray-400" @click="importFromCodex()">
588
- Import from Codex
589
- </button>
590
- </div>
591
- </div>
592
- </td>
593
- </tr>
594
- </template>
488
+ <div class="view-card">
489
+ <template x-if="accounts.length === 0">
490
+ <div class="py-14 text-center">
491
+ <div class="flex flex-col items-center gap-4 max-w-lg mx-auto">
492
+ <svg class="w-20 h-20 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
493
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5.121 17.804A9 9 0 1118.88 17.8M15 11a3 3 0 11-6 0 3 3 0 016 0zm-7.5 8a6 6 0 019 0" />
494
+ </svg>
495
+ <h3 class="text-xl font-semibold text-gray-400">No Account Configured</h3>
496
+ <p class="text-sm text-gray-600 max-w-md leading-relaxed">Add your ChatGPT account via OAuth, or import your local Codex app account. Importing or adding an account replaces the existing local account.</p>
497
+ <div class="flex flex-wrap items-center justify-center gap-3 mt-2">
498
+ <button class="btn bg-neon-purple hover:bg-purple-600 border-none text-white btn-sm gap-2 shadow-lg shadow-neon-purple/20"
499
+ @click="showAddModal = true">
500
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
501
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
502
+ </svg>
503
+ <span>Add Account</span>
504
+ </button>
505
+ <button class="btn btn-outline btn-sm text-gray-400" @click="importFromCodex()">
506
+ Import from Codex
507
+ </button>
508
+ </div>
509
+ </div>
510
+ </div>
511
+ </template>
512
+
513
+ <template x-if="accounts.length > 0">
514
+ <div class="space-y-6">
515
+ <div class="flex flex-wrap items-start justify-between gap-4">
516
+ <div class="min-w-0">
517
+ <div class="flex items-center gap-2 mb-2">
518
+ <div class="w-2 h-2 rounded-full bg-neon-green shadow-[0_0_8px_rgba(34,197,94,0.6)] animate-pulse"></div>
519
+ <span class="text-xs font-mono font-semibold text-neon-green">ACTIVE</span>
520
+ </div>
521
+ <p class="font-mono text-base text-white truncate max-w-full" x-text="accounts[0].email"></p>
522
+ <p class="text-xs text-gray-500 mt-1">This is the only configured account used for proxy requests.</p>
523
+ </div>
524
+ <span class="text-[10px] font-bold uppercase px-2 py-1 rounded"
525
+ :class="{
526
+ 'bg-yellow-500/10 text-yellow-400 border border-yellow-500/30': accounts[0].planType === 'plus',
527
+ 'bg-blue-500/10 text-blue-400 border border-blue-500/30': accounts[0].planType === 'pro',
528
+ 'bg-gray-500/10 text-gray-400 border border-gray-500/30': accounts[0].planType === 'free'
529
+ }"
530
+ x-text="(accounts[0].planType || 'free').toUpperCase()"></span>
531
+ </div>
595
532
 
596
- <template x-for="acc in filteredAccounts" :key="acc.email">
597
- <tr class="border-b border-space-border/30 last:border-0 hover:bg-white/5 transition-colors group">
598
- <td class="py-4 pl-6">
599
- <div class="flex items-center gap-2">
600
- <div class="w-2 h-2 rounded-full flex-shrink-0"
601
- :class="acc.isActive ? 'bg-neon-green shadow-[0_0_8px_rgba(34,197,94,0.6)] animate-pulse' : 'bg-gray-500'">
533
+ <div class="grid gap-3 sm:grid-cols-3">
534
+ <div class="rounded-lg border border-space-border/40 bg-space-900/40 p-3">
535
+ <div class="text-[10px] font-mono uppercase tracking-wider text-gray-500 mb-1">Token</div>
536
+ <div class="text-sm font-mono"
537
+ :class="accounts[0].tokenExpired ? 'text-red-400' : 'text-neon-green'"
538
+ x-text="accounts[0].tokenExpired ? 'Expired' : 'Valid'"></div>
539
+ </div>
540
+ <div class="rounded-lg border border-space-border/40 bg-space-900/40 p-3">
541
+ <div class="text-[10px] font-mono uppercase tracking-wider text-gray-500 mb-1">Quota</div>
542
+ <template x-if="getRemainingPercentage(accounts[0]) !== null">
543
+ <button class="flex items-center gap-2 text-left" @click="showQuotaModal(accounts[0])">
544
+ <div class="w-20 bg-gray-700 rounded-full h-2 overflow-hidden">
545
+ <div class="h-full rounded-full transition-all"
546
+ :class="quotaBarClass(accounts[0])"
547
+ :style="`width: ${getRemainingPercentage(accounts[0])}%`"></div>
602
548
  </div>
603
- <span class="text-xs font-mono font-semibold"
604
- :class="acc.isActive ? 'text-neon-green' : 'text-gray-500'"
605
- x-text="acc.isActive ? 'ACTIVE' : 'IDLE'">
606
- </span>
607
- </div>
608
- </td>
609
- <td class="py-4">
610
- <span class="font-mono text-sm text-gray-300 truncate max-w-[320px] inline-block group-hover:text-white transition-colors"
611
- x-text="acc.email"></span>
612
- </td>
613
- <td class="py-4">
614
- <span class="text-[10px] font-bold uppercase px-2 py-1 rounded"
615
- :class="{
616
- 'bg-yellow-500/10 text-yellow-400 border border-yellow-500/30': acc.planType === 'plus',
617
- 'bg-blue-500/10 text-blue-400 border border-blue-500/30': acc.planType === 'pro',
618
- 'bg-gray-500/10 text-gray-400 border border-gray-500/30': acc.planType === 'free'
619
- }"
620
- x-text="(acc.planType || 'free').toUpperCase()"></span>
621
- </td>
622
- <td class="py-4">
623
- <div class="flex items-center gap-2">
624
549
  <span class="text-xs font-mono"
625
- :class="acc.tokenExpired ? 'text-red-400' : 'text-neon-green'"
626
- x-text="acc.tokenExpired ? 'Expired' : 'Valid'"></span>
627
- </div>
628
- </td>
629
- <td class="py-4 cursor-pointer" @click="showQuotaModal(acc)">
630
- <template x-if="getRemainingPercentage(acc) !== null">
631
- <div class="flex flex-col gap-1">
632
- <div class="flex items-center gap-2">
633
- <div class="w-16 bg-gray-700 rounded-full h-2 overflow-hidden">
634
- <div class="h-full rounded-full transition-all"
635
- :class="quotaBarClass(acc)"
636
- :style="`width: ${getRemainingPercentage(acc)}%`">
637
- </div>
638
- </div>
639
- <template x-if="isQuotaExhausted(acc)">
640
- <span class="text-[10px] font-bold uppercase text-red-400 bg-red-500/10 px-1.5 py-0.5 rounded border border-red-500/30">USED</span>
641
- </template>
642
- <template x-if="!isQuotaExhausted(acc)">
643
- <span class="text-xs font-mono"
644
- :class="quotaTextClass(acc)"
645
- x-text="quotaLabel(acc)"></span>
646
- </template>
647
- </div>
648
- <template x-if="quotaResetSummary(acc)">
649
- <span class="text-[10px] font-mono text-gray-500" x-text="quotaResetSummary(acc)"></span>
650
- </template>
651
- </div>
652
- </template>
653
- <template x-if="getRemainingPercentage(acc) === null">
654
- <span class="text-xs text-gray-600">-</span>
655
- </template>
656
- </td>
657
- <td class="py-4 pr-6">
658
- <div class="flex justify-end gap-2">
659
- <button x-show="!acc.isActive"
660
- class="px-3 py-1 text-[10px] font-bold font-mono uppercase tracking-wider rounded bg-neon-purple/10 text-neon-purple hover:bg-neon-purple/20 border border-neon-purple/30 hover:border-neon-purple/50 transition-all"
661
- @click="switchAccount(acc.email)">
662
- SWITCH
663
- </button>
664
- <button class="btn btn-xs btn-ghost p-2 rounded hover:bg-white/10 text-gray-500 hover:text-white transition-colors"
665
- @click="refreshToken(acc.email)" title="Refresh token">
666
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
667
- <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" />
668
- </svg>
669
- </button>
670
- <button class="btn btn-xs btn-ghost p-2 rounded hover:bg-red-500/10 text-gray-500 hover:text-red-400 transition-colors"
671
- @click="confirmDelete(acc.email)" title="Delete account">
672
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
673
- <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" />
674
- </svg>
675
- </button>
676
- </div>
677
- </td>
678
- </tr>
679
- </template>
550
+ :class="quotaTextClass(accounts[0])"
551
+ x-text="isQuotaExhausted(accounts[0]) ? 'USED' : quotaLabel(accounts[0])"></span>
552
+ </button>
553
+ </template>
554
+ <template x-if="getRemainingPercentage(accounts[0]) === null">
555
+ <span class="text-xs text-gray-600">Unavailable</span>
556
+ </template>
557
+ </div>
558
+ <div class="rounded-lg border border-space-border/40 bg-space-900/40 p-3">
559
+ <div class="text-[10px] font-mono uppercase tracking-wider text-gray-500 mb-1">Reset</div>
560
+ <div class="text-xs font-mono text-gray-300" x-text="quotaResetSummary(accounts[0]) || '-'"></div>
561
+ </div>
562
+ </div>
680
563
 
681
- <template x-if="accounts.length > 0 && filteredAccounts.length === 0">
682
- <tr>
683
- <td colspan="6" class="py-12 text-center">
684
- <div class="flex flex-col items-center gap-3">
685
- <svg class="w-12 h-12 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
686
- <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" />
687
- </svg>
688
- <p class="text-sm text-gray-600">No accounts match your search</p>
689
- <button class="text-xs text-neon-cyan hover:underline" @click="searchQuery = ''">Clear Search</button>
690
- </div>
691
- </td>
692
- </tr>
693
- </template>
694
- </tbody>
695
- </table>
564
+ <div class="flex flex-wrap justify-end gap-2">
565
+ <button class="btn btn-outline btn-sm text-gray-400 hover:text-white" @click="refreshToken(accounts[0].email)">
566
+ Refresh
567
+ </button>
568
+ <button class="btn btn-outline btn-sm text-gray-400 hover:text-white" @click="showAddModal = true">
569
+ Replace
570
+ </button>
571
+ <button class="btn btn-outline btn-sm text-gray-400 hover:text-white" @click="importFromCodex()">
572
+ Import
573
+ </button>
574
+ <button class="btn btn-outline btn-sm text-red-400 border-red-500/30 hover:bg-red-500/10" @click="confirmDelete(accounts[0].email)">
575
+ Delete
576
+ </button>
577
+ </div>
578
+ </div>
579
+ </template>
696
580
  </div>
697
581
  </div>
698
582
 
699
- <div x-show="activeTab === 'logs'" x-transition class="view-container h-full flex flex-col">
700
- <div class="flex items-center justify-between gap-4 mb-6">
701
- <div class="flex flex-wrap items-center gap-4">
583
+ <div x-show="activeTab === 'logs'" x-transition class="view-container logs-view h-full flex flex-col">
584
+ <div class="section-header flex items-center justify-between gap-4 mb-6">
585
+ <div class="section-heading flex flex-wrap items-center gap-4">
702
586
  <h1 class="text-2xl font-bold text-white tracking-tight">Server Logs</h1>
703
- <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">
704
- <span class="text-[10px] font-mono text-gray-400 uppercase tracking-wider">Real-time log stream</span>
587
+ <div class="log-stream-status" :class="'is-' + logStreamStatus">
588
+ <span class="log-stream-dot"></span>
589
+ <span x-text="logStreamStatusText()"></span>
705
590
  </div>
706
591
  </div>
707
- <div class="flex items-center gap-3">
708
- <div class="text-[10px] font-mono text-gray-600">
709
- <span x-text="logs.length"></span> entries
592
+ <div class="section-actions logs-count-strip">
593
+ <span><strong x-text="filteredLogs.length"></strong> shown</span>
594
+ <span><strong x-text="logs.length"></strong> total</span>
595
+ </div>
596
+ </div>
597
+
598
+ <div class="logs-shell view-card !p-0 flex flex-col min-h-0">
599
+ <div class="logs-toolbar">
600
+ <div class="logs-search">
601
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
602
+ <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" />
603
+ </svg>
604
+ <input type="text" x-model="logSearchQuery" placeholder="Search logs, models, account..."
605
+ class="logs-search-input">
710
606
  </div>
711
- <button class="btn btn-xs btn-ghost text-gray-400 hover:text-white" @click="clearLogs()" title="Clear logs">
607
+
608
+ <div class="logs-filter-row">
609
+ <template x-for="level in ['INFO', 'SUCCESS', 'WARN', 'ERROR', 'DEBUG']" :key="level">
610
+ <label class="logs-filter-chip" :class="[logFilters[level] ? 'active' : '', 'is-' + level.toLowerCase()]">
611
+ <input type="checkbox" x-model="logFilters[level]">
612
+ <span x-text="level"></span>
613
+ <span class="logs-filter-count" x-text="logLevelCounts[level] || 0"></span>
614
+ </label>
615
+ </template>
616
+ </div>
617
+
618
+ <button class="btn btn-xs btn-ghost logs-clear-button" @click="clearLogs()" title="Clear logs">
712
619
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
713
620
  <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" />
714
621
  </svg>
622
+ <span>Clear</span>
715
623
  </button>
716
624
  </div>
717
- </div>
718
625
 
719
- <div class="view-card !p-0 flex flex-col flex-1 min-h-0">
720
- <div class="bg-space-900 flex items-center p-2 px-4 border-b border-space-border gap-4">
721
- <div class="flex gap-2">
722
- <div class="w-3 h-3 rounded-full bg-red-500/20 border border-red-500/50"></div>
723
- <div class="w-3 h-3 rounded-full bg-yellow-500/20 border border-yellow-500/50"></div>
724
- <div class="w-3 h-3 rounded-full bg-green-500/20 border border-green-500/50"></div>
626
+ <div id="logs-container" class="logs-grid">
627
+ <div class="logs-grid-head">
628
+ <span>Time</span>
629
+ <span>Level</span>
630
+ <span>Message</span>
631
+ <span>Details</span>
725
632
  </div>
726
- <span class="text-xs font-mono text-gray-500 hidden sm:inline-block">~/logs</span>
727
-
728
- <div class="flex-1 flex items-center justify-center gap-4">
729
- <div class="relative w-full max-w-xs">
730
- <input type="text" x-model="logSearchQuery" placeholder="Search logs..."
731
- 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">
732
- <svg class="w-3 h-3 absolute left-2.5 top-2 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
733
- <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" />
734
- </svg>
735
- </div>
736
-
737
- <div class="hidden md:flex gap-3 text-[10px] font-mono font-bold uppercase select-none">
738
- <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}">
739
- <input type="checkbox" class="checkbox checkbox-xs rounded-[2px] w-3 h-3" x-model="logFilters.INFO"> INFO
740
- </label>
741
- <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}">
742
- <input type="checkbox" class="checkbox checkbox-xs rounded-[2px] w-3 h-3" x-model="logFilters.SUCCESS"> SUCCESS
743
- </label>
744
- <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}">
745
- <input type="checkbox" class="checkbox checkbox-xs rounded-[2px] w-3 h-3" x-model="logFilters.WARN"> WARN
746
- </label>
747
- <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}">
748
- <input type="checkbox" class="checkbox checkbox-xs rounded-[2px] w-3 h-3" x-model="logFilters.ERROR"> ERROR
749
- </label>
750
- </div>
751
- </div>
752
- </div>
753
-
754
- <div id="logs-container" class="flex-1 overflow-auto p-4 font-mono text-[11px] leading-relaxed bg-space-950 custom-scrollbar">
755
- <template x-for="(log, idx) in filteredLogs" :key="idx">
756
- <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">
757
- <span class="text-zinc-600 w-16 shrink-0 select-none group-hover:text-zinc-500 transition-colors"
758
- x-text="new Date(log.timestamp).toLocaleTimeString([], {hour12:false})"></span>
759
- <div class="w-16 shrink-0 flex items-center">
760
- <span class="px-1.5 py-0.5 rounded-[2px] text-[10px] font-bold uppercase tracking-wider leading-none border"
761
- :class="{
762
- 'bg-blue-500/10 text-blue-400 border-blue-500/20': log.level === 'INFO',
763
- 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20': log.level === 'WARN',
764
- 'bg-red-500/10 text-red-500 border-red-500/20': log.level === 'ERROR',
765
- 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20': log.level === 'SUCCESS',
766
- 'bg-purple-500/10 text-purple-400 border-purple-500/20': log.level === 'DEBUG'
767
- }" x-text="log.level"></span>
768
- </div>
769
- <div class="flex-1 flex flex-col gap-0.5 min-w-0">
770
- <span class="text-zinc-200 break-all group-hover:text-white transition-colors"
771
- x-text="formatLogMessage(log.message)"></span>
633
+
634
+ <template x-for="(log, idx) in filteredLogs" :key="log.timestamp + '-' + idx">
635
+ <div class="logs-row" :class="'is-' + log.level.toLowerCase()">
636
+ <span class="logs-time" x-text="formatLogTime(log.timestamp)"></span>
637
+ <span class="logs-level" x-text="log.level"></span>
638
+ <span class="logs-message" x-text="formatLogMessage(log.message)"></span>
639
+ <div class="logs-details">
772
640
  <template x-if="getLogDetails(log.message)">
773
- <div class="flex flex-wrap gap-2 text-[10px] text-zinc-500 mt-0.5">
641
+ <div class="logs-detail-list">
774
642
  <template x-for="(detail, key) in getLogDetails(log.message)" :key="key">
775
- <span class="px-1.5 py-0.5 bg-space-800/50 rounded border border-space-border/30">
776
- <span class="text-zinc-600" x-text="key"></span>=<span class="text-zinc-400" x-text="detail"></span>
643
+ <span class="logs-detail-pill">
644
+ <span x-text="key"></span>=<strong x-text="detail"></strong>
777
645
  </span>
778
646
  </template>
779
647
  </div>
@@ -781,17 +649,20 @@
781
649
  </div>
782
650
  </div>
783
651
  </template>
784
- <div class="h-3 w-1.5 bg-zinc-600 animate-pulse mt-1 inline-block" x-show="filteredLogs.length === logs.length && !logSearchQuery"></div>
785
- <div x-show="filteredLogs.length === 0 && logs.length > 0" class="text-zinc-700 italic mt-8 text-center">
786
- No logs match filter
652
+
653
+ <div x-show="filteredLogs.length === 0 && logs.length === 0" class="logs-empty">
654
+ Waiting for log events
655
+ </div>
656
+ <div x-show="filteredLogs.length === 0 && logs.length > 0" class="logs-empty">
657
+ No logs match the current filters
787
658
  </div>
788
659
  </div>
789
660
  </div>
790
661
  </div>
791
662
 
792
663
  <div x-show="activeTab === 'settings'" x-transition class="view-container">
793
- <div class="flex items-center justify-between gap-4 mb-6">
794
- <div class="flex flex-wrap items-center gap-4">
664
+ <div class="section-header flex items-center justify-between gap-4 mb-6">
665
+ <div class="section-heading flex flex-wrap items-center gap-4">
795
666
  <h1 class="text-2xl font-bold text-white tracking-tight">Settings</h1>
796
667
  <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">
797
668
  <span class="text-[10px] font-mono text-gray-400 uppercase tracking-wider">Server configuration</span>
@@ -934,38 +805,65 @@
934
805
  </div>
935
806
  </div>
936
807
 
937
- <div class="view-card mt-6">
938
- <div class="flex items-center gap-2.5 mb-4">
939
- <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">
940
- <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>
941
- </svg>
942
- <h3 class="text-xs font-mono text-gray-400 uppercase tracking-widest">Account Selection Strategy</h3>
943
- </div>
944
- <div class="flex flex-wrap items-center justify-between gap-4">
945
- <div>
946
- <div class="text-sm text-gray-300">Strategy</div>
947
- <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>
948
- </div>
949
- <div class="inline-flex items-center gap-2">
950
- <button class="btn btn-xs" :class="accountStrategy === 'sticky' ? 'bg-neon-purple text-white' : 'btn-outline text-gray-400'"
951
- @click="setAccountStrategy('sticky')" :disabled="!multiAccountRotationEnabled || strategySaving">Sticky</button>
952
- <button class="btn btn-xs" :class="accountStrategy === 'round-robin' ? 'bg-neon-purple text-white' : 'btn-outline text-gray-400'"
953
- @click="setAccountStrategy('round-robin')" :disabled="!multiAccountRotationEnabled || strategySaving">Round-Robin</button>
954
- </div>
955
- </div>
956
- </div>
957
808
  </div>
958
- </div>
809
+ </main>
810
+
811
+ <nav class="bottom-nav" aria-label="Primary navigation">
812
+ <button class="bottom-nav-item" :class="{'active': activeTab === 'dashboard'}" @click="setActiveTab('dashboard')" type="button" aria-label="Dashboard">
813
+ <span class="bottom-nav-icon">
814
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
815
+ <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" />
816
+ </svg>
817
+ </span>
818
+ <span class="bottom-nav-label">Dashboard</span>
819
+ </button>
820
+ <button class="bottom-nav-item" :class="{'active': activeTab === 'metrics'}" @click="setActiveTab('metrics')" type="button" aria-label="Metrics">
821
+ <span class="bottom-nav-icon">
822
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
823
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3v18h18M7 15l3-3 3 2 5-7" />
824
+ </svg>
825
+ </span>
826
+ <span class="bottom-nav-label">Metrics</span>
827
+ <span class="bottom-nav-badge" x-text="formatTokenCount(metricsTotals.totalTokens)"></span>
828
+ </button>
829
+ <button class="bottom-nav-item" :class="{'active': activeTab === 'logs'}" @click="setActiveTab('logs')" type="button" aria-label="Server Logs">
830
+ <span class="bottom-nav-icon">
831
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
832
+ <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" />
833
+ </svg>
834
+ </span>
835
+ <span class="bottom-nav-label">Logs</span>
836
+ <span class="bottom-nav-badge" x-text="logs.length"></span>
837
+ </button>
838
+ <button class="bottom-nav-item" :class="{'active': activeTab === 'account'}" @click="setActiveTab('account')" type="button" aria-label="Account">
839
+ <span class="bottom-nav-icon">
840
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
841
+ <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" />
842
+ </svg>
843
+ </span>
844
+ <span class="bottom-nav-label">Account</span>
845
+ <span class="bottom-nav-badge" x-text="stats.total"></span>
846
+ </button>
847
+ <button class="bottom-nav-item" :class="{'active': activeTab === 'settings'}" @click="setActiveTab('settings')" type="button" aria-label="Settings">
848
+ <span class="bottom-nav-icon">
849
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
850
+ <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" />
851
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
852
+ </svg>
853
+ </span>
854
+ <span class="bottom-nav-label">Settings</span>
855
+ </button>
856
+ </nav>
959
857
  </div>
960
858
 
961
859
  <div x-show="showAddModal" x-transition class="fixed inset-0 z-50 flex items-center justify-center p-4" style="display: none;">
962
860
  <div class="absolute inset-0 bg-black/70 backdrop-blur-sm" @click="showAddModal = false"></div>
963
861
  <div class="relative bg-space-900 border border-space-border rounded-xl shadow-2xl max-w-md w-full p-6">
964
- <h3 class="font-bold text-lg text-white mb-4">Add New Account</h3>
862
+ <h3 class="font-bold text-lg text-white mb-4">Configure Account</h3>
965
863
 
966
864
  <template x-if="!oauthManualMode">
967
865
  <div>
968
- <p class="text-sm text-gray-400 mb-6">Connect a ChatGPT account to use with the proxy. The account will be used for API calls.</p>
866
+ <p class="text-sm text-gray-400 mb-6">Connect your ChatGPT account to use with the proxy. Adding or importing an account replaces the existing local account.</p>
969
867
 
970
868
  <div class="flex flex-col gap-3">
971
869
  <button class="btn bg-neon-purple hover:bg-purple-600 border-none text-white btn-sm gap-3 h-11"
@@ -1038,7 +936,7 @@
1038
936
  <button class="btn btn-outline btn-sm text-gray-400" @click="oauthManualMode = false">Back</button>
1039
937
  <button class="btn bg-neon-purple hover:bg-purple-600 border-none text-white btn-sm"
1040
938
  @click="submitManualOAuth()" :disabled="!oauthManualCode">
1041
- Add Account
939
+ Configure Account
1042
940
  </button>
1043
941
  </div>
1044
942
  </div>
@@ -1100,12 +998,12 @@
1100
998
  <div class="mt-3 pt-3 border-t border-space-border/30 space-y-1">
1101
999
  <div class="flex items-center justify-between text-xs">
1102
1000
  <span class="text-gray-500">Reset window</span>
1103
- <span class="font-mono text-gray-300" x-text="quotaResetSummary(selectedAccount) || '-'"></span>
1001
+ <span class="quota-reset-summary font-mono text-gray-300" x-text="quotaResetSummary(selectedAccount) || '-'"></span>
1104
1002
  </div>
1105
1003
  <template x-if="quotaResetAtLabel(selectedAccount)">
1106
1004
  <div class="flex items-center justify-between text-xs">
1107
1005
  <span class="text-gray-500">Resets at</span>
1108
- <span class="font-mono text-gray-300" x-text="quotaResetAtLabel(selectedAccount)"></span>
1006
+ <span class="quota-reset-summary font-mono text-gray-300" x-text="quotaResetAtLabel(selectedAccount)"></span>
1109
1007
  </div>
1110
1008
  </template>
1111
1009
  </div>