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.
- package/LICENSE +21 -0
- package/README.md +757 -0
- package/bin/cli.js +146 -0
- package/package.json +97 -0
- package/public/Complaint Details.pdf +0 -0
- package/public/Cyber Crime Portal.pdf +0 -0
- package/public/app.js +229 -0
- package/public/css/src/input.css +523 -0
- package/public/css/style.css +1 -0
- package/public/favicon.png +0 -0
- package/public/index.html +549 -0
- package/public/js/components/account-manager.js +356 -0
- package/public/js/components/add-account-modal.js +414 -0
- package/public/js/components/claude-config.js +420 -0
- package/public/js/components/dashboard/charts.js +605 -0
- package/public/js/components/dashboard/filters.js +362 -0
- package/public/js/components/dashboard/stats.js +110 -0
- package/public/js/components/dashboard.js +236 -0
- package/public/js/components/logs-viewer.js +100 -0
- package/public/js/components/models.js +36 -0
- package/public/js/components/server-config.js +349 -0
- package/public/js/config/constants.js +102 -0
- package/public/js/data-store.js +375 -0
- package/public/js/settings-store.js +58 -0
- package/public/js/store.js +99 -0
- package/public/js/translations/en.js +367 -0
- package/public/js/translations/id.js +412 -0
- package/public/js/translations/pt.js +308 -0
- package/public/js/translations/tr.js +358 -0
- package/public/js/translations/zh.js +373 -0
- package/public/js/utils/account-actions.js +189 -0
- package/public/js/utils/error-handler.js +96 -0
- package/public/js/utils/model-config.js +42 -0
- package/public/js/utils/ui-logger.js +143 -0
- package/public/js/utils/validators.js +77 -0
- package/public/js/utils.js +69 -0
- package/public/proxy-server-64.png +0 -0
- package/public/views/accounts.html +361 -0
- package/public/views/dashboard.html +484 -0
- package/public/views/logs.html +97 -0
- package/public/views/models.html +331 -0
- package/public/views/settings.html +1327 -0
- package/src/account-manager/credentials.js +378 -0
- package/src/account-manager/index.js +462 -0
- package/src/account-manager/onboarding.js +112 -0
- package/src/account-manager/rate-limits.js +369 -0
- package/src/account-manager/storage.js +160 -0
- package/src/account-manager/strategies/base-strategy.js +109 -0
- package/src/account-manager/strategies/hybrid-strategy.js +339 -0
- package/src/account-manager/strategies/index.js +79 -0
- package/src/account-manager/strategies/round-robin-strategy.js +76 -0
- package/src/account-manager/strategies/sticky-strategy.js +138 -0
- package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
- package/src/account-manager/strategies/trackers/index.js +9 -0
- package/src/account-manager/strategies/trackers/quota-tracker.js +120 -0
- package/src/account-manager/strategies/trackers/token-bucket-tracker.js +155 -0
- package/src/auth/database.js +169 -0
- package/src/auth/oauth.js +548 -0
- package/src/auth/token-extractor.js +117 -0
- package/src/cli/accounts.js +648 -0
- package/src/cloudcode/index.js +29 -0
- package/src/cloudcode/message-handler.js +510 -0
- package/src/cloudcode/model-api.js +248 -0
- package/src/cloudcode/rate-limit-parser.js +235 -0
- package/src/cloudcode/request-builder.js +93 -0
- package/src/cloudcode/session-manager.js +47 -0
- package/src/cloudcode/sse-parser.js +121 -0
- package/src/cloudcode/sse-streamer.js +293 -0
- package/src/cloudcode/streaming-handler.js +615 -0
- package/src/config.js +125 -0
- package/src/constants.js +407 -0
- package/src/errors.js +242 -0
- package/src/fallback-config.js +29 -0
- package/src/format/content-converter.js +193 -0
- package/src/format/index.js +20 -0
- package/src/format/request-converter.js +255 -0
- package/src/format/response-converter.js +120 -0
- package/src/format/schema-sanitizer.js +673 -0
- package/src/format/signature-cache.js +88 -0
- package/src/format/thinking-utils.js +648 -0
- package/src/index.js +148 -0
- package/src/modules/usage-stats.js +205 -0
- package/src/providers/anthropic-provider.js +258 -0
- package/src/providers/base-provider.js +157 -0
- package/src/providers/cloudcode.js +94 -0
- package/src/providers/copilot.js +399 -0
- package/src/providers/github-provider.js +287 -0
- package/src/providers/google-provider.js +192 -0
- package/src/providers/index.js +211 -0
- package/src/providers/openai-compatible.js +265 -0
- package/src/providers/openai-provider.js +271 -0
- package/src/providers/openrouter-provider.js +325 -0
- package/src/providers/setup.js +83 -0
- package/src/server.js +870 -0
- package/src/utils/claude-config.js +245 -0
- package/src/utils/helpers.js +51 -0
- package/src/utils/logger.js +142 -0
- package/src/utils/native-module-helper.js +162 -0
- package/src/webui/index.js +1134 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Charts Module
|
|
3
|
+
* 职责:使用 Chart.js 渲染配额分布图和使用趋势图
|
|
4
|
+
*
|
|
5
|
+
* 调用时机:
|
|
6
|
+
* - dashboard 组件 init() 时初始化图表
|
|
7
|
+
* - 筛选器变化时更新图表数据
|
|
8
|
+
* - $store.data 更新时刷新图表
|
|
9
|
+
*
|
|
10
|
+
* 图表类型:
|
|
11
|
+
* 1. Quota Distribution(饼图):按模型家族或具体模型显示配额分布
|
|
12
|
+
* 2. Usage Trend(折线图):显示历史使用趋势
|
|
13
|
+
*
|
|
14
|
+
* 特殊处理:
|
|
15
|
+
* - 使用 _trendChartUpdateLock 防止并发更新导致的竞争条件
|
|
16
|
+
* - 通过 debounce 优化频繁更新的性能
|
|
17
|
+
* - 响应式处理:移动端自动调整图表大小和标签显示
|
|
18
|
+
*
|
|
19
|
+
* @module DashboardCharts
|
|
20
|
+
*/
|
|
21
|
+
window.DashboardCharts = window.DashboardCharts || {};
|
|
22
|
+
|
|
23
|
+
// Helper to get CSS variable values (alias to window.utils.getThemeColor)
|
|
24
|
+
const getThemeColor = (name) => window.utils.getThemeColor(name);
|
|
25
|
+
|
|
26
|
+
// Color palette for different families and models
|
|
27
|
+
const FAMILY_COLORS = {
|
|
28
|
+
get claude() {
|
|
29
|
+
return getThemeColor("--color-neon-purple");
|
|
30
|
+
},
|
|
31
|
+
get gemini() {
|
|
32
|
+
return getThemeColor("--color-neon-green");
|
|
33
|
+
},
|
|
34
|
+
get gpt() {
|
|
35
|
+
return '#10b981';
|
|
36
|
+
},
|
|
37
|
+
get o1() {
|
|
38
|
+
return '#f97316';
|
|
39
|
+
},
|
|
40
|
+
get other() {
|
|
41
|
+
return getThemeColor("--color-neon-cyan");
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const PROVIDER_COLORS = {
|
|
46
|
+
google: '#4285f4',
|
|
47
|
+
anthropic: '#d97706',
|
|
48
|
+
openai: '#10b981',
|
|
49
|
+
github: '#6366f1',
|
|
50
|
+
copilot: '#f97316',
|
|
51
|
+
openrouter: '#6d28d9'
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const MODEL_COLORS = Array.from({ length: 16 }, (_, i) =>
|
|
55
|
+
getThemeColor(`--color-chart-${i + 1}`)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Export constants for filter module
|
|
59
|
+
window.DashboardConstants = { FAMILY_COLORS, MODEL_COLORS, PROVIDER_COLORS };
|
|
60
|
+
|
|
61
|
+
// Module-level lock to prevent concurrent chart updates (fixes race condition)
|
|
62
|
+
let _trendChartUpdateLock = false;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Convert hex color to rgba
|
|
66
|
+
* @param {string} hex - Hex color string
|
|
67
|
+
* @param {number} alpha - Alpha value (0-1)
|
|
68
|
+
* @returns {string} rgba color string
|
|
69
|
+
*/
|
|
70
|
+
window.DashboardCharts.hexToRgba = function (hex, alpha) {
|
|
71
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
72
|
+
if (result) {
|
|
73
|
+
return `rgba(${parseInt(result[1], 16)}, ${parseInt(
|
|
74
|
+
result[2],
|
|
75
|
+
16
|
|
76
|
+
)}, ${parseInt(result[3], 16)}, ${alpha})`;
|
|
77
|
+
}
|
|
78
|
+
return hex;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if canvas is ready for Chart creation
|
|
83
|
+
* @param {HTMLCanvasElement} canvas - Canvas element
|
|
84
|
+
* @returns {boolean} True if canvas is ready
|
|
85
|
+
*/
|
|
86
|
+
function isCanvasReady(canvas) {
|
|
87
|
+
if (!canvas || !canvas.isConnected) return false;
|
|
88
|
+
if (canvas.offsetWidth === 0 || canvas.offsetHeight === 0) return false;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const ctx = canvas.getContext("2d");
|
|
92
|
+
return !!ctx;
|
|
93
|
+
} catch (e) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Create a Chart.js dataset with gradient fill
|
|
100
|
+
* @param {string} label - Dataset label
|
|
101
|
+
* @param {Array} data - Data points
|
|
102
|
+
* @param {string} color - Line color
|
|
103
|
+
* @param {HTMLCanvasElement} canvas - Canvas element
|
|
104
|
+
* @returns {object} Chart.js dataset configuration
|
|
105
|
+
*/
|
|
106
|
+
window.DashboardCharts.createDataset = function (label, data, color, canvas) {
|
|
107
|
+
let gradient;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// Safely create gradient with fallback
|
|
111
|
+
if (canvas && canvas.getContext) {
|
|
112
|
+
const ctx = canvas.getContext("2d");
|
|
113
|
+
if (ctx && ctx.createLinearGradient) {
|
|
114
|
+
gradient = ctx.createLinearGradient(0, 0, 0, 200);
|
|
115
|
+
gradient.addColorStop(0, window.DashboardCharts.hexToRgba(color, 0.12));
|
|
116
|
+
gradient.addColorStop(
|
|
117
|
+
0.6,
|
|
118
|
+
window.DashboardCharts.hexToRgba(color, 0.05)
|
|
119
|
+
);
|
|
120
|
+
gradient.addColorStop(1, "rgba(0, 0, 0, 0)");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch (e) {
|
|
124
|
+
if (window.UILogger) window.UILogger.debug("Gradient fallback:", e.message);
|
|
125
|
+
gradient = null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Fallback to solid color if gradient creation failed
|
|
129
|
+
const backgroundColor =
|
|
130
|
+
gradient || window.DashboardCharts.hexToRgba(color, 0.08);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
label,
|
|
134
|
+
data,
|
|
135
|
+
borderColor: color,
|
|
136
|
+
backgroundColor: backgroundColor,
|
|
137
|
+
borderWidth: 2.5,
|
|
138
|
+
tension: 0.35,
|
|
139
|
+
fill: true,
|
|
140
|
+
pointRadius: 2.5,
|
|
141
|
+
pointHoverRadius: 6,
|
|
142
|
+
pointBackgroundColor: color,
|
|
143
|
+
pointBorderColor: "rgba(9, 9, 11, 0.8)",
|
|
144
|
+
pointBorderWidth: 1.5,
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Update quota distribution donut chart
|
|
150
|
+
* @param {object} component - Dashboard component instance
|
|
151
|
+
*/
|
|
152
|
+
window.DashboardCharts.updateCharts = function (component) {
|
|
153
|
+
const canvas = document.getElementById("quotaChart");
|
|
154
|
+
|
|
155
|
+
// Safety checks
|
|
156
|
+
if (!canvas) {
|
|
157
|
+
console.debug("quotaChart canvas not found");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// FORCE DESTROY: Check for existing chart on the canvas element property
|
|
162
|
+
// This handles cases where Component state is lost but DOM persists
|
|
163
|
+
if (canvas._chartInstance) {
|
|
164
|
+
console.debug("Destroying existing quota chart from canvas property");
|
|
165
|
+
try {
|
|
166
|
+
canvas._chartInstance.destroy();
|
|
167
|
+
} catch(e) { if (window.UILogger) window.UILogger.debug(e); }
|
|
168
|
+
canvas._chartInstance = null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Also check component state as backup
|
|
172
|
+
if (component.charts.quotaDistribution) {
|
|
173
|
+
try {
|
|
174
|
+
component.charts.quotaDistribution.destroy();
|
|
175
|
+
} catch(e) { }
|
|
176
|
+
component.charts.quotaDistribution = null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Also try Chart.js registry
|
|
180
|
+
if (typeof Chart !== "undefined" && Chart.getChart) {
|
|
181
|
+
const regChart = Chart.getChart(canvas);
|
|
182
|
+
if (regChart) {
|
|
183
|
+
try { regChart.destroy(); } catch(e) {}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (typeof Chart === "undefined") {
|
|
188
|
+
if (window.UILogger) window.UILogger.warn("Chart.js not loaded");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (!isCanvasReady(canvas)) {
|
|
192
|
+
if (window.UILogger) window.UILogger.debug("quotaChart canvas not ready, skipping update");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Use UNFILTERED data for global health chart
|
|
197
|
+
const rows = Alpine.store("data").getUnfilteredQuotaData();
|
|
198
|
+
if (!rows || rows.length === 0) return;
|
|
199
|
+
|
|
200
|
+
const healthByFamily = {};
|
|
201
|
+
let totalHealthSum = 0;
|
|
202
|
+
let totalModelCount = 0;
|
|
203
|
+
|
|
204
|
+
rows.forEach((row) => {
|
|
205
|
+
const family = row.family || "unknown";
|
|
206
|
+
if (!healthByFamily[family]) {
|
|
207
|
+
healthByFamily[family] = { total: 0, weighted: 0 };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Calculate average health from quotaInfo (each entry has { pct })
|
|
211
|
+
// Health = average of all account quotas for this model
|
|
212
|
+
const quotaInfo = row.quotaInfo || [];
|
|
213
|
+
let avgHealth = 0;
|
|
214
|
+
|
|
215
|
+
if (quotaInfo.length > 0) {
|
|
216
|
+
avgHealth = quotaInfo.reduce((sum, q) => sum + (q.pct || 0), 0) / quotaInfo.length;
|
|
217
|
+
}
|
|
218
|
+
// If quotaInfo is empty, avgHealth remains 0 (depleted/unknown)
|
|
219
|
+
|
|
220
|
+
healthByFamily[family].total++;
|
|
221
|
+
healthByFamily[family].weighted += avgHealth;
|
|
222
|
+
totalHealthSum += avgHealth;
|
|
223
|
+
totalModelCount++;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Update overall health for dashboard display
|
|
227
|
+
component.stats.overallHealth = totalModelCount > 0
|
|
228
|
+
? Math.round(totalHealthSum / totalModelCount)
|
|
229
|
+
: 0;
|
|
230
|
+
|
|
231
|
+
const familyColors = {
|
|
232
|
+
claude: getThemeColor("--color-neon-purple") || "#a855f7",
|
|
233
|
+
gemini: getThemeColor("--color-neon-green") || "#22c55e",
|
|
234
|
+
unknown: getThemeColor("--color-neon-cyan") || "#06b6d4",
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const data = [];
|
|
238
|
+
const colors = [];
|
|
239
|
+
const labels = [];
|
|
240
|
+
|
|
241
|
+
const totalFamilies = Object.keys(healthByFamily).length;
|
|
242
|
+
const segmentSize = 100 / totalFamilies;
|
|
243
|
+
|
|
244
|
+
Object.entries(healthByFamily).forEach(([family, { total, weighted }]) => {
|
|
245
|
+
const health = weighted / total;
|
|
246
|
+
const activeVal = (health / 100) * segmentSize;
|
|
247
|
+
const inactiveVal = segmentSize - activeVal;
|
|
248
|
+
|
|
249
|
+
const familyColor = familyColors[family] || familyColors["unknown"];
|
|
250
|
+
|
|
251
|
+
// Get translation keys
|
|
252
|
+
const store = Alpine.store("global");
|
|
253
|
+
const familyKey =
|
|
254
|
+
"family" + family.charAt(0).toUpperCase() + family.slice(1);
|
|
255
|
+
const familyName = store.t(familyKey);
|
|
256
|
+
|
|
257
|
+
// Labels using translations if possible
|
|
258
|
+
const activeLabel =
|
|
259
|
+
family === "claude"
|
|
260
|
+
? store.t("claudeActive")
|
|
261
|
+
: family === "gemini"
|
|
262
|
+
? store.t("geminiActive")
|
|
263
|
+
: `${familyName} ${store.t("activeSuffix")}`;
|
|
264
|
+
|
|
265
|
+
const depletedLabel =
|
|
266
|
+
family === "claude"
|
|
267
|
+
? store.t("claudeEmpty")
|
|
268
|
+
: family === "gemini"
|
|
269
|
+
? store.t("geminiEmpty")
|
|
270
|
+
: `${familyName} ${store.t("depleted")}`;
|
|
271
|
+
|
|
272
|
+
// Active segment
|
|
273
|
+
data.push(activeVal);
|
|
274
|
+
colors.push(familyColor);
|
|
275
|
+
labels.push(activeLabel);
|
|
276
|
+
|
|
277
|
+
// Inactive segment
|
|
278
|
+
data.push(inactiveVal);
|
|
279
|
+
// Use higher opacity (0.6) to ensure the ring color matches the legend more closely
|
|
280
|
+
// while still differentiating "depleted" from "active" (1.0 opacity)
|
|
281
|
+
colors.push(window.DashboardCharts.hexToRgba(familyColor, 0.6));
|
|
282
|
+
labels.push(depletedLabel);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Create Chart
|
|
286
|
+
try {
|
|
287
|
+
const newChart = new Chart(canvas, {
|
|
288
|
+
// ... config
|
|
289
|
+
type: "doughnut",
|
|
290
|
+
data: {
|
|
291
|
+
labels: labels,
|
|
292
|
+
datasets: [
|
|
293
|
+
{
|
|
294
|
+
data: data,
|
|
295
|
+
backgroundColor: colors,
|
|
296
|
+
borderColor: getThemeColor("--color-space-950"),
|
|
297
|
+
borderWidth: 0,
|
|
298
|
+
hoverOffset: 0,
|
|
299
|
+
borderRadius: 0,
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
options: {
|
|
304
|
+
responsive: true,
|
|
305
|
+
maintainAspectRatio: false,
|
|
306
|
+
cutout: "85%",
|
|
307
|
+
rotation: -90,
|
|
308
|
+
circumference: 360,
|
|
309
|
+
plugins: {
|
|
310
|
+
legend: { display: false },
|
|
311
|
+
tooltip: { enabled: false },
|
|
312
|
+
title: { display: false },
|
|
313
|
+
},
|
|
314
|
+
animation: {
|
|
315
|
+
// Disable animation for quota chart to prevent "double refresh" visual glitch
|
|
316
|
+
duration: 0
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// SAVE INSTANCE TO CANVAS AND COMPONENT
|
|
322
|
+
canvas._chartInstance = newChart;
|
|
323
|
+
component.charts.quotaDistribution = newChart;
|
|
324
|
+
|
|
325
|
+
} catch (e) {
|
|
326
|
+
console.error("Failed to create quota chart:", e);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Update usage trend line chart
|
|
332
|
+
* @param {object} component - Dashboard component instance
|
|
333
|
+
*/
|
|
334
|
+
window.DashboardCharts.updateTrendChart = function (component) {
|
|
335
|
+
// Prevent concurrent updates (fixes race condition on rapid toggling)
|
|
336
|
+
if (_trendChartUpdateLock) {
|
|
337
|
+
if (window.UILogger) window.UILogger.debug("[updateTrendChart] Update already in progress, skipping");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
_trendChartUpdateLock = true;
|
|
341
|
+
|
|
342
|
+
const logger = window.UILogger || console;
|
|
343
|
+
logger.debug("[updateTrendChart] Starting update...");
|
|
344
|
+
|
|
345
|
+
const canvas = document.getElementById("usageTrendChart");
|
|
346
|
+
|
|
347
|
+
// FORCE DESTROY: Check for existing chart on the canvas element property
|
|
348
|
+
if (canvas) {
|
|
349
|
+
if (canvas._chartInstance) {
|
|
350
|
+
console.debug("Destroying existing trend chart from canvas property");
|
|
351
|
+
try {
|
|
352
|
+
canvas._chartInstance.stop();
|
|
353
|
+
canvas._chartInstance.destroy();
|
|
354
|
+
} catch(e) { if (window.UILogger) window.UILogger.debug(e); }
|
|
355
|
+
canvas._chartInstance = null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Also try Chart.js registry
|
|
359
|
+
if (typeof Chart !== "undefined" && Chart.getChart) {
|
|
360
|
+
const regChart = Chart.getChart(canvas);
|
|
361
|
+
if (regChart) {
|
|
362
|
+
try { regChart.stop(); regChart.destroy(); } catch(e) {}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Also check component state
|
|
368
|
+
if (component.charts.usageTrend) {
|
|
369
|
+
try {
|
|
370
|
+
component.charts.usageTrend.stop();
|
|
371
|
+
component.charts.usageTrend.destroy();
|
|
372
|
+
} catch (e) { }
|
|
373
|
+
component.charts.usageTrend = null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Safety checks
|
|
377
|
+
if (!canvas) {
|
|
378
|
+
if (window.UILogger) window.UILogger.debug("[updateTrendChart] Canvas not found in DOM");
|
|
379
|
+
_trendChartUpdateLock = false;
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (typeof Chart === "undefined") {
|
|
383
|
+
if (window.UILogger) window.UILogger.warn("[updateTrendChart] Chart.js not loaded");
|
|
384
|
+
_trendChartUpdateLock = false;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (window.UILogger) window.UILogger.debug("[updateTrendChart] Canvas element:", {
|
|
389
|
+
exists: !!canvas,
|
|
390
|
+
isConnected: canvas.isConnected,
|
|
391
|
+
width: canvas.offsetWidth,
|
|
392
|
+
height: canvas.offsetHeight,
|
|
393
|
+
parentElement: canvas.parentElement?.tagName,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
if (!isCanvasReady(canvas)) {
|
|
397
|
+
if (window.UILogger) window.UILogger.debug("[updateTrendChart] Canvas not ready", {
|
|
398
|
+
isConnected: canvas.isConnected,
|
|
399
|
+
width: canvas.offsetWidth,
|
|
400
|
+
height: canvas.offsetHeight,
|
|
401
|
+
});
|
|
402
|
+
_trendChartUpdateLock = false;
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Clear canvas to ensure clean state after destroy
|
|
407
|
+
try {
|
|
408
|
+
const ctx = canvas.getContext("2d");
|
|
409
|
+
if (ctx) {
|
|
410
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
411
|
+
}
|
|
412
|
+
} catch (e) {
|
|
413
|
+
if (window.UILogger) window.UILogger.debug("[updateTrendChart] Failed to clear canvas:", e.message);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (window.UILogger) window.UILogger.debug(
|
|
417
|
+
"[updateTrendChart] Canvas is ready, proceeding with chart creation"
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
// Use filtered history data based on time range
|
|
421
|
+
const history = window.DashboardFilters.getFilteredHistoryData(component);
|
|
422
|
+
if (!history || Object.keys(history).length === 0) {
|
|
423
|
+
if (window.UILogger) window.UILogger.debug("No history data available for trend chart (after filtering)");
|
|
424
|
+
component.hasFilteredTrendData = false;
|
|
425
|
+
_trendChartUpdateLock = false;
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
component.hasFilteredTrendData = true;
|
|
430
|
+
|
|
431
|
+
// Sort entries by timestamp for correct order
|
|
432
|
+
const sortedEntries = Object.entries(history).sort(
|
|
433
|
+
([a], [b]) => new Date(a).getTime() - new Date(b).getTime()
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
// Determine if data spans multiple days (for smart label formatting)
|
|
437
|
+
const timestamps = sortedEntries.map(([iso]) => new Date(iso));
|
|
438
|
+
const isMultiDay = timestamps.length > 1 &&
|
|
439
|
+
timestamps[0].toDateString() !== timestamps[timestamps.length - 1].toDateString();
|
|
440
|
+
|
|
441
|
+
// Helper to format X-axis labels based on time range and multi-day status
|
|
442
|
+
const formatLabel = (date) => {
|
|
443
|
+
const timeRange = component.timeRange || '24h';
|
|
444
|
+
|
|
445
|
+
if (timeRange === '7d') {
|
|
446
|
+
// Week view: show MM/DD
|
|
447
|
+
return date.toLocaleDateString([], { month: '2-digit', day: '2-digit' });
|
|
448
|
+
} else if (isMultiDay || timeRange === 'all') {
|
|
449
|
+
// Multi-day data: show MM/DD HH:MM
|
|
450
|
+
return date.toLocaleDateString([], { month: '2-digit', day: '2-digit' }) + ' ' +
|
|
451
|
+
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
452
|
+
} else {
|
|
453
|
+
// Same day: show HH:MM only
|
|
454
|
+
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const labels = [];
|
|
459
|
+
const datasets = [];
|
|
460
|
+
|
|
461
|
+
if (component.displayMode === "family") {
|
|
462
|
+
// Aggregate by family
|
|
463
|
+
const dataByFamily = {};
|
|
464
|
+
component.selectedFamilies.forEach((family) => {
|
|
465
|
+
dataByFamily[family] = [];
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
sortedEntries.forEach(([iso, hourData]) => {
|
|
469
|
+
const date = new Date(iso);
|
|
470
|
+
labels.push(formatLabel(date));
|
|
471
|
+
|
|
472
|
+
component.selectedFamilies.forEach((family) => {
|
|
473
|
+
const familyData = hourData[family];
|
|
474
|
+
const count = familyData?._subtotal || 0;
|
|
475
|
+
dataByFamily[family].push(count);
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Build datasets for families
|
|
480
|
+
component.selectedFamilies.forEach((family) => {
|
|
481
|
+
const color = window.DashboardFilters.getFamilyColor(family);
|
|
482
|
+
const familyKey =
|
|
483
|
+
"family" + family.charAt(0).toUpperCase() + family.slice(1);
|
|
484
|
+
const label = Alpine.store("global").t(familyKey);
|
|
485
|
+
datasets.push(
|
|
486
|
+
window.DashboardCharts.createDataset(
|
|
487
|
+
label,
|
|
488
|
+
dataByFamily[family],
|
|
489
|
+
color,
|
|
490
|
+
canvas
|
|
491
|
+
)
|
|
492
|
+
);
|
|
493
|
+
});
|
|
494
|
+
} else {
|
|
495
|
+
// Show individual models
|
|
496
|
+
const dataByModel = {};
|
|
497
|
+
|
|
498
|
+
// Initialize data arrays
|
|
499
|
+
component.families.forEach((family) => {
|
|
500
|
+
(component.selectedModels[family] || []).forEach((model) => {
|
|
501
|
+
const key = `${family}:${model}`;
|
|
502
|
+
dataByModel[key] = [];
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
sortedEntries.forEach(([iso, hourData]) => {
|
|
507
|
+
const date = new Date(iso);
|
|
508
|
+
labels.push(formatLabel(date));
|
|
509
|
+
|
|
510
|
+
component.families.forEach((family) => {
|
|
511
|
+
const familyData = hourData[family] || {};
|
|
512
|
+
(component.selectedModels[family] || []).forEach((model) => {
|
|
513
|
+
const key = `${family}:${model}`;
|
|
514
|
+
dataByModel[key].push(familyData[model] || 0);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// Build datasets for models
|
|
520
|
+
component.families.forEach((family) => {
|
|
521
|
+
(component.selectedModels[family] || []).forEach((model, modelIndex) => {
|
|
522
|
+
const key = `${family}:${model}`;
|
|
523
|
+
const color = window.DashboardFilters.getModelColor(family, modelIndex);
|
|
524
|
+
datasets.push(
|
|
525
|
+
window.DashboardCharts.createDataset(
|
|
526
|
+
model,
|
|
527
|
+
dataByModel[key],
|
|
528
|
+
color,
|
|
529
|
+
canvas
|
|
530
|
+
)
|
|
531
|
+
);
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
const newChart = new Chart(canvas, {
|
|
538
|
+
type: "line",
|
|
539
|
+
data: { labels, datasets },
|
|
540
|
+
options: {
|
|
541
|
+
responsive: true,
|
|
542
|
+
maintainAspectRatio: false,
|
|
543
|
+
animation: {
|
|
544
|
+
duration: 300, // Reduced animation for faster updates
|
|
545
|
+
},
|
|
546
|
+
interaction: {
|
|
547
|
+
mode: "index",
|
|
548
|
+
intersect: false,
|
|
549
|
+
},
|
|
550
|
+
plugins: {
|
|
551
|
+
legend: { display: false },
|
|
552
|
+
tooltip: {
|
|
553
|
+
backgroundColor:
|
|
554
|
+
getThemeColor("--color-space-950") || "rgba(24, 24, 27, 0.9)",
|
|
555
|
+
titleColor: getThemeColor("--color-text-main"),
|
|
556
|
+
bodyColor: getThemeColor("--color-text-bright"),
|
|
557
|
+
borderColor: getThemeColor("--color-space-border"),
|
|
558
|
+
borderWidth: 1,
|
|
559
|
+
padding: 10,
|
|
560
|
+
displayColors: true,
|
|
561
|
+
callbacks: {
|
|
562
|
+
label: function (context) {
|
|
563
|
+
return context.dataset.label + ": " + context.parsed.y;
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
scales: {
|
|
569
|
+
x: {
|
|
570
|
+
display: true,
|
|
571
|
+
grid: { display: false },
|
|
572
|
+
ticks: {
|
|
573
|
+
color: getThemeColor("--color-text-muted"),
|
|
574
|
+
font: { size: 10 },
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
y: {
|
|
578
|
+
display: true,
|
|
579
|
+
beginAtZero: true,
|
|
580
|
+
grid: {
|
|
581
|
+
display: true,
|
|
582
|
+
color:
|
|
583
|
+
getThemeColor("--color-space-border") + "1a" ||
|
|
584
|
+
"rgba(255,255,255,0.05)",
|
|
585
|
+
},
|
|
586
|
+
ticks: {
|
|
587
|
+
color: getThemeColor("--color-text-muted"),
|
|
588
|
+
font: { size: 10 },
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// SAVE INSTANCE
|
|
596
|
+
canvas._chartInstance = newChart;
|
|
597
|
+
component.charts.usageTrend = newChart;
|
|
598
|
+
|
|
599
|
+
} catch (e) {
|
|
600
|
+
console.error("Failed to create trend chart:", e);
|
|
601
|
+
} finally {
|
|
602
|
+
// Always release lock
|
|
603
|
+
_trendChartUpdateLock = false;
|
|
604
|
+
}
|
|
605
|
+
};
|