@pikoloo/codex-proxy 1.0.7 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +76 -0
- package/README.md +28 -11
- package/bin/cli.js +15 -15
- package/docs/ACCOUNT.md +104 -0
- package/docs/API.md +21 -29
- package/docs/ARCHITECTURE.md +9 -9
- package/docs/CLAUDE_INTEGRATION.md +3 -3
- package/docs/OAUTH.md +13 -13
- package/docs/OPENCLAW.md +1 -1
- package/docs/legal.md +6 -0
- package/images/dashboard-screenshot.png +0 -0
- package/images/readme-cover.png +0 -0
- package/images/settings-screenshot.png +0 -0
- package/package.json +19 -10
- package/public/css/style.css +802 -22
- package/public/index.html +236 -338
- package/public/js/app.js +140 -118
- package/src/account-manager.js +210 -292
- package/src/cli/account.js +236 -0
- package/src/direct-api.js +7 -9
- package/src/index.js +7 -7
- package/src/middleware/credentials.js +6 -47
- package/src/oauth.js +2 -1
- package/src/routes/{accounts-route.js → account-route.js} +25 -109
- package/src/routes/api-routes.js +18 -30
- package/src/routes/chat-route.js +3 -3
- package/src/routes/messages-route.js +37 -199
- package/src/routes/models-route.js +11 -21
- package/src/routes/settings-route.js +1 -41
- package/src/security.js +1 -1
- package/src/server-settings.js +30 -38
- package/src/utils/logger.js +14 -1
- package/docs/ACCOUNTS.md +0 -202
- package/images/demo-screenshot.png +0 -0
- package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
- package/src/account-rotation/index.js +0 -93
- package/src/account-rotation/rate-limits.js +0 -293
- package/src/account-rotation/strategies/base-strategy.js +0 -48
- package/src/account-rotation/strategies/index.js +0 -31
- package/src/account-rotation/strategies/round-robin-strategy.js +0 -42
- package/src/account-rotation/strategies/sticky-strategy.js +0 -97
- package/src/cli/accounts.js +0 -557
package/public/js/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
document.addEventListener('alpine:init', () => {
|
|
2
|
-
const validTabs = ['dashboard', 'metrics', '
|
|
2
|
+
const validTabs = ['dashboard', 'metrics', 'account', 'logs', 'settings'];
|
|
3
3
|
const initialTab = () => {
|
|
4
4
|
const params = new URLSearchParams(window.location.search);
|
|
5
5
|
const requested = params.get('tab') || window.location.hash.replace(/^#/, '');
|
|
@@ -7,16 +7,14 @@ document.addEventListener('alpine:init', () => {
|
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
Alpine.data('app', () => ({
|
|
10
|
-
version: '1.
|
|
10
|
+
version: '1.2.2',
|
|
11
11
|
connectionStatus: 'connecting',
|
|
12
12
|
activeTab: initialTab(),
|
|
13
|
-
sidebarOpen: window.innerWidth >= 1024,
|
|
14
13
|
loading: false,
|
|
15
14
|
toast: null,
|
|
16
15
|
currentTime: '',
|
|
17
16
|
|
|
18
17
|
accounts: [],
|
|
19
|
-
searchQuery: '',
|
|
20
18
|
stats: { total: 0, active: 0, expired: 0, planType: '-' },
|
|
21
19
|
metricsRange: '24h',
|
|
22
20
|
metricsStatusFilter: '',
|
|
@@ -49,10 +47,7 @@ document.addEventListener('alpine:init', () => {
|
|
|
49
47
|
reasoningLevelOptions: [],
|
|
50
48
|
modelMappingSaving: null,
|
|
51
49
|
reasoningMappingSaving: null,
|
|
52
|
-
accountStrategy: 'sticky',
|
|
53
|
-
multiAccountRotationEnabled: false,
|
|
54
50
|
haikuModelSaving: false,
|
|
55
|
-
strategySaving: false,
|
|
56
51
|
configureClaudeOnStartup: false,
|
|
57
52
|
claudeProxyConfiguring: false,
|
|
58
53
|
claudeProxyStartupSaving: false,
|
|
@@ -73,42 +68,83 @@ document.addEventListener('alpine:init', () => {
|
|
|
73
68
|
|
|
74
69
|
testPrompt: 'Say hello',
|
|
75
70
|
testResponse: '',
|
|
71
|
+
testStatus: 'idle',
|
|
72
|
+
testError: '',
|
|
73
|
+
testMeta: null,
|
|
76
74
|
testing: false,
|
|
77
75
|
|
|
78
76
|
haikuTestPrompt: 'Say hello',
|
|
79
77
|
haikuTestResponse: '',
|
|
78
|
+
haikuTestStatus: 'idle',
|
|
79
|
+
haikuTestError: '',
|
|
80
|
+
haikuTestMeta: null,
|
|
80
81
|
haikuTesting: false,
|
|
81
82
|
|
|
82
83
|
haikuModelLabel() {
|
|
83
84
|
return this.modelOptionName(this.modelMappings?.haiku || 'gpt-5.4-mini');
|
|
84
85
|
},
|
|
85
86
|
|
|
87
|
+
get testStatusText() {
|
|
88
|
+
const labels = {
|
|
89
|
+
idle: 'Ready',
|
|
90
|
+
running: 'Sending request',
|
|
91
|
+
success: 'Response received',
|
|
92
|
+
error: 'Request failed'
|
|
93
|
+
};
|
|
94
|
+
return labels[this.testStatus] || 'Ready';
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
get haikuTestStatusText() {
|
|
98
|
+
const labels = {
|
|
99
|
+
idle: 'Ready',
|
|
100
|
+
running: 'Sending Haiku request',
|
|
101
|
+
success: 'Response received',
|
|
102
|
+
error: 'Request failed'
|
|
103
|
+
};
|
|
104
|
+
return labels[this.haikuTestStatus] || 'Ready';
|
|
105
|
+
},
|
|
106
|
+
|
|
86
107
|
async testHaikuChat() {
|
|
87
108
|
if (!this.haikuTestPrompt.trim()) return;
|
|
109
|
+
const startedAt = Date.now();
|
|
88
110
|
this.haikuTesting = true;
|
|
89
111
|
this.haikuTestResponse = '';
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
112
|
+
this.haikuTestStatus = 'running';
|
|
113
|
+
this.haikuTestError = '';
|
|
114
|
+
this.haikuTestMeta = null;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const { ok, data, error } = await this.api('/v1/chat/completions', {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
body: JSON.stringify({
|
|
120
|
+
model: 'claude-haiku-4',
|
|
121
|
+
messages: [{ role: 'user', content: this.haikuTestPrompt }]
|
|
122
|
+
})
|
|
123
|
+
});
|
|
124
|
+
const durationMs = Date.now() - startedAt;
|
|
125
|
+
this.haikuTestMeta = { durationMs, usage: data?.usage || null };
|
|
126
|
+
|
|
127
|
+
if (ok && data.choices) {
|
|
128
|
+
this.haikuTestResponse = data.choices[0].message.content;
|
|
129
|
+
this.haikuTestStatus = 'success';
|
|
130
|
+
} else {
|
|
131
|
+
this.haikuTestError = data?.error?.message || error || 'Request failed';
|
|
132
|
+
this.haikuTestResponse = this.haikuTestError;
|
|
133
|
+
this.haikuTestStatus = 'error';
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
this.haikuTesting = false;
|
|
102
137
|
}
|
|
103
138
|
},
|
|
104
139
|
|
|
105
|
-
configPath: '~/.codex-claude-proxy/
|
|
140
|
+
configPath: '~/.codex-claude-proxy/account.json',
|
|
106
141
|
serverUrl: window.location.origin,
|
|
107
142
|
|
|
108
143
|
logs: [],
|
|
109
144
|
logSearchQuery: '',
|
|
110
145
|
logFilters: { INFO: true, SUCCESS: true, WARN: true, ERROR: true, DEBUG: false },
|
|
111
146
|
logEventSource: null,
|
|
147
|
+
logStreamStatus: 'connecting',
|
|
112
148
|
|
|
113
149
|
get filteredLogs() {
|
|
114
150
|
const query = this.logSearchQuery.trim().toLowerCase();
|
|
@@ -119,10 +155,11 @@ document.addEventListener('alpine:init', () => {
|
|
|
119
155
|
});
|
|
120
156
|
},
|
|
121
157
|
|
|
122
|
-
get
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
158
|
+
get logLevelCounts() {
|
|
159
|
+
return this.logs.reduce((counts, log) => {
|
|
160
|
+
counts[log.level] = (counts[log.level] || 0) + 1;
|
|
161
|
+
return counts;
|
|
162
|
+
}, { INFO: 0, SUCCESS: 0, WARN: 0, ERROR: 0, DEBUG: 0 });
|
|
126
163
|
},
|
|
127
164
|
|
|
128
165
|
get metricsTotals() {
|
|
@@ -159,14 +196,9 @@ document.addEventListener('alpine:init', () => {
|
|
|
159
196
|
this.startLogStream();
|
|
160
197
|
this.loadModelMappingsSetting();
|
|
161
198
|
this.loadHaikuModelSetting();
|
|
162
|
-
this.loadAccountStrategySetting();
|
|
163
199
|
this.loadClaudeProxySetting();
|
|
164
200
|
this.loadMetrics();
|
|
165
201
|
|
|
166
|
-
window.addEventListener('resize', () => {
|
|
167
|
-
this.sidebarOpen = window.innerWidth >= 1024;
|
|
168
|
-
});
|
|
169
|
-
|
|
170
202
|
window.addEventListener('message', (event) => {
|
|
171
203
|
if (event.data && event.data.type === 'oauth-success') {
|
|
172
204
|
this.showToast(`Account ${event.data.email} added!`, 'success');
|
|
@@ -195,9 +227,6 @@ document.addEventListener('alpine:init', () => {
|
|
|
195
227
|
nextUrl.hash = '';
|
|
196
228
|
}
|
|
197
229
|
window.history.replaceState({}, '', nextUrl);
|
|
198
|
-
if (window.innerWidth < 1024) {
|
|
199
|
-
this.sidebarOpen = false;
|
|
200
|
-
}
|
|
201
230
|
},
|
|
202
231
|
|
|
203
232
|
async api(endpoint, options = {}) {
|
|
@@ -220,15 +249,15 @@ document.addEventListener('alpine:init', () => {
|
|
|
220
249
|
|
|
221
250
|
async refreshAccounts() {
|
|
222
251
|
this.loading = true;
|
|
223
|
-
const { ok, data } = await this.api('/
|
|
252
|
+
const { ok, data } = await this.api('/account');
|
|
224
253
|
|
|
225
|
-
if (ok
|
|
226
|
-
this.accounts = data.
|
|
254
|
+
if (ok) {
|
|
255
|
+
this.accounts = data.account ? [data.account] : [];
|
|
227
256
|
this.stats = {
|
|
228
|
-
total: data.total ||
|
|
229
|
-
active:
|
|
230
|
-
expired:
|
|
231
|
-
planType:
|
|
257
|
+
total: data.total || this.accounts.length,
|
|
258
|
+
active: this.accounts.filter(a => a.isActive).length,
|
|
259
|
+
expired: this.accounts.filter(a => a.tokenExpired).length,
|
|
260
|
+
planType: this.accounts.find(a => a.isActive)?.planType || '-'
|
|
232
261
|
};
|
|
233
262
|
|
|
234
263
|
await this.refreshAllQuotaData();
|
|
@@ -305,6 +334,15 @@ document.addEventListener('alpine:init', () => {
|
|
|
305
334
|
return `${number}ms`;
|
|
306
335
|
},
|
|
307
336
|
|
|
337
|
+
formatUsageSummary(usage) {
|
|
338
|
+
if (!usage) return '';
|
|
339
|
+
const input = Number(usage.prompt_tokens ?? usage.input_tokens) || 0;
|
|
340
|
+
const output = Number(usage.completion_tokens ?? usage.output_tokens) || 0;
|
|
341
|
+
const total = Number(usage.total_tokens ?? (input + output)) || 0;
|
|
342
|
+
if (total <= 0) return '';
|
|
343
|
+
return `${this.formatTokenCount(total)} tokens (${this.formatTokenCount(input)} in, ${this.formatTokenCount(output)} out)`;
|
|
344
|
+
},
|
|
345
|
+
|
|
308
346
|
formatBytes(value) {
|
|
309
347
|
const number = Number(value) || 0;
|
|
310
348
|
if (number >= 1024 * 1024) return `${(number / (1024 * 1024)).toFixed(1)} MB`;
|
|
@@ -328,16 +366,12 @@ document.addEventListener('alpine:init', () => {
|
|
|
328
366
|
|
|
329
367
|
async refreshAllQuotaData() {
|
|
330
368
|
if (!this.accounts.length) return;
|
|
331
|
-
const { ok, data } = await this.api('/
|
|
332
|
-
if (!ok || !data?.
|
|
333
|
-
|
|
334
|
-
const quotaMap = new Map(
|
|
335
|
-
data.accounts.map((entry) => [entry.email, entry.quota || null])
|
|
336
|
-
);
|
|
369
|
+
const { ok, data } = await this.api('/account/quota');
|
|
370
|
+
if (!ok || !data?.email) return;
|
|
337
371
|
|
|
338
372
|
this.accounts = this.accounts.map((account) => ({
|
|
339
373
|
...account,
|
|
340
|
-
quota:
|
|
374
|
+
quota: account.email === data.email ? data.quota || null : account.quota
|
|
341
375
|
}));
|
|
342
376
|
|
|
343
377
|
if (this.selectedAccount?.email) {
|
|
@@ -446,8 +480,8 @@ document.addEventListener('alpine:init', () => {
|
|
|
446
480
|
},
|
|
447
481
|
|
|
448
482
|
async startOAuth() {
|
|
449
|
-
await this.api('/
|
|
450
|
-
const { ok, data } = await this.api('/
|
|
483
|
+
await this.api('/account/oauth/cleanup', { method: 'POST' });
|
|
484
|
+
const { ok, data } = await this.api('/account/add', { method: 'POST' });
|
|
451
485
|
|
|
452
486
|
if (ok && data.oauth_url) {
|
|
453
487
|
const width = 500, height = 700;
|
|
@@ -456,8 +490,8 @@ document.addEventListener('alpine:init', () => {
|
|
|
456
490
|
window.open(data.oauth_url, 'ChatGPT Login', `width=${width},height=${height},left=${left},top=${top}`);
|
|
457
491
|
|
|
458
492
|
const checkAdded = setInterval(async () => {
|
|
459
|
-
const { ok, data } = await this.api('/
|
|
460
|
-
if (ok && data.
|
|
493
|
+
const { ok, data } = await this.api('/account');
|
|
494
|
+
if (ok && data.account) {
|
|
461
495
|
clearInterval(checkAdded);
|
|
462
496
|
this.showAddModal = false;
|
|
463
497
|
this.refreshAccounts();
|
|
@@ -471,8 +505,8 @@ document.addEventListener('alpine:init', () => {
|
|
|
471
505
|
},
|
|
472
506
|
|
|
473
507
|
async startManualOAuth() {
|
|
474
|
-
await this.api('/
|
|
475
|
-
const { ok, data } = await this.api('/
|
|
508
|
+
await this.api('/account/oauth/cleanup', { method: 'POST' });
|
|
509
|
+
const { ok, data } = await this.api('/account/add', { method: 'POST' });
|
|
476
510
|
|
|
477
511
|
if (ok && data.oauth_url) {
|
|
478
512
|
this.oauthManualUrl = data.oauth_url;
|
|
@@ -487,7 +521,7 @@ document.addEventListener('alpine:init', () => {
|
|
|
487
521
|
async submitManualOAuth() {
|
|
488
522
|
if (!this.oauthManualCode) return;
|
|
489
523
|
|
|
490
|
-
const { ok, data } = await this.api('/
|
|
524
|
+
const { ok, data } = await this.api('/account/add/manual', {
|
|
491
525
|
method: 'POST',
|
|
492
526
|
body: JSON.stringify({
|
|
493
527
|
code: this.oauthManualCode,
|
|
@@ -515,7 +549,7 @@ document.addEventListener('alpine:init', () => {
|
|
|
515
549
|
},
|
|
516
550
|
|
|
517
551
|
async importFromCodex() {
|
|
518
|
-
const { ok, data } = await this.api('/
|
|
552
|
+
const { ok, data } = await this.api('/account/import', { method: 'POST' });
|
|
519
553
|
if (ok && data.success) {
|
|
520
554
|
this.showToast(data.message, 'success');
|
|
521
555
|
this.showAddModal = false;
|
|
@@ -525,21 +559,8 @@ document.addEventListener('alpine:init', () => {
|
|
|
525
559
|
}
|
|
526
560
|
},
|
|
527
561
|
|
|
528
|
-
async switchAccount(email) {
|
|
529
|
-
const { ok, data } = await this.api('/accounts/switch', {
|
|
530
|
-
method: 'POST',
|
|
531
|
-
body: JSON.stringify({ email })
|
|
532
|
-
});
|
|
533
|
-
if (ok && data.success) {
|
|
534
|
-
this.showToast(data.message, 'success');
|
|
535
|
-
this.refreshAccounts();
|
|
536
|
-
} else {
|
|
537
|
-
this.showToast(data?.message || 'Failed to switch', 'error');
|
|
538
|
-
}
|
|
539
|
-
},
|
|
540
|
-
|
|
541
562
|
async refreshToken(email) {
|
|
542
|
-
const { ok, data } = await this.api(
|
|
563
|
+
const { ok, data } = await this.api('/account/refresh', { method: 'POST' });
|
|
543
564
|
if (ok && data.success) {
|
|
544
565
|
this.showToast(data.message, 'success');
|
|
545
566
|
this.refreshAccounts();
|
|
@@ -548,24 +569,13 @@ document.addEventListener('alpine:init', () => {
|
|
|
548
569
|
}
|
|
549
570
|
},
|
|
550
571
|
|
|
551
|
-
async refreshAllTokens() {
|
|
552
|
-
this.showToast('Refreshing all tokens...', 'info');
|
|
553
|
-
const { ok, data } = await this.api('/accounts/refresh/all', { method: 'POST' });
|
|
554
|
-
if (ok) {
|
|
555
|
-
this.showToast(data.message, 'success');
|
|
556
|
-
this.refreshAccounts();
|
|
557
|
-
} else {
|
|
558
|
-
this.showToast(data?.message || 'Failed', 'error');
|
|
559
|
-
}
|
|
560
|
-
},
|
|
561
|
-
|
|
562
572
|
confirmDelete(email) {
|
|
563
573
|
this.deleteTarget = email;
|
|
564
574
|
this.showDeleteModal = true;
|
|
565
575
|
},
|
|
566
576
|
|
|
567
577
|
async executeDelete() {
|
|
568
|
-
const { ok, data } = await this.api(
|
|
578
|
+
const { ok, data } = await this.api('/account', { method: 'DELETE' });
|
|
569
579
|
this.showDeleteModal = false;
|
|
570
580
|
if (ok && data.success) {
|
|
571
581
|
this.showToast(data.message, 'success');
|
|
@@ -582,20 +592,34 @@ document.addEventListener('alpine:init', () => {
|
|
|
582
592
|
|
|
583
593
|
async testChat() {
|
|
584
594
|
if (!this.testPrompt.trim()) return;
|
|
595
|
+
const startedAt = Date.now();
|
|
585
596
|
this.testing = true;
|
|
586
597
|
this.testResponse = '';
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
598
|
+
this.testStatus = 'running';
|
|
599
|
+
this.testError = '';
|
|
600
|
+
this.testMeta = null;
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
const { ok, data, error } = await this.api('/v1/chat/completions', {
|
|
604
|
+
method: 'POST',
|
|
605
|
+
body: JSON.stringify({
|
|
606
|
+
model: 'gpt-5.5',
|
|
607
|
+
messages: [{ role: 'user', content: this.testPrompt }]
|
|
608
|
+
})
|
|
609
|
+
});
|
|
610
|
+
const durationMs = Date.now() - startedAt;
|
|
611
|
+
this.testMeta = { durationMs, usage: data?.usage || null };
|
|
612
|
+
|
|
613
|
+
if (ok && data.choices) {
|
|
614
|
+
this.testResponse = data.choices[0].message.content;
|
|
615
|
+
this.testStatus = 'success';
|
|
616
|
+
} else {
|
|
617
|
+
this.testError = data?.error?.message || error || 'Request failed';
|
|
618
|
+
this.testResponse = this.testError;
|
|
619
|
+
this.testStatus = 'error';
|
|
620
|
+
}
|
|
621
|
+
} finally {
|
|
622
|
+
this.testing = false;
|
|
599
623
|
}
|
|
600
624
|
},
|
|
601
625
|
|
|
@@ -725,31 +749,6 @@ document.addEventListener('alpine:init', () => {
|
|
|
725
749
|
}
|
|
726
750
|
},
|
|
727
751
|
|
|
728
|
-
async loadAccountStrategySetting() {
|
|
729
|
-
const { ok, data } = await this.api('/settings/account-strategy');
|
|
730
|
-
if (ok && data?.accountStrategy) {
|
|
731
|
-
this.accountStrategy = data.accountStrategy;
|
|
732
|
-
this.multiAccountRotationEnabled = data.rotationEnabled === true;
|
|
733
|
-
}
|
|
734
|
-
},
|
|
735
|
-
|
|
736
|
-
async setAccountStrategy(strategy) {
|
|
737
|
-
if (!this.multiAccountRotationEnabled || this.strategySaving || this.accountStrategy === strategy) return;
|
|
738
|
-
this.strategySaving = true;
|
|
739
|
-
const { ok, data } = await this.api('/settings/account-strategy', {
|
|
740
|
-
method: 'POST',
|
|
741
|
-
body: JSON.stringify({ accountStrategy: strategy })
|
|
742
|
-
});
|
|
743
|
-
this.strategySaving = false;
|
|
744
|
-
if (ok && data?.accountStrategy) {
|
|
745
|
-
this.accountStrategy = data.accountStrategy;
|
|
746
|
-
this.multiAccountRotationEnabled = data.rotationEnabled === true;
|
|
747
|
-
this.showToast(`Account strategy set to ${data.accountStrategy === 'sticky' ? 'Sticky' : 'Round-Robin'}`, 'success');
|
|
748
|
-
} else {
|
|
749
|
-
this.showToast(data?.error || 'Failed to update strategy', 'error');
|
|
750
|
-
}
|
|
751
|
-
},
|
|
752
|
-
|
|
753
752
|
async loadClaudeProxySetting() {
|
|
754
753
|
const { ok, data } = await this.api('/settings/claude-proxy');
|
|
755
754
|
if (ok && typeof data?.configureClaudeOnStartup === 'boolean') {
|
|
@@ -802,11 +801,16 @@ document.addEventListener('alpine:init', () => {
|
|
|
802
801
|
|
|
803
802
|
startLogStream() {
|
|
804
803
|
if (this.logEventSource) this.logEventSource.close();
|
|
805
|
-
|
|
804
|
+
|
|
805
|
+
this.logStreamStatus = 'connecting';
|
|
806
806
|
this.logEventSource = new EventSource('/api/logs/stream?history=true');
|
|
807
|
+
this.logEventSource.onopen = () => {
|
|
808
|
+
this.logStreamStatus = 'connected';
|
|
809
|
+
};
|
|
807
810
|
this.logEventSource.onmessage = (event) => {
|
|
808
811
|
try {
|
|
809
812
|
const log = JSON.parse(event.data);
|
|
813
|
+
this.logStreamStatus = 'connected';
|
|
810
814
|
this.logs.unshift(log);
|
|
811
815
|
|
|
812
816
|
if (this.logs.length > 500) {
|
|
@@ -816,6 +820,8 @@ document.addEventListener('alpine:init', () => {
|
|
|
816
820
|
};
|
|
817
821
|
|
|
818
822
|
this.logEventSource.onerror = () => {
|
|
823
|
+
this.logStreamStatus = 'disconnected';
|
|
824
|
+
if (this.logEventSource) this.logEventSource.close();
|
|
819
825
|
setTimeout(() => this.startLogStream(), 3000);
|
|
820
826
|
};
|
|
821
827
|
},
|
|
@@ -833,6 +839,22 @@ document.addEventListener('alpine:init', () => {
|
|
|
833
839
|
return message;
|
|
834
840
|
},
|
|
835
841
|
|
|
842
|
+
formatLogTime(timestamp) {
|
|
843
|
+
if (!timestamp) return '--:--:--';
|
|
844
|
+
const date = new Date(timestamp);
|
|
845
|
+
if (Number.isNaN(date.getTime())) return '--:--:--';
|
|
846
|
+
return date.toLocaleTimeString([], { hour12: false });
|
|
847
|
+
},
|
|
848
|
+
|
|
849
|
+
logStreamStatusText() {
|
|
850
|
+
const labels = {
|
|
851
|
+
connecting: 'Connecting',
|
|
852
|
+
connected: 'Live',
|
|
853
|
+
disconnected: 'Reconnecting'
|
|
854
|
+
};
|
|
855
|
+
return labels[this.logStreamStatus] || 'Unknown';
|
|
856
|
+
},
|
|
857
|
+
|
|
836
858
|
getLogDetails(message) {
|
|
837
859
|
if (!message) return null;
|
|
838
860
|
const details = {};
|