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,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Store
|
|
3
|
+
* Holds Accounts, Models, and Computed Quota Rows
|
|
4
|
+
* Shared between Dashboard and AccountManager
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// utils is loaded globally as window.utils in utils.js
|
|
8
|
+
|
|
9
|
+
document.addEventListener('alpine:init', () => {
|
|
10
|
+
Alpine.store('data', {
|
|
11
|
+
accounts: [],
|
|
12
|
+
models: [], // Source of truth
|
|
13
|
+
modelConfig: {}, // Model metadata (hidden, pinned, alias)
|
|
14
|
+
quotaRows: [], // Filtered view
|
|
15
|
+
usageHistory: {}, // Usage statistics history (from /account-limits?includeHistory=true)
|
|
16
|
+
maxAccounts: 10, // Maximum number of accounts allowed (from config)
|
|
17
|
+
loading: false,
|
|
18
|
+
initialLoad: true, // Track first load for skeleton screen
|
|
19
|
+
connectionStatus: 'connecting',
|
|
20
|
+
lastUpdated: '-',
|
|
21
|
+
healthCheckTimer: null,
|
|
22
|
+
|
|
23
|
+
// Filters state
|
|
24
|
+
filters: {
|
|
25
|
+
account: 'all',
|
|
26
|
+
family: 'all',
|
|
27
|
+
search: '',
|
|
28
|
+
sortCol: 'avgQuota',
|
|
29
|
+
sortAsc: true
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// Settings for calculation
|
|
33
|
+
// We need to access global settings? Or duplicate?
|
|
34
|
+
// Let's assume settings are passed or in another store.
|
|
35
|
+
// For simplicity, let's keep relevant filters here.
|
|
36
|
+
|
|
37
|
+
init() {
|
|
38
|
+
// Restore from cache first for instant render
|
|
39
|
+
this.loadFromCache();
|
|
40
|
+
|
|
41
|
+
// Watch filters to recompute
|
|
42
|
+
// Alpine stores don't have $watch automatically unless inside a component?
|
|
43
|
+
// We can manually call compute when filters change.
|
|
44
|
+
|
|
45
|
+
// Start health check monitoring
|
|
46
|
+
this.startHealthCheck();
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
loadFromCache() {
|
|
50
|
+
try {
|
|
51
|
+
const cached = localStorage.getItem('ag_data_cache');
|
|
52
|
+
if (cached) {
|
|
53
|
+
const data = JSON.parse(cached);
|
|
54
|
+
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
|
55
|
+
|
|
56
|
+
// Check TTL
|
|
57
|
+
if (data.timestamp && (Date.now() - data.timestamp > CACHE_TTL)) {
|
|
58
|
+
if (window.UILogger) window.UILogger.debug('Cache expired, skipping restoration');
|
|
59
|
+
localStorage.removeItem('ag_data_cache');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Basic validity check
|
|
64
|
+
if (data.accounts && data.models) {
|
|
65
|
+
this.accounts = data.accounts;
|
|
66
|
+
this.models = data.models;
|
|
67
|
+
this.modelConfig = data.modelConfig || {};
|
|
68
|
+
this.usageHistory = data.usageHistory || {};
|
|
69
|
+
|
|
70
|
+
// Don't show loading on initial load if we have cache
|
|
71
|
+
this.initialLoad = false;
|
|
72
|
+
this.computeQuotaRows();
|
|
73
|
+
if (window.UILogger) window.UILogger.debug('Restored data from cache');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch (e) {
|
|
77
|
+
if (window.UILogger) window.UILogger.debug('Failed to load cache', e.message);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
saveToCache() {
|
|
82
|
+
try {
|
|
83
|
+
const cacheData = {
|
|
84
|
+
accounts: this.accounts,
|
|
85
|
+
models: this.models,
|
|
86
|
+
modelConfig: this.modelConfig,
|
|
87
|
+
usageHistory: this.usageHistory,
|
|
88
|
+
timestamp: Date.now()
|
|
89
|
+
};
|
|
90
|
+
localStorage.setItem('ag_data_cache', JSON.stringify(cacheData));
|
|
91
|
+
} catch (e) {
|
|
92
|
+
if (window.UILogger) window.UILogger.debug('Failed to save cache', e.message);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
async fetchData() {
|
|
97
|
+
// Only show skeleton on initial load if we didn't restore from cache
|
|
98
|
+
if (this.initialLoad) {
|
|
99
|
+
this.loading = true;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
// Get password from global store
|
|
103
|
+
const password = Alpine.store('global').webuiPassword;
|
|
104
|
+
|
|
105
|
+
// Include history for dashboard (single API call optimization)
|
|
106
|
+
const url = '/account-limits?includeHistory=true';
|
|
107
|
+
const { response, newPassword } = await window.utils.request(url, {}, password);
|
|
108
|
+
|
|
109
|
+
if (newPassword) Alpine.store('global').webuiPassword = newPassword;
|
|
110
|
+
|
|
111
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
112
|
+
|
|
113
|
+
const data = await response.json();
|
|
114
|
+
this.accounts = data.accounts || [];
|
|
115
|
+
if (data.models && data.models.length > 0) {
|
|
116
|
+
this.models = data.models;
|
|
117
|
+
}
|
|
118
|
+
this.modelConfig = data.modelConfig || {};
|
|
119
|
+
|
|
120
|
+
// Store usage history if included (for dashboard)
|
|
121
|
+
if (data.history) {
|
|
122
|
+
this.usageHistory = data.history;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.saveToCache(); // Save fresh data
|
|
126
|
+
this.computeQuotaRows();
|
|
127
|
+
|
|
128
|
+
this.lastUpdated = new Date().toLocaleTimeString();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
// Keep error logging for actual fetch failures
|
|
131
|
+
console.error('Fetch error:', error);
|
|
132
|
+
const store = Alpine.store('global');
|
|
133
|
+
store.showToast(store.t('connectionLost'), 'error');
|
|
134
|
+
} finally {
|
|
135
|
+
this.loading = false;
|
|
136
|
+
this.initialLoad = false; // Mark initial load as complete
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
async performHealthCheck() {
|
|
141
|
+
try {
|
|
142
|
+
// Get password from global store
|
|
143
|
+
const password = Alpine.store('global').webuiPassword;
|
|
144
|
+
|
|
145
|
+
// Use lightweight endpoint (no quota fetching)
|
|
146
|
+
const { response, newPassword } = await window.utils.request('/api/config', {}, password);
|
|
147
|
+
|
|
148
|
+
if (newPassword) Alpine.store('global').webuiPassword = newPassword;
|
|
149
|
+
|
|
150
|
+
if (response.ok) {
|
|
151
|
+
this.connectionStatus = 'connected';
|
|
152
|
+
} else {
|
|
153
|
+
this.connectionStatus = 'disconnected';
|
|
154
|
+
}
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error('Health check error:', error);
|
|
157
|
+
this.connectionStatus = 'disconnected';
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
startHealthCheck() {
|
|
162
|
+
// Clear existing timer
|
|
163
|
+
if (this.healthCheckTimer) {
|
|
164
|
+
clearInterval(this.healthCheckTimer);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Setup visibility change listener (only once)
|
|
168
|
+
if (!this._healthVisibilitySetup) {
|
|
169
|
+
this._healthVisibilitySetup = true;
|
|
170
|
+
this._visibilityHandler = () => {
|
|
171
|
+
if (document.hidden) {
|
|
172
|
+
// Tab hidden - stop health checks
|
|
173
|
+
this.stopHealthCheck();
|
|
174
|
+
} else {
|
|
175
|
+
// Tab visible - restart health checks
|
|
176
|
+
this.startHealthCheck();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
document.addEventListener('visibilitychange', this._visibilityHandler);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Perform immediate health check
|
|
183
|
+
this.performHealthCheck();
|
|
184
|
+
|
|
185
|
+
// Schedule regular health checks every 15 seconds
|
|
186
|
+
this.healthCheckTimer = setInterval(() => {
|
|
187
|
+
// Only perform health check if tab is visible
|
|
188
|
+
if (!document.hidden) {
|
|
189
|
+
this.performHealthCheck();
|
|
190
|
+
}
|
|
191
|
+
}, 15000);
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
stopHealthCheck() {
|
|
195
|
+
if (this.healthCheckTimer) {
|
|
196
|
+
clearInterval(this.healthCheckTimer);
|
|
197
|
+
this.healthCheckTimer = null;
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
computeQuotaRows() {
|
|
202
|
+
const models = this.models || [];
|
|
203
|
+
const rows = [];
|
|
204
|
+
const showExhausted = Alpine.store('settings')?.showExhausted ?? true;
|
|
205
|
+
|
|
206
|
+
models.forEach(modelId => {
|
|
207
|
+
// Config
|
|
208
|
+
const config = this.modelConfig[modelId] || {};
|
|
209
|
+
const family = this.getModelFamily(modelId);
|
|
210
|
+
|
|
211
|
+
// Visibility Logic for Models Page (quotaRows):
|
|
212
|
+
// 1. If explicitly hidden via config, ALWAYS hide (clean interface)
|
|
213
|
+
// 2. If no config, default 'unknown' families to HIDDEN
|
|
214
|
+
// 3. Known families (Claude/Gemini) default to VISIBLE
|
|
215
|
+
// Note: To manage hidden models, use Settings → Models tab
|
|
216
|
+
let isHidden = config.hidden;
|
|
217
|
+
if (isHidden === undefined) {
|
|
218
|
+
isHidden = (family === 'other' || family === 'unknown');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Models Page: Check settings for visibility
|
|
222
|
+
const showHidden = Alpine.store('settings')?.showHiddenModels ?? false;
|
|
223
|
+
if (isHidden && !showHidden) return;
|
|
224
|
+
|
|
225
|
+
// Filters
|
|
226
|
+
if (this.filters.family !== 'all' && this.filters.family !== family) return;
|
|
227
|
+
if (this.filters.search) {
|
|
228
|
+
const searchLower = this.filters.search.toLowerCase();
|
|
229
|
+
const idMatch = modelId.toLowerCase().includes(searchLower);
|
|
230
|
+
if (!idMatch) return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Data Collection
|
|
234
|
+
const quotaInfo = [];
|
|
235
|
+
let minQuota = 100;
|
|
236
|
+
let totalQuotaSum = 0;
|
|
237
|
+
let validAccountCount = 0;
|
|
238
|
+
let minResetTime = null;
|
|
239
|
+
|
|
240
|
+
this.accounts.forEach(acc => {
|
|
241
|
+
if (acc.enabled === false) return;
|
|
242
|
+
if (this.filters.account !== 'all' && acc.email !== this.filters.account) return;
|
|
243
|
+
|
|
244
|
+
const limit = acc.limits?.[modelId];
|
|
245
|
+
if (!limit) return;
|
|
246
|
+
|
|
247
|
+
const pct = limit.remainingFraction !== null ? Math.round(limit.remainingFraction * 100) : 0;
|
|
248
|
+
minQuota = Math.min(minQuota, pct);
|
|
249
|
+
|
|
250
|
+
// Accumulate for average
|
|
251
|
+
totalQuotaSum += pct;
|
|
252
|
+
validAccountCount++;
|
|
253
|
+
|
|
254
|
+
if (limit.resetTime && (!minResetTime || new Date(limit.resetTime) < new Date(minResetTime))) {
|
|
255
|
+
minResetTime = limit.resetTime;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
quotaInfo.push({
|
|
259
|
+
email: acc.email.split('@')[0],
|
|
260
|
+
fullEmail: acc.email,
|
|
261
|
+
pct: pct,
|
|
262
|
+
resetTime: limit.resetTime,
|
|
263
|
+
provider: acc.provider || 'google'
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (quotaInfo.length === 0) return;
|
|
268
|
+
const avgQuota = validAccountCount > 0 ? Math.round(totalQuotaSum / validAccountCount) : 0;
|
|
269
|
+
|
|
270
|
+
if (!showExhausted && minQuota === 0) return;
|
|
271
|
+
|
|
272
|
+
rows.push({
|
|
273
|
+
modelId,
|
|
274
|
+
displayName: modelId, // Simplified: no longer using alias
|
|
275
|
+
family,
|
|
276
|
+
minQuota,
|
|
277
|
+
avgQuota, // Added Average Quota
|
|
278
|
+
minResetTime,
|
|
279
|
+
resetIn: minResetTime ? window.utils.formatTimeUntil(minResetTime) : '-',
|
|
280
|
+
quotaInfo,
|
|
281
|
+
pinned: !!config.pinned,
|
|
282
|
+
hidden: !!isHidden, // Use computed visibility
|
|
283
|
+
activeCount: quotaInfo.filter(q => q.pct > 0).length,
|
|
284
|
+
providers: [...new Set(quotaInfo.map(q => q.provider || 'google'))] // Unique providers for this model
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Sort: Pinned first, then by selected column
|
|
289
|
+
const sortCol = this.filters.sortCol;
|
|
290
|
+
const sortAsc = this.filters.sortAsc;
|
|
291
|
+
|
|
292
|
+
this.quotaRows = rows.sort((a, b) => {
|
|
293
|
+
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
|
|
294
|
+
|
|
295
|
+
let valA = a[sortCol];
|
|
296
|
+
let valB = b[sortCol];
|
|
297
|
+
|
|
298
|
+
// Handle nulls (always push to bottom)
|
|
299
|
+
if (valA === valB) return 0;
|
|
300
|
+
if (valA === null || valA === undefined) return 1;
|
|
301
|
+
if (valB === null || valB === undefined) return -1;
|
|
302
|
+
|
|
303
|
+
if (typeof valA === 'string' && typeof valB === 'string') {
|
|
304
|
+
return sortAsc ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return sortAsc ? valA - valB : valB - valA;
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Trigger Dashboard Update if active
|
|
311
|
+
// Ideally dashboard watches this store.
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
setSort(col) {
|
|
315
|
+
if (this.filters.sortCol === col) {
|
|
316
|
+
this.filters.sortAsc = !this.filters.sortAsc;
|
|
317
|
+
} else {
|
|
318
|
+
this.filters.sortCol = col;
|
|
319
|
+
// Default sort direction: Descending for numbers/stats, Ascending for text/time
|
|
320
|
+
if (['avgQuota', 'activeCount'].includes(col)) {
|
|
321
|
+
this.filters.sortAsc = false;
|
|
322
|
+
} else {
|
|
323
|
+
this.filters.sortAsc = true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
this.computeQuotaRows();
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
getModelFamily(modelId) {
|
|
330
|
+
const lower = modelId.toLowerCase();
|
|
331
|
+
if (lower.includes('claude')) return 'claude';
|
|
332
|
+
if (lower.includes('gemini')) return 'gemini';
|
|
333
|
+
if (lower.includes('gpt')) return 'gpt';
|
|
334
|
+
if (lower.startsWith('o1') || lower.startsWith('o3')) return 'o1';
|
|
335
|
+
return 'other';
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get quota data without filters applied (for Dashboard global charts)
|
|
340
|
+
* Returns array of { modelId, family, quotaInfo: [{pct}] }
|
|
341
|
+
*/
|
|
342
|
+
getUnfilteredQuotaData() {
|
|
343
|
+
const models = this.models || [];
|
|
344
|
+
const rows = [];
|
|
345
|
+
const showHidden = Alpine.store('settings')?.showHiddenModels ?? false;
|
|
346
|
+
|
|
347
|
+
models.forEach(modelId => {
|
|
348
|
+
const config = this.modelConfig[modelId] || {};
|
|
349
|
+
const family = this.getModelFamily(modelId);
|
|
350
|
+
|
|
351
|
+
// Smart visibility (same logic as computeQuotaRows)
|
|
352
|
+
let isHidden = config.hidden;
|
|
353
|
+
if (isHidden === undefined) {
|
|
354
|
+
isHidden = (family === 'other' || family === 'unknown');
|
|
355
|
+
}
|
|
356
|
+
if (isHidden && !showHidden) return;
|
|
357
|
+
|
|
358
|
+
const quotaInfo = [];
|
|
359
|
+
// Use ALL accounts (no account filter)
|
|
360
|
+
this.accounts.forEach(acc => {
|
|
361
|
+
if (acc.enabled === false) return;
|
|
362
|
+
const limit = acc.limits?.[modelId];
|
|
363
|
+
if (!limit) return;
|
|
364
|
+
const pct = limit.remainingFraction !== null ? Math.round(limit.remainingFraction * 100) : 0;
|
|
365
|
+
quotaInfo.push({ pct });
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// treat missing quotaInfo as 0%/unknown; still include row
|
|
369
|
+
rows.push({ modelId, family, quotaInfo });
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
return rows;
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Store
|
|
3
|
+
*/
|
|
4
|
+
document.addEventListener('alpine:init', () => {
|
|
5
|
+
Alpine.store('settings', {
|
|
6
|
+
refreshInterval: 60,
|
|
7
|
+
logLimit: 2000,
|
|
8
|
+
showExhausted: true,
|
|
9
|
+
showHiddenModels: false, // New field
|
|
10
|
+
compact: false,
|
|
11
|
+
port: 8080, // Display only
|
|
12
|
+
|
|
13
|
+
init() {
|
|
14
|
+
this.loadSettings();
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
// Call this method when toggling settings in the UI
|
|
18
|
+
toggle(key) {
|
|
19
|
+
if (this.hasOwnProperty(key) && typeof this[key] === 'boolean') {
|
|
20
|
+
this[key] = !this[key];
|
|
21
|
+
this.saveSettings(true);
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
loadSettings() {
|
|
26
|
+
const saved = localStorage.getItem('commons_settings');
|
|
27
|
+
if (saved) {
|
|
28
|
+
const parsed = JSON.parse(saved);
|
|
29
|
+
Object.keys(parsed).forEach(k => {
|
|
30
|
+
// Only load keys that exist in our default state (safety)
|
|
31
|
+
if (this.hasOwnProperty(k)) this[k] = parsed[k];
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
saveSettings(silent = false) {
|
|
37
|
+
const toSave = {
|
|
38
|
+
refreshInterval: this.refreshInterval,
|
|
39
|
+
logLimit: this.logLimit,
|
|
40
|
+
showExhausted: this.showExhausted,
|
|
41
|
+
showHiddenModels: this.showHiddenModels,
|
|
42
|
+
compact: this.compact
|
|
43
|
+
};
|
|
44
|
+
localStorage.setItem('commons_settings', JSON.stringify(toSave));
|
|
45
|
+
|
|
46
|
+
if (!silent) {
|
|
47
|
+
const store = Alpine.store('global');
|
|
48
|
+
store.showToast(store.t('configSaved'), 'success');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Trigger updates
|
|
52
|
+
document.dispatchEvent(new CustomEvent('refresh-interval-changed'));
|
|
53
|
+
if (Alpine.store('data')) {
|
|
54
|
+
Alpine.store('data').computeQuotaRows();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Store for Antigravity Console
|
|
3
|
+
* Handles Translations, Toasts, and Shared Config
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
document.addEventListener('alpine:init', () => {
|
|
7
|
+
Alpine.store('global', {
|
|
8
|
+
init() {
|
|
9
|
+
// Hash-based routing
|
|
10
|
+
const validTabs = ['dashboard', 'models', 'accounts', 'logs', 'settings'];
|
|
11
|
+
const getHash = () => window.location.hash.substring(1);
|
|
12
|
+
|
|
13
|
+
// 1. Initial load from hash
|
|
14
|
+
const initialHash = getHash();
|
|
15
|
+
if (validTabs.includes(initialHash)) {
|
|
16
|
+
this.activeTab = initialHash;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 2. Sync State -> URL
|
|
20
|
+
Alpine.effect(() => {
|
|
21
|
+
if (validTabs.includes(this.activeTab) && getHash() !== this.activeTab) {
|
|
22
|
+
window.location.hash = this.activeTab;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// 3. Sync URL -> State (Back/Forward buttons)
|
|
27
|
+
window.addEventListener('hashchange', () => {
|
|
28
|
+
const hash = getHash();
|
|
29
|
+
if (validTabs.includes(hash) && this.activeTab !== hash) {
|
|
30
|
+
this.activeTab = hash;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// 4. Fetch version from API
|
|
35
|
+
this.fetchVersion();
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async fetchVersion() {
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch('/api/config');
|
|
41
|
+
if (response.ok) {
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
if (data.version) {
|
|
44
|
+
this.version = data.version;
|
|
45
|
+
}
|
|
46
|
+
// Update maxAccounts in data store
|
|
47
|
+
if (data.config && typeof data.config.maxAccounts === 'number') {
|
|
48
|
+
Alpine.store('data').maxAccounts = data.config.maxAccounts;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.debug('Could not fetch version:', error);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// App State
|
|
57
|
+
version: '1.0.0',
|
|
58
|
+
activeTab: 'dashboard',
|
|
59
|
+
webuiPassword: localStorage.getItem('commons_webui_password') || '',
|
|
60
|
+
|
|
61
|
+
// i18n
|
|
62
|
+
lang: localStorage.getItem('app_lang') || 'en',
|
|
63
|
+
translations: window.translations || {},
|
|
64
|
+
|
|
65
|
+
// Toast Messages
|
|
66
|
+
toast: null,
|
|
67
|
+
|
|
68
|
+
// OAuth Progress
|
|
69
|
+
oauthProgress: {
|
|
70
|
+
active: false,
|
|
71
|
+
current: 0,
|
|
72
|
+
max: 60,
|
|
73
|
+
cancel: null
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
t(key, params = {}) {
|
|
77
|
+
let str = this.translations[this.lang][key] || key;
|
|
78
|
+
if (typeof str === 'string') {
|
|
79
|
+
Object.keys(params).forEach(p => {
|
|
80
|
+
str = str.replace(`{${p}}`, params[p]);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return str;
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
setLang(l) {
|
|
87
|
+
this.lang = l;
|
|
88
|
+
localStorage.setItem('app_lang', l);
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
showToast(message, type = 'info') {
|
|
92
|
+
const id = Date.now();
|
|
93
|
+
this.toast = { message, type, id };
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
if (this.toast && this.toast.id === id) this.toast = null;
|
|
96
|
+
}, 3000);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|