commons-proxy 2.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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +757 -0
  3. package/bin/cli.js +146 -0
  4. package/package.json +97 -0
  5. package/public/Complaint Details.pdf +0 -0
  6. package/public/Cyber Crime Portal.pdf +0 -0
  7. package/public/app.js +229 -0
  8. package/public/css/src/input.css +523 -0
  9. package/public/css/style.css +1 -0
  10. package/public/favicon.png +0 -0
  11. package/public/index.html +549 -0
  12. package/public/js/components/account-manager.js +356 -0
  13. package/public/js/components/add-account-modal.js +414 -0
  14. package/public/js/components/claude-config.js +420 -0
  15. package/public/js/components/dashboard/charts.js +605 -0
  16. package/public/js/components/dashboard/filters.js +362 -0
  17. package/public/js/components/dashboard/stats.js +110 -0
  18. package/public/js/components/dashboard.js +236 -0
  19. package/public/js/components/logs-viewer.js +100 -0
  20. package/public/js/components/models.js +36 -0
  21. package/public/js/components/server-config.js +349 -0
  22. package/public/js/config/constants.js +102 -0
  23. package/public/js/data-store.js +375 -0
  24. package/public/js/settings-store.js +58 -0
  25. package/public/js/store.js +99 -0
  26. package/public/js/translations/en.js +367 -0
  27. package/public/js/translations/id.js +412 -0
  28. package/public/js/translations/pt.js +308 -0
  29. package/public/js/translations/tr.js +358 -0
  30. package/public/js/translations/zh.js +373 -0
  31. package/public/js/utils/account-actions.js +189 -0
  32. package/public/js/utils/error-handler.js +96 -0
  33. package/public/js/utils/model-config.js +42 -0
  34. package/public/js/utils/ui-logger.js +143 -0
  35. package/public/js/utils/validators.js +77 -0
  36. package/public/js/utils.js +69 -0
  37. package/public/proxy-server-64.png +0 -0
  38. package/public/views/accounts.html +361 -0
  39. package/public/views/dashboard.html +484 -0
  40. package/public/views/logs.html +97 -0
  41. package/public/views/models.html +331 -0
  42. package/public/views/settings.html +1327 -0
  43. package/src/account-manager/credentials.js +378 -0
  44. package/src/account-manager/index.js +462 -0
  45. package/src/account-manager/onboarding.js +112 -0
  46. package/src/account-manager/rate-limits.js +369 -0
  47. package/src/account-manager/storage.js +160 -0
  48. package/src/account-manager/strategies/base-strategy.js +109 -0
  49. package/src/account-manager/strategies/hybrid-strategy.js +339 -0
  50. package/src/account-manager/strategies/index.js +79 -0
  51. package/src/account-manager/strategies/round-robin-strategy.js +76 -0
  52. package/src/account-manager/strategies/sticky-strategy.js +138 -0
  53. package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
  54. package/src/account-manager/strategies/trackers/index.js +9 -0
  55. package/src/account-manager/strategies/trackers/quota-tracker.js +120 -0
  56. package/src/account-manager/strategies/trackers/token-bucket-tracker.js +155 -0
  57. package/src/auth/database.js +169 -0
  58. package/src/auth/oauth.js +548 -0
  59. package/src/auth/token-extractor.js +117 -0
  60. package/src/cli/accounts.js +648 -0
  61. package/src/cloudcode/index.js +29 -0
  62. package/src/cloudcode/message-handler.js +510 -0
  63. package/src/cloudcode/model-api.js +248 -0
  64. package/src/cloudcode/rate-limit-parser.js +235 -0
  65. package/src/cloudcode/request-builder.js +93 -0
  66. package/src/cloudcode/session-manager.js +47 -0
  67. package/src/cloudcode/sse-parser.js +121 -0
  68. package/src/cloudcode/sse-streamer.js +293 -0
  69. package/src/cloudcode/streaming-handler.js +615 -0
  70. package/src/config.js +125 -0
  71. package/src/constants.js +407 -0
  72. package/src/errors.js +242 -0
  73. package/src/fallback-config.js +29 -0
  74. package/src/format/content-converter.js +193 -0
  75. package/src/format/index.js +20 -0
  76. package/src/format/request-converter.js +255 -0
  77. package/src/format/response-converter.js +120 -0
  78. package/src/format/schema-sanitizer.js +673 -0
  79. package/src/format/signature-cache.js +88 -0
  80. package/src/format/thinking-utils.js +648 -0
  81. package/src/index.js +148 -0
  82. package/src/modules/usage-stats.js +205 -0
  83. package/src/providers/anthropic-provider.js +258 -0
  84. package/src/providers/base-provider.js +157 -0
  85. package/src/providers/cloudcode.js +94 -0
  86. package/src/providers/copilot.js +399 -0
  87. package/src/providers/github-provider.js +287 -0
  88. package/src/providers/google-provider.js +192 -0
  89. package/src/providers/index.js +211 -0
  90. package/src/providers/openai-compatible.js +265 -0
  91. package/src/providers/openai-provider.js +271 -0
  92. package/src/providers/openrouter-provider.js +325 -0
  93. package/src/providers/setup.js +83 -0
  94. package/src/server.js +870 -0
  95. package/src/utils/claude-config.js +245 -0
  96. package/src/utils/helpers.js +51 -0
  97. package/src/utils/logger.js +142 -0
  98. package/src/utils/native-module-helper.js +162 -0
  99. package/src/webui/index.js +1134 -0
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Dashboard Filters Module
3
+ * 职责:管理图表筛选器的状态和持久化
4
+ *
5
+ * 功能:
6
+ * 1. 时间范围筛选(1h/6h/24h/7d/all)
7
+ * 2. 显示模式切换(按家族/按模型)
8
+ * 3. 模型/家族多选筛选
9
+ * 4. 筛选器状态持久化到 localStorage
10
+ *
11
+ * 调用时机:
12
+ * - 组件初始化时加载用户偏好
13
+ * - 筛选器变化时保存并触发图表更新
14
+ *
15
+ * 持久化键:
16
+ * - localStorage['dashboard_chart_prefs']
17
+ *
18
+ * @module DashboardFilters
19
+ */
20
+ window.DashboardFilters = window.DashboardFilters || {};
21
+
22
+ /**
23
+ * Get initial filter state
24
+ * @returns {object} Initial state for filter properties
25
+ */
26
+ window.DashboardFilters.getInitialState = function() {
27
+ return {
28
+ timeRange: '24h', // '1h', '6h', '24h', '7d', 'all'
29
+ displayMode: 'model',
30
+ selectedFamilies: [],
31
+ selectedModels: {},
32
+ showModelFilter: false,
33
+ showTimeRangeDropdown: false,
34
+ showDisplayModeDropdown: false
35
+ };
36
+ };
37
+
38
+ /**
39
+ * Load filter preferences from localStorage
40
+ * @param {object} component - Dashboard component instance
41
+ */
42
+ window.DashboardFilters.loadPreferences = function(component) {
43
+ try {
44
+ const saved = localStorage.getItem('dashboard_chart_prefs');
45
+ if (saved) {
46
+ const prefs = JSON.parse(saved);
47
+ component.timeRange = prefs.timeRange || '24h';
48
+ component.displayMode = prefs.displayMode || 'model';
49
+ component.selectedFamilies = prefs.selectedFamilies || [];
50
+ component.selectedModels = prefs.selectedModels || {};
51
+ }
52
+ } catch (e) {
53
+ if (window.UILogger) window.UILogger.debug('Failed to load dashboard preferences:', e.message);
54
+ }
55
+ };
56
+
57
+ /**
58
+ * Save filter preferences to localStorage
59
+ * @param {object} component - Dashboard component instance
60
+ */
61
+ window.DashboardFilters.savePreferences = function(component) {
62
+ try {
63
+ localStorage.setItem('dashboard_chart_prefs', JSON.stringify({
64
+ timeRange: component.timeRange,
65
+ displayMode: component.displayMode,
66
+ selectedFamilies: component.selectedFamilies,
67
+ selectedModels: component.selectedModels
68
+ }));
69
+ } catch (e) {
70
+ if (window.UILogger) window.UILogger.debug('Failed to save dashboard preferences:', e.message);
71
+ }
72
+ };
73
+
74
+ /**
75
+ * Set display mode (family or model)
76
+ * @param {object} component - Dashboard component instance
77
+ * @param {string} mode - 'family' or 'model'
78
+ */
79
+ window.DashboardFilters.setDisplayMode = function(component, mode) {
80
+ component.displayMode = mode;
81
+ component.showDisplayModeDropdown = false;
82
+ window.DashboardFilters.savePreferences(component);
83
+ // updateTrendChart uses debounce internally, call directly
84
+ component.updateTrendChart();
85
+ };
86
+
87
+ /**
88
+ * Set time range filter
89
+ * @param {object} component - Dashboard component instance
90
+ * @param {string} range - '1h', '6h', '24h', '7d', 'all'
91
+ */
92
+ window.DashboardFilters.setTimeRange = function(component, range) {
93
+ component.timeRange = range;
94
+ component.showTimeRangeDropdown = false;
95
+ window.DashboardFilters.savePreferences(component);
96
+ component.updateTrendChart();
97
+ };
98
+
99
+ /**
100
+ * Get time range cutoff timestamp
101
+ * @param {string} range - Time range code
102
+ * @returns {number|null} Cutoff timestamp or null for 'all'
103
+ */
104
+ window.DashboardFilters.getTimeRangeCutoff = function(range) {
105
+ const now = Date.now();
106
+ switch (range) {
107
+ case '1h': return now - 1 * 60 * 60 * 1000;
108
+ case '6h': return now - 6 * 60 * 60 * 1000;
109
+ case '24h': return now - 24 * 60 * 60 * 1000;
110
+ case '7d': return now - 7 * 24 * 60 * 60 * 1000;
111
+ default: return null; // 'all'
112
+ }
113
+ };
114
+
115
+ /**
116
+ * Get filtered history data based on time range
117
+ * @param {object} component - Dashboard component instance
118
+ * @returns {object} Filtered history data
119
+ */
120
+ window.DashboardFilters.getFilteredHistoryData = function(component) {
121
+ const history = component.historyData;
122
+ if (!history || Object.keys(history).length === 0) return {};
123
+
124
+ const cutoff = window.DashboardFilters.getTimeRangeCutoff(component.timeRange);
125
+ if (!cutoff) return history; // 'all' - return everything
126
+
127
+ const filtered = {};
128
+ Object.entries(history).forEach(([iso, data]) => {
129
+ const timestamp = new Date(iso).getTime();
130
+ if (timestamp >= cutoff) {
131
+ filtered[iso] = data;
132
+ }
133
+ });
134
+ return filtered;
135
+ };
136
+
137
+ /**
138
+ * Get time range label for display
139
+ * @param {object} component - Dashboard component instance
140
+ * @returns {string} Translated label
141
+ */
142
+ window.DashboardFilters.getTimeRangeLabel = function(component) {
143
+ const store = Alpine.store('global');
144
+ switch (component.timeRange) {
145
+ case '1h': return store.t('last1Hour');
146
+ case '6h': return store.t('last6Hours');
147
+ case '24h': return store.t('last24Hours');
148
+ case '7d': return store.t('last7Days');
149
+ default: return store.t('allTime');
150
+ }
151
+ };
152
+
153
+ /**
154
+ * Toggle family selection
155
+ * @param {object} component - Dashboard component instance
156
+ * @param {string} family - Family name (e.g., 'claude', 'gemini')
157
+ */
158
+ window.DashboardFilters.toggleFamily = function(component, family) {
159
+ const index = component.selectedFamilies.indexOf(family);
160
+ if (index > -1) {
161
+ component.selectedFamilies.splice(index, 1);
162
+ } else {
163
+ component.selectedFamilies.push(family);
164
+ }
165
+ window.DashboardFilters.savePreferences(component);
166
+ // updateTrendChart uses debounce internally, call directly
167
+ component.updateTrendChart();
168
+ };
169
+
170
+ /**
171
+ * Toggle model selection within a family
172
+ * @param {object} component - Dashboard component instance
173
+ * @param {string} family - Family name
174
+ * @param {string} model - Model name
175
+ */
176
+ window.DashboardFilters.toggleModel = function(component, family, model) {
177
+ if (!component.selectedModels[family]) {
178
+ component.selectedModels[family] = [];
179
+ }
180
+ const index = component.selectedModels[family].indexOf(model);
181
+ if (index > -1) {
182
+ component.selectedModels[family].splice(index, 1);
183
+ } else {
184
+ component.selectedModels[family].push(model);
185
+ }
186
+ window.DashboardFilters.savePreferences(component);
187
+ // updateTrendChart uses debounce internally, call directly
188
+ component.updateTrendChart();
189
+ };
190
+
191
+ /**
192
+ * Check if family is selected
193
+ * @param {object} component - Dashboard component instance
194
+ * @param {string} family - Family name
195
+ * @returns {boolean}
196
+ */
197
+ window.DashboardFilters.isFamilySelected = function(component, family) {
198
+ return component.selectedFamilies.includes(family);
199
+ };
200
+
201
+ /**
202
+ * Check if model is selected
203
+ * @param {object} component - Dashboard component instance
204
+ * @param {string} family - Family name
205
+ * @param {string} model - Model name
206
+ * @returns {boolean}
207
+ */
208
+ window.DashboardFilters.isModelSelected = function(component, family, model) {
209
+ return component.selectedModels[family]?.includes(model) || false;
210
+ };
211
+
212
+ /**
213
+ * Select all families and models
214
+ * @param {object} component - Dashboard component instance
215
+ */
216
+ window.DashboardFilters.selectAll = function(component) {
217
+ component.selectedFamilies = [...component.families];
218
+ component.families.forEach(family => {
219
+ component.selectedModels[family] = [...(component.modelTree[family] || [])];
220
+ });
221
+ window.DashboardFilters.savePreferences(component);
222
+ // updateTrendChart uses debounce internally, call directly
223
+ component.updateTrendChart();
224
+ };
225
+
226
+ /**
227
+ * Deselect all families and models
228
+ * @param {object} component - Dashboard component instance
229
+ */
230
+ window.DashboardFilters.deselectAll = function(component) {
231
+ component.selectedFamilies = [];
232
+ component.selectedModels = {};
233
+ window.DashboardFilters.savePreferences(component);
234
+ // updateTrendChart uses debounce internally, call directly
235
+ component.updateTrendChart();
236
+ };
237
+
238
+ /**
239
+ * Get color for a family
240
+ * @param {string} family - Family name
241
+ * @returns {string} Color value
242
+ */
243
+ window.DashboardFilters.getFamilyColor = function(family) {
244
+ const FAMILY_COLORS = window.DashboardConstants?.FAMILY_COLORS || {};
245
+ return FAMILY_COLORS[family] || FAMILY_COLORS.other;
246
+ };
247
+
248
+ /**
249
+ * Get color for a model (with index for variation within family)
250
+ * @param {string} family - Family name
251
+ * @param {number} modelIndex - Index of model within family
252
+ * @returns {string} Color value
253
+ */
254
+ window.DashboardFilters.getModelColor = function(family, modelIndex) {
255
+ const MODEL_COLORS = window.DashboardConstants?.MODEL_COLORS || [];
256
+ const baseIndex = family === 'claude' ? 0 : (family === 'gemini' ? 4 : (family === 'gpt' ? 8 : (family === 'o1' ? 12 : 8)));
257
+ return MODEL_COLORS[(baseIndex + modelIndex) % MODEL_COLORS.length];
258
+ };
259
+
260
+ /**
261
+ * Get count of selected items for display
262
+ * @param {object} component - Dashboard component instance
263
+ * @returns {string} Selected count string (e.g., "3/5")
264
+ */
265
+ window.DashboardFilters.getSelectedCount = function(component) {
266
+ if (component.displayMode === 'family') {
267
+ return `${component.selectedFamilies.length}/${component.families.length}`;
268
+ }
269
+ let selected = 0, total = 0;
270
+ component.families.forEach(family => {
271
+ const models = component.modelTree[family] || [];
272
+ total += models.length;
273
+ selected += (component.selectedModels[family] || []).length;
274
+ });
275
+ return `${selected}/${total}`;
276
+ };
277
+
278
+ /**
279
+ * Auto-select new families/models that haven't been configured
280
+ * @param {object} component - Dashboard component instance
281
+ */
282
+ window.DashboardFilters.autoSelectNew = function(component) {
283
+ // If no preferences saved, select all
284
+ if (component.selectedFamilies.length === 0 && Object.keys(component.selectedModels).length === 0) {
285
+ component.selectedFamilies = [...component.families];
286
+ component.families.forEach(family => {
287
+ component.selectedModels[family] = [...(component.modelTree[family] || [])];
288
+ });
289
+ window.DashboardFilters.savePreferences(component);
290
+ return;
291
+ }
292
+
293
+ // Add new families/models that appeared
294
+ component.families.forEach(family => {
295
+ if (!component.selectedFamilies.includes(family)) {
296
+ component.selectedFamilies.push(family);
297
+ }
298
+ if (!component.selectedModels[family]) {
299
+ component.selectedModels[family] = [];
300
+ }
301
+ (component.modelTree[family] || []).forEach(model => {
302
+ if (!component.selectedModels[family].includes(model)) {
303
+ component.selectedModels[family].push(model);
304
+ }
305
+ });
306
+ });
307
+ };
308
+
309
+ /**
310
+ * Auto-select top N models by usage (past 24 hours)
311
+ * @param {object} component - Dashboard component instance
312
+ * @param {number} n - Number of models to select (default: 5)
313
+ */
314
+ window.DashboardFilters.autoSelectTopN = function(component, n = 5) {
315
+ // Calculate usage for each model over past 24 hours
316
+ const usage = {};
317
+ const now = Date.now();
318
+ const dayAgo = now - 24 * 60 * 60 * 1000;
319
+
320
+ Object.entries(component.historyData).forEach(([iso, hourData]) => {
321
+ const timestamp = new Date(iso).getTime();
322
+ if (timestamp < dayAgo) return;
323
+
324
+ Object.entries(hourData).forEach(([family, familyData]) => {
325
+ if (typeof familyData === 'object' && family !== '_total') {
326
+ Object.entries(familyData).forEach(([model, count]) => {
327
+ if (model !== '_subtotal') {
328
+ const key = `${family}:${model}`;
329
+ usage[key] = (usage[key] || 0) + count;
330
+ }
331
+ });
332
+ }
333
+ });
334
+ });
335
+
336
+ // Sort by usage and take top N
337
+ const sorted = Object.entries(usage)
338
+ .sort((a, b) => b[1] - a[1])
339
+ .slice(0, n);
340
+
341
+ // Clear current selection
342
+ component.selectedFamilies = [];
343
+ component.selectedModels = {};
344
+
345
+ // Select top models and their families
346
+ sorted.forEach(([key, _]) => {
347
+ const [family, model] = key.split(':');
348
+ if (!component.selectedFamilies.includes(family)) {
349
+ component.selectedFamilies.push(family);
350
+ }
351
+ if (!component.selectedModels[family]) {
352
+ component.selectedModels[family] = [];
353
+ }
354
+ if (!component.selectedModels[family].includes(model)) {
355
+ component.selectedModels[family].push(model);
356
+ }
357
+ });
358
+
359
+ window.DashboardFilters.savePreferences(component);
360
+ // updateTrendChart uses debounce internally, call directly
361
+ component.updateTrendChart();
362
+ };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Dashboard Stats Module
3
+ * 职责:根据 Alpine.store('data') 计算账号统计数据
4
+ *
5
+ * 调用时机:
6
+ * - dashboard 组件 init() 时
7
+ * - $store.data 更新时(通过 $watch 监听)
8
+ *
9
+ * 统计维度:
10
+ * - total: 启用账号总数(排除禁用账号)
11
+ * - active: 有可用配额的账号数
12
+ * - limited: 配额受限或失效的账号数
13
+ * - subscription: 按订阅级别分类(ultra/pro/free)
14
+ *
15
+ * @module DashboardStats
16
+ */
17
+ window.DashboardStats = window.DashboardStats || {};
18
+
19
+ /**
20
+ * 更新账号统计数据
21
+ *
22
+ * 统计逻辑:
23
+ * 1. 仅统计启用的账号(enabled !== false)
24
+ * 2. 检查账号下所有追踪模型的配额
25
+ * 3. 如果任一追踪模型配额 <= 5%,则标记为 limited (Rate Limited Cooldown)
26
+ * 4. 如果所有追踪模型配额 > 5%,则标记为 active
27
+ * 5. 状态非 'ok' 的账号归为 limited
28
+ *
29
+ * @param {object} component - Dashboard 组件实例(Alpine.js 上下文)
30
+ * @param {object} component.stats - 统计数据对象(会被修改)
31
+ * @param {number} component.stats.total - 启用账号总数
32
+ * @param {number} component.stats.active - 活跃账号数
33
+ * @param {number} component.stats.limited - 受限账号数
34
+ * @param {object} component.stats.subscription - 订阅级别分布
35
+ * @returns {void}
36
+ */
37
+ window.DashboardStats.updateStats = function(component) {
38
+ const accounts = Alpine.store('data').accounts;
39
+ let active = 0, limited = 0;
40
+
41
+ // Only count enabled accounts in statistics
42
+ const enabledAccounts = accounts.filter(acc => acc.enabled !== false);
43
+
44
+ enabledAccounts.forEach(acc => {
45
+ if (acc.status === 'ok') {
46
+ const limits = Object.entries(acc.limits || {});
47
+
48
+ if (limits.length === 0) {
49
+ // No limit data available, consider limited to be safe
50
+ limited++;
51
+ return;
52
+ }
53
+
54
+ // Check if ANY tracked model is rate limited (<= 5%)
55
+ // We consider all models in the limits object as "tracked"
56
+ const hasRateLimitedModel = limits.some(([_, l]) => {
57
+ // Treat null/undefined fraction as 0 (limited)
58
+ if (!l || l.remainingFraction === null || l.remainingFraction === undefined) return true;
59
+ return l.remainingFraction <= 0.05;
60
+ });
61
+
62
+ if (hasRateLimitedModel) {
63
+ limited++;
64
+ } else {
65
+ active++;
66
+ }
67
+ } else {
68
+ limited++;
69
+ }
70
+ });
71
+
72
+ // TOTAL shows only enabled accounts
73
+ // Disabled accounts are excluded from all statistics
74
+ component.stats.total = enabledAccounts.length;
75
+ component.stats.active = active;
76
+ component.stats.limited = limited;
77
+
78
+ // Calculate model usage for rate limit details
79
+ let totalLimitedModels = 0;
80
+ let totalTrackedModels = 0;
81
+
82
+ enabledAccounts.forEach(acc => {
83
+ const limits = Object.entries(acc.limits || {});
84
+ limits.forEach(([id, l]) => {
85
+ totalTrackedModels++;
86
+ if (!l || l.remainingFraction == null || l.remainingFraction <= 0.05) {
87
+ totalLimitedModels++;
88
+ }
89
+ });
90
+ });
91
+
92
+ component.stats.modelUsage = {
93
+ limited: totalLimitedModels,
94
+ total: totalTrackedModels
95
+ };
96
+
97
+ // Calculate subscription tier distribution
98
+ const subscription = { ultra: 0, pro: 0, free: 0 };
99
+ enabledAccounts.forEach(acc => {
100
+ const tier = acc.subscription?.tier || 'free';
101
+ if (tier === 'ultra') {
102
+ subscription.ultra++;
103
+ } else if (tier === 'pro') {
104
+ subscription.pro++;
105
+ } else {
106
+ subscription.free++;
107
+ }
108
+ });
109
+ component.stats.subscription = subscription;
110
+ };
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Dashboard Component (Refactored)
3
+ * Orchestrates stats, charts, and filters modules
4
+ * Registers itself to window.Components for Alpine.js to consume
5
+ */
6
+ window.Components = window.Components || {};
7
+
8
+ window.Components.dashboard = () => ({
9
+ // Core state
10
+ stats: { total: 0, active: 0, limited: 0, overallHealth: 0, hasTrendData: false },
11
+ hasFilteredTrendData: true,
12
+ charts: { quotaDistribution: null, usageTrend: null },
13
+ usageStats: { total: 0, today: 0, thisHour: 0 },
14
+ historyData: {},
15
+ modelTree: {},
16
+ families: [],
17
+
18
+ // Filter state (from module)
19
+ ...window.DashboardFilters.getInitialState(),
20
+
21
+ // Debounced chart update to prevent rapid successive updates
22
+ _debouncedUpdateTrendChart: null,
23
+
24
+ init() {
25
+ // Create debounced version of updateTrendChart (300ms delay for stability)
26
+ this._debouncedUpdateTrendChart = window.utils.debounce(() => {
27
+ window.DashboardCharts.updateTrendChart(this);
28
+ }, 300);
29
+
30
+ // Load saved preferences from localStorage
31
+ window.DashboardFilters.loadPreferences(this);
32
+
33
+ // Update stats when dashboard becomes active (skip initial trigger)
34
+ this.$watch('$store.global.activeTab', (val, oldVal) => {
35
+ if (val === 'dashboard' && oldVal !== undefined) {
36
+ this.$nextTick(() => {
37
+ this.updateStats();
38
+ this.updateCharts();
39
+ this.updateTrendChart();
40
+ });
41
+ }
42
+ });
43
+
44
+ // Watch for data changes
45
+ this.$watch('$store.data.accounts', () => {
46
+ if (this.$store.global.activeTab === 'dashboard') {
47
+ this.updateStats();
48
+ // Debounce chart updates to prevent rapid flickering
49
+ if (this._debouncedUpdateCharts) {
50
+ this._debouncedUpdateCharts();
51
+ } else {
52
+ this._debouncedUpdateCharts = window.utils.debounce(() => this.updateCharts(), 100);
53
+ this._debouncedUpdateCharts();
54
+ }
55
+ }
56
+ });
57
+
58
+ // Watch for history updates from data-store (automatically loaded with account data)
59
+ this.$watch('$store.data.usageHistory', (newHistory) => {
60
+ if (this.$store.global.activeTab === 'dashboard' && newHistory && Object.keys(newHistory).length > 0) {
61
+ // Optimization: Skip if data hasn't changed (prevents double render on load)
62
+ if (this.historyData && JSON.stringify(newHistory) === JSON.stringify(this.historyData)) {
63
+ return;
64
+ }
65
+
66
+ this.historyData = newHistory;
67
+ this.processHistory(newHistory);
68
+ this.stats.hasTrendData = true;
69
+ }
70
+ });
71
+
72
+ // Initial update if already on dashboard
73
+ // Note: Alpine.store('data') may already have data from cache if initialized before this component
74
+ if (this.$store.global.activeTab === 'dashboard') {
75
+ this.$nextTick(() => {
76
+ this.updateStats();
77
+ this.updateCharts();
78
+
79
+ // Optimization: Only process history if it hasn't been processed yet
80
+ // The usageHistory watcher above will handle updates if data changes
81
+ const history = Alpine.store('data').usageHistory;
82
+ if (history && Object.keys(history).length > 0) {
83
+ // Check if we already have this data to avoid redundant chart update
84
+ if (!this.historyData || JSON.stringify(history) !== JSON.stringify(this.historyData)) {
85
+ this.historyData = history;
86
+ this.processHistory(history);
87
+ this.stats.hasTrendData = true;
88
+ }
89
+ }
90
+ });
91
+ }
92
+ },
93
+
94
+ processHistory(history) {
95
+ // Build model tree from hierarchical data
96
+ const tree = {};
97
+ let total = 0, today = 0, thisHour = 0;
98
+
99
+ const now = new Date();
100
+ const todayStart = new Date(now);
101
+ todayStart.setHours(0, 0, 0, 0);
102
+ const currentHour = new Date(now);
103
+ currentHour.setMinutes(0, 0, 0);
104
+
105
+ Object.entries(history).forEach(([iso, hourData]) => {
106
+ const timestamp = new Date(iso);
107
+
108
+ // Process each family in the hour data
109
+ Object.entries(hourData).forEach(([key, value]) => {
110
+ // Skip metadata keys
111
+ if (key === '_total' || key === 'total') return;
112
+
113
+ // Handle hierarchical format: { claude: { "opus-4-5": 10, "_subtotal": 10 } }
114
+ if (typeof value === 'object' && value !== null) {
115
+ if (!tree[key]) tree[key] = new Set();
116
+
117
+ Object.keys(value).forEach(modelName => {
118
+ if (modelName !== '_subtotal') {
119
+ tree[key].add(modelName);
120
+ }
121
+ });
122
+ }
123
+ });
124
+
125
+ // Calculate totals
126
+ const hourTotal = hourData._total || hourData.total || 0;
127
+ total += hourTotal;
128
+
129
+ if (timestamp >= todayStart) {
130
+ today += hourTotal;
131
+ }
132
+ if (timestamp.getTime() === currentHour.getTime()) {
133
+ thisHour = hourTotal;
134
+ }
135
+ });
136
+
137
+ this.usageStats = { total, today, thisHour };
138
+
139
+ // Convert Sets to sorted arrays
140
+ this.modelTree = {};
141
+ Object.entries(tree).forEach(([family, models]) => {
142
+ this.modelTree[family] = Array.from(models).sort();
143
+ });
144
+ this.families = Object.keys(this.modelTree).sort();
145
+
146
+ // Auto-select new families/models that haven't been configured
147
+ this.autoSelectNew();
148
+
149
+ this.updateTrendChart();
150
+ },
151
+
152
+ // Delegation methods for stats
153
+ updateStats() {
154
+ window.DashboardStats.updateStats(this);
155
+ },
156
+
157
+ // Delegation methods for charts
158
+ updateCharts() {
159
+ window.DashboardCharts.updateCharts(this);
160
+ },
161
+
162
+ updateTrendChart() {
163
+ // Use debounced version to prevent rapid successive updates
164
+ if (this._debouncedUpdateTrendChart) {
165
+ this._debouncedUpdateTrendChart();
166
+ } else {
167
+ // Fallback if debounced version not initialized
168
+ window.DashboardCharts.updateTrendChart(this);
169
+ }
170
+ },
171
+
172
+ // Delegation methods for filters
173
+ loadPreferences() {
174
+ window.DashboardFilters.loadPreferences(this);
175
+ },
176
+
177
+ savePreferences() {
178
+ window.DashboardFilters.savePreferences(this);
179
+ },
180
+
181
+ setDisplayMode(mode) {
182
+ window.DashboardFilters.setDisplayMode(this, mode);
183
+ },
184
+
185
+ setTimeRange(range) {
186
+ window.DashboardFilters.setTimeRange(this, range);
187
+ },
188
+
189
+ getTimeRangeLabel() {
190
+ return window.DashboardFilters.getTimeRangeLabel(this);
191
+ },
192
+
193
+ toggleFamily(family) {
194
+ window.DashboardFilters.toggleFamily(this, family);
195
+ },
196
+
197
+ toggleModel(family, model) {
198
+ window.DashboardFilters.toggleModel(this, family, model);
199
+ },
200
+
201
+ isFamilySelected(family) {
202
+ return window.DashboardFilters.isFamilySelected(this, family);
203
+ },
204
+
205
+ isModelSelected(family, model) {
206
+ return window.DashboardFilters.isModelSelected(this, family, model);
207
+ },
208
+
209
+ selectAll() {
210
+ window.DashboardFilters.selectAll(this);
211
+ },
212
+
213
+ deselectAll() {
214
+ window.DashboardFilters.deselectAll(this);
215
+ },
216
+
217
+ getFamilyColor(family) {
218
+ return window.DashboardFilters.getFamilyColor(family);
219
+ },
220
+
221
+ getModelColor(family, modelIndex) {
222
+ return window.DashboardFilters.getModelColor(family, modelIndex);
223
+ },
224
+
225
+ getSelectedCount() {
226
+ return window.DashboardFilters.getSelectedCount(this);
227
+ },
228
+
229
+ autoSelectNew() {
230
+ window.DashboardFilters.autoSelectNew(this);
231
+ },
232
+
233
+ autoSelectTopN(n = 5) {
234
+ window.DashboardFilters.autoSelectTopN(this, n);
235
+ }
236
+ });