@kamel-ahmed/proxy-claude 1.0.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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +622 -0
  3. package/bin/cli.js +124 -0
  4. package/package.json +80 -0
  5. package/public/app.js +228 -0
  6. package/public/css/src/input.css +523 -0
  7. package/public/css/style.css +1 -0
  8. package/public/favicon.svg +10 -0
  9. package/public/index.html +381 -0
  10. package/public/js/components/account-manager.js +245 -0
  11. package/public/js/components/claude-config.js +420 -0
  12. package/public/js/components/dashboard/charts.js +589 -0
  13. package/public/js/components/dashboard/filters.js +362 -0
  14. package/public/js/components/dashboard/stats.js +110 -0
  15. package/public/js/components/dashboard.js +236 -0
  16. package/public/js/components/logs-viewer.js +100 -0
  17. package/public/js/components/models.js +36 -0
  18. package/public/js/components/server-config.js +349 -0
  19. package/public/js/config/constants.js +102 -0
  20. package/public/js/data-store.js +386 -0
  21. package/public/js/settings-store.js +58 -0
  22. package/public/js/store.js +78 -0
  23. package/public/js/translations/en.js +351 -0
  24. package/public/js/translations/id.js +396 -0
  25. package/public/js/translations/pt.js +287 -0
  26. package/public/js/translations/tr.js +342 -0
  27. package/public/js/translations/zh.js +357 -0
  28. package/public/js/utils/account-actions.js +189 -0
  29. package/public/js/utils/error-handler.js +96 -0
  30. package/public/js/utils/model-config.js +42 -0
  31. package/public/js/utils/validators.js +77 -0
  32. package/public/js/utils.js +69 -0
  33. package/public/views/accounts.html +329 -0
  34. package/public/views/dashboard.html +484 -0
  35. package/public/views/logs.html +97 -0
  36. package/public/views/models.html +331 -0
  37. package/public/views/settings.html +1329 -0
  38. package/src/account-manager/credentials.js +243 -0
  39. package/src/account-manager/index.js +380 -0
  40. package/src/account-manager/onboarding.js +117 -0
  41. package/src/account-manager/rate-limits.js +237 -0
  42. package/src/account-manager/storage.js +136 -0
  43. package/src/account-manager/strategies/base-strategy.js +104 -0
  44. package/src/account-manager/strategies/hybrid-strategy.js +195 -0
  45. package/src/account-manager/strategies/index.js +79 -0
  46. package/src/account-manager/strategies/round-robin-strategy.js +76 -0
  47. package/src/account-manager/strategies/sticky-strategy.js +138 -0
  48. package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
  49. package/src/account-manager/strategies/trackers/index.js +8 -0
  50. package/src/account-manager/strategies/trackers/token-bucket-tracker.js +121 -0
  51. package/src/auth/database.js +169 -0
  52. package/src/auth/oauth.js +419 -0
  53. package/src/auth/token-extractor.js +117 -0
  54. package/src/cli/accounts.js +512 -0
  55. package/src/cli/refresh.js +201 -0
  56. package/src/cli/setup.js +338 -0
  57. package/src/cloudcode/index.js +29 -0
  58. package/src/cloudcode/message-handler.js +386 -0
  59. package/src/cloudcode/model-api.js +248 -0
  60. package/src/cloudcode/rate-limit-parser.js +181 -0
  61. package/src/cloudcode/request-builder.js +93 -0
  62. package/src/cloudcode/session-manager.js +47 -0
  63. package/src/cloudcode/sse-parser.js +121 -0
  64. package/src/cloudcode/sse-streamer.js +293 -0
  65. package/src/cloudcode/streaming-handler.js +492 -0
  66. package/src/config.js +107 -0
  67. package/src/constants.js +278 -0
  68. package/src/errors.js +238 -0
  69. package/src/fallback-config.js +29 -0
  70. package/src/format/content-converter.js +193 -0
  71. package/src/format/index.js +20 -0
  72. package/src/format/request-converter.js +248 -0
  73. package/src/format/response-converter.js +120 -0
  74. package/src/format/schema-sanitizer.js +673 -0
  75. package/src/format/signature-cache.js +88 -0
  76. package/src/format/thinking-utils.js +558 -0
  77. package/src/index.js +146 -0
  78. package/src/modules/usage-stats.js +205 -0
  79. package/src/server.js +861 -0
  80. package/src/utils/claude-config.js +245 -0
  81. package/src/utils/helpers.js +51 -0
  82. package/src/utils/logger.js +142 -0
  83. package/src/utils/native-module-helper.js +162 -0
  84. package/src/webui/index.js +707 -0
@@ -0,0 +1,484 @@
1
+ <div x-data="dashboard" class="view-container">
2
+ <!-- Compact Header -->
3
+ <div class="flex items-center justify-between gap-4 mb-6">
4
+ <!-- Title with inline subtitle -->
5
+ <div class="flex flex-wrap items-center gap-4">
6
+ <h1 class="text-2xl font-bold text-white tracking-tight" x-text="$store.global.t('dashboard')">
7
+ Dashboard
8
+ </h1>
9
+ <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">
10
+ <span class="text-[10px] font-mono text-gray-400 uppercase tracking-wider"
11
+ x-text="$store.global.t('systemDesc')">
12
+ CLAUDE PROXY SYSTEM
13
+ </span>
14
+ </div>
15
+ </div>
16
+
17
+ <!-- Compact Status Indicator -->
18
+ <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">
19
+ <div class="relative flex items-center justify-center">
20
+ <span class="absolute w-1.5 h-1.5 bg-neon-green rounded-full animate-ping opacity-75"></span>
21
+ <span class="relative w-1.5 h-1.5 bg-neon-green rounded-full"></span>
22
+ </div>
23
+ <span class="text-[10px] font-mono text-gray-500 uppercase tracking-wider" x-text="$store.global.t('live')">Live</span>
24
+ <span class="text-gray-700">•</span>
25
+ <span class="text-[10px] font-mono text-gray-400 tabular-nums"
26
+ x-text="new Date().toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', second: '2-digit'})">
27
+ </span>
28
+ </div>
29
+ </div>
30
+
31
+ <!-- Skeleton Loading (仅在首次加载时显示) -->
32
+ <div x-show="$store.data.initialLoad" class="space-y-6">
33
+ <!-- Skeleton Stats Grid -->
34
+ <div class="grid grid-cols-2 sm:grid-cols-5 gap-3">
35
+ <div class="skeleton-stat-card"></div>
36
+ <div class="skeleton-stat-card"></div>
37
+ <div class="skeleton-stat-card"></div>
38
+ <div class="skeleton-stat-card"></div>
39
+ <div class="skeleton-stat-card col-span-2 sm:col-span-1"></div>
40
+ </div>
41
+
42
+ <!-- Skeleton Charts -->
43
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
44
+ <div class="skeleton-chart"></div>
45
+ <div class="skeleton-chart"></div>
46
+ </div>
47
+ </div>
48
+
49
+ <!-- Actual Content (首次加载完成后显示) -->
50
+ <div x-show="!$store.data.initialLoad" class="space-y-6">
51
+ <!-- Stats Grid -->
52
+ <div class="grid grid-cols-2 sm:grid-cols-5 gap-2 lg:gap-3">
53
+ <div
54
+ class="stat bg-space-900/40 border border-space-border/30 rounded-xl p-3 lg:p-4 hover:border-cyan-500/30 hover:bg-cyan-500/5 transition-all duration-300 group relative cursor-pointer min-w-0"
55
+ @click="$store.global.activeTab = 'accounts'"
56
+ :title="$store.global.t('clickToViewAllAccounts')">
57
+ <!-- Icon -->
58
+ <div class="absolute top-3 right-3 text-gray-700/40 group-hover:text-cyan-400/70 transition-colors">
59
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 sm:w-5 sm:h-5 stroke-current">
60
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
61
+ 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 0z">
62
+ </path>
63
+ </svg>
64
+ </div>
65
+ <!-- Value -->
66
+ <div class="stat-value text-white font-mono text-2xl lg:text-3xl font-bold mb-1 truncate" x-text="stats.total"></div>
67
+ <!-- Title -->
68
+ <div class="stat-title text-gray-500 font-mono text-[10px] uppercase tracking-wider truncate"
69
+ x-text="$store.global.t('totalAccounts')"></div>
70
+ <!-- Desc -->
71
+ <div class="stat-desc text-cyan-400/60 text-[10px] truncate flex items-center gap-1">
72
+ <span x-text="$store.global.t('linkedAccounts')" class="truncate"></span>
73
+ <svg class="w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
74
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
75
+ </svg>
76
+ </div>
77
+ <!-- Tiers -->
78
+ <div class="flex items-center gap-1 mt-2 text-[10px] font-mono flex-wrap" x-show="stats.subscription">
79
+ <template x-if="stats.subscription?.ultra > 0">
80
+ <span class="px-1.5 py-0.5 rounded bg-yellow-500/10 text-yellow-400 border border-yellow-500/30">
81
+ <span x-text="stats.subscription.ultra"></span> <span x-text="$store.global.t('tierUltra')">Ultra</span>
82
+ </span>
83
+ </template>
84
+ <template x-if="stats.subscription?.pro > 0">
85
+ <span class="px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-400 border border-blue-500/30">
86
+ <span x-text="stats.subscription.pro"></span> <span x-text="$store.global.t('tierPro')">Pro</span>
87
+ </span>
88
+ </template>
89
+ <template x-if="stats.subscription?.free > 0">
90
+ <span class="px-1.5 py-0.5 rounded bg-gray-500/10 text-gray-400 border border-gray-500/30">
91
+ <span x-text="stats.subscription.free"></span> <span x-text="$store.global.t('tierFree')">Free</span>
92
+ </span>
93
+ </template>
94
+ </div>
95
+ </div>
96
+
97
+ <div
98
+ 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"
99
+ @click="$store.global.activeTab = 'models'"
100
+ :title="$store.global.t('clickToViewModels')">
101
+ <div class="absolute top-3 right-3 text-gray-700/40 group-hover:text-green-400/70 transition-colors">
102
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 sm:w-5 sm:h-5 stroke-current">
103
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
104
+ d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
105
+ </svg>
106
+ </div>
107
+ <div class="stat-value text-white font-mono text-2xl lg:text-3xl font-bold mb-1 truncate" x-text="stats.active"></div>
108
+ <div class="stat-title text-gray-500 font-mono text-[10px] uppercase tracking-wider truncate"
109
+ x-text="$store.global.t('active')"></div>
110
+ <div class="stat-desc text-green-400/60 text-[10px] truncate flex items-center gap-1">
111
+ <span x-text="$store.global.t('operational')" class="truncate"></span>
112
+ <svg class="w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
113
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
114
+ </svg>
115
+ </div>
116
+ </div>
117
+
118
+ <div
119
+ class="stat bg-space-900/40 border border-space-border/30 rounded-xl p-3 lg:p-4 hover:border-red-500/30 hover:bg-red-500/5 transition-all duration-300 group relative cursor-pointer min-w-0"
120
+ @click="$store.global.activeTab = 'accounts'"
121
+ :title="$store.global.t('clickToViewLimitedAccounts')">
122
+ <div class="absolute top-3 right-3 text-gray-700/40 group-hover:text-red-500/70 transition-colors">
123
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 sm:w-5 sm:h-5 stroke-current">
124
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
125
+ d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
126
+ </svg>
127
+ </div>
128
+ <div class="stat-value text-white font-mono text-2xl lg:text-3xl font-bold mb-1 truncate" x-text="stats.limited"></div>
129
+ <div class="stat-title text-gray-500 font-mono text-[10px] uppercase tracking-wider truncate"
130
+ x-text="$store.global.t('rateLimited')"></div>
131
+ <div class="stat-desc text-red-500/60 text-[10px] truncate flex items-center gap-1">
132
+ <span x-text="$store.global.t('cooldown')" class="truncate"></span>
133
+ <svg class="w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
134
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
135
+ </svg>
136
+ </div>
137
+ </div>
138
+
139
+ <div
140
+ class="stat bg-space-900/40 border border-space-border/30 rounded-xl p-3 lg:p-4 hover:border-orange-500/30 hover:bg-orange-500/5 transition-all duration-300 group relative cursor-pointer min-w-0"
141
+ @click="$store.global.activeTab = 'models'"
142
+ :title="$store.global.t('clickToViewModels')">
143
+ <div class="absolute top-3 right-3 text-gray-700/40 group-hover:text-orange-500/70 transition-colors">
144
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 sm:w-5 sm:h-5 stroke-current">
145
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
146
+ d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
147
+ </svg>
148
+ </div>
149
+ <div class="stat-value text-white font-mono text-2xl lg:text-3xl font-bold mb-1 truncate" x-text="stats.modelUsage ? stats.modelUsage.limited : 0"></div>
150
+ <div class="stat-title text-gray-500 font-mono text-[10px] lg:text-xs uppercase tracking-wider truncate"
151
+ x-text="$store.global.t('quotasDepletedTitle')"></div>
152
+ <div class="stat-desc text-orange-500/60 text-[10px] truncate flex items-center gap-1">
153
+ <span x-text="$store.global.t('outOfTracked', {total: stats.modelUsage ? stats.modelUsage.total : 0})" class="truncate"></span>
154
+ <svg class="w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
155
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
156
+ </svg>
157
+ </div>
158
+ </div>
159
+
160
+ <!-- Global Quota Chart -->
161
+ <div
162
+ class="stat bg-space-900/40 border border-space-border/30 rounded-xl p-3 xl:p-4 h-full flex flex-row sm:flex-col items-center justify-between gap-2 overflow-hidden relative group hover:border-space-border/60 transition-colors col-span-2 sm:col-span-1 min-w-0">
163
+ <!-- Chart Container -->
164
+ <div class="h-14 w-14 xl:h-16 xl:w-16 relative flex-shrink-0 self-center">
165
+ <canvas id="quotaChart"></canvas>
166
+ <div class="absolute inset-0 flex items-center justify-center pointer-events-none">
167
+ <div class="text-[10px] font-bold text-white font-mono" x-text="stats.overallHealth + '%'">%</div>
168
+ </div>
169
+ </div>
170
+
171
+ <!-- Legend / Info -->
172
+ <div class="flex flex-col justify-center gap-1 flex-grow min-w-0 w-full sm:text-center">
173
+ <div class="flex items-center justify-between sm:justify-center h-full">
174
+ <span class="text-[10px] text-gray-500 uppercase font-mono leading-tight whitespace-normal sm:px-1"
175
+ x-text="$store.global.t('globalQuota')">Global Quota</span>
176
+ </div>
177
+
178
+ <!-- Custom Legend -->
179
+ <div class="space-y-0.5 sm:flex sm:flex-col sm:items-center w-full">
180
+ <div class="flex items-center justify-between sm:justify-center sm:gap-2 text-[10px] text-gray-400 cursor-pointer hover:text-neon-purple transition-colors group/legend w-full sm:w-auto"
181
+ @click="$store.global.activeTab = 'models'; $nextTick(() => { $store.data.filters.family = 'claude'; $store.data.computeQuotaRows(); })"
182
+ :title="$store.global.t('clickToFilterClaude')">
183
+ <div class="flex items-center gap-1.5">
184
+ <div class="w-1.5 h-1.5 rounded-full bg-neon-purple flex-shrink-0"></div>
185
+ <span class="truncate" x-text="$store.global.t('familyClaude')">Claude</span>
186
+ <!-- Hidden arrow on desktop/stacked view to save space -->
187
+ <svg class="w-2.5 h-2.5 opacity-0 group-hover/legend:opacity-100 transition-opacity flex-shrink-0 sm:hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor">
188
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
189
+ </svg>
190
+ </div>
191
+ </div>
192
+ <div class="flex items-center justify-between sm:justify-center sm:gap-2 text-[10px] text-gray-400 cursor-pointer hover:text-neon-green transition-colors group/legend w-full sm:w-auto"
193
+ @click="$store.global.activeTab = 'models'; $nextTick(() => { $store.data.filters.family = 'gemini'; $store.data.computeQuotaRows(); })"
194
+ :title="$store.global.t('clickToFilterGemini')">
195
+ <div class="flex items-center gap-1.5">
196
+ <div class="w-1.5 h-1.5 rounded-full bg-neon-green flex-shrink-0"></div>
197
+ <span class="truncate" x-text="$store.global.t('familyGemini')">Gemini</span>
198
+ <!-- Hidden arrow on desktop/stacked view to save space -->
199
+ <svg class="w-2.5 h-2.5 opacity-0 group-hover/legend:opacity-100 transition-opacity flex-shrink-0 sm:hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor">
200
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
201
+ </svg>
202
+ </div>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </div>
207
+ </div>
208
+ <!-- Usage Trend Chart -->
209
+ <div class="view-card">
210
+ <!-- Header with Stats and Filter -->
211
+ <div class="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-6 mb-8">
212
+ <div class="flex flex-wrap items-center gap-5">
213
+ <div class="flex items-center gap-2.5">
214
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
215
+ class="w-4 h-4 text-neon-purple">
216
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
217
+ d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path>
218
+ </svg>
219
+ <h3 class="text-xs font-mono text-gray-400 uppercase tracking-widest whitespace-nowrap"
220
+ x-text="$store.global.t('requestVolume')">Request Volume</h3>
221
+ </div>
222
+
223
+ <!-- Usage Stats Pills -->
224
+ <div class="flex flex-wrap gap-2.5 text-[10px] font-mono">
225
+ <div class="px-2.5 py-1 rounded bg-space-850 border border-space-border/60 whitespace-nowrap">
226
+ <span class="text-gray-500" x-text="$store.global.t('totalColon')">Total:</span>
227
+ <span class="text-white ml-1 font-bold" x-text="usageStats.total"></span>
228
+ </div>
229
+ <div class="px-2.5 py-1 rounded bg-space-850 border border-space-border/60 whitespace-nowrap">
230
+ <span class="text-gray-500" x-text="$store.global.t('todayColon')">Today:</span>
231
+ <span class="text-neon-cyan ml-1 font-bold" x-text="usageStats.today"></span>
232
+ </div>
233
+ <div class="px-2.5 py-1 rounded bg-space-850 border border-space-border/60 whitespace-nowrap">
234
+ <span class="text-gray-500" x-text="$store.global.t('hour1Colon')">1H:</span>
235
+ <span class="text-neon-green ml-1 font-bold" x-text="usageStats.thisHour"></span>
236
+ </div>
237
+ </div>
238
+ </div>
239
+
240
+ <div class="flex items-center gap-2 sm:gap-3 w-full sm:w-auto justify-start sm:justify-end flex-wrap lg:flex-nowrap lg:gap-4 lg:bg-space-900/40 lg:p-1.5 lg:rounded-lg lg:border lg:border-space-border/30 lg:whitespace-nowrap lg:flex-shrink-0">
241
+ <!-- Time Range Dropdown -->
242
+ <div class="relative flex-1 sm:flex-none">
243
+ <button @click="showTimeRangeDropdown = !showTimeRangeDropdown; showDisplayModeDropdown = false; showModelFilter = false"
244
+ class="filter-control">
245
+ <svg class="w-3.5 h-3.5 lg:w-4 lg:h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
246
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
247
+ d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
248
+ </svg>
249
+ <span x-text="getTimeRangeLabel()"></span>
250
+ <svg class="w-3 h-3 transition-transform" :class="{'rotate-180': showTimeRangeDropdown}" fill="none"
251
+ viewBox="0 0 24 24" stroke="currentColor">
252
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
253
+ </svg>
254
+ </button>
255
+ <div x-show="showTimeRangeDropdown" @click.outside="showTimeRangeDropdown = false"
256
+ x-transition:enter="transition ease-out duration-100"
257
+ x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
258
+ x-transition:leave="transition ease-in duration-75"
259
+ x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
260
+ class="absolute right-0 mt-1 w-36 bg-space-900 border border-space-border rounded-lg shadow-xl z-50 py-1"
261
+ style="display: none;">
262
+ <button @click="setTimeRange('1h')" class="filter-control-item"
263
+ :class="timeRange === '1h' ? 'text-neon-cyan' : 'text-gray-400'"
264
+ x-text="$store.global.t('last1Hour')"></button>
265
+ <button @click="setTimeRange('6h')" class="filter-control-item"
266
+ :class="timeRange === '6h' ? 'text-neon-cyan' : 'text-gray-400'"
267
+ x-text="$store.global.t('last6Hours')"></button>
268
+ <button @click="setTimeRange('24h')" class="filter-control-item"
269
+ :class="timeRange === '24h' ? 'text-neon-cyan' : 'text-gray-400'"
270
+ x-text="$store.global.t('last24Hours')"></button>
271
+ <button @click="setTimeRange('7d')" class="filter-control-item"
272
+ :class="timeRange === '7d' ? 'text-neon-cyan' : 'text-gray-400'"
273
+ x-text="$store.global.t('last7Days')"></button>
274
+ <button @click="setTimeRange('all')" class="filter-control-item"
275
+ :class="timeRange === 'all' ? 'text-neon-cyan' : 'text-gray-400'"
276
+ x-text="$store.global.t('allTime')"></button>
277
+ </div>
278
+ </div>
279
+
280
+ <!-- Display Mode Dropdown -->
281
+ <div class="relative flex-1 sm:flex-none">
282
+ <button @click="showDisplayModeDropdown = !showDisplayModeDropdown; showTimeRangeDropdown = false; showModelFilter = false"
283
+ class="flex items-center justify-center gap-2 px-3 py-1.5 lg:px-4 lg:py-2 text-[10px] lg:text-xs font-mono font-medium text-gray-400 bg-space-800 lg:bg-transparent border border-space-border/50 lg:border-transparent rounded lg:rounded-md hover:text-white lg:hover:bg-space-800 hover:border-neon-purple/50 lg:hover:border-neon-purple/30 lg:hover:shadow-lg lg:hover:shadow-neon-purple/10 transition-all duration-200 whitespace-nowrap w-full sm:w-auto">
284
+ <svg class="w-3 h-3 lg:w-4 lg:h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
285
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
286
+ d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
287
+ </svg>
288
+ <span x-text="displayMode === 'family' ? $store.global.t('family') : $store.global.t('model')"></span>
289
+ <svg class="w-3 h-3 transition-transform" :class="{'rotate-180': showDisplayModeDropdown}" fill="none"
290
+ viewBox="0 0 24 24" stroke="currentColor">
291
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
292
+ </svg>
293
+ </button>
294
+ <div x-show="showDisplayModeDropdown" @click.outside="showDisplayModeDropdown = false"
295
+ x-transition:enter="transition ease-out duration-100"
296
+ x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
297
+ x-transition:leave="transition ease-in duration-75"
298
+ x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
299
+ class="absolute right-0 mt-1 w-32 bg-space-900 border border-space-border rounded-lg shadow-xl z-50 py-1"
300
+ style="display: none;">
301
+ <button @click="setDisplayMode('family')" class="filter-control-item"
302
+ :class="displayMode === 'family' ? 'text-neon-purple' : 'text-gray-400'"
303
+ x-text="$store.global.t('family')"></button>
304
+ <button @click="setDisplayMode('model')" class="filter-control-item"
305
+ :class="displayMode === 'model' ? 'text-neon-purple' : 'text-gray-400'"
306
+ x-text="$store.global.t('model')"></button>
307
+ </div>
308
+ </div>
309
+
310
+ <!-- Filter Dropdown -->
311
+ <div class="relative flex-1 sm:flex-none min-w-[120px]">
312
+ <button @click="showModelFilter = !showModelFilter; showTimeRangeDropdown = false; showDisplayModeDropdown = false"
313
+ class="flex items-center justify-center gap-2 px-3 py-1.5 lg:px-4 lg:py-2 text-[10px] lg:text-xs font-mono font-medium text-gray-400 bg-space-800 lg:bg-transparent border border-space-border/50 lg:border-transparent rounded lg:rounded-md hover:text-white lg:hover:bg-space-800 hover:border-neon-purple/50 lg:hover:border-neon-purple/30 lg:hover:shadow-lg lg:hover:shadow-neon-purple/10 transition-all duration-200 whitespace-nowrap w-full sm:w-auto">
314
+ <svg class="w-3 h-3 lg:w-4 lg:h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
315
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
316
+ d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
317
+ </svg>
318
+ <span x-text="$store.global.t('filter') + ' (' + getSelectedCount() + ')'">Filter (0/0)</span>
319
+ <svg class="w-3 h-3 transition-transform" :class="{'rotate-180': showModelFilter}" fill="none"
320
+ viewBox="0 0 24 24" stroke="currentColor">
321
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
322
+ </svg>
323
+ </button>
324
+
325
+ <!-- Dropdown Menu -->
326
+ <div x-show="showModelFilter" @click.outside="showModelFilter = false"
327
+ x-transition:enter="transition ease-out duration-100"
328
+ x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100"
329
+ x-transition:leave="transition ease-in duration-75"
330
+ x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
331
+ class="absolute right-0 mt-1 w-72 bg-space-900 border border-space-border rounded-lg shadow-xl z-50 overflow-hidden"
332
+ style="display: none;">
333
+
334
+ <!-- Header -->
335
+ <div
336
+ class="flex items-center justify-between px-3 py-2 border-b border-space-border/50 bg-space-800/50">
337
+ <span class="text-[10px] font-mono text-gray-500 uppercase"
338
+ x-text="displayMode === 'family' ? $store.global.t('selectFamilies') : $store.global.t('selectModels')"></span>
339
+ <div class="flex gap-1">
340
+ <button @click="autoSelectTopN(5)" class="text-[10px] text-neon-purple hover:underline"
341
+ :title="$store.global.t('smartTitle')" x-text="$store.global.t('frequentModels')">
342
+ Smart
343
+ </button>
344
+ <span class="text-gray-600">|</span>
345
+ <button @click="selectAll()" class="text-[10px] text-neon-cyan hover:underline"
346
+ x-text="$store.global.t('all')">All</button>
347
+ <span class="text-gray-600">|</span>
348
+ <button @click="deselectAll()" class="text-[10px] text-gray-500 hover:underline"
349
+ x-text="$store.global.t('none')">None</button>
350
+ </div>
351
+ </div>
352
+
353
+ <!-- Hierarchical List -->
354
+ <div class="max-h-64 overflow-y-auto p-2 space-y-2">
355
+ <template x-for="family in families" :key="family">
356
+ <div class="space-y-1">
357
+ <!-- Family Header -->
358
+ <label
359
+ class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-white/5 cursor-pointer group"
360
+ x-show="displayMode === 'family'">
361
+ <input type="checkbox" :checked="isFamilySelected(family)"
362
+ @change="toggleFamily(family)"
363
+ class="checkbox checkbox-xs checkbox-primary">
364
+ <div class="w-2 h-2 rounded-full flex-shrink-0"
365
+ :style="'background-color:' + getFamilyColor(family)"></div>
366
+ <span class="text-xs text-gray-300 font-medium group-hover:text-white"
367
+ x-text="$store.global.t('family' + family.charAt(0).toUpperCase() + family.slice(1))"></span>
368
+ <span class="text-[10px] text-gray-600 ml-auto"
369
+ x-text="'(' + (modelTree[family] || []).length + ')'"></span>
370
+ </label>
371
+
372
+ <!-- Family Section Header (Model Mode) -->
373
+ <div class="flex items-center gap-2 px-2 py-1 text-[10px] text-gray-500 uppercase font-bold"
374
+ x-show="displayMode === 'model'">
375
+ <div class="w-1.5 h-1.5 rounded-full"
376
+ :style="'background-color:' + getFamilyColor(family)"></div>
377
+ <span
378
+ x-text="$store.global.t('family' + family.charAt(0).toUpperCase() + family.slice(1))"></span>
379
+ </div>
380
+
381
+ <!-- Models in Family -->
382
+ <template x-if="displayMode === 'model'">
383
+ <div class="ml-4 space-y-0.5">
384
+ <template x-for="(model, modelIndex) in (modelTree[family] || [])"
385
+ :key="family + ':' + model">
386
+ <label
387
+ class="flex items-center gap-2 px-2 py-1 rounded hover:bg-white/5 cursor-pointer group">
388
+ <input type="checkbox" :checked="isModelSelected(family, model)"
389
+ @change="toggleModel(family, model)"
390
+ class="checkbox checkbox-xs checkbox-primary">
391
+ <div class="w-2 h-2 rounded-full flex-shrink-0"
392
+ :style="'background-color:' + getModelColor(family, modelIndex)">
393
+ </div>
394
+ <span class="text-xs text-gray-400 truncate group-hover:text-white"
395
+ x-text="model"></span>
396
+ </label>
397
+ </template>
398
+ </div>
399
+ </template>
400
+ </div>
401
+ </template>
402
+
403
+ <!-- Empty State -->
404
+ <div x-show="families.length === 0" class="text-center py-4 text-gray-600 text-xs"
405
+ x-text="$store.global.t('noDataTracked')">
406
+ No data tracked yet
407
+ </div>
408
+ </div>
409
+ </div>
410
+ </div>
411
+ </div>
412
+ </div>
413
+
414
+ <!-- Dynamic Legend -->
415
+ <div class="flex flex-wrap gap-3 mb-5"
416
+ x-show="displayMode === 'family' ? selectedFamilies.length > 0 : Object.values(selectedModels).flat().length > 0">
417
+ <!-- Family Mode Legend -->
418
+ <template x-if="displayMode === 'family'">
419
+ <template x-for="family in selectedFamilies" :key="family">
420
+ <div class="flex items-center gap-1.5 text-[10px] font-mono">
421
+ <div class="w-2 h-2 rounded-full" :style="'background-color:' + getFamilyColor(family)"></div>
422
+ <span class="text-gray-400"
423
+ x-text="$store.global.t('family' + family.charAt(0).toUpperCase() + family.slice(1))"></span>
424
+ </div>
425
+ </template>
426
+ </template>
427
+ <!-- Model Mode Legend -->
428
+ <template x-if="displayMode === 'model'">
429
+ <template x-for="family in families" :key="'legend-' + family">
430
+ <template x-for="(model, modelIndex) in (selectedModels[family] || [])" :key="family + ':' + model">
431
+ <div class="flex items-center gap-1.5 text-[10px] font-mono">
432
+ <div class="w-2 h-2 rounded-full"
433
+ :style="'background-color:' + getModelColor(family, modelIndex)"></div>
434
+ <span class="text-gray-400" x-text="model"></span>
435
+ </div>
436
+ </template>
437
+ </template>
438
+ </template>
439
+ </div>
440
+
441
+ <!-- Chart -->
442
+ <div class="h-48 w-full relative">
443
+ <canvas id="usageTrendChart"></canvas>
444
+
445
+ <!-- Overall Loading State -->
446
+ <div x-show="!stats.hasTrendData"
447
+ class="absolute inset-0 flex items-center justify-center bg-space-900/50 backdrop-blur-sm z-10"
448
+ style="display: none;">
449
+ <div class="text-xs font-mono text-gray-500 flex items-center gap-2">
450
+ <span class="loading loading-spinner loading-xs"></span>
451
+ <span x-text="$store.global.t('syncing')">SYNCING...</span>
452
+ </div>
453
+ </div>
454
+
455
+ <!-- Empty State (After Filtering) -->
456
+ <div x-show="stats.hasTrendData && !hasFilteredTrendData"
457
+ class="absolute inset-0 flex flex-col items-center justify-center bg-space-900/30 z-10"
458
+ style="display: none;">
459
+ <div class="flex flex-col items-center gap-4 animate-fade-in">
460
+ <div class="w-12 h-12 rounded-full bg-space-850 flex items-center justify-center text-gray-600 border border-space-border/50">
461
+ <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
462
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
463
+ d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
464
+ </svg>
465
+ </div>
466
+ <div class="text-xs font-mono text-gray-500 text-center">
467
+ <p x-text="$store.global.t('noDataTracked')">No data tracked yet</p>
468
+ <p class="text-[10px] opacity-60 mt-1" x-text="'[' + getTimeRangeLabel() + ']'"></p>
469
+ </div>
470
+ </div>
471
+ </div>
472
+
473
+ <!-- No Selection State -->
474
+ <div x-show="stats.hasTrendData && hasFilteredTrendData && (displayMode === 'family' ? selectedFamilies.length === 0 : Object.values(selectedModels).flat().length === 0)"
475
+ class="absolute inset-0 flex items-center justify-center bg-space-900/30 z-10"
476
+ style="display: none;">
477
+ <div class="text-xs font-mono text-gray-500"
478
+ x-text="displayMode === 'family' ? $store.global.t('selectFamilies') : $store.global.t('selectModels')">
479
+ </div>
480
+ </div>
481
+ </div>
482
+ </div>
483
+ </div> <!-- End of x-show="!$store.data.loading" -->
484
+ </div>
@@ -0,0 +1,97 @@
1
+ <div x-data="logsViewer" class="view-container h-full flex flex-col">
2
+ <div class="view-card !p-0 flex flex-col flex-1 min-h-0">
3
+ <!-- Toolbar -->
4
+ <div class="bg-space-900 flex flex-wrap gap-y-2 justify-between items-center p-2 px-4 border-b border-space-border select-none min-h-[48px] shrink-0">
5
+
6
+ <!-- Left: Decor & Title -->
7
+ <div class="flex items-center gap-3 shrink-0">
8
+ <div class="flex gap-2">
9
+ <div class="w-3 h-3 rounded-full bg-red-500/20 border border-red-500/50"></div>
10
+ <div class="w-3 h-3 rounded-full bg-yellow-500/20 border border-yellow-500/50"></div>
11
+ <div class="w-3 h-3 rounded-full bg-green-500/20 border border-green-500/50"></div>
12
+ </div>
13
+ <span class="text-xs font-mono text-gray-500 hidden sm:inline-block">~/logs</span>
14
+ </div>
15
+
16
+ <!-- Center: Search & Filters -->
17
+ <div class="flex-1 flex items-center justify-center gap-4 px-4 min-w-0">
18
+ <!-- Search -->
19
+ <div class="relative w-full max-w-xs group">
20
+ <div class="absolute inset-y-0 left-0 pl-2 flex items-center pointer-events-none">
21
+ <svg class="h-3 w-3 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
22
+ <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" />
23
+ </svg>
24
+ </div>
25
+ <input type="text" x-model="searchQuery" :placeholder="$store.global.t('grepLogs')"
26
+ 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">
27
+ </div>
28
+
29
+ <!-- Filters -->
30
+ <div class="hidden md:flex gap-3 text-[10px] font-mono font-bold uppercase select-none">
31
+ <label class="flex items-center gap-1.5 cursor-pointer text-blue-400 opacity-50 hover:opacity-100 transition-opacity" :class="{'opacity-100': filters.INFO}">
32
+ <input type="checkbox" class="checkbox checkbox-xs checkbox-info rounded-[2px] w-3 h-3 border-blue-400/50" x-model="filters.INFO"> <span x-text="$store.global.t('logLevelInfo')">INFO</span>
33
+ </label>
34
+ <label class="flex items-center gap-1.5 cursor-pointer text-neon-green opacity-50 hover:opacity-100 transition-opacity" :class="{'opacity-100': filters.SUCCESS}">
35
+ <input type="checkbox" class="checkbox checkbox-xs checkbox-success rounded-[2px] w-3 h-3 border-neon-green/50" x-model="filters.SUCCESS"> <span x-text="$store.global.t('logLevelSuccess')">SUCCESS</span>
36
+ </label>
37
+ <label class="flex items-center gap-1.5 cursor-pointer text-yellow-400 opacity-50 hover:opacity-100 transition-opacity" :class="{'opacity-100': filters.WARN}">
38
+ <input type="checkbox" class="checkbox checkbox-xs checkbox-warning rounded-[2px] w-3 h-3 border-yellow-400/50" x-model="filters.WARN"> <span x-text="$store.global.t('logLevelWarn')">WARN</span>
39
+ </label>
40
+ <label class="flex items-center gap-1.5 cursor-pointer text-red-500 opacity-50 hover:opacity-100 transition-opacity" :class="{'opacity-100': filters.ERROR}">
41
+ <input type="checkbox" class="checkbox checkbox-xs checkbox-error rounded-[2px] w-3 h-3 border-red-500/50" x-model="filters.ERROR"> <span x-text="$store.global.t('logLevelError')">ERR</span>
42
+ </label>
43
+ </div>
44
+ </div>
45
+
46
+ <!-- Right: Controls -->
47
+ <div class="flex items-center gap-4 shrink-0">
48
+ <div class="text-[10px] font-mono text-gray-600 hidden lg:block">
49
+ <span x-text="filteredLogs.length"></span>/<span x-text="logs.length"></span>
50
+ </div>
51
+ <label class="cursor-pointer flex items-center gap-2">
52
+ <span class="text-[10px] font-mono text-gray-500 uppercase hidden sm:inline-block"
53
+ x-text="$store.global.t('autoScroll')">Auto-Scroll</span>
54
+ <input type="checkbox" class="toggle toggle-xs toggle-success" x-model="isAutoScroll">
55
+ </label>
56
+ <button class="btn-action-ghost-square" @click="clearLogs" :title="$store.global.t('clearLogs')">
57
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
58
+ <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" />
59
+ </svg>
60
+ </button>
61
+ </div>
62
+ </div>
63
+
64
+ <!-- Log Content -->
65
+ <div id="logs-container" class="flex-1 overflow-auto p-4 font-mono text-[11px] leading-relaxed bg-space-950 custom-scrollbar">
66
+ <template x-for="(log, idx) in filteredLogs" :key="idx">
67
+ <div class="flex gap-4 px-2 py-0.5 -mx-2 hover:bg-white/[0.03] transition-colors group">
68
+ <!-- Timestamp: Muted & Fixed Width -->
69
+ <span class="text-zinc-600 w-16 shrink-0 select-none group-hover:text-zinc-500 transition-colors"
70
+ x-text="new Date(log.timestamp).toLocaleTimeString([], {hour12:false})"></span>
71
+
72
+ <!-- Level: Tag Style -->
73
+ <div class="w-14 shrink-0 flex items-center">
74
+ <span class="px-1.5 py-0.5 rounded-[2px] text-[10px] font-bold uppercase tracking-wider leading-none border"
75
+ :class="{
76
+ 'bg-blue-500/10 text-blue-400 border-blue-500/20': log.level === 'INFO',
77
+ 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20': log.level === 'WARN',
78
+ 'bg-red-500/10 text-red-500 border-red-500/20': log.level === 'ERROR',
79
+ 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20': log.level === 'SUCCESS',
80
+ 'bg-purple-500/10 text-purple-400 border-purple-500/20': log.level === 'DEBUG'
81
+ }" x-text="log.level"></span>
82
+ </div>
83
+
84
+ <!-- Message: Clean & High Contrast -->
85
+ <span class="text-zinc-300 break-all group-hover:text-white transition-colors flex-1"
86
+ x-html="log.message.replace(/\n/g, '<br>')"></span>
87
+ </div>
88
+ </template>
89
+ <!-- Blinking Cursor -->
90
+ <div class="h-3 w-1.5 bg-zinc-600 animate-pulse mt-1 inline-block" x-show="filteredLogs.length === logs.length && !searchQuery"></div>
91
+ <div x-show="filteredLogs.length === 0 && logs.length > 0" class="text-zinc-700 italic mt-8 text-center"
92
+ x-text="$store.global.t('noLogsMatch')">
93
+ No logs match filter
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>