codex-lb 0.1.2__py3-none-any.whl
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.
- app/__init__.py +5 -0
- app/cli.py +24 -0
- app/core/__init__.py +0 -0
- app/core/auth/__init__.py +96 -0
- app/core/auth/models.py +49 -0
- app/core/auth/refresh.py +144 -0
- app/core/balancer/__init__.py +19 -0
- app/core/balancer/logic.py +140 -0
- app/core/balancer/types.py +9 -0
- app/core/clients/__init__.py +0 -0
- app/core/clients/http.py +39 -0
- app/core/clients/oauth.py +340 -0
- app/core/clients/proxy.py +265 -0
- app/core/clients/usage.py +143 -0
- app/core/config/__init__.py +0 -0
- app/core/config/settings.py +69 -0
- app/core/crypto.py +37 -0
- app/core/errors.py +73 -0
- app/core/openai/__init__.py +0 -0
- app/core/openai/models.py +122 -0
- app/core/openai/parsing.py +55 -0
- app/core/openai/requests.py +59 -0
- app/core/types.py +4 -0
- app/core/usage/__init__.py +185 -0
- app/core/usage/logs.py +57 -0
- app/core/usage/models.py +35 -0
- app/core/usage/pricing.py +172 -0
- app/core/usage/types.py +95 -0
- app/core/utils/__init__.py +0 -0
- app/core/utils/request_id.py +30 -0
- app/core/utils/retry.py +16 -0
- app/core/utils/sse.py +13 -0
- app/core/utils/time.py +19 -0
- app/db/__init__.py +0 -0
- app/db/models.py +82 -0
- app/db/session.py +44 -0
- app/dependencies.py +123 -0
- app/main.py +124 -0
- app/modules/__init__.py +0 -0
- app/modules/accounts/__init__.py +0 -0
- app/modules/accounts/api.py +81 -0
- app/modules/accounts/repository.py +80 -0
- app/modules/accounts/schemas.py +66 -0
- app/modules/accounts/service.py +211 -0
- app/modules/health/__init__.py +0 -0
- app/modules/health/api.py +10 -0
- app/modules/oauth/__init__.py +0 -0
- app/modules/oauth/api.py +57 -0
- app/modules/oauth/schemas.py +32 -0
- app/modules/oauth/service.py +356 -0
- app/modules/oauth/templates/oauth_success.html +122 -0
- app/modules/proxy/__init__.py +0 -0
- app/modules/proxy/api.py +76 -0
- app/modules/proxy/auth_manager.py +51 -0
- app/modules/proxy/load_balancer.py +208 -0
- app/modules/proxy/schemas.py +85 -0
- app/modules/proxy/service.py +707 -0
- app/modules/proxy/types.py +37 -0
- app/modules/proxy/usage_updater.py +147 -0
- app/modules/request_logs/__init__.py +0 -0
- app/modules/request_logs/api.py +31 -0
- app/modules/request_logs/repository.py +86 -0
- app/modules/request_logs/schemas.py +25 -0
- app/modules/request_logs/service.py +77 -0
- app/modules/shared/__init__.py +0 -0
- app/modules/shared/schemas.py +8 -0
- app/modules/usage/__init__.py +0 -0
- app/modules/usage/api.py +31 -0
- app/modules/usage/repository.py +113 -0
- app/modules/usage/schemas.py +62 -0
- app/modules/usage/service.py +246 -0
- app/static/7.css +1336 -0
- app/static/index.css +543 -0
- app/static/index.html +457 -0
- app/static/index.js +1898 -0
- codex_lb-0.1.2.dist-info/METADATA +108 -0
- codex_lb-0.1.2.dist-info/RECORD +80 -0
- codex_lb-0.1.2.dist-info/WHEEL +4 -0
- codex_lb-0.1.2.dist-info/entry_points.txt +2 -0
- codex_lb-0.1.2.dist-info/licenses/LICENSE +21 -0
app/static/index.js
ADDED
|
@@ -0,0 +1,1898 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
"use strict";
|
|
3
|
+
const API_ENDPOINTS = {
|
|
4
|
+
accounts: "/api/accounts",
|
|
5
|
+
accountsImport: "/api/accounts/import",
|
|
6
|
+
accountReactivate: (accountId) =>
|
|
7
|
+
`/api/accounts/${encodeURIComponent(accountId)}/reactivate`,
|
|
8
|
+
accountPause: (accountId) =>
|
|
9
|
+
`/api/accounts/${encodeURIComponent(accountId)}/pause`,
|
|
10
|
+
accountDelete: (accountId) =>
|
|
11
|
+
`/api/accounts/${encodeURIComponent(accountId)}`,
|
|
12
|
+
usageSummary: "/api/usage/summary",
|
|
13
|
+
usageWindow: (window) =>
|
|
14
|
+
`/api/usage/window?window=${encodeURIComponent(window)}`,
|
|
15
|
+
requestLogs: (limit) => `/api/request-logs?limit=${limit}`,
|
|
16
|
+
oauthStart: "/api/oauth/start",
|
|
17
|
+
oauthStatus: "/api/oauth/status",
|
|
18
|
+
oauthComplete: "/api/oauth/complete",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const PAGES = [
|
|
22
|
+
{
|
|
23
|
+
id: "dashboard",
|
|
24
|
+
tabId: "tab-dashboard",
|
|
25
|
+
label: "Dashboard",
|
|
26
|
+
title: "Codex Load Balancer - Dashboard",
|
|
27
|
+
path: "/dashboard",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "accounts",
|
|
31
|
+
tabId: "tab-accounts",
|
|
32
|
+
label: "Accounts",
|
|
33
|
+
title: "Codex Load Balancer - Accounts",
|
|
34
|
+
path: "/accounts",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const BASE_PATH = "/dashboard";
|
|
39
|
+
const PATH_TO_VIEW = PAGES.reduce((acc, page) => {
|
|
40
|
+
acc[page.path] = page.id;
|
|
41
|
+
return acc;
|
|
42
|
+
}, {});
|
|
43
|
+
PATH_TO_VIEW[BASE_PATH] = "dashboard";
|
|
44
|
+
|
|
45
|
+
const getViewFromPath = (pathname) => {
|
|
46
|
+
const normalized = pathname.replace(/\/+$/, "") || BASE_PATH;
|
|
47
|
+
return PATH_TO_VIEW[normalized] || "dashboard";
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const getPathFromView = (viewId) => {
|
|
51
|
+
const page = PAGES.find((p) => p.id === viewId);
|
|
52
|
+
return page?.path || BASE_PATH;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const STATUS_LABELS = {
|
|
56
|
+
active: "Active",
|
|
57
|
+
paused: "Paused",
|
|
58
|
+
limited: "Rate limited",
|
|
59
|
+
exceeded: "Quota exceeded",
|
|
60
|
+
deactivated: "Deactivated",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const REQUEST_STATUS_LABELS = {
|
|
64
|
+
ok: "OK",
|
|
65
|
+
rate_limit: "Rate limit",
|
|
66
|
+
quota: "Quota",
|
|
67
|
+
error: "Error",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const REQUEST_STATUS_CLASSES = {
|
|
71
|
+
ok: "active",
|
|
72
|
+
rate_limit: "limited",
|
|
73
|
+
quota: "exceeded",
|
|
74
|
+
error: "deactivated",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const PLAN_LABELS = {
|
|
78
|
+
plus: "Plus",
|
|
79
|
+
team: "Team",
|
|
80
|
+
free: "Free",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const ROUTING_LABELS = {
|
|
84
|
+
usage_weighted: "usage weighted",
|
|
85
|
+
round_robin: "round robin",
|
|
86
|
+
sticky: "sticky",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const ERROR_LABELS = {
|
|
90
|
+
rate_limit: "rate limit",
|
|
91
|
+
quota: "quota",
|
|
92
|
+
timeout: "timeout",
|
|
93
|
+
upstream: "upstream",
|
|
94
|
+
rate_limit_exceeded: "rate limit",
|
|
95
|
+
usage_limit_reached: "rate limit",
|
|
96
|
+
insufficient_quota: "quota",
|
|
97
|
+
usage_not_included: "quota",
|
|
98
|
+
quota_exceeded: "quota",
|
|
99
|
+
upstream_error: "upstream",
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const PROGRESS_CLASS_BY_STATUS = {
|
|
103
|
+
paused: "paused",
|
|
104
|
+
limited: "paused",
|
|
105
|
+
exceeded: "error",
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const ACCOUNT_STATUS_MAP = {
|
|
109
|
+
paused: "paused",
|
|
110
|
+
rate_limited: "limited",
|
|
111
|
+
quota_exceeded: "exceeded",
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const MESSAGE_TONE_META = {
|
|
115
|
+
success: {
|
|
116
|
+
label: "Success",
|
|
117
|
+
className: "active",
|
|
118
|
+
defaultTitle: "Import complete",
|
|
119
|
+
},
|
|
120
|
+
error: {
|
|
121
|
+
label: "Error",
|
|
122
|
+
className: "deactivated",
|
|
123
|
+
defaultTitle: "Import failed",
|
|
124
|
+
},
|
|
125
|
+
warning: {
|
|
126
|
+
label: "Warning",
|
|
127
|
+
className: "limited",
|
|
128
|
+
defaultTitle: "Attention",
|
|
129
|
+
},
|
|
130
|
+
info: {
|
|
131
|
+
label: "Info",
|
|
132
|
+
className: "limited",
|
|
133
|
+
defaultTitle: "Message",
|
|
134
|
+
},
|
|
135
|
+
question: {
|
|
136
|
+
label: "Question",
|
|
137
|
+
className: "limited",
|
|
138
|
+
defaultTitle: "Confirm",
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const AUTH_STATUS_LABELS = {
|
|
143
|
+
pendingBrowser: "Waiting for browser sign-in...",
|
|
144
|
+
pendingDevice: "Waiting for device verification...",
|
|
145
|
+
success: "Account linked. Return to the dashboard.",
|
|
146
|
+
error: "Authorization failed.",
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const DONUT_COLORS = [
|
|
150
|
+
"#7bb661",
|
|
151
|
+
"#d9a441",
|
|
152
|
+
"#4b6ea8",
|
|
153
|
+
"#c35d5d",
|
|
154
|
+
"#8d6bd6",
|
|
155
|
+
"#4aa0a8",
|
|
156
|
+
];
|
|
157
|
+
const CONSUMED_COLOR = "#d3d3d3";
|
|
158
|
+
const RESET_ERROR_LABEL = "--";
|
|
159
|
+
|
|
160
|
+
const numberFormatter = new Intl.NumberFormat("en-US");
|
|
161
|
+
const compactFormatter = new Intl.NumberFormat("en-US", {
|
|
162
|
+
notation: "compact",
|
|
163
|
+
maximumFractionDigits: 2,
|
|
164
|
+
});
|
|
165
|
+
const currencyFormatter = new Intl.NumberFormat("en-US", {
|
|
166
|
+
style: "currency",
|
|
167
|
+
currency: "USD",
|
|
168
|
+
minimumFractionDigits: 2,
|
|
169
|
+
maximumFractionDigits: 2,
|
|
170
|
+
});
|
|
171
|
+
const timeLongFormatter = new Intl.DateTimeFormat("en-US", {
|
|
172
|
+
hour: "2-digit",
|
|
173
|
+
minute: "2-digit",
|
|
174
|
+
second: "2-digit",
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const createEmptyDashboardData = () => ({
|
|
178
|
+
lastSyncAt: "",
|
|
179
|
+
routing: {
|
|
180
|
+
strategy: "usage_weighted",
|
|
181
|
+
rotationEnabled: true,
|
|
182
|
+
},
|
|
183
|
+
metrics: {
|
|
184
|
+
requests7d: 0,
|
|
185
|
+
tokensSecondaryWindow: null,
|
|
186
|
+
cost7d: 0,
|
|
187
|
+
errorRate7d: null,
|
|
188
|
+
topError: "",
|
|
189
|
+
},
|
|
190
|
+
usage: {
|
|
191
|
+
primary: {
|
|
192
|
+
remaining: 0,
|
|
193
|
+
capacity: 0,
|
|
194
|
+
resetAt: null,
|
|
195
|
+
windowMinutes: null,
|
|
196
|
+
byAccount: [],
|
|
197
|
+
},
|
|
198
|
+
secondary: {
|
|
199
|
+
remaining: 0,
|
|
200
|
+
capacity: 0,
|
|
201
|
+
resetAt: null,
|
|
202
|
+
windowMinutes: null,
|
|
203
|
+
byAccount: [],
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
recentRequests: [],
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const createEmptyDashboardView = () => ({
|
|
210
|
+
badges: [],
|
|
211
|
+
stats: [],
|
|
212
|
+
donuts: [],
|
|
213
|
+
accountCards: [],
|
|
214
|
+
requests: [],
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const createUiConfig = () => ({
|
|
218
|
+
usageWindows: buildUsageWindowConfig(null),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const createPages = () => PAGES.map((page) => ({ ...page }));
|
|
222
|
+
|
|
223
|
+
const toNumber = (value) => (Number.isFinite(value) ? value : null);
|
|
224
|
+
|
|
225
|
+
const formatNumber = (value) => {
|
|
226
|
+
const numeric = toNumber(value);
|
|
227
|
+
if (numeric === null) {
|
|
228
|
+
return "--";
|
|
229
|
+
}
|
|
230
|
+
return numberFormatter.format(numeric);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const formatCompactNumber = (value) => {
|
|
234
|
+
const numeric = toNumber(value);
|
|
235
|
+
if (numeric === null) {
|
|
236
|
+
return "--";
|
|
237
|
+
}
|
|
238
|
+
return compactFormatter.format(numeric);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const formatCurrency = (value) => {
|
|
242
|
+
const numeric = toNumber(value);
|
|
243
|
+
if (numeric === null) {
|
|
244
|
+
return "--";
|
|
245
|
+
}
|
|
246
|
+
return currencyFormatter.format(numeric);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const formatPercent = (value) => {
|
|
250
|
+
const numeric = toNumber(value);
|
|
251
|
+
if (numeric === null) {
|
|
252
|
+
return "0%";
|
|
253
|
+
}
|
|
254
|
+
return `${Math.round(numeric)}%`;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const formatWindowMinutes = (value) => {
|
|
258
|
+
const minutes = toNumber(value);
|
|
259
|
+
if (minutes === null || minutes <= 0) {
|
|
260
|
+
return "--";
|
|
261
|
+
}
|
|
262
|
+
if (minutes % 1440 === 0) {
|
|
263
|
+
return `${minutes / 1440}d`;
|
|
264
|
+
}
|
|
265
|
+
if (minutes % 60 === 0) {
|
|
266
|
+
return `${minutes / 60}h`;
|
|
267
|
+
}
|
|
268
|
+
return `${minutes}m`;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const formatWindowLabel = (key, minutes) => {
|
|
272
|
+
const formatted = formatWindowMinutes(minutes);
|
|
273
|
+
if (formatted !== "--") {
|
|
274
|
+
return formatted;
|
|
275
|
+
}
|
|
276
|
+
if (key === "secondary") {
|
|
277
|
+
return "7d";
|
|
278
|
+
}
|
|
279
|
+
if (key === "primary") {
|
|
280
|
+
return "5h";
|
|
281
|
+
}
|
|
282
|
+
return "--";
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const formatPercentValue = (value) => {
|
|
286
|
+
const numeric = toNumber(value);
|
|
287
|
+
if (numeric === null) {
|
|
288
|
+
return 0;
|
|
289
|
+
}
|
|
290
|
+
return Math.round(numeric);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const formatRate = (value) => {
|
|
294
|
+
const numeric = toNumber(value);
|
|
295
|
+
if (numeric === null) {
|
|
296
|
+
return "--";
|
|
297
|
+
}
|
|
298
|
+
return `${(numeric * 100).toFixed(1)}%`;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const truncateText = (value, maxLen = 80) => {
|
|
302
|
+
if (!value) {
|
|
303
|
+
return "";
|
|
304
|
+
}
|
|
305
|
+
const text = String(value);
|
|
306
|
+
if (text.length <= maxLen) {
|
|
307
|
+
return text;
|
|
308
|
+
}
|
|
309
|
+
if (maxLen <= 3) {
|
|
310
|
+
return text.slice(0, maxLen);
|
|
311
|
+
}
|
|
312
|
+
return `${text.slice(0, maxLen - 3)}...`;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const parseDate = (iso) => {
|
|
316
|
+
if (!iso) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
const date = new Date(iso);
|
|
320
|
+
if (Number.isNaN(date.getTime())) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
return date;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const formatTimeLong = (iso) => {
|
|
327
|
+
const date = parseDate(iso);
|
|
328
|
+
if (!date) {
|
|
329
|
+
return "--";
|
|
330
|
+
}
|
|
331
|
+
return timeLongFormatter.format(date);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const formatRelative = (ms) => {
|
|
335
|
+
const minutes = Math.ceil(ms / 60000);
|
|
336
|
+
if (minutes < 60) {
|
|
337
|
+
return `in ${minutes}m`;
|
|
338
|
+
}
|
|
339
|
+
const hours = Math.ceil(minutes / 60);
|
|
340
|
+
if (hours < 24) {
|
|
341
|
+
return `in ${hours}h`;
|
|
342
|
+
}
|
|
343
|
+
const days = Math.ceil(hours / 24);
|
|
344
|
+
return `in ${days}d`;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const formatCountdown = (seconds) => {
|
|
348
|
+
const clamped = Math.max(0, Math.floor(seconds || 0));
|
|
349
|
+
const minutes = Math.floor(clamped / 60);
|
|
350
|
+
const remainder = clamped % 60;
|
|
351
|
+
return `${minutes}:${String(remainder).padStart(2, "0")}`;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const formatQuotaResetLabel = (resetAt) => {
|
|
355
|
+
const date = parseDate(resetAt);
|
|
356
|
+
if (!date || date.getTime() <= 0) {
|
|
357
|
+
return RESET_ERROR_LABEL;
|
|
358
|
+
}
|
|
359
|
+
const diffMs = date.getTime() - Date.now();
|
|
360
|
+
if (diffMs <= 0) {
|
|
361
|
+
return "now";
|
|
362
|
+
}
|
|
363
|
+
return formatRelative(diffMs);
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const formatQuotaResetMeta = (resetAtSecondary, windowMinutesSecondary) => {
|
|
367
|
+
const labelSecondary = formatQuotaResetLabel(resetAtSecondary);
|
|
368
|
+
const windowSecondary = formatWindowLabel(
|
|
369
|
+
"secondary",
|
|
370
|
+
windowMinutesSecondary,
|
|
371
|
+
);
|
|
372
|
+
const secondaryOk = labelSecondary !== RESET_ERROR_LABEL;
|
|
373
|
+
if (!secondaryOk) {
|
|
374
|
+
return "Quota reset unavailable";
|
|
375
|
+
}
|
|
376
|
+
return `Quota reset (${windowSecondary}) · ${labelSecondary}`;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const buildUsageWindowTitle = (key, minutes) =>
|
|
380
|
+
`Remaining quota by account (${formatWindowLabel(key, minutes)})`;
|
|
381
|
+
|
|
382
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
383
|
+
|
|
384
|
+
const adjustHexColor = (hex, amount) => {
|
|
385
|
+
if (typeof hex !== "string" || !hex.startsWith("#") || hex.length !== 7) {
|
|
386
|
+
return hex;
|
|
387
|
+
}
|
|
388
|
+
const raw = hex.slice(1);
|
|
389
|
+
const intValue = Number.parseInt(raw, 16);
|
|
390
|
+
if (!Number.isFinite(intValue)) {
|
|
391
|
+
return hex;
|
|
392
|
+
}
|
|
393
|
+
const r = (intValue >> 16) & 255;
|
|
394
|
+
const g = (intValue >> 8) & 255;
|
|
395
|
+
const b = intValue & 255;
|
|
396
|
+
const mix = amount >= 0 ? 255 : 0;
|
|
397
|
+
const factor = clamp(Math.abs(amount), 0, 1);
|
|
398
|
+
const next = (channel) => Math.round(channel + (mix - channel) * factor);
|
|
399
|
+
const toHex = (channel) =>
|
|
400
|
+
clamp(channel, 0, 255).toString(16).padStart(2, "0");
|
|
401
|
+
return `#${toHex(next(r))}${toHex(next(g))}${toHex(next(b))}`;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const buildDonutPalette = (count) => {
|
|
405
|
+
const base = DONUT_COLORS.slice();
|
|
406
|
+
if (count <= base.length) {
|
|
407
|
+
return base.slice(0, count);
|
|
408
|
+
}
|
|
409
|
+
const palette = base.slice();
|
|
410
|
+
const shifts = [0.2, -0.18, 0.32, -0.28];
|
|
411
|
+
let index = 0;
|
|
412
|
+
while (palette.length < count) {
|
|
413
|
+
const baseColor = base[index % base.length];
|
|
414
|
+
const shift = shifts[index % shifts.length];
|
|
415
|
+
palette.push(adjustHexColor(baseColor, shift));
|
|
416
|
+
index += 1;
|
|
417
|
+
}
|
|
418
|
+
return palette;
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const buildUsageWindowConfig = (summary) => {
|
|
422
|
+
const primaryMinutes = toNumber(summary?.primaryWindow?.windowMinutes);
|
|
423
|
+
const secondaryMinutes = toNumber(summary?.secondaryWindow?.windowMinutes);
|
|
424
|
+
return [
|
|
425
|
+
{
|
|
426
|
+
key: "primary",
|
|
427
|
+
title: buildUsageWindowTitle("primary", primaryMinutes),
|
|
428
|
+
range: formatWindowLabel("primary", primaryMinutes),
|
|
429
|
+
windowMinutes: primaryMinutes ?? null,
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
key: "secondary",
|
|
433
|
+
title: buildUsageWindowTitle("secondary", secondaryMinutes),
|
|
434
|
+
range: formatWindowLabel("secondary", secondaryMinutes),
|
|
435
|
+
windowMinutes: secondaryMinutes ?? null,
|
|
436
|
+
},
|
|
437
|
+
];
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const statusBadgeText = (status) =>
|
|
441
|
+
STATUS_LABELS[status] || status || "unknown";
|
|
442
|
+
const statusLabel = (status) => STATUS_LABELS[status] || "Unknown";
|
|
443
|
+
const requestStatusLabel = (status) =>
|
|
444
|
+
REQUEST_STATUS_LABELS[status] || "Unknown";
|
|
445
|
+
const requestStatusClass = (status) =>
|
|
446
|
+
REQUEST_STATUS_CLASSES[status] || "deactivated";
|
|
447
|
+
const planLabel = (plan) => PLAN_LABELS[plan] || "Unknown";
|
|
448
|
+
const routingLabel = (strategy) => ROUTING_LABELS[strategy] || "unknown";
|
|
449
|
+
const errorLabel = (code) => ERROR_LABELS[code] || "--";
|
|
450
|
+
const progressClass = (status) => PROGRESS_CLASS_BY_STATUS[status] || "";
|
|
451
|
+
|
|
452
|
+
const normalizeSearchInput = (value) =>
|
|
453
|
+
String(value ?? "")
|
|
454
|
+
.trim()
|
|
455
|
+
.toLowerCase();
|
|
456
|
+
|
|
457
|
+
const buildSearchTokens = (value) => {
|
|
458
|
+
const normalized = normalizeSearchInput(value);
|
|
459
|
+
if (!normalized) {
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
return normalized.split(/\s+/).filter(Boolean);
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const buildAccountSearchHaystack = (account) => {
|
|
466
|
+
if (!account || typeof account !== "object") {
|
|
467
|
+
return "";
|
|
468
|
+
}
|
|
469
|
+
const parts = [
|
|
470
|
+
account.id,
|
|
471
|
+
account.email,
|
|
472
|
+
account.displayName,
|
|
473
|
+
account.plan,
|
|
474
|
+
planLabel(account.plan),
|
|
475
|
+
account.status,
|
|
476
|
+
statusLabel(account.status),
|
|
477
|
+
]
|
|
478
|
+
.filter(Boolean)
|
|
479
|
+
.map((part) => String(part).trim())
|
|
480
|
+
.filter(Boolean);
|
|
481
|
+
return normalizeSearchInput(parts.join(" "));
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const filterAccountsByQuery = (accounts, query) => {
|
|
485
|
+
const list = Array.isArray(accounts) ? accounts : [];
|
|
486
|
+
const tokens = buildSearchTokens(query);
|
|
487
|
+
if (!tokens.length) {
|
|
488
|
+
return list;
|
|
489
|
+
}
|
|
490
|
+
return list.filter((account) => {
|
|
491
|
+
const haystack = buildAccountSearchHaystack(account);
|
|
492
|
+
return tokens.every((token) => haystack.includes(token));
|
|
493
|
+
});
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const normalizeAccountStatus = (status) =>
|
|
497
|
+
ACCOUNT_STATUS_MAP[status] || status || "deactivated";
|
|
498
|
+
|
|
499
|
+
const normalizeAccount = (account) => {
|
|
500
|
+
const usage = account.usage || {};
|
|
501
|
+
const email = account.displayName || account.email || account.accountId;
|
|
502
|
+
return {
|
|
503
|
+
id: account.accountId,
|
|
504
|
+
email,
|
|
505
|
+
plan: account.planType,
|
|
506
|
+
status: normalizeAccountStatus(account.status),
|
|
507
|
+
usage: {
|
|
508
|
+
primaryRemainingPercent: toNumber(usage.primaryRemainingPercent) ?? 0,
|
|
509
|
+
secondaryRemainingPercent:
|
|
510
|
+
toNumber(usage.secondaryRemainingPercent) ?? 0,
|
|
511
|
+
},
|
|
512
|
+
resetAtPrimary: account.resetAtPrimary ?? null,
|
|
513
|
+
resetAtSecondary: account.resetAtSecondary ?? null,
|
|
514
|
+
auth: account.auth ?? {},
|
|
515
|
+
displayName: email,
|
|
516
|
+
};
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const formatAccountLabel = (accountId, accounts) => {
|
|
520
|
+
if (!accountId) {
|
|
521
|
+
return "";
|
|
522
|
+
}
|
|
523
|
+
const account = accounts?.find((item) => item.id === accountId);
|
|
524
|
+
return account?.displayName || account?.email || accountId;
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const normalizeAccountsPayload = (payload) => {
|
|
528
|
+
return payload.accounts.map(normalizeAccount);
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const normalizeUsageEntry = (entry) => {
|
|
532
|
+
return {
|
|
533
|
+
accountId: entry.accountId,
|
|
534
|
+
requestCount: entry.requestCount ?? 0,
|
|
535
|
+
remainingPercentAvg: toNumber(entry.remainingPercentAvg),
|
|
536
|
+
capacityCredits: toNumber(entry.capacityCredits) ?? 0,
|
|
537
|
+
remainingCredits: toNumber(entry.remainingCredits) ?? 0,
|
|
538
|
+
costUsd: toNumber(entry.costUsd) ?? 0,
|
|
539
|
+
};
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const normalizeUsagePayload = (payload) => {
|
|
543
|
+
return payload.accounts.map(normalizeUsageEntry);
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const normalizeRequestLog = (entry) => {
|
|
547
|
+
return {
|
|
548
|
+
timestamp: entry.requestedAt,
|
|
549
|
+
accountId: entry.accountId,
|
|
550
|
+
requestId: entry.requestId,
|
|
551
|
+
model: entry.model,
|
|
552
|
+
status: entry.status,
|
|
553
|
+
tokens: toNumber(entry.tokens),
|
|
554
|
+
cost: toNumber(entry.costUsd),
|
|
555
|
+
errorCode: entry.errorCode ?? null,
|
|
556
|
+
errorMessage: entry.errorMessage ?? null,
|
|
557
|
+
};
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const normalizeRequestLogsPayload = (payload) => {
|
|
561
|
+
return payload.requests.map(normalizeRequestLog);
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const buildUsageIndex = (entries) =>
|
|
565
|
+
entries.reduce((acc, entry) => {
|
|
566
|
+
if (entry.accountId) {
|
|
567
|
+
acc[entry.accountId] = entry;
|
|
568
|
+
}
|
|
569
|
+
return acc;
|
|
570
|
+
}, {});
|
|
571
|
+
|
|
572
|
+
const mergeUsageIntoAccounts = (
|
|
573
|
+
accounts,
|
|
574
|
+
primaryUsage,
|
|
575
|
+
secondaryUsage,
|
|
576
|
+
summary,
|
|
577
|
+
) => {
|
|
578
|
+
const primaryMap = buildUsageIndex(primaryUsage || []);
|
|
579
|
+
const secondaryMap = buildUsageIndex(secondaryUsage || []);
|
|
580
|
+
const resetAtPrimary = summary?.primaryWindow?.resetAt ?? null;
|
|
581
|
+
const resetAtSecondary = summary?.secondaryWindow?.resetAt ?? null;
|
|
582
|
+
return accounts.map((account) => {
|
|
583
|
+
const primaryRow = primaryMap[account.id];
|
|
584
|
+
const secondaryRow = secondaryMap[account.id];
|
|
585
|
+
const primaryRemainingPercent = toNumber(primaryRow?.remainingPercentAvg);
|
|
586
|
+
const secondaryRemainingPercent = toNumber(
|
|
587
|
+
secondaryRow?.remainingPercentAvg,
|
|
588
|
+
);
|
|
589
|
+
return {
|
|
590
|
+
...account,
|
|
591
|
+
usage: {
|
|
592
|
+
primaryRemainingPercent:
|
|
593
|
+
primaryRemainingPercent ??
|
|
594
|
+
account.usage?.primaryRemainingPercent ??
|
|
595
|
+
0,
|
|
596
|
+
secondaryRemainingPercent:
|
|
597
|
+
secondaryRemainingPercent ??
|
|
598
|
+
account.usage?.secondaryRemainingPercent ??
|
|
599
|
+
0,
|
|
600
|
+
},
|
|
601
|
+
resetAtPrimary: resetAtPrimary ?? account.resetAtPrimary ?? null,
|
|
602
|
+
resetAtSecondary: resetAtSecondary ?? account.resetAtSecondary ?? null,
|
|
603
|
+
};
|
|
604
|
+
});
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
const buildUsageWindow = (entries, summaryWindow) => {
|
|
608
|
+
const entryList = entries || [];
|
|
609
|
+
const capacityFromEntries = entryList.reduce(
|
|
610
|
+
(acc, entry) => acc + (toNumber(entry.capacityCredits) || 0),
|
|
611
|
+
0,
|
|
612
|
+
);
|
|
613
|
+
const remainingFromEntries = entryList.reduce(
|
|
614
|
+
(acc, entry) => acc + (toNumber(entry.remainingCredits) || 0),
|
|
615
|
+
0,
|
|
616
|
+
);
|
|
617
|
+
const capacity = Math.max(
|
|
618
|
+
toNumber(summaryWindow?.capacityCredits) || 0,
|
|
619
|
+
capacityFromEntries,
|
|
620
|
+
remainingFromEntries,
|
|
621
|
+
);
|
|
622
|
+
const remaining = Math.max(
|
|
623
|
+
toNumber(summaryWindow?.remainingCredits) || 0,
|
|
624
|
+
remainingFromEntries,
|
|
625
|
+
);
|
|
626
|
+
return {
|
|
627
|
+
capacity,
|
|
628
|
+
remaining,
|
|
629
|
+
resetAt: summaryWindow?.resetAt ?? null,
|
|
630
|
+
windowMinutes: summaryWindow?.windowMinutes ?? null,
|
|
631
|
+
byAccount: entryList.map((entry) => ({
|
|
632
|
+
accountId: entry.accountId,
|
|
633
|
+
capacityCredits: toNumber(entry.capacityCredits) || 0,
|
|
634
|
+
remainingCredits: toNumber(entry.remainingCredits) || 0,
|
|
635
|
+
})),
|
|
636
|
+
};
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
const buildDashboardDataFromApi = ({
|
|
640
|
+
summary,
|
|
641
|
+
primaryUsage,
|
|
642
|
+
secondaryUsage,
|
|
643
|
+
requestLogs,
|
|
644
|
+
}) => {
|
|
645
|
+
const metrics = summary?.metrics || {};
|
|
646
|
+
const requests7d = toNumber(metrics.requests7d) ?? 0;
|
|
647
|
+
const tokensSecondaryWindow = toNumber(metrics.tokensSecondaryWindow);
|
|
648
|
+
const errorRate7d = toNumber(metrics.errorRate7d);
|
|
649
|
+
const topError = metrics.topError || "";
|
|
650
|
+
return {
|
|
651
|
+
lastSyncAt: new Date().toISOString(),
|
|
652
|
+
routing: {
|
|
653
|
+
strategy: "usage_weighted",
|
|
654
|
+
rotationEnabled: true,
|
|
655
|
+
},
|
|
656
|
+
metrics: {
|
|
657
|
+
requests7d,
|
|
658
|
+
tokensSecondaryWindow,
|
|
659
|
+
cost7d: toNumber(summary?.cost?.totalUsd7d) || 0,
|
|
660
|
+
errorRate7d,
|
|
661
|
+
topError,
|
|
662
|
+
},
|
|
663
|
+
usage: {
|
|
664
|
+
primary: buildUsageWindow(primaryUsage || [], summary?.primaryWindow),
|
|
665
|
+
secondary: buildUsageWindow(
|
|
666
|
+
secondaryUsage || [],
|
|
667
|
+
summary?.secondaryWindow,
|
|
668
|
+
),
|
|
669
|
+
},
|
|
670
|
+
recentRequests: requestLogs,
|
|
671
|
+
};
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const avgPerHour = (value, windowMinutes) => {
|
|
675
|
+
const numeric = toNumber(value);
|
|
676
|
+
if (numeric === null) {
|
|
677
|
+
return 0;
|
|
678
|
+
}
|
|
679
|
+
const hours =
|
|
680
|
+
typeof windowMinutes === "number" && windowMinutes > 0
|
|
681
|
+
? windowMinutes / 60
|
|
682
|
+
: 24 * 7;
|
|
683
|
+
return Math.round(numeric / hours);
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
const countByStatus = (accounts) =>
|
|
687
|
+
accounts.reduce((acc, account) => {
|
|
688
|
+
const status = account.status || "unknown";
|
|
689
|
+
acc[status] = (acc[status] || 0) + 1;
|
|
690
|
+
return acc;
|
|
691
|
+
}, {});
|
|
692
|
+
|
|
693
|
+
const buildRemainingItems = (entries, accounts, capacity) => {
|
|
694
|
+
const accountMap = new Map(
|
|
695
|
+
(accounts || []).map((account) => [account.id, account]),
|
|
696
|
+
);
|
|
697
|
+
const items = (entries || []).map((entry) => {
|
|
698
|
+
const account = accountMap.get(entry.accountId);
|
|
699
|
+
const label = account ? account.email : entry.accountId;
|
|
700
|
+
const value = toNumber(entry.remainingCredits) || 0;
|
|
701
|
+
const rawPercent = capacity > 0 ? (value / capacity) * 100 : 0;
|
|
702
|
+
const remainingPercent = Math.min(100, Math.max(0, rawPercent));
|
|
703
|
+
return {
|
|
704
|
+
accountId: entry.accountId,
|
|
705
|
+
label,
|
|
706
|
+
value,
|
|
707
|
+
remainingPercent,
|
|
708
|
+
};
|
|
709
|
+
});
|
|
710
|
+
items.sort((a, b) => {
|
|
711
|
+
const labelA = String(a.label || "").toLowerCase();
|
|
712
|
+
const labelB = String(b.label || "").toLowerCase();
|
|
713
|
+
if (labelA < labelB) {
|
|
714
|
+
return -1;
|
|
715
|
+
}
|
|
716
|
+
if (labelA > labelB) {
|
|
717
|
+
return 1;
|
|
718
|
+
}
|
|
719
|
+
return String(a.accountId || "").localeCompare(String(b.accountId || ""));
|
|
720
|
+
});
|
|
721
|
+
const palette = buildDonutPalette(items.length);
|
|
722
|
+
return items.map((item, index) => ({
|
|
723
|
+
...item,
|
|
724
|
+
color: palette[index % palette.length],
|
|
725
|
+
}));
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
const buildDonutGradient = (items, total) => {
|
|
729
|
+
if (!items.length || total <= 0) {
|
|
730
|
+
return `conic-gradient(${CONSUMED_COLOR} 0 100%)`;
|
|
731
|
+
}
|
|
732
|
+
const values = items.map((item) => Math.max(0, item.value || 0));
|
|
733
|
+
const remainingTotal = values.reduce((acc, value) => acc + value, 0);
|
|
734
|
+
if (remainingTotal <= 0) {
|
|
735
|
+
return `conic-gradient(${CONSUMED_COLOR} 0 100%)`;
|
|
736
|
+
}
|
|
737
|
+
const minPositive = Math.min(...values.filter((value) => value > 0));
|
|
738
|
+
const fallback =
|
|
739
|
+
Number.isFinite(minPositive) && minPositive > 0 ? minPositive * 0.05 : 0;
|
|
740
|
+
const displayValues =
|
|
741
|
+
fallback > 0
|
|
742
|
+
? values.map((value) => (value > 0 ? value : fallback))
|
|
743
|
+
: values;
|
|
744
|
+
const displayTotal = displayValues.reduce((acc, value) => acc + value, 0);
|
|
745
|
+
const remainingPercentTotal = Math.min(
|
|
746
|
+
100,
|
|
747
|
+
((displayTotal > 0 ? remainingTotal : 0) / total) * 100,
|
|
748
|
+
);
|
|
749
|
+
let start = 0;
|
|
750
|
+
const segments = displayValues.map((value, index) => {
|
|
751
|
+
const percent =
|
|
752
|
+
displayTotal > 0 ? (value / displayTotal) * remainingPercentTotal : 0;
|
|
753
|
+
const end = start + percent;
|
|
754
|
+
const color = items[index]?.color || CONSUMED_COLOR;
|
|
755
|
+
const segment = `${color} ${start}% ${end}%`;
|
|
756
|
+
start = end;
|
|
757
|
+
return segment;
|
|
758
|
+
});
|
|
759
|
+
if (remainingPercentTotal < 100) {
|
|
760
|
+
segments.push(`${CONSUMED_COLOR} ${start}% 100%`);
|
|
761
|
+
}
|
|
762
|
+
return `conic-gradient(${segments.join(", ")})`;
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
const buildDashboardView = (state) => {
|
|
766
|
+
const accounts = state.accounts.rows;
|
|
767
|
+
const statusCounts = countByStatus(accounts);
|
|
768
|
+
const secondaryWindowMinutes =
|
|
769
|
+
state.dashboardData.usage?.secondary?.windowMinutes ?? null;
|
|
770
|
+
|
|
771
|
+
const badges = ["active", "paused", "limited", "exceeded", "deactivated"]
|
|
772
|
+
.map((status) => {
|
|
773
|
+
const count = statusCounts[status] || 0;
|
|
774
|
+
if (!count) {
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
return {
|
|
778
|
+
status,
|
|
779
|
+
label: `${count} ${statusBadgeText(status)}`,
|
|
780
|
+
};
|
|
781
|
+
})
|
|
782
|
+
.filter(Boolean);
|
|
783
|
+
|
|
784
|
+
const metrics = state.dashboardData.metrics;
|
|
785
|
+
const stats = [
|
|
786
|
+
{
|
|
787
|
+
title: `Tokens (${formatWindowLabel("secondary", secondaryWindowMinutes)})`,
|
|
788
|
+
value: formatCompactNumber(metrics.tokensSecondaryWindow),
|
|
789
|
+
meta: "Scope: responses",
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
title: "Cost (7d)",
|
|
793
|
+
value: formatCurrency(metrics.cost7d),
|
|
794
|
+
meta: `Avg per hour: ${formatCurrency(
|
|
795
|
+
avgPerHour(metrics.cost7d, secondaryWindowMinutes),
|
|
796
|
+
)}`,
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
title: "Active accounts",
|
|
800
|
+
value: `${statusCounts.active || 0} / ${accounts.length}`,
|
|
801
|
+
meta: `Rotation: ${routingLabel(state.dashboardData.routing?.strategy)}`,
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
title: "Error rate (7d)",
|
|
805
|
+
value: formatRate(metrics.errorRate7d),
|
|
806
|
+
meta: `Top error: ${errorLabel(metrics.topError)}`,
|
|
807
|
+
},
|
|
808
|
+
];
|
|
809
|
+
|
|
810
|
+
const donuts = state.ui.usageWindows.map((window) => {
|
|
811
|
+
const usage = state.dashboardData.usage?.[window.key] || {
|
|
812
|
+
capacity: 0,
|
|
813
|
+
remaining: 0,
|
|
814
|
+
resetAt: null,
|
|
815
|
+
byAccount: [],
|
|
816
|
+
};
|
|
817
|
+
const remaining = toNumber(usage.remaining) || 0;
|
|
818
|
+
const capacity = Math.max(remaining, toNumber(usage.capacity) || 0);
|
|
819
|
+
const consumed = Math.max(0, capacity - remaining);
|
|
820
|
+
const items = buildRemainingItems(
|
|
821
|
+
usage.byAccount || [],
|
|
822
|
+
accounts,
|
|
823
|
+
capacity,
|
|
824
|
+
);
|
|
825
|
+
const gradient = buildDonutGradient(items, capacity);
|
|
826
|
+
const legendItems = items.map((item) => ({
|
|
827
|
+
label: item.label,
|
|
828
|
+
detail: `Remaining ${formatPercent(item.remainingPercent)}`,
|
|
829
|
+
color: item.color,
|
|
830
|
+
}));
|
|
831
|
+
if (capacity > 0 && consumed > 0) {
|
|
832
|
+
const consumedPercent = Math.min(
|
|
833
|
+
100,
|
|
834
|
+
Math.max(0, (consumed / capacity) * 100),
|
|
835
|
+
);
|
|
836
|
+
legendItems.push({
|
|
837
|
+
label: "Consumed",
|
|
838
|
+
detail: `${formatPercent(consumedPercent)}`,
|
|
839
|
+
color: CONSUMED_COLOR,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
return {
|
|
843
|
+
title: window.title,
|
|
844
|
+
total: `${formatCompactNumber(remaining)}/${formatCompactNumber(capacity)}`,
|
|
845
|
+
range: window.range,
|
|
846
|
+
gradient,
|
|
847
|
+
items: legendItems,
|
|
848
|
+
};
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
const accountCards = accounts.map((account) => {
|
|
852
|
+
const secondaryRemaining =
|
|
853
|
+
toNumber(account.usage?.secondaryRemainingPercent) || 0;
|
|
854
|
+
const remainingRounded = formatPercentValue(secondaryRemaining);
|
|
855
|
+
return {
|
|
856
|
+
email: account.email,
|
|
857
|
+
accountId: account.id,
|
|
858
|
+
plan: planLabel(account.plan),
|
|
859
|
+
status: {
|
|
860
|
+
class: account.status,
|
|
861
|
+
label: statusLabel(account.status),
|
|
862
|
+
},
|
|
863
|
+
remaining: remainingRounded,
|
|
864
|
+
remainingText: formatPercent(secondaryRemaining),
|
|
865
|
+
progressClass: progressClass(account.status),
|
|
866
|
+
marquee: account.status === "deactivated",
|
|
867
|
+
meta: formatQuotaResetMeta(
|
|
868
|
+
account.resetAtSecondary,
|
|
869
|
+
secondaryWindowMinutes,
|
|
870
|
+
),
|
|
871
|
+
actions: buildAccountActions(account),
|
|
872
|
+
};
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
const requests = state.dashboardData.recentRequests.map((request) => {
|
|
876
|
+
const rawError = request.errorMessage || request.errorCode || "";
|
|
877
|
+
const accountLabel = formatAccountLabel(request.accountId, accounts);
|
|
878
|
+
return {
|
|
879
|
+
key: `${request.requestId}-${request.timestamp}`,
|
|
880
|
+
requestId: request.requestId,
|
|
881
|
+
time: formatTimeLong(request.timestamp),
|
|
882
|
+
account: accountLabel,
|
|
883
|
+
model: request.model,
|
|
884
|
+
status: {
|
|
885
|
+
class: requestStatusClass(request.status),
|
|
886
|
+
label: requestStatusLabel(request.status),
|
|
887
|
+
},
|
|
888
|
+
tokens: formatNumber(request.tokens),
|
|
889
|
+
cost: formatCurrency(request.cost),
|
|
890
|
+
error: rawError ? truncateText(rawError, 80) : "--",
|
|
891
|
+
errorTitle: rawError,
|
|
892
|
+
};
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
return {
|
|
896
|
+
heroTitle: state.ui.heroTitle,
|
|
897
|
+
heroBody: state.ui.heroBody,
|
|
898
|
+
badges,
|
|
899
|
+
stats,
|
|
900
|
+
donuts,
|
|
901
|
+
accountCards,
|
|
902
|
+
requests,
|
|
903
|
+
};
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
const buildAccountActions = (account) => {
|
|
907
|
+
if (account.status === "deactivated") {
|
|
908
|
+
return [
|
|
909
|
+
{ label: "Details", type: "details" },
|
|
910
|
+
{ label: "Re-authenticate", type: "reauth" },
|
|
911
|
+
];
|
|
912
|
+
}
|
|
913
|
+
if (account.status === "paused") {
|
|
914
|
+
return [
|
|
915
|
+
{ label: "Details", type: "details" },
|
|
916
|
+
{ label: "Resume", type: "resume" },
|
|
917
|
+
];
|
|
918
|
+
}
|
|
919
|
+
return [{ label: "Details", type: "details" }];
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
const formatAccessTokenLabel = (auth) => {
|
|
923
|
+
const expiresAt = auth?.access?.expiresAt;
|
|
924
|
+
if (!expiresAt) {
|
|
925
|
+
return "Missing";
|
|
926
|
+
}
|
|
927
|
+
const expiresDate = parseDate(expiresAt);
|
|
928
|
+
if (!expiresDate) {
|
|
929
|
+
return "Unknown";
|
|
930
|
+
}
|
|
931
|
+
const diffMs = expiresDate.getTime() - Date.now();
|
|
932
|
+
if (diffMs <= 0) {
|
|
933
|
+
return "Expired";
|
|
934
|
+
}
|
|
935
|
+
return `Valid (${formatRelative(diffMs)})`;
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
const formatRefreshTokenLabel = (auth) => {
|
|
939
|
+
const state = auth?.refresh?.state;
|
|
940
|
+
const map = {
|
|
941
|
+
stored: "Stored",
|
|
942
|
+
missing: "Missing",
|
|
943
|
+
expired: "Expired",
|
|
944
|
+
};
|
|
945
|
+
return map[state] || "Unknown";
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
const formatIdTokenLabel = (auth) => {
|
|
949
|
+
const state = auth?.idToken?.state;
|
|
950
|
+
const map = {
|
|
951
|
+
parsed: "Parsed",
|
|
952
|
+
unknown: "Unknown",
|
|
953
|
+
};
|
|
954
|
+
return map[state] || "Unknown";
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
const readResponsePayload = async (response) => response.json();
|
|
958
|
+
|
|
959
|
+
const extractErrorMessage = (payload) => {
|
|
960
|
+
if (!payload) {
|
|
961
|
+
return "";
|
|
962
|
+
}
|
|
963
|
+
if (typeof payload === "string") {
|
|
964
|
+
return payload;
|
|
965
|
+
}
|
|
966
|
+
if (payload.error?.message) {
|
|
967
|
+
return payload.error.message;
|
|
968
|
+
}
|
|
969
|
+
if (payload.message) {
|
|
970
|
+
return payload.message;
|
|
971
|
+
}
|
|
972
|
+
return "";
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
const buildImportSummary = (payload) => {
|
|
976
|
+
if (!payload || typeof payload !== "object") {
|
|
977
|
+
return "auth.json imported.";
|
|
978
|
+
}
|
|
979
|
+
const email = payload.email || "Account";
|
|
980
|
+
const id = payload.accountId;
|
|
981
|
+
const plan = payload.planType;
|
|
982
|
+
const status = payload.status ? normalizeAccountStatus(payload.status) : "";
|
|
983
|
+
const details = [];
|
|
984
|
+
if (id) {
|
|
985
|
+
details.push(id);
|
|
986
|
+
}
|
|
987
|
+
if (plan) {
|
|
988
|
+
details.push(planLabel(plan));
|
|
989
|
+
}
|
|
990
|
+
if (status) {
|
|
991
|
+
details.push(statusLabel(status));
|
|
992
|
+
}
|
|
993
|
+
if (details.length) {
|
|
994
|
+
return `${email} (${details.join(" | ")}) imported.`;
|
|
995
|
+
}
|
|
996
|
+
return `${email} imported.`;
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
const fetchJson = async (url, label) => {
|
|
1000
|
+
const response = await fetch(url, { cache: "no-store" });
|
|
1001
|
+
const payload = await readResponsePayload(response);
|
|
1002
|
+
if (!response.ok) {
|
|
1003
|
+
const message = extractErrorMessage(payload);
|
|
1004
|
+
throw new Error(
|
|
1005
|
+
message || `Failed to load ${label} (${response.status})`,
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
return payload;
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
const postJson = async (url, payload, label) => {
|
|
1012
|
+
const response = await fetch(url, {
|
|
1013
|
+
method: "POST",
|
|
1014
|
+
headers: { "Content-Type": "application/json" },
|
|
1015
|
+
body: JSON.stringify(payload || {}),
|
|
1016
|
+
});
|
|
1017
|
+
const responsePayload = await readResponsePayload(response);
|
|
1018
|
+
if (!response.ok) {
|
|
1019
|
+
const message = extractErrorMessage(responsePayload);
|
|
1020
|
+
throw new Error(message || `Failed to ${label} (${response.status})`);
|
|
1021
|
+
}
|
|
1022
|
+
return responsePayload;
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
const deleteJson = async (url, label) => {
|
|
1026
|
+
const response = await fetch(url, { method: "DELETE" });
|
|
1027
|
+
const responsePayload = await readResponsePayload(response);
|
|
1028
|
+
if (!response.ok) {
|
|
1029
|
+
const message = extractErrorMessage(responsePayload);
|
|
1030
|
+
throw new Error(message || `Failed to ${label} (${response.status})`);
|
|
1031
|
+
}
|
|
1032
|
+
return responsePayload;
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
const fetchAccounts = async () => {
|
|
1036
|
+
const payload = await fetchJson(API_ENDPOINTS.accounts, "accounts");
|
|
1037
|
+
return normalizeAccountsPayload(payload);
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
const fetchUsageSummary = async () =>
|
|
1041
|
+
fetchJson(API_ENDPOINTS.usageSummary, "usage summary");
|
|
1042
|
+
|
|
1043
|
+
const fetchUsageWindow = async (window) =>
|
|
1044
|
+
fetchJson(API_ENDPOINTS.usageWindow(window), `usage window (${window})`);
|
|
1045
|
+
|
|
1046
|
+
const fetchRequestLogs = async (limit) =>
|
|
1047
|
+
fetchJson(API_ENDPOINTS.requestLogs(limit), "request logs");
|
|
1048
|
+
|
|
1049
|
+
const registerApp = () => {
|
|
1050
|
+
Alpine.data("feApp", () => ({
|
|
1051
|
+
view: "dashboard",
|
|
1052
|
+
pages: createPages(),
|
|
1053
|
+
backendPath: "/backend-api",
|
|
1054
|
+
ui: createUiConfig(),
|
|
1055
|
+
dashboardData: createEmptyDashboardData(),
|
|
1056
|
+
dashboard: createEmptyDashboardView(),
|
|
1057
|
+
accounts: {
|
|
1058
|
+
selectedId: "",
|
|
1059
|
+
rows: [],
|
|
1060
|
+
searchQuery: "",
|
|
1061
|
+
},
|
|
1062
|
+
importState: {
|
|
1063
|
+
isLoading: false,
|
|
1064
|
+
fileName: "",
|
|
1065
|
+
},
|
|
1066
|
+
messageBox: {
|
|
1067
|
+
open: false,
|
|
1068
|
+
title: "",
|
|
1069
|
+
message: "",
|
|
1070
|
+
details: "",
|
|
1071
|
+
iconTone: "",
|
|
1072
|
+
mode: "alert",
|
|
1073
|
+
confirmLabel: "OK",
|
|
1074
|
+
cancelLabel: "Cancel",
|
|
1075
|
+
},
|
|
1076
|
+
messageBoxResolver: null,
|
|
1077
|
+
authDialog: {
|
|
1078
|
+
open: false,
|
|
1079
|
+
stage: "intro",
|
|
1080
|
+
selectedMethod: "browser",
|
|
1081
|
+
isLoading: false,
|
|
1082
|
+
authorizationUrl: "",
|
|
1083
|
+
verificationUrl: "",
|
|
1084
|
+
userCode: "",
|
|
1085
|
+
deviceAuthId: "",
|
|
1086
|
+
intervalSeconds: 5,
|
|
1087
|
+
expiresInSeconds: 0,
|
|
1088
|
+
expiresAt: 0,
|
|
1089
|
+
remainingSeconds: 0,
|
|
1090
|
+
status: "idle",
|
|
1091
|
+
statusLabel: "",
|
|
1092
|
+
errorMessage: "",
|
|
1093
|
+
pollTimerId: null,
|
|
1094
|
+
countdownTimerId: null,
|
|
1095
|
+
},
|
|
1096
|
+
isLoading: true,
|
|
1097
|
+
hasInitialized: false,
|
|
1098
|
+
refreshPromise: null,
|
|
1099
|
+
async init() {
|
|
1100
|
+
if (this.hasInitialized) {
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
this.hasInitialized = true;
|
|
1104
|
+
this.view = getViewFromPath(window.location.pathname);
|
|
1105
|
+
await this.loadData();
|
|
1106
|
+
this.syncTitle();
|
|
1107
|
+
this.syncUrl(true);
|
|
1108
|
+
this.$watch("view", () => {
|
|
1109
|
+
this.syncTitle();
|
|
1110
|
+
this.syncUrl(false);
|
|
1111
|
+
});
|
|
1112
|
+
this.$watch("accounts.searchQuery", () => {
|
|
1113
|
+
this.syncAccountSearchSelection();
|
|
1114
|
+
});
|
|
1115
|
+
window.addEventListener("popstate", (event) => {
|
|
1116
|
+
const newView =
|
|
1117
|
+
event.state?.view || getViewFromPath(window.location.pathname);
|
|
1118
|
+
if (newView !== this.view) {
|
|
1119
|
+
this.view = newView;
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
},
|
|
1123
|
+
async loadData() {
|
|
1124
|
+
try {
|
|
1125
|
+
await this.refreshAll({ silent: true });
|
|
1126
|
+
if (!this.pages.some((page) => page.id === this.view)) {
|
|
1127
|
+
this.view = this.pages[0].id;
|
|
1128
|
+
}
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
const message = error.message || "Failed to load data.";
|
|
1131
|
+
this.openMessageBox({
|
|
1132
|
+
tone: "error",
|
|
1133
|
+
title: "Dashboard load failed",
|
|
1134
|
+
message,
|
|
1135
|
+
});
|
|
1136
|
+
} finally {
|
|
1137
|
+
this.isLoading = false;
|
|
1138
|
+
}
|
|
1139
|
+
},
|
|
1140
|
+
async refreshAll(options = {}) {
|
|
1141
|
+
if (this.refreshPromise) {
|
|
1142
|
+
return this.refreshPromise;
|
|
1143
|
+
}
|
|
1144
|
+
const { preferredId, silent = false } = options;
|
|
1145
|
+
this.refreshPromise = (async () => {
|
|
1146
|
+
const [
|
|
1147
|
+
accountsResult,
|
|
1148
|
+
summaryResult,
|
|
1149
|
+
primaryResult,
|
|
1150
|
+
secondaryResult,
|
|
1151
|
+
requestLogsResult,
|
|
1152
|
+
] = await Promise.allSettled([
|
|
1153
|
+
fetchAccounts(),
|
|
1154
|
+
fetchUsageSummary(),
|
|
1155
|
+
fetchUsageWindow("primary"),
|
|
1156
|
+
fetchUsageWindow("secondary"),
|
|
1157
|
+
fetchRequestLogs(50),
|
|
1158
|
+
]);
|
|
1159
|
+
const errors = [];
|
|
1160
|
+
if (accountsResult.status !== "fulfilled") {
|
|
1161
|
+
throw accountsResult.reason;
|
|
1162
|
+
}
|
|
1163
|
+
const summary =
|
|
1164
|
+
summaryResult.status === "fulfilled" ? summaryResult.value : null;
|
|
1165
|
+
if (summaryResult.status === "rejected") {
|
|
1166
|
+
errors.push(summaryResult.reason);
|
|
1167
|
+
}
|
|
1168
|
+
const primaryUsage =
|
|
1169
|
+
primaryResult.status === "fulfilled"
|
|
1170
|
+
? normalizeUsagePayload(primaryResult.value)
|
|
1171
|
+
: [];
|
|
1172
|
+
if (primaryResult.status === "rejected") {
|
|
1173
|
+
errors.push(primaryResult.reason);
|
|
1174
|
+
}
|
|
1175
|
+
const secondaryUsage =
|
|
1176
|
+
secondaryResult.status === "fulfilled"
|
|
1177
|
+
? normalizeUsagePayload(secondaryResult.value)
|
|
1178
|
+
: [];
|
|
1179
|
+
if (secondaryResult.status === "rejected") {
|
|
1180
|
+
errors.push(secondaryResult.reason);
|
|
1181
|
+
}
|
|
1182
|
+
const requestLogs =
|
|
1183
|
+
requestLogsResult.status === "fulfilled"
|
|
1184
|
+
? normalizeRequestLogsPayload(requestLogsResult.value)
|
|
1185
|
+
: [];
|
|
1186
|
+
if (requestLogsResult.status === "rejected") {
|
|
1187
|
+
errors.push(requestLogsResult.reason);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const mergedAccounts = mergeUsageIntoAccounts(
|
|
1191
|
+
accountsResult.value,
|
|
1192
|
+
primaryUsage,
|
|
1193
|
+
secondaryUsage,
|
|
1194
|
+
summary,
|
|
1195
|
+
);
|
|
1196
|
+
this.applyData(
|
|
1197
|
+
{
|
|
1198
|
+
accounts: mergedAccounts,
|
|
1199
|
+
summary,
|
|
1200
|
+
primaryUsage,
|
|
1201
|
+
secondaryUsage,
|
|
1202
|
+
requestLogs,
|
|
1203
|
+
},
|
|
1204
|
+
preferredId,
|
|
1205
|
+
);
|
|
1206
|
+
|
|
1207
|
+
if (errors.length && !silent) {
|
|
1208
|
+
this.openMessageBox({
|
|
1209
|
+
tone: "warning",
|
|
1210
|
+
title: "Partial data loaded",
|
|
1211
|
+
message:
|
|
1212
|
+
"Some usage endpoints failed. Account list loaded, but usage data may be incomplete.",
|
|
1213
|
+
details: errors
|
|
1214
|
+
.map((err) => err?.message || String(err))
|
|
1215
|
+
.join("\n"),
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
})();
|
|
1219
|
+
try {
|
|
1220
|
+
await this.refreshPromise;
|
|
1221
|
+
} finally {
|
|
1222
|
+
this.refreshPromise = null;
|
|
1223
|
+
}
|
|
1224
|
+
},
|
|
1225
|
+
applyData(data, preferredId) {
|
|
1226
|
+
this.accounts.rows = Array.isArray(data.accounts) ? data.accounts : [];
|
|
1227
|
+
const hasPreferred =
|
|
1228
|
+
preferredId &&
|
|
1229
|
+
this.accounts.rows.some((account) => account.id === preferredId);
|
|
1230
|
+
if (hasPreferred) {
|
|
1231
|
+
this.accounts.selectedId = preferredId;
|
|
1232
|
+
} else if (
|
|
1233
|
+
this.accounts.selectedId &&
|
|
1234
|
+
!this.accounts.rows.some(
|
|
1235
|
+
(account) => account.id === this.accounts.selectedId,
|
|
1236
|
+
)
|
|
1237
|
+
) {
|
|
1238
|
+
this.accounts.selectedId = this.accounts.rows[0]?.id || "";
|
|
1239
|
+
} else if (!this.accounts.selectedId && this.accounts.rows.length > 0) {
|
|
1240
|
+
this.accounts.selectedId = this.accounts.rows[0].id;
|
|
1241
|
+
}
|
|
1242
|
+
this.dashboardData = buildDashboardDataFromApi({
|
|
1243
|
+
summary: data.summary,
|
|
1244
|
+
primaryUsage: data.primaryUsage,
|
|
1245
|
+
secondaryUsage: data.secondaryUsage,
|
|
1246
|
+
requestLogs: data.requestLogs,
|
|
1247
|
+
});
|
|
1248
|
+
this.ui.usageWindows = buildUsageWindowConfig(data.summary);
|
|
1249
|
+
this.dashboard = buildDashboardView(this);
|
|
1250
|
+
this.syncAccountSearchSelection();
|
|
1251
|
+
},
|
|
1252
|
+
focusAccountSearch() {
|
|
1253
|
+
this.$refs.accountSearch?.focus();
|
|
1254
|
+
},
|
|
1255
|
+
clearAccountSearch() {
|
|
1256
|
+
this.accounts.searchQuery = "";
|
|
1257
|
+
this.focusAccountSearch();
|
|
1258
|
+
},
|
|
1259
|
+
syncAccountSearchSelection() {
|
|
1260
|
+
const query = normalizeSearchInput(this.accounts.searchQuery);
|
|
1261
|
+
if (!query) {
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
const filtered = filterAccountsByQuery(this.accounts.rows, query);
|
|
1265
|
+
if (!filtered.length) {
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
const hasSelected = filtered.some(
|
|
1269
|
+
(account) => account.id === this.accounts.selectedId,
|
|
1270
|
+
);
|
|
1271
|
+
if (!hasSelected) {
|
|
1272
|
+
this.accounts.selectedId = filtered[0].id;
|
|
1273
|
+
}
|
|
1274
|
+
},
|
|
1275
|
+
async handleAuthImport(event) {
|
|
1276
|
+
console.info("[auth-import] change event", {
|
|
1277
|
+
hasFiles: Boolean(event?.target?.files?.length),
|
|
1278
|
+
});
|
|
1279
|
+
const file = event.target.files && event.target.files[0];
|
|
1280
|
+
event.target.value = "";
|
|
1281
|
+
if (!file) {
|
|
1282
|
+
console.warn("[auth-import] no file selected");
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
await this.importAuthFile(file);
|
|
1286
|
+
},
|
|
1287
|
+
async importAuthFile(file) {
|
|
1288
|
+
console.info("[auth-import] start", {
|
|
1289
|
+
name: file?.name,
|
|
1290
|
+
size: file?.size,
|
|
1291
|
+
type: file?.type,
|
|
1292
|
+
});
|
|
1293
|
+
this.importState.isLoading = true;
|
|
1294
|
+
this.importState.fileName = file.name || "auth.json";
|
|
1295
|
+
try {
|
|
1296
|
+
const formData = new FormData();
|
|
1297
|
+
formData.append("auth_json", file, file.name || "auth.json");
|
|
1298
|
+
const response = await fetch(API_ENDPOINTS.accountsImport, {
|
|
1299
|
+
method: "POST",
|
|
1300
|
+
body: formData,
|
|
1301
|
+
});
|
|
1302
|
+
if (!response.ok) {
|
|
1303
|
+
const payload = await readResponsePayload(response);
|
|
1304
|
+
const message = extractErrorMessage(payload);
|
|
1305
|
+
throw new Error(message || `Import failed (${response.status})`);
|
|
1306
|
+
}
|
|
1307
|
+
const payload = await readResponsePayload(response);
|
|
1308
|
+
const summary = buildImportSummary(payload);
|
|
1309
|
+
const preferredId = payload?.accountId;
|
|
1310
|
+
let details = "";
|
|
1311
|
+
let tone = "success";
|
|
1312
|
+
let title = "";
|
|
1313
|
+
try {
|
|
1314
|
+
await this.refreshAll({ preferredId, silent: true });
|
|
1315
|
+
} catch (error) {
|
|
1316
|
+
tone = "error";
|
|
1317
|
+
title = "Import complete, refresh failed";
|
|
1318
|
+
details =
|
|
1319
|
+
error.message ||
|
|
1320
|
+
"Import completed but the account list could not refresh.";
|
|
1321
|
+
}
|
|
1322
|
+
this.openMessageBox({ tone, title, message: summary, details });
|
|
1323
|
+
console.info("[auth-import] complete via backend");
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
console.error("[auth-import] failed", error);
|
|
1326
|
+
this.openMessageBox({
|
|
1327
|
+
tone: "error",
|
|
1328
|
+
message: error.message || "Import failed.",
|
|
1329
|
+
});
|
|
1330
|
+
} finally {
|
|
1331
|
+
this.importState.isLoading = false;
|
|
1332
|
+
this.importState.fileName = "";
|
|
1333
|
+
}
|
|
1334
|
+
},
|
|
1335
|
+
logImportClick(source) {
|
|
1336
|
+
console.info("[auth-import] click", { source });
|
|
1337
|
+
},
|
|
1338
|
+
openMessageBox({ tone = "info", title, message, details } = {}) {
|
|
1339
|
+
if (this.messageBoxResolver) {
|
|
1340
|
+
const resolve = this.messageBoxResolver;
|
|
1341
|
+
this.messageBoxResolver = null;
|
|
1342
|
+
resolve(false);
|
|
1343
|
+
}
|
|
1344
|
+
const toneKey = MESSAGE_TONE_META[tone] ? tone : "info";
|
|
1345
|
+
const meta = MESSAGE_TONE_META[toneKey] || MESSAGE_TONE_META.info;
|
|
1346
|
+
this.messageBox.open = true;
|
|
1347
|
+
this.messageBox.title = title || meta.defaultTitle;
|
|
1348
|
+
this.messageBox.message = message || "";
|
|
1349
|
+
this.messageBox.details = details || "";
|
|
1350
|
+
this.messageBox.iconTone = toneKey;
|
|
1351
|
+
this.messageBox.mode = "alert";
|
|
1352
|
+
this.messageBox.confirmLabel = "OK";
|
|
1353
|
+
this.messageBox.cancelLabel = "Cancel";
|
|
1354
|
+
},
|
|
1355
|
+
openConfirmBox({
|
|
1356
|
+
tone = "question",
|
|
1357
|
+
title,
|
|
1358
|
+
message,
|
|
1359
|
+
details,
|
|
1360
|
+
confirmLabel = "OK",
|
|
1361
|
+
cancelLabel = "Cancel",
|
|
1362
|
+
} = {}) {
|
|
1363
|
+
if (this.messageBoxResolver) {
|
|
1364
|
+
const resolve = this.messageBoxResolver;
|
|
1365
|
+
this.messageBoxResolver = null;
|
|
1366
|
+
resolve(false);
|
|
1367
|
+
}
|
|
1368
|
+
const toneKey = MESSAGE_TONE_META[tone] ? tone : "question";
|
|
1369
|
+
const meta = MESSAGE_TONE_META[toneKey] || MESSAGE_TONE_META.info;
|
|
1370
|
+
this.messageBox.open = true;
|
|
1371
|
+
this.messageBox.title = title || meta.defaultTitle;
|
|
1372
|
+
this.messageBox.message = message || "";
|
|
1373
|
+
this.messageBox.details = details || "";
|
|
1374
|
+
this.messageBox.iconTone = toneKey;
|
|
1375
|
+
this.messageBox.mode = "confirm";
|
|
1376
|
+
this.messageBox.confirmLabel = confirmLabel;
|
|
1377
|
+
this.messageBox.cancelLabel = cancelLabel;
|
|
1378
|
+
return new Promise((resolve) => {
|
|
1379
|
+
this.messageBoxResolver = resolve;
|
|
1380
|
+
});
|
|
1381
|
+
},
|
|
1382
|
+
resolveMessageBox(confirmed) {
|
|
1383
|
+
if (this.messageBoxResolver) {
|
|
1384
|
+
const resolve = this.messageBoxResolver;
|
|
1385
|
+
this.messageBoxResolver = null;
|
|
1386
|
+
resolve(Boolean(confirmed));
|
|
1387
|
+
}
|
|
1388
|
+
this.messageBox.open = false;
|
|
1389
|
+
this.messageBox.mode = "alert";
|
|
1390
|
+
},
|
|
1391
|
+
confirmMessageBox() {
|
|
1392
|
+
this.resolveMessageBox(true);
|
|
1393
|
+
},
|
|
1394
|
+
cancelMessageBox() {
|
|
1395
|
+
this.resolveMessageBox(false);
|
|
1396
|
+
},
|
|
1397
|
+
closeMessageBox() {
|
|
1398
|
+
this.resolveMessageBox(false);
|
|
1399
|
+
},
|
|
1400
|
+
openAddAccountDialog() {
|
|
1401
|
+
this.resetAuthDialogState();
|
|
1402
|
+
this.authDialog.open = true;
|
|
1403
|
+
},
|
|
1404
|
+
closeAddAccountDialog() {
|
|
1405
|
+
this.authDialog.open = false;
|
|
1406
|
+
this.resetAuthDialogState();
|
|
1407
|
+
},
|
|
1408
|
+
resetAuthDialogState() {
|
|
1409
|
+
this.stopAuthTimers();
|
|
1410
|
+
this.authDialog.stage = "intro";
|
|
1411
|
+
this.authDialog.selectedMethod = "browser";
|
|
1412
|
+
this.authDialog.isLoading = false;
|
|
1413
|
+
this.authDialog.authorizationUrl = "";
|
|
1414
|
+
this.authDialog.verificationUrl = "";
|
|
1415
|
+
this.authDialog.userCode = "";
|
|
1416
|
+
this.authDialog.deviceAuthId = "";
|
|
1417
|
+
this.authDialog.intervalSeconds = 5;
|
|
1418
|
+
this.authDialog.expiresInSeconds = 0;
|
|
1419
|
+
this.authDialog.expiresAt = 0;
|
|
1420
|
+
this.authDialog.remainingSeconds = 0;
|
|
1421
|
+
this.authDialog.status = "idle";
|
|
1422
|
+
this.authDialog.statusLabel = "";
|
|
1423
|
+
this.authDialog.errorMessage = "";
|
|
1424
|
+
},
|
|
1425
|
+
async startOAuth() {
|
|
1426
|
+
if (this.authDialog.isLoading) {
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
this.stopAuthTimers();
|
|
1430
|
+
this.authDialog.isLoading = true;
|
|
1431
|
+
this.authDialog.errorMessage = "";
|
|
1432
|
+
this.authDialog.statusLabel = "Requesting authorization...";
|
|
1433
|
+
const forceMethod = this.authDialog.selectedMethod;
|
|
1434
|
+
let autoCompleteDevice = false;
|
|
1435
|
+
try {
|
|
1436
|
+
const payload = await postJson(
|
|
1437
|
+
API_ENDPOINTS.oauthStart,
|
|
1438
|
+
forceMethod ? { forceMethod } : {},
|
|
1439
|
+
"start OAuth",
|
|
1440
|
+
);
|
|
1441
|
+
if (payload?.method === "browser") {
|
|
1442
|
+
this.authDialog.stage = "browser";
|
|
1443
|
+
this.authDialog.authorizationUrl = payload.authorizationUrl || "";
|
|
1444
|
+
this.authDialog.status = "pending";
|
|
1445
|
+
this.authDialog.statusLabel = AUTH_STATUS_LABELS.pendingBrowser;
|
|
1446
|
+
this.startAuthPolling();
|
|
1447
|
+
} else if (payload?.method === "device") {
|
|
1448
|
+
this.authDialog.stage = "device";
|
|
1449
|
+
this.authDialog.verificationUrl = payload.verificationUrl || "";
|
|
1450
|
+
this.authDialog.userCode = payload.userCode || "";
|
|
1451
|
+
this.authDialog.deviceAuthId = payload.deviceAuthId || "";
|
|
1452
|
+
this.authDialog.intervalSeconds =
|
|
1453
|
+
Number(payload.intervalSeconds) || 5;
|
|
1454
|
+
this.authDialog.expiresInSeconds =
|
|
1455
|
+
Number(payload.expiresInSeconds) || 0;
|
|
1456
|
+
this.authDialog.expiresAt =
|
|
1457
|
+
Date.now() + this.authDialog.expiresInSeconds * 1000;
|
|
1458
|
+
this.authDialog.status = "pending";
|
|
1459
|
+
this.authDialog.statusLabel = AUTH_STATUS_LABELS.pendingDevice;
|
|
1460
|
+
this.startAuthPolling();
|
|
1461
|
+
this.startAuthCountdown();
|
|
1462
|
+
autoCompleteDevice = true;
|
|
1463
|
+
} else {
|
|
1464
|
+
throw new Error("Unexpected OAuth response.");
|
|
1465
|
+
}
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
this.authDialog.stage = "error";
|
|
1468
|
+
this.authDialog.status = "error";
|
|
1469
|
+
this.authDialog.statusLabel = AUTH_STATUS_LABELS.error;
|
|
1470
|
+
this.authDialog.errorMessage =
|
|
1471
|
+
error.message || "Failed to start OAuth flow.";
|
|
1472
|
+
} finally {
|
|
1473
|
+
this.authDialog.isLoading = false;
|
|
1474
|
+
}
|
|
1475
|
+
if (autoCompleteDevice) {
|
|
1476
|
+
await this.completeDeviceAuth();
|
|
1477
|
+
}
|
|
1478
|
+
},
|
|
1479
|
+
openAuthorizationUrl() {
|
|
1480
|
+
if (this.authDialog.authorizationUrl) {
|
|
1481
|
+
window.open(this.authDialog.authorizationUrl, "_blank", "noopener");
|
|
1482
|
+
}
|
|
1483
|
+
},
|
|
1484
|
+
openVerificationUrl() {
|
|
1485
|
+
if (this.authDialog.verificationUrl) {
|
|
1486
|
+
window.open(this.authDialog.verificationUrl, "_blank", "noopener");
|
|
1487
|
+
}
|
|
1488
|
+
},
|
|
1489
|
+
async copyToClipboard(value, label) {
|
|
1490
|
+
if (!value) {
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
try {
|
|
1494
|
+
if (navigator.clipboard?.writeText) {
|
|
1495
|
+
await navigator.clipboard.writeText(value);
|
|
1496
|
+
} else {
|
|
1497
|
+
const textarea = document.createElement("textarea");
|
|
1498
|
+
textarea.value = value;
|
|
1499
|
+
textarea.setAttribute("readonly", "");
|
|
1500
|
+
textarea.style.position = "absolute";
|
|
1501
|
+
textarea.style.left = "-9999px";
|
|
1502
|
+
document.body.appendChild(textarea);
|
|
1503
|
+
textarea.select();
|
|
1504
|
+
document.execCommand("copy");
|
|
1505
|
+
document.body.removeChild(textarea);
|
|
1506
|
+
}
|
|
1507
|
+
if (this.authDialog.open) {
|
|
1508
|
+
const previousLabel = this.authDialog.statusLabel;
|
|
1509
|
+
this.authDialog.statusLabel = `${label} copied.`;
|
|
1510
|
+
window.setTimeout(() => {
|
|
1511
|
+
if (this.authDialog.statusLabel === `${label} copied.`) {
|
|
1512
|
+
this.authDialog.statusLabel = previousLabel;
|
|
1513
|
+
}
|
|
1514
|
+
}, 2000);
|
|
1515
|
+
} else {
|
|
1516
|
+
this.openMessageBox({
|
|
1517
|
+
tone: "success",
|
|
1518
|
+
title: "Copied",
|
|
1519
|
+
message: `${label} copied to clipboard.`,
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
} catch (error) {
|
|
1523
|
+
if (this.authDialog.open) {
|
|
1524
|
+
this.authDialog.statusLabel = "Copy failed.";
|
|
1525
|
+
} else {
|
|
1526
|
+
this.openMessageBox({
|
|
1527
|
+
tone: "error",
|
|
1528
|
+
title: "Copy failed",
|
|
1529
|
+
message: "Unable to copy to clipboard.",
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
},
|
|
1534
|
+
stopAuthPolling() {
|
|
1535
|
+
if (this.authDialog.pollTimerId) {
|
|
1536
|
+
clearInterval(this.authDialog.pollTimerId);
|
|
1537
|
+
this.authDialog.pollTimerId = null;
|
|
1538
|
+
}
|
|
1539
|
+
},
|
|
1540
|
+
stopAuthCountdown() {
|
|
1541
|
+
if (this.authDialog.countdownTimerId) {
|
|
1542
|
+
clearInterval(this.authDialog.countdownTimerId);
|
|
1543
|
+
this.authDialog.countdownTimerId = null;
|
|
1544
|
+
}
|
|
1545
|
+
},
|
|
1546
|
+
stopAuthTimers() {
|
|
1547
|
+
this.stopAuthPolling();
|
|
1548
|
+
this.stopAuthCountdown();
|
|
1549
|
+
},
|
|
1550
|
+
startAuthPolling() {
|
|
1551
|
+
this.stopAuthPolling();
|
|
1552
|
+
const intervalMs = Math.max(this.authDialog.intervalSeconds, 2) * 1000;
|
|
1553
|
+
this.authDialog.pollTimerId = window.setInterval(() => {
|
|
1554
|
+
this.checkAuthStatus();
|
|
1555
|
+
}, intervalMs);
|
|
1556
|
+
this.checkAuthStatus();
|
|
1557
|
+
},
|
|
1558
|
+
startAuthCountdown() {
|
|
1559
|
+
this.stopAuthCountdown();
|
|
1560
|
+
const update = () => {
|
|
1561
|
+
const remaining = Math.ceil(
|
|
1562
|
+
(this.authDialog.expiresAt - Date.now()) / 1000,
|
|
1563
|
+
);
|
|
1564
|
+
this.authDialog.remainingSeconds = Math.max(0, remaining);
|
|
1565
|
+
if (this.authDialog.remainingSeconds <= 0) {
|
|
1566
|
+
if (this.authDialog.status === "pending") {
|
|
1567
|
+
this.authDialog.stage = "error";
|
|
1568
|
+
this.authDialog.status = "error";
|
|
1569
|
+
this.authDialog.statusLabel = AUTH_STATUS_LABELS.error;
|
|
1570
|
+
this.authDialog.errorMessage =
|
|
1571
|
+
"Device code expired. Start the sign-in again.";
|
|
1572
|
+
this.stopAuthTimers();
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
update();
|
|
1577
|
+
this.authDialog.countdownTimerId = window.setInterval(update, 1000);
|
|
1578
|
+
},
|
|
1579
|
+
async checkAuthStatus() {
|
|
1580
|
+
if (!this.authDialog.open) {
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
try {
|
|
1584
|
+
const payload = await fetchJson(
|
|
1585
|
+
API_ENDPOINTS.oauthStatus,
|
|
1586
|
+
"oauth status",
|
|
1587
|
+
);
|
|
1588
|
+
if (payload?.status === "success") {
|
|
1589
|
+
this.authDialog.status = "success";
|
|
1590
|
+
this.authDialog.statusLabel = AUTH_STATUS_LABELS.success;
|
|
1591
|
+
this.authDialog.stage = "success";
|
|
1592
|
+
this.stopAuthTimers();
|
|
1593
|
+
try {
|
|
1594
|
+
await this.refreshAll({ silent: true });
|
|
1595
|
+
} catch (error) {
|
|
1596
|
+
console.warn("[oauth] refresh accounts failed", error);
|
|
1597
|
+
}
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
if (payload?.status === "error") {
|
|
1601
|
+
this.authDialog.status = "error";
|
|
1602
|
+
this.authDialog.statusLabel = AUTH_STATUS_LABELS.error;
|
|
1603
|
+
this.authDialog.stage = "error";
|
|
1604
|
+
this.authDialog.errorMessage =
|
|
1605
|
+
payload?.errorMessage || "Authorization failed.";
|
|
1606
|
+
this.stopAuthTimers();
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
this.authDialog.status = "pending";
|
|
1610
|
+
} catch (error) {
|
|
1611
|
+
this.authDialog.status = "error";
|
|
1612
|
+
this.authDialog.statusLabel = AUTH_STATUS_LABELS.error;
|
|
1613
|
+
this.authDialog.stage = "error";
|
|
1614
|
+
this.authDialog.errorMessage =
|
|
1615
|
+
error.message || "Failed to fetch OAuth status.";
|
|
1616
|
+
this.stopAuthTimers();
|
|
1617
|
+
}
|
|
1618
|
+
},
|
|
1619
|
+
async completeDeviceAuth() {
|
|
1620
|
+
if (this.authDialog.isLoading) {
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
this.authDialog.isLoading = true;
|
|
1624
|
+
try {
|
|
1625
|
+
const payload = {};
|
|
1626
|
+
if (this.authDialog.deviceAuthId) {
|
|
1627
|
+
payload.deviceAuthId = this.authDialog.deviceAuthId;
|
|
1628
|
+
}
|
|
1629
|
+
if (this.authDialog.userCode) {
|
|
1630
|
+
payload.userCode = this.authDialog.userCode;
|
|
1631
|
+
}
|
|
1632
|
+
await postJson(
|
|
1633
|
+
API_ENDPOINTS.oauthComplete,
|
|
1634
|
+
payload,
|
|
1635
|
+
"complete OAuth",
|
|
1636
|
+
);
|
|
1637
|
+
this.authDialog.statusLabel = AUTH_STATUS_LABELS.pendingDevice;
|
|
1638
|
+
} catch (error) {
|
|
1639
|
+
this.authDialog.status = "error";
|
|
1640
|
+
this.authDialog.statusLabel = AUTH_STATUS_LABELS.error;
|
|
1641
|
+
this.authDialog.stage = "error";
|
|
1642
|
+
this.authDialog.errorMessage =
|
|
1643
|
+
error.message || "Failed to complete device code flow.";
|
|
1644
|
+
this.stopAuthTimers();
|
|
1645
|
+
} finally {
|
|
1646
|
+
this.authDialog.isLoading = false;
|
|
1647
|
+
}
|
|
1648
|
+
},
|
|
1649
|
+
syncTitle() {
|
|
1650
|
+
document.title = this.currentPage.title;
|
|
1651
|
+
},
|
|
1652
|
+
syncUrl(replace = false) {
|
|
1653
|
+
const targetPath = getPathFromView(this.view);
|
|
1654
|
+
const currentPath =
|
|
1655
|
+
window.location.pathname.replace(/\/+$/, "") || BASE_PATH;
|
|
1656
|
+
if (currentPath !== targetPath) {
|
|
1657
|
+
const state = { view: this.view };
|
|
1658
|
+
if (replace) {
|
|
1659
|
+
window.history.replaceState(state, "", targetPath);
|
|
1660
|
+
} else {
|
|
1661
|
+
window.history.pushState(state, "", targetPath);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
},
|
|
1665
|
+
setView(id) {
|
|
1666
|
+
if (this.view !== id) {
|
|
1667
|
+
this.view = id;
|
|
1668
|
+
}
|
|
1669
|
+
},
|
|
1670
|
+
navigateTo(path) {
|
|
1671
|
+
const viewId = getViewFromPath(path);
|
|
1672
|
+
this.setView(viewId);
|
|
1673
|
+
},
|
|
1674
|
+
get currentPage() {
|
|
1675
|
+
return (
|
|
1676
|
+
this.pages.find((page) => page.id === this.view) ||
|
|
1677
|
+
this.pages[0] || {
|
|
1678
|
+
title: "Codex ChatGPT Proxy - FE Concepts",
|
|
1679
|
+
status: [],
|
|
1680
|
+
}
|
|
1681
|
+
);
|
|
1682
|
+
},
|
|
1683
|
+
get statusItems() {
|
|
1684
|
+
const lastSync = formatTimeLong(this.dashboardData.lastSyncAt);
|
|
1685
|
+
const items =
|
|
1686
|
+
this.view === "accounts"
|
|
1687
|
+
? [
|
|
1688
|
+
`Selection: ${this.accounts.selectedId || "--"}`,
|
|
1689
|
+
`Rotation: ${this.dashboardData.routing?.rotationEnabled ? "enabled" : "disabled"}`,
|
|
1690
|
+
`Last sync: ${lastSync}`,
|
|
1691
|
+
]
|
|
1692
|
+
: [
|
|
1693
|
+
`Last sync: ${lastSync}`,
|
|
1694
|
+
`Routing: ${routingLabel(this.dashboardData.routing?.strategy)}`,
|
|
1695
|
+
`Backend: ${this.backendPath}`,
|
|
1696
|
+
];
|
|
1697
|
+
if (this.importState.isLoading) {
|
|
1698
|
+
items.unshift(
|
|
1699
|
+
`Importing ${this.importState.fileName || "auth.json"}...`,
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
return items;
|
|
1703
|
+
},
|
|
1704
|
+
get filteredAccounts() {
|
|
1705
|
+
return filterAccountsByQuery(
|
|
1706
|
+
this.accounts.rows,
|
|
1707
|
+
this.accounts.searchQuery,
|
|
1708
|
+
);
|
|
1709
|
+
},
|
|
1710
|
+
get selectedAccount() {
|
|
1711
|
+
return (
|
|
1712
|
+
this.accounts.rows.find(
|
|
1713
|
+
(account) => account.id === this.accounts.selectedId,
|
|
1714
|
+
) ||
|
|
1715
|
+
this.accounts.rows[0] ||
|
|
1716
|
+
{}
|
|
1717
|
+
);
|
|
1718
|
+
},
|
|
1719
|
+
selectAccount(id) {
|
|
1720
|
+
this.accounts.selectedId = id;
|
|
1721
|
+
},
|
|
1722
|
+
handleAccountAction(action, card) {
|
|
1723
|
+
if (!action || !card) {
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
if (action.type === "details") {
|
|
1727
|
+
this.view = "accounts";
|
|
1728
|
+
this.selectAccount(card.accountId);
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
if (action.type === "reauth") {
|
|
1732
|
+
this.startReauthFlow();
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
if (action.type === "resume") {
|
|
1736
|
+
this.resumeAccount(card.accountId);
|
|
1737
|
+
}
|
|
1738
|
+
},
|
|
1739
|
+
startReauthFlow() {
|
|
1740
|
+
this.openAddAccountDialog();
|
|
1741
|
+
},
|
|
1742
|
+
async resumeAccount(accountId) {
|
|
1743
|
+
if (!accountId) {
|
|
1744
|
+
this.openMessageBox({
|
|
1745
|
+
tone: "warning",
|
|
1746
|
+
title: "No account selected",
|
|
1747
|
+
message: "Select an account before resuming.",
|
|
1748
|
+
});
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
const accountLabel = formatAccountLabel(accountId, this.accounts.rows);
|
|
1752
|
+
const confirmed = await this.openConfirmBox({
|
|
1753
|
+
title: "Resume account?",
|
|
1754
|
+
message: `Resume account ${accountLabel}? Routing will include it again.`,
|
|
1755
|
+
confirmLabel: "Resume",
|
|
1756
|
+
cancelLabel: "Cancel",
|
|
1757
|
+
});
|
|
1758
|
+
if (!confirmed) {
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
this.authDialog.isLoading = true;
|
|
1762
|
+
postJson(
|
|
1763
|
+
API_ENDPOINTS.accountReactivate(accountId),
|
|
1764
|
+
{},
|
|
1765
|
+
"resume account",
|
|
1766
|
+
)
|
|
1767
|
+
.then(() => this.refreshAll({ preferredId: accountId }))
|
|
1768
|
+
.then(() => {
|
|
1769
|
+
this.openMessageBox({
|
|
1770
|
+
tone: "success",
|
|
1771
|
+
title: "Account resumed",
|
|
1772
|
+
message: `${accountLabel} is active again.`,
|
|
1773
|
+
});
|
|
1774
|
+
})
|
|
1775
|
+
.catch((error) => {
|
|
1776
|
+
this.openMessageBox({
|
|
1777
|
+
tone: "error",
|
|
1778
|
+
title: "Resume failed",
|
|
1779
|
+
message: error.message || "Failed to resume account.",
|
|
1780
|
+
});
|
|
1781
|
+
})
|
|
1782
|
+
.finally(() => {
|
|
1783
|
+
this.authDialog.isLoading = false;
|
|
1784
|
+
});
|
|
1785
|
+
},
|
|
1786
|
+
async resumeSelectedAccount() {
|
|
1787
|
+
return this.resumeAccount(this.selectedAccount.id);
|
|
1788
|
+
},
|
|
1789
|
+
async pauseSelectedAccount() {
|
|
1790
|
+
const accountId = this.selectedAccount.id;
|
|
1791
|
+
if (!accountId) {
|
|
1792
|
+
this.openMessageBox({
|
|
1793
|
+
tone: "warning",
|
|
1794
|
+
title: "No account selected",
|
|
1795
|
+
message: "Select an account before pausing.",
|
|
1796
|
+
});
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
const accountLabel = formatAccountLabel(accountId, this.accounts.rows);
|
|
1800
|
+
const confirmed = await this.openConfirmBox({
|
|
1801
|
+
title: "Pause account?",
|
|
1802
|
+
message: `Pause account ${accountLabel}? Routing will skip it until resumed.`,
|
|
1803
|
+
confirmLabel: "Pause",
|
|
1804
|
+
cancelLabel: "Cancel",
|
|
1805
|
+
});
|
|
1806
|
+
if (!confirmed) {
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
this.authDialog.isLoading = true;
|
|
1810
|
+
postJson(API_ENDPOINTS.accountPause(accountId), {}, "pause account")
|
|
1811
|
+
.then(() => this.refreshAll({ preferredId: accountId }))
|
|
1812
|
+
.then(() => {
|
|
1813
|
+
this.openMessageBox({
|
|
1814
|
+
tone: "success",
|
|
1815
|
+
title: "Account paused",
|
|
1816
|
+
message: `${accountLabel} paused.`,
|
|
1817
|
+
});
|
|
1818
|
+
})
|
|
1819
|
+
.catch((error) => {
|
|
1820
|
+
this.openMessageBox({
|
|
1821
|
+
tone: "error",
|
|
1822
|
+
title: "Pause failed",
|
|
1823
|
+
message: error.message || "Failed to pause account.",
|
|
1824
|
+
});
|
|
1825
|
+
})
|
|
1826
|
+
.finally(() => {
|
|
1827
|
+
this.authDialog.isLoading = false;
|
|
1828
|
+
});
|
|
1829
|
+
},
|
|
1830
|
+
async deleteSelectedAccount() {
|
|
1831
|
+
const accountId = this.selectedAccount.id;
|
|
1832
|
+
if (!accountId) {
|
|
1833
|
+
this.openMessageBox({
|
|
1834
|
+
tone: "warning",
|
|
1835
|
+
title: "No account selected",
|
|
1836
|
+
message: "Select an account before deleting.",
|
|
1837
|
+
});
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
const accountLabel = formatAccountLabel(accountId, this.accounts.rows);
|
|
1841
|
+
const confirmed = await this.openConfirmBox({
|
|
1842
|
+
title: "Delete account?",
|
|
1843
|
+
message: `Delete account ${accountLabel}? This cannot be undone.`,
|
|
1844
|
+
confirmLabel: "Delete",
|
|
1845
|
+
cancelLabel: "Cancel",
|
|
1846
|
+
});
|
|
1847
|
+
if (!confirmed) {
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
try {
|
|
1851
|
+
await deleteJson(
|
|
1852
|
+
API_ENDPOINTS.accountDelete(accountId),
|
|
1853
|
+
"delete account",
|
|
1854
|
+
);
|
|
1855
|
+
await this.refreshAll({ preferredId: "" });
|
|
1856
|
+
this.openMessageBox({
|
|
1857
|
+
tone: "success",
|
|
1858
|
+
title: "Account deleted",
|
|
1859
|
+
message: `${accountLabel} removed.`,
|
|
1860
|
+
});
|
|
1861
|
+
} catch (error) {
|
|
1862
|
+
this.openMessageBox({
|
|
1863
|
+
tone: "error",
|
|
1864
|
+
title: "Delete failed",
|
|
1865
|
+
message: error.message || "Failed to delete account.",
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
},
|
|
1869
|
+
statusBadgeText,
|
|
1870
|
+
statusLabel,
|
|
1871
|
+
requestStatusLabel,
|
|
1872
|
+
requestStatusClass,
|
|
1873
|
+
progressClass,
|
|
1874
|
+
planLabel,
|
|
1875
|
+
routingLabel,
|
|
1876
|
+
errorLabel,
|
|
1877
|
+
formatNumber,
|
|
1878
|
+
formatCompactNumber,
|
|
1879
|
+
formatPercent,
|
|
1880
|
+
formatWindowMinutes,
|
|
1881
|
+
formatWindowLabel,
|
|
1882
|
+
formatPercentValue,
|
|
1883
|
+
formatRate,
|
|
1884
|
+
formatTimeLong,
|
|
1885
|
+
formatCountdown,
|
|
1886
|
+
formatQuotaResetLabel,
|
|
1887
|
+
formatAccessTokenLabel,
|
|
1888
|
+
formatRefreshTokenLabel,
|
|
1889
|
+
formatIdTokenLabel,
|
|
1890
|
+
}));
|
|
1891
|
+
};
|
|
1892
|
+
|
|
1893
|
+
if (window.Alpine) {
|
|
1894
|
+
registerApp();
|
|
1895
|
+
} else {
|
|
1896
|
+
document.addEventListener("alpine:init", registerApp, { once: true });
|
|
1897
|
+
}
|
|
1898
|
+
})();
|