@goodandready/dsh-key-rotation 0.8.76 → 0.8.78
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 +14 -0
- package/lib/client-src/00-header.js +40 -0
- package/lib/client-src/10-locales.js +585 -0
- package/lib/client-src/20-hooks.js +55 -0
- package/lib/client-src/30-utils.js +185 -0
- package/lib/client-src/40-components.js +346 -0
- package/lib/client-src/50-card-main.js +647 -0
- package/lib/client-src/51-card-pool-mutators.js +501 -0
- package/lib/client-src/52-card-pool-rows.js +590 -0
- package/lib/client-src/53-card-dialogs-and-render.js +568 -0
- package/lib/client-src/60-header-chip.js +137 -0
- package/lib/client-src/90-apply.js +69 -0
- package/lib/error-taxonomy.js +59 -10
- package/lib/errors.js +302 -0
- package/lib/index.js +6 -0
- package/lib/model-catalog.js +296 -0
- package/lib/pool-selection.js +126 -28
- package/lib/pool.js +19 -0
- package/lib/quota-runway.js +222 -0
- package/lib/rotate.js +61 -11
- package/lib/session-router.js +265 -0
- package/lib/stream-metrics.js +13 -5
- package/lib/transports/anthropic.js +125 -0
- package/lib/transports/index.js +23 -0
- package/lib/transports/openai.js +93 -0
- package/lib/transports/sse.js +160 -0
- package/lib/transports/tokens.js +124 -0
- package/package.json +5 -1
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
const searchInput = h('input', { className: 'krot-in', placeholder: 'Search providers…', value: search, onChange: (e) => setSearch(e.target.value), style: { marginBottom: '8px' } });
|
|
2
|
+
const providerRows = providerList.map(({ entry, pIndex }) => {
|
|
3
|
+
const options = [h('option', { key: '', value: '' }, t('chooseProvider'))];
|
|
4
|
+
if (entry.provider && !providerById.has(entry.provider)) {
|
|
5
|
+
options.push(h('option', { key: entry.provider, value: entry.provider }, entry.provider + ' (' + (catalog.status === 'ready' ? t('notRegistered')
|
|
6
|
+
: catalog.status === 'loading' ? t('loading') : t('providerCatalogUnavailable')) + ')'));
|
|
7
|
+
}
|
|
8
|
+
options.push(...providers.map((prov) =>
|
|
9
|
+
h('option', { key: prov.id, value: prov.id }, prov.name + (prov.id !== prov.name ? ' — ' + prov.id : ''))));
|
|
10
|
+
|
|
11
|
+
const keys = entry.keys ?? [];
|
|
12
|
+
const entryWeights = entry.weights ?? [];
|
|
13
|
+
const entryPaused = entry.paused ?? [];
|
|
14
|
+
|
|
15
|
+
let readyCount = 0, cooldownCount = 0, errorCount = 0;
|
|
16
|
+
for (let idx = 0; idx < keys.length; idx++) {
|
|
17
|
+
const k = keys[idx];
|
|
18
|
+
const st = keyStatus(entry.provider, k, Boolean(entryPaused[idx]));
|
|
19
|
+
const isCool = (st && st.cooling) && !optimisticReset[k];
|
|
20
|
+
const tr = testResult[k];
|
|
21
|
+
if (tr && !tr.ok) errorCount++;
|
|
22
|
+
if (isCool) cooldownCount++;
|
|
23
|
+
if (st && st.ready) readyCount++;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const filteredIndices = keys.map((k, idx) => ({ key: k, kIndex: idx })).filter(({ key: k, kIndex: idx }) => {
|
|
27
|
+
if (statusFilter === 'all') return true;
|
|
28
|
+
const st = keyStatus(entry.provider, k, Boolean(entryPaused[idx]));
|
|
29
|
+
const isCool = (st && st.cooling) && !optimisticReset[k];
|
|
30
|
+
const tr = testResult[k];
|
|
31
|
+
if (statusFilter === 'ready') return Boolean(st && st.ready);
|
|
32
|
+
if (statusFilter === 'cooldown') return isCool;
|
|
33
|
+
if (statusFilter === 'error') return tr && !tr.ok;
|
|
34
|
+
return true;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const filterBar = keys.length > 1 ? h('div', { className: 'krot-filter-bar' },
|
|
38
|
+
h('button', { type: 'button', className: 'krot-pill' + (statusFilter === 'all' ? ' krot-pill-active' : ''), onClick: () => setStatusFilter('all') }, t('filterAll') + ' (' + keys.length + ')'),
|
|
39
|
+
h('button', { type: 'button', className: 'krot-pill' + (statusFilter === 'ready' ? ' krot-pill-active' : ''), onClick: () => setStatusFilter('ready') }, t('filterReady') + ' (' + readyCount + ')'),
|
|
40
|
+
cooldownCount > 0 ? h('button', { type: 'button', className: 'krot-pill krot-pill-warn' + (statusFilter === 'cooldown' ? ' krot-pill-active' : ''), onClick: () => setStatusFilter('cooldown') }, t('filterCooldown') + ' (' + cooldownCount + ')') : null,
|
|
41
|
+
errorCount > 0 ? h('button', { type: 'button', className: 'krot-pill krot-pill-err' + (statusFilter === 'error' ? ' krot-pill-active' : ''), onClick: () => setStatusFilter('error') }, t('filterErrors') + ' (' + errorCount + ')') : null,
|
|
42
|
+
) : null;
|
|
43
|
+
|
|
44
|
+
const keyRows = filteredIndices.map(({ key, kIndex }) => {
|
|
45
|
+
const isP = Boolean(entryPaused[kIndex]);
|
|
46
|
+
const st = keyStatus(entry.provider, key, isP);
|
|
47
|
+
const info = keyInfo(entry.provider, key);
|
|
48
|
+
const rowKey = entry.provider + '/' + key;
|
|
49
|
+
const typed = secretDraft[rowKey];
|
|
50
|
+
const fromEnv = Boolean(info && info.source === 'env');
|
|
51
|
+
|
|
52
|
+
// Key name owns a full row so neighbouring keys stay distinguishable.
|
|
53
|
+
const nameRow = [
|
|
54
|
+
h('span', { className: 'krot-num', key: 'n' }, String(kIndex + 1)),
|
|
55
|
+
h('span', { key: 'i', className: 'krot-name', title: key + ' (click to copy)', style: { cursor: 'copy' }, onClick: () => {
|
|
56
|
+
if (navigator.clipboard) navigator.clipboard.writeText(key).then(() => setSecretDraft((cur) => ({ ...cur, ['copied:' + key]: true }))).catch(() => {});
|
|
57
|
+
setTimeout(() => setSecretDraft((cur) => ({ ...cur, ['copied:' + key]: false })), 1500);
|
|
58
|
+
} },
|
|
59
|
+
t('keyLabel').replace('{n}', String(kIndex + 1)),
|
|
60
|
+
h('span', null, secretDraft['copied:' + key] ? ' ✓' : '')),
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const meta = [
|
|
64
|
+
h('span', { key: 'd', className: 'krot-dot', style: { background: st ? st.color : 'var(--dsw-alias-border-l2)' } }),
|
|
65
|
+
h('span', { key: 's', className: 'krot-state' }, st ? st.text : ''),
|
|
66
|
+
];
|
|
67
|
+
if (info && info.pinned) {
|
|
68
|
+
meta.push(h('span', { key: 'pin', className: 'krot-state', title: 'Pinned key' }, '📌'));
|
|
69
|
+
}
|
|
70
|
+
if (info && info.label) {
|
|
71
|
+
meta.push(h('span', { key: 'lbl', className: 'krot-tag', title: 'Key label' }, info.label));
|
|
72
|
+
}
|
|
73
|
+
if (fromEnv) {
|
|
74
|
+
meta.push(h('span', { key: 'v', className: 'krot-tail', title: t('keyFromEnv') },
|
|
75
|
+
info.tail ? '••••' + info.tail : t('keyFromEnv')));
|
|
76
|
+
} else {
|
|
77
|
+
meta.push(h('input', {
|
|
78
|
+
key: 'v',
|
|
79
|
+
type: 'password',
|
|
80
|
+
className: 'krot-in krot-secret',
|
|
81
|
+
value: typed ?? '',
|
|
82
|
+
placeholder: info && info.tail ? '••••' + info.tail : t('keyValuePlaceholder'),
|
|
83
|
+
onChange: (e) => setSecretDraft((cur) => ({ ...cur, [rowKey]: e.target.value })),
|
|
84
|
+
}));
|
|
85
|
+
if (typed) meta.push(btn('✓', () => saveSecret(key, rowKey), { title: t('keySave'), key: 'save-secret' }));
|
|
86
|
+
}
|
|
87
|
+
if (info && typeof info.usage === 'number' && info.usage > 0) {
|
|
88
|
+
let tip = 'requests through this key';
|
|
89
|
+
if (info.byModel && Object.keys(info.byModel).length > 0) {
|
|
90
|
+
tip = Object.entries(info.byModel).map(([m, c]) => m + ': ' + c).join('\n');
|
|
91
|
+
}
|
|
92
|
+
meta.push(h('span', { key: 'u', className: 'krot-tail', title: tip }, String(info.usage)));
|
|
93
|
+
if (info.usageDays && Object.keys(info.usageDays).length > 0) {
|
|
94
|
+
const days = Object.entries(info.usageDays);
|
|
95
|
+
const max = Math.max(1, ...days.map(([, c]) => c));
|
|
96
|
+
meta.push(h('span', { key: 'g', className: 'krot-graph', title: days.map(([d, c]) => d + ': ' + c).join('\n'), style: { display: 'inline-flex', gap: '1px', alignItems: 'flex-end', height: '12px' } },
|
|
97
|
+
days.slice(-14).map(([d, c]) => h('span', { key: d, style: { width: '3px', height: Math.max(2, (c / max) * 12) + 'px', background: 'var(--dsw-alias-state-success-primary)', borderRadius: '1px' } }))
|
|
98
|
+
));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (info && info.lastUsedAt) meta.push(h('span', { key: 'lu', className: 'krot-tail', title: 'last used' }, formatAgo((k)=>t(k), info.lastUsedAt)));
|
|
102
|
+
// (issue 215): per-key weight input (default 1)
|
|
103
|
+
meta.push(h('input', { key: 'w', type: 'number', min: 1, max: 1000, className: 'krot-in krot-weight',
|
|
104
|
+
value: (entryWeights[kIndex] ?? info?.weight ?? 1),
|
|
105
|
+
title: t('weightHint'),
|
|
106
|
+
onChange: (e) => setKeyWeight(pIndex, kIndex, e.target.value),
|
|
107
|
+
style: { width: '52px', padding: '2px 6px', fontSize: '12px' } }));
|
|
108
|
+
meta.push(h('button', {
|
|
109
|
+
key: 'pause-btn',
|
|
110
|
+
type: 'button',
|
|
111
|
+
className: 'krot-btn' + (isP ? ' krot-pill-warn' : ''),
|
|
112
|
+
title: isP ? t('resumeKey') : t('pauseKey'),
|
|
113
|
+
style: { padding: '2px 6px', fontSize: '11px' },
|
|
114
|
+
onClick: () => togglePauseKey(pIndex, kIndex),
|
|
115
|
+
}, isP ? '⏸ ' + t('keyPaused') : '▶'));
|
|
116
|
+
// (issue 210): RPM capacity indicator (only when rpmLimit is active)
|
|
117
|
+
if (info && info.rpm) meta.push(h('span', { key: 'rpm', className: 'krot-tail',
|
|
118
|
+
title: t('rpmTitle').replace('{u}', String(info.rpm.used)).replace('{r}', String(info.rpm.remaining)),
|
|
119
|
+
style: info.rpm.remaining === 0 ? { color: 'var(--dsw-alias-state-error-primary)', fontWeight: 700 } : undefined },
|
|
120
|
+
'⏱' + info.rpm.remaining));
|
|
121
|
+
// TPM capacity indicator (only when tpmLimit is active)
|
|
122
|
+
if (info && info.tpm) meta.push(h('span', { key: 'tpm', className: 'krot-tail',
|
|
123
|
+
title: t('tpmTitle').replace('{u}', String(info.tpm.used)).replace('{r}', String(info.tpm.remaining)),
|
|
124
|
+
style: info.tpm.remaining === 0 ? { color: 'var(--dsw-alias-state-error-primary)', fontWeight: 700 } : undefined },
|
|
125
|
+
'⚡' + info.tpm.remaining));
|
|
126
|
+
if (info && typeof info.cost === 'number' && info.cost > 0) meta.push(h('span', { key: 'c', className: 'krot-tail', title: 'cost' }, '$' + info.cost.toFixed(2)));
|
|
127
|
+
const tr = testResult[key];
|
|
128
|
+
if (tr) meta.push(h('span', { key: 'tr', className: 'krot-tail',
|
|
129
|
+
title: (tr.message || (tr.ok ? t('testOk') : t('testFail')))
|
|
130
|
+
+ (!tr.ok && tr.code ? ' · ' + tr.code : '')
|
|
131
|
+
+ (tr.ok && tr.modelsCount ? ' · ' + tr.modelsCount + ' models' : '')
|
|
132
|
+
+ (tr.ok && tr.latencyMs ? ' · ' + tr.latencyMs + 'ms' : ''),
|
|
133
|
+
style: { color: tr.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)', fontWeight: 700 } },
|
|
134
|
+
tr.ok ? (tr.modelsCount ? tr.modelsCount + 'm' : '✓') : ('✕' + (tr.code ? ' ' + tr.code : ''))));
|
|
135
|
+
// (issue 219): last probe from the sandbox cache, greyed when older than 24h
|
|
136
|
+
else if (!Object.prototype.hasOwnProperty.call(testResult, key) && probeCache && probeCache[key]) {
|
|
137
|
+
const pc = probeCache[key];
|
|
138
|
+
const stale = Date.now() - (pc.at ?? 0) > 86400000;
|
|
139
|
+
meta.push(h('span', { key: 'pc', className: 'krot-tail',
|
|
140
|
+
title: 'last probe ' + (pc.at ? new Date(pc.at).toLocaleTimeString() : '') + (pc.ok ? ' ok' : ' ' + (pc.code ?? 'fail')),
|
|
141
|
+
style: { opacity: stale ? 0.4 : 0.7, color: pc.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)' } },
|
|
142
|
+
(pc.ok ? '✓' : '✕') + (pc.latencyMs ? ' ' + pc.latencyMs + 'ms' : '')));
|
|
143
|
+
}
|
|
144
|
+
meta.push(h('button', { key: 't', className: 'krot-btn', onClick: () => doTest(key), disabled: Boolean(testing || testAllProvider), title: t('testKey') }, testing === key ? '…' : t('testKey')));
|
|
145
|
+
// (issue 223): broken keys get a one-click live re-test + auto-unbreak
|
|
146
|
+
if (info && info.broken) {
|
|
147
|
+
meta.push(h('button', { key: 'rt', className: 'krot-btn', onClick: () => retestBroken(key), title: t('retestBroken') }, t('retestBroken')));
|
|
148
|
+
}
|
|
149
|
+
if (info && info.balance) {
|
|
150
|
+
const b = info.balance;
|
|
151
|
+
const isZero = b.isAvailable === false || (typeof b.total === 'number' && b.total <= 0);
|
|
152
|
+
const cur = b.currency || '';
|
|
153
|
+
const amtStr = typeof b.total === 'number' ? b.total.toFixed(2) : String(b.total || 0);
|
|
154
|
+
meta.push(h('span', {
|
|
155
|
+
key: 'bal',
|
|
156
|
+
className: 'krot-tail' + (isZero ? ' krot-badge-bad' : ''),
|
|
157
|
+
title: isZero ? t('balanceEmpty') : ('Upstream balance: ' + amtStr + ' ' + cur),
|
|
158
|
+
style: isZero ? { color: 'var(--dsw-alias-state-error-primary)', fontWeight: 700 } : undefined,
|
|
159
|
+
}, '💰 ' + amtStr + (cur ? (' ' + cur) : '')));
|
|
160
|
+
}
|
|
161
|
+
if (info && info.detectedLimits) {
|
|
162
|
+
const dl = info.detectedLimits;
|
|
163
|
+
const parts = [];
|
|
164
|
+
if (dl.rpmLimit) parts.push(dl.rpmLimit + ' RPM');
|
|
165
|
+
if (dl.tpmLimit) parts.push(dl.tpmLimit + ' TPM');
|
|
166
|
+
if (parts.length > 0) {
|
|
167
|
+
meta.push(h('span', {
|
|
168
|
+
key: 'dl',
|
|
169
|
+
className: 'krot-tail',
|
|
170
|
+
title: 'Live upstream limits:\n' + JSON.stringify(dl, null, 2),
|
|
171
|
+
style: { fontSize: '11px', opacity: 0.8, color: 'var(--dsw-alias-brand-primary)' }
|
|
172
|
+
}, '⚡ ' + parts.join(' · ')));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (info && info.modelCapabilities && info.modelCapabilities.length > 0) {
|
|
176
|
+
meta.push(h('span', {
|
|
177
|
+
key: 'caps',
|
|
178
|
+
className: 'krot-tail',
|
|
179
|
+
title: 'Discovered models (' + info.modelCapabilities.length + '):\n' + info.modelCapabilities.join('\n'),
|
|
180
|
+
style: { fontSize: '11px', opacity: 0.85 }
|
|
181
|
+
}, '🤖 ' + info.modelCapabilities.length + 'm'));
|
|
182
|
+
}
|
|
183
|
+
meta.push(h('span', { key: 'a', className: 'krot-acts' },
|
|
184
|
+
btn('↑', () => moveKey(pIndex, kIndex, -1), { disabled: kIndex === 0, title: t('moveUp') }),
|
|
185
|
+
btn('↓', () => moveKey(pIndex, kIndex, 1), { disabled: kIndex === keys.length - 1, title: t('moveDown') }),
|
|
186
|
+
btn('✕', () => removeKey(pIndex, kIndex), { title: t('removeKey') }),
|
|
187
|
+
));
|
|
188
|
+
|
|
189
|
+
return h('div', { key, className: 'krot-key' },
|
|
190
|
+
nameRow,
|
|
191
|
+
h('div', { className: 'krot-meta' }, meta),
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const providerStatus = status[entry.provider];
|
|
196
|
+
const switchesLine = h('p', { className: 'krot-hint' },
|
|
197
|
+
providerStatus && providerStatus.switches > 0
|
|
198
|
+
? t('switchesSome')
|
|
199
|
+
.replace('{n}', String(providerStatus.switches))
|
|
200
|
+
.replace('{reason}', String(providerStatus.lastReason || '—'))
|
|
201
|
+
.replace('{ago}', formatAgo(t, providerStatus.lastSwitchAt))
|
|
202
|
+
: t('switchesNone'));
|
|
203
|
+
const exhaustionWarning = providerStatus && providerStatus.lastExhaustionAt && (Date.now() - providerStatus.lastExhaustionAt) < 3600000
|
|
204
|
+
? h('p', { className: 'krot-err' }, t('poolExhausted') + ' (' + formatAgo(t, providerStatus.lastExhaustionAt) + ')')
|
|
205
|
+
: null;
|
|
206
|
+
// (issue 208): budget line (warn color at >=80%, red at 100%)
|
|
207
|
+
const budgetLine = providerStatus && (providerStatus.budgetDaily > 0 || providerStatus.budgetWeekly > 0)
|
|
208
|
+
? (() => {
|
|
209
|
+
const dayRatio = providerStatus.budgetDaily > 0 ? providerStatus.todayCost / providerStatus.budgetDaily : 0;
|
|
210
|
+
const weekRatio = providerStatus.budgetWeekly > 0 ? providerStatus.weeklyCost / providerStatus.budgetWeekly : 0;
|
|
211
|
+
const worst = Math.max(dayRatio, weekRatio);
|
|
212
|
+
const color = worst >= 1 ? 'var(--dsw-alias-state-error-primary)' : worst >= 0.8 ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-label-tertiary)';
|
|
213
|
+
const parts = [];
|
|
214
|
+
if (providerStatus.budgetDaily > 0) parts.push('$' + (providerStatus.todayCost ?? 0).toFixed(2) + '/' + '$' + providerStatus.budgetDaily);
|
|
215
|
+
if (providerStatus.budgetWeekly > 0) parts.push('week $' + (providerStatus.weeklyCost ?? 0).toFixed(2) + '/' + '$' + providerStatus.budgetWeekly);
|
|
216
|
+
if (worst >= 1 && providerStatus.pauseOnBudget) parts.push('· ' + t('pausedBudget'));
|
|
217
|
+
return h('p', { className: 'krot-hint', style: { color } }, t('budgetLabel') + ' ' + parts.join(' · '));
|
|
218
|
+
})()
|
|
219
|
+
: null;
|
|
220
|
+
// (issue 225): provider p95 latency + SLO marker
|
|
221
|
+
const sloLine = providerStatus && providerStatus.p95 != null
|
|
222
|
+
? (() => {
|
|
223
|
+
const over = providerStatus.latencySloMs && providerStatus.p95 > providerStatus.latencySloMs;
|
|
224
|
+
return h('p', { className: 'krot-hint', style: over ? { color: 'var(--dsw-alias-state-warn-primary)' } : undefined },
|
|
225
|
+
'p95 ' + providerStatus.p95 + 'ms' + (providerStatus.latencySloMs ? ' / ' + providerStatus.latencySloMs + 'ms SLO' : ''));
|
|
226
|
+
})()
|
|
227
|
+
: null;
|
|
228
|
+
// (issue 209): CSV export for this provider's usage (last 7 days)
|
|
229
|
+
const exportCsv = h('button', { className: 'krot-btn', title: t('exportCsv'),
|
|
230
|
+
onClick: () => {
|
|
231
|
+
const url = '/dsh-key-rotation/usage?format=csv&days=7&provider=' + encodeURIComponent(entry.provider);
|
|
232
|
+
const a = document.createElement('a');
|
|
233
|
+
a.href = url; a.download = 'usage-' + entry.provider + '.csv';
|
|
234
|
+
document.body.appendChild(a); a.click(); a.remove();
|
|
235
|
+
} }, t('exportCsv'));
|
|
236
|
+
|
|
237
|
+
const loadChart = (() => {
|
|
238
|
+
if (keys.length === 0) return null;
|
|
239
|
+
const ps = status[entry.provider];
|
|
240
|
+
const keyUsages = keys.map((k, idx) => {
|
|
241
|
+
const hit = ps && Array.isArray(ps.keys) ? ps.keys.find((x) => x.ref === k) : null;
|
|
242
|
+
const u = hit && typeof hit.usage === 'number' ? hit.usage : 0;
|
|
243
|
+
return { ref: k, index: idx, usage: u };
|
|
244
|
+
});
|
|
245
|
+
const total = keyUsages.reduce((acc, x) => acc + x.usage, 0);
|
|
246
|
+
const palette = [
|
|
247
|
+
'var(--dsw-alias-brand-primary)',
|
|
248
|
+
'var(--dsw-alias-state-success-primary)',
|
|
249
|
+
'var(--dsw-alias-state-warn-primary)',
|
|
250
|
+
'var(--dsw-alias-brand-primary)',
|
|
251
|
+
'var(--krot-chart-4)',
|
|
252
|
+
'var(--krot-chart-5)',
|
|
253
|
+
'var(--krot-chart-6)',
|
|
254
|
+
];
|
|
255
|
+
return h('div', { className: 'krot-load-chart' },
|
|
256
|
+
h('div', { className: 'krot-load-header' },
|
|
257
|
+
h('span', { style: { fontWeight: 600 } }, '📊 ' + t('loadDistribution')),
|
|
258
|
+
h('span', { className: 'krot-sr-only', style: { position: 'absolute', width: 1, height: 1, padding: 0, margin: -1, overflow: 'hidden', clip: 'rect(0,0,0,0)', border: 0 } },
|
|
259
|
+
t('loadChartAria') + ': ' + (total > 0 ? keyUsages.map((x) => t('keyLabel').replace('{n}', String(x.index + 1)) + ' ' + x.usage).join(', ') : t('noTrafficYet'))),
|
|
260
|
+
h('span', null, total > 0 ? (total + ' ' + t('statRequests')) : t('noTrafficYet'))
|
|
261
|
+
),
|
|
262
|
+
h('div', { className: 'krot-load-bar' },
|
|
263
|
+
total > 0
|
|
264
|
+
? keyUsages.map((x, i) => {
|
|
265
|
+
if (x.usage === 0) return null;
|
|
266
|
+
const pct = Math.max(1, Math.round((x.usage / total) * 100));
|
|
267
|
+
const color = palette[i % palette.length];
|
|
268
|
+
return h('div', {
|
|
269
|
+
key: x.ref,
|
|
270
|
+
className: 'krot-load-segment',
|
|
271
|
+
style: { width: pct + '%', background: color },
|
|
272
|
+
title: x.ref + ': ' + x.usage + ' (' + pct + '%)',
|
|
273
|
+
});
|
|
274
|
+
})
|
|
275
|
+
: h('div', { className: 'krot-load-segment', style: { width: '100%', background: 'var(--dsw-alias-bg-layer-2)', opacity: 0.6 } })
|
|
276
|
+
),
|
|
277
|
+
total > 0 ? h('div', { className: 'krot-load-legend' },
|
|
278
|
+
keyUsages.map((x, i) => {
|
|
279
|
+
const pct = total > 0 ? Math.round((x.usage / total) * 100) : 0;
|
|
280
|
+
const color = palette[i % palette.length];
|
|
281
|
+
return h('span', { key: x.ref, className: 'krot-load-item' },
|
|
282
|
+
h('span', { className: 'krot-load-dot', style: { background: color } }),
|
|
283
|
+
h('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t('keyLabel').replace('{n}', String(x.index + 1)) + ': ' + x.usage + ' (' + pct + '%)')
|
|
284
|
+
);
|
|
285
|
+
})
|
|
286
|
+
) : null,
|
|
287
|
+
total > 0 && keyUsages.length > 1 ? h('div', { style: { marginTop: '8px', display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
288
|
+
h('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, '📈 ' + t('sparklineTraffic') + ':'),
|
|
289
|
+
h(Sparkline, { points: keyUsages.map(x => x.usage), width: 140, height: 20 })
|
|
290
|
+
) : null,
|
|
291
|
+
(() => {
|
|
292
|
+
const an = ps && ps.analytics24h ? ps.analytics24h : null;
|
|
293
|
+
if (!an || an.totalRequests === 0) return null;
|
|
294
|
+
const hColor = an.healthIndexPct >= 80 ? 'var(--dsw-alias-state-success-primary)' : an.healthIndexPct >= 50 ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-state-error-primary)';
|
|
295
|
+
return h('div', { style: { marginTop: '8px', display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '6px' } },
|
|
296
|
+
h('span', { className: 'krot-badge', style: { color: hColor, fontWeight: 700 } }, '🛡️ ' + t('healthIndex') + ': ' + an.healthIndexPct + '% (' + an.firstAttemptSuccesses + '/' + an.totalRequests + ' ' + t('cleanRequests') + ')'),
|
|
297
|
+
an.errorBreakdown.rateLimit > 0 ? h('span', { className: 'krot-badge krot-badge-warn' }, '429: ' + an.errorBreakdown.rateLimit) : null,
|
|
298
|
+
an.errorBreakdown.server > 0 ? h('span', { className: 'krot-badge krot-badge-warn' }, '5xx: ' + an.errorBreakdown.server) : null,
|
|
299
|
+
an.errorBreakdown.timeout > 0 ? h('span', { className: 'krot-badge krot-badge-warn' }, 'Timeout: ' + an.errorBreakdown.timeout) : null,
|
|
300
|
+
an.errorBreakdown.quota > 0 ? h('span', { className: 'krot-badge krot-badge-warn' }, 'Quota: ' + an.errorBreakdown.quota) : null,
|
|
301
|
+
an.cascades > 0 ? h('span', { className: 'krot-badge' }, '🔀 Cascades: ' + an.cascades) : null,
|
|
302
|
+
an.avgCooldownRecoveryMs > 0 ? h('span', { className: 'krot-badge' }, '⏱️ ' + t('avgRecovery') + ': ' + Math.round(an.avgCooldownRecoveryMs / 1000) + 's') : null
|
|
303
|
+
);
|
|
304
|
+
})()
|
|
305
|
+
);
|
|
306
|
+
})();
|
|
307
|
+
|
|
308
|
+
const promptCacheWidget = (() => {
|
|
309
|
+
const ps = status[entry.provider];
|
|
310
|
+
const pc = ps && ps.promptCache ? ps.promptCache : null;
|
|
311
|
+
if (!pc || (!pc.d30?.cachedTokens && !pc.allTime?.cachedTokens)) return null;
|
|
312
|
+
const d1 = pc.d1 || { cachedTokens: 0, savingsUsd: 0, hitRatioPct: 0 };
|
|
313
|
+
const d7 = pc.d7 || { cachedTokens: 0, savingsUsd: 0, hitRatioPct: 0 };
|
|
314
|
+
const d30 = pc.d30 || { cachedTokens: 0, savingsUsd: 0, hitRatioPct: 0 };
|
|
315
|
+
const savedStr = d30.savingsUsd > 0 ? '$' + d30.savingsUsd.toFixed(2) : '$0.00';
|
|
316
|
+
return h('div', {
|
|
317
|
+
className: 'krot-prompt-cache-card',
|
|
318
|
+
style: {
|
|
319
|
+
marginTop: '6px',
|
|
320
|
+
marginBottom: '6px',
|
|
321
|
+
padding: '8px 10px',
|
|
322
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
323
|
+
borderRadius: '6px',
|
|
324
|
+
border: '1px solid var(--dsw-alias-border-subtle)',
|
|
325
|
+
display: 'flex',
|
|
326
|
+
flexDirection: 'column',
|
|
327
|
+
gap: '4px'
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
|
|
331
|
+
h('span', { style: { fontWeight: 600, fontSize: '12px' } }, '⚡ ' + t('promptCacheTitle')),
|
|
332
|
+
h('span', { className: 'krot-badge krot-badge-ok', style: { fontSize: '11px', fontWeight: 600 } }, savedStr + ' ' + t('promptCacheSaved'))
|
|
333
|
+
),
|
|
334
|
+
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '10px', fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } },
|
|
335
|
+
h('span', null, h('strong', { style: { color: 'var(--dsw-alias-label-primary)' } }, '24h: '), (d1.cachedTokens || 0).toLocaleString() + ' tok (' + d1.hitRatioPct + '%)'),
|
|
336
|
+
h('span', null, h('strong', { style: { color: 'var(--dsw-alias-label-primary)' } }, '7d: '), (d7.cachedTokens || 0).toLocaleString() + ' tok (' + d7.hitRatioPct + '%)'),
|
|
337
|
+
h('span', null, h('strong', { style: { color: 'var(--dsw-alias-label-primary)' } }, '30d: '), (d30.cachedTokens || 0).toLocaleString() + ' tok (' + d30.hitRatioPct + '%)')
|
|
338
|
+
)
|
|
339
|
+
);
|
|
340
|
+
})();
|
|
341
|
+
|
|
342
|
+
// ── per-model sub-pools: keys + token quotas, plus live usage meters ──
|
|
343
|
+
const modelPools = (() => {
|
|
344
|
+
const models = modelEntriesOf(entry);
|
|
345
|
+
const statusFor = (model) => status[entry.provider + '::' + model] ?? null;
|
|
346
|
+
const runtimeKeysOf = (model) => {
|
|
347
|
+
const ps = statusFor(model);
|
|
348
|
+
return ps && Array.isArray(ps.keys) ? ps.keys : null;
|
|
349
|
+
};
|
|
350
|
+
const rendered = models.map(([model, mp]) => {
|
|
351
|
+
const keys = Array.isArray(mp.keys) ? mp.keys : [];
|
|
352
|
+
const limits = quotaMapOf(mp);
|
|
353
|
+
const rtKeys = runtimeKeysOf(model);
|
|
354
|
+
const rows = keys.map((ref, kIndex) => {
|
|
355
|
+
const info = rtKeys ? rtKeys.find((x) => x.ref === ref) : null;
|
|
356
|
+
const quota = info && info.modelQuota ? info.modelQuota : null;
|
|
357
|
+
const limits2 = { ...limits };
|
|
358
|
+
const limitValue = limits2[ref]?.tokenLimit;
|
|
359
|
+
const cells = [
|
|
360
|
+
h('span', { key: 'r', className: 'krot-tail', title: ref }, ref),
|
|
361
|
+
h('input', {
|
|
362
|
+
key: 'l',
|
|
363
|
+
type: 'number',
|
|
364
|
+
min: 1,
|
|
365
|
+
step: 1,
|
|
366
|
+
className: 'krot-in krot-limit',
|
|
367
|
+
value: limitValue === undefined ? '' : String(limitValue),
|
|
368
|
+
placeholder: t('tokenLimitPlaceholder'),
|
|
369
|
+
title: t('tokenLimitHint'),
|
|
370
|
+
disabled: !stateRef.current.writable,
|
|
371
|
+
onChange: (e) => setModelTokenLimit(pIndex, model, ref, e.target.value),
|
|
372
|
+
}),
|
|
373
|
+
];
|
|
374
|
+
if (!quota) {
|
|
375
|
+
cells.push(h('span', { key: 'm', className: 'krot-q-unlimited', title: t('mtLocalOnly') }, t('mtUnlimited')));
|
|
376
|
+
} else {
|
|
377
|
+
const pct = quota.limit > 0 ? Math.min(100, Math.round((quota.used / quota.limit) * 100)) : 0;
|
|
378
|
+
const color = quota.exhausted
|
|
379
|
+
? 'var(--dsw-alias-state-error-primary)'
|
|
380
|
+
: pct >= 80 ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-state-success-primary)';
|
|
381
|
+
const resetIn = quota.resetAt ? formatCountdown(quota.resetAt - Date.now()) : null;
|
|
382
|
+
cells.push(h('span', { key: 'm', style: { display: 'inline-flex', alignItems: 'center', gap: '6px' } },
|
|
383
|
+
h('span', { className: 'krot-qbar', title: t('mtUsedOf').replace('{u}', String(quota.used)).replace('{l}', String(quota.limit)) },
|
|
384
|
+
h('span', { className: 'krot-qbar-fill', style: { width: pct + '%', background: color } })),
|
|
385
|
+
h('span', {
|
|
386
|
+
className: quota.exhausted ? 'krot-q-exhausted' : 'krot-qmeta',
|
|
387
|
+
title: [t('mtUsedOf').replace('{u}', String(quota.used)).replace('{l}', String(quota.limit)),
|
|
388
|
+
t('mtRemaining').replace('{n}', String(quota.remaining)),
|
|
389
|
+
resetIn ? t('mtResetIn').replace('{t}', resetIn) : '',
|
|
390
|
+
t('mtResetWindowHint')].filter(Boolean).join(' · '),
|
|
391
|
+
}, quota.exhausted
|
|
392
|
+
? t('mtExhausted')
|
|
393
|
+
: (pct >= 1 || quota.used > 0 ? pct + '%' : t('mtRemaining').replace('{n}', String(quota.remaining)))),
|
|
394
|
+
));
|
|
395
|
+
}
|
|
396
|
+
cells.push(h('span', { key: 'a', className: 'krot-acts' },
|
|
397
|
+
btn('↑', () => reorderModelKeys(pIndex, model, -1, kIndex), { disabled: kIndex === 0, title: t('moveUp') }),
|
|
398
|
+
btn('↓', () => reorderModelKeys(pIndex, model, 1, kIndex), { disabled: kIndex === keys.length - 1, title: t('moveDown') }),
|
|
399
|
+
btn('✕', () => toggleModelKey(pIndex, model, ref, false), { title: t('removeKey') }),
|
|
400
|
+
));
|
|
401
|
+
return h('div', { key: ref, className: 'krot-qrow' }, cells);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const available = (entry.keys ?? []).filter((ref) => !keys.includes(ref));
|
|
405
|
+
return h('div', { key: model, className: 'krot-model' },
|
|
406
|
+
h('div', { className: 'krot-model-head' },
|
|
407
|
+
h('input', {
|
|
408
|
+
className: 'krot-in',
|
|
409
|
+
defaultValue: model,
|
|
410
|
+
title: t('modelName'),
|
|
411
|
+
placeholder: t('modelNamePlaceholder'),
|
|
412
|
+
disabled: !stateRef.current.writable,
|
|
413
|
+
onBlur: (e) => renameModel(pIndex, model, e.target.value.trim()),
|
|
414
|
+
style: { maxWidth: '220px' },
|
|
415
|
+
}),
|
|
416
|
+
h('span', { className: 'krot-tail' }, t('modelKeys') + ': ' + keys.length),
|
|
417
|
+
btn(t('removeModel'), () => removeModel(pIndex, model), { title: t('removeModel') }),
|
|
418
|
+
),
|
|
419
|
+
rows.length > 0 ? h('div', { className: 'krot-keys' }, rows)
|
|
420
|
+
: h('p', { className: 'krot-hint' }, t('modelPoolsEmpty')),
|
|
421
|
+
available.length > 0 ? h('div', { className: 'krot-foot', style: { marginTop: '6px' } },
|
|
422
|
+
h('span', { className: 'krot-hint' }, t('addKey') + ':'),
|
|
423
|
+
available.map((ref) => btn(ref, () => toggleModelKey(pIndex, model, ref, true), { key: 'add-' + ref, title: ref })),
|
|
424
|
+
) : null,
|
|
425
|
+
);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
return h('div', { className: 'krot-models' },
|
|
429
|
+
h('div', { className: 'krot-models-title' }, '🎯 ' + t('modelPoolsTitle')),
|
|
430
|
+
h('p', { className: 'krot-models-hint' }, t('modelPoolsHint')),
|
|
431
|
+
rendered.length > 0 ? h('div', null, rendered) : h('p', { className: 'krot-hint' }, t('modelPoolsEmpty')),
|
|
432
|
+
btn(t('addModel'), () => addModel(pIndex), { title: t('addModel'), disabled: !stateRef.current.writable }),
|
|
433
|
+
);
|
|
434
|
+
})();
|
|
435
|
+
|
|
436
|
+
return h('div', { key: pIndex, className: 'krot-prov' },
|
|
437
|
+
h('div', { className: 'krot-prov-head' },
|
|
438
|
+
h('input', {
|
|
439
|
+
type: 'checkbox',
|
|
440
|
+
'aria-label': t('selectProvider').replace('{p}', entry.provider),
|
|
441
|
+
checked: selected.has(entry.provider),
|
|
442
|
+
onChange: (e) => { const ns = new Set(selected); if (e.target.checked) ns.add(entry.provider); else ns.delete(entry.provider); setSelected(ns); },
|
|
443
|
+
}),
|
|
444
|
+
btn(t('exportOne'), () => {
|
|
445
|
+
const data = JSON.stringify([entry], null, 2);
|
|
446
|
+
const blob = new Blob([data], { type: 'application/json' });
|
|
447
|
+
const url = URL.createObjectURL(blob);
|
|
448
|
+
const a = document.createElement('a'); a.href = url; a.download = entry.provider + '.json'; a.click(); URL.revokeObjectURL(url);
|
|
449
|
+
}, { title: t('exportProvider') }),
|
|
450
|
+
btn('⇅', () => {
|
|
451
|
+
const ps = status[entry.provider];
|
|
452
|
+
if (!ps || !Array.isArray(ps.keys)) return;
|
|
453
|
+
const usageOf = (ref) => { const hit = ps.keys.find((k) => k.ref === ref); return hit && typeof hit.usage === 'number' ? hit.usage : 0; };
|
|
454
|
+
setField((cur) => {
|
|
455
|
+
const next = [...(cur.providers ?? [])];
|
|
456
|
+
if (!next[pIndex]) return cur;
|
|
457
|
+
const entry = next[pIndex];
|
|
458
|
+
const order = entry.keys.map((_, index) => index).sort((a, b) => usageOf(entry.keys[b]) - usageOf(entry.keys[a]));
|
|
459
|
+
next[pIndex] = reorderKeys(entry, order);
|
|
460
|
+
return { ...cur, providers: next };
|
|
461
|
+
});
|
|
462
|
+
}, { title: t('sortUsage') }),
|
|
463
|
+
h('select', { className: 'krot-in', value: entry.provider, onChange: (e) => setProvider(pIndex, e.target.value) }, options),
|
|
464
|
+
btn(t('presetLimitsBtn') || '⚡ Preset Limits', () => applyPreset(pIndex, entry.provider), { title: t('presetLimitsBtn') || '⚡ Preset Limits', disabled: !entry.provider.trim() || !stateRef.current.writable }),
|
|
465
|
+
presetNotice ? h('span', { className: 'krot-badge krot-badge-ok', style: { marginLeft: '4px' } }, presetNotice) : null,
|
|
466
|
+
(() => {
|
|
467
|
+
const ps = status[entry.provider];
|
|
468
|
+
const score = ps && typeof ps.healthScore === 'number' ? ps.healthScore : null;
|
|
469
|
+
if (score === null) return null;
|
|
470
|
+
const color = score > 80 ? 'var(--dsw-alias-state-success-primary)' : score >= 50 ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-state-error-primary)';
|
|
471
|
+
return h('span', { className: 'krot-tail', title: t('healthScoreTitle'), style: { flex: 'none', color, fontWeight: 700 } }, String(score));
|
|
472
|
+
})(),
|
|
473
|
+
(() => { const ps = status[entry.provider]; const tot = ps && typeof ps.totalUsage === 'number' ? ps.totalUsage : null; return tot !== null ? h('span', { className: 'krot-tail', title: t('totalRequestsTitle'), style: { flex: 'none' } }, String(tot)) : null; })(),
|
|
474
|
+
(() => {
|
|
475
|
+
const q = status.quota || (providerStatus && providerStatus.quota);
|
|
476
|
+
let nearestReset = null;
|
|
477
|
+
if (q && typeof q === 'object') {
|
|
478
|
+
for (const k of (entry.keys || [])) {
|
|
479
|
+
const entryQ = q[k];
|
|
480
|
+
if (entryQ && typeof entryQ.reset === 'number' && entryQ.reset > Date.now()) {
|
|
481
|
+
if (!nearestReset || entryQ.reset < nearestReset) nearestReset = entryQ.reset;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (!nearestReset) return null;
|
|
486
|
+
const diffMs = nearestReset - Date.now();
|
|
487
|
+
if (diffMs <= 0) return null;
|
|
488
|
+
const mins = Math.ceil(diffMs / 60000);
|
|
489
|
+
const timeStr = mins > 60 ? Math.floor(mins / 60) + 'h ' + (mins % 60) + 'm' : mins + 'm';
|
|
490
|
+
return h('span', { className: 'krot-badge krot-badge-warn', title: 'Quota window reset' }, t('quotaResetIn').replace('{time}', timeStr));
|
|
491
|
+
})(),
|
|
492
|
+
btn('✕', () => setConfirmModal({
|
|
493
|
+
title: t('confirmRemoveProvTitle').replace('{p}', entry.provider),
|
|
494
|
+
desc: t('confirmRemoveProvDesc'),
|
|
495
|
+
actionLabel: t('removeProvider'),
|
|
496
|
+
danger: true,
|
|
497
|
+
onConfirm: () => removeProvider(pIndex),
|
|
498
|
+
}), { title: t('removeProvider') }),
|
|
499
|
+
),
|
|
500
|
+
filterBar,
|
|
501
|
+
loadChart,
|
|
502
|
+
promptCacheWidget,
|
|
503
|
+
h('div', { className: 'krot-keys' }, keyRows),
|
|
504
|
+
modelPools,
|
|
505
|
+
h('div', { className: 'krot-foot' },
|
|
506
|
+
btn(t('addKey'), () => addKey(pIndex), { title: t('addKeyTitle'), disabled: !entry.provider.trim() }),
|
|
507
|
+
btn('📋 ' + t('bulkImport'), () => setBulkOpen(cur => ({ ...cur, [pIndex]: !cur[pIndex] })), { disabled: !entry.provider.trim(), title: t('bulkImport') }),
|
|
508
|
+
bulkOpen[pIndex] ? h('div', { className: 'krot-bulk-import-box', style: { width: '100%', marginTop: '8px', padding: '8px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: '6px' } },
|
|
509
|
+
h('p', { style: { margin: '0 0 6px 0', fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, t('bulkImportDesc')),
|
|
510
|
+
h('textarea', {
|
|
511
|
+
className: 'krot-in',
|
|
512
|
+
rows: 4,
|
|
513
|
+
style: { width: '100%', fontFamily: 'monospace', fontSize: '11px', resize: 'vertical' },
|
|
514
|
+
placeholder: 'KEY_1\nKEY_2, weight=5\nOPENAI_KEY=sk-...',
|
|
515
|
+
value: bulkText[pIndex] || '',
|
|
516
|
+
onChange: (e) => setBulkText(cur => ({ ...cur, [pIndex]: e.target.value })),
|
|
517
|
+
}),
|
|
518
|
+
h('div', { style: { display: 'flex', gap: '8px', marginTop: '6px' } },
|
|
519
|
+
btn(t('bulkImportBtn'), () => applyBulkImport(pIndex), { primary: true }),
|
|
520
|
+
btn(t('bulkImportCancel'), () => setBulkOpen(cur => ({ ...cur, [pIndex]: false })))
|
|
521
|
+
)
|
|
522
|
+
) : null,
|
|
523
|
+
switchesLine,
|
|
524
|
+
budgetLine,
|
|
525
|
+
sloLine,
|
|
526
|
+
exhaustionWarning,
|
|
527
|
+
(providerStatus && Array.isArray(providerStatus.events) && providerStatus.events.length > 0 ? h('div', { style: { display: 'flex', gap: '2px', alignItems: 'end', height: '24px', marginTop: '4px' } }, (() => { const now = Date.now(); const buckets = Array(24).fill(0); for (const ev of providerStatus.events) { const h = Math.floor((now - ev.at) / 3600000); if (h >= 0 && h < 24) buckets[23 - h]++; } const max = Math.max(1, ...buckets); return buckets.map((c, i) => h('div', { key: i, title: c + ' switches', style: { flex: 1, background: c ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-border-l2)', height: (c / max * 24) + 'px', minHeight: '2px', borderRadius: '2px' } })); })()) : null),
|
|
528
|
+
// (issue 224): 7-day switches per day (client-side, from the same events)
|
|
529
|
+
(providerStatus && Array.isArray(providerStatus.events) && providerStatus.events.length > 0 ? h('div', { style: { display: 'flex', gap: '2px', alignItems: 'end', height: '16px', marginTop: '2px' } }, (() => { const now = Date.now(); const days = Array(7).fill(0); for (const ev of providerStatus.events) { const d = Math.floor((now - ev.at) / 86400000); if (d >= 0 && d < 7) days[6 - d]++; } const max = Math.max(1, ...days); return days.map((c, i) => h('div', { key: i, title: c + ' switches · day -' + (6 - i), style: { flex: 1, background: c ? 'var(--dsw-alias-brand-primary, var(--dsw-alias-state-warn-primary))' : 'var(--dsw-alias-border-l2)', height: (c / max * 16) + 'px', minHeight: '2px', borderRadius: '2px' } })); })()) : null),
|
|
530
|
+
(providerStatus && Array.isArray(providerStatus.events) && providerStatus.events.length > 0 ? h('details', { className: 'krot-event-stream' },
|
|
531
|
+
h('summary', { style: { display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', fontWeight: 600, fontSize: '12px' } },
|
|
532
|
+
h('span', null, '📜 ' + t('liveEventStream')),
|
|
533
|
+
h('span', { className: 'krot-badge' }, String(providerStatus.events.length))
|
|
534
|
+
),
|
|
535
|
+
h('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px', marginTop: '8px' } },
|
|
536
|
+
providerStatus.events.slice().reverse().map((ev, i) => {
|
|
537
|
+
const isHeal = ev.type === 'heal' || ev.reason === 'auto-unbreak' || ev.reason === 'self-heal';
|
|
538
|
+
const isBad = ev.code === 'AUTH' || ev.reason === 'AUTH' || ev.type === 'broken';
|
|
539
|
+
const badgeClass = isHeal ? 'krot-badge-ok' : isBad ? 'krot-badge-bad' : 'krot-badge-warn';
|
|
540
|
+
const timeStr = ev.at ? new Date(ev.at).toLocaleTimeString() : '';
|
|
541
|
+
return h('div', { key: i, className: 'krot-event-row' },
|
|
542
|
+
h('span', { className: 'krot-event-time' }, timeStr),
|
|
543
|
+
h('span', { className: 'krot-tail' }, ev.ref || 'pool'),
|
|
544
|
+
h('span', { className: 'krot-badge ' + badgeClass }, String(ev.reason || ev.code || ev.type)),
|
|
545
|
+
ev.cooldownMs > 0 ? h('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-tertiary)' } }, 'cd: ' + Math.round(ev.cooldownMs / 1000) + 's') : null
|
|
546
|
+
);
|
|
547
|
+
})
|
|
548
|
+
)
|
|
549
|
+
) : null),
|
|
550
|
+
btn(t('resetCooldown'), () => setConfirmModal({
|
|
551
|
+
title: t('confirmResetTitle').replace('{p}', entry.provider),
|
|
552
|
+
desc: t('confirmResetDesc'),
|
|
553
|
+
actionLabel: t('resetCooldown'),
|
|
554
|
+
danger: false,
|
|
555
|
+
onConfirm: () => doReset(entry.provider),
|
|
556
|
+
}), { disabled: !(providerStatus && providerStatus.switches > 0) || resetting === entry.provider, title: t('resetCooldown') }),
|
|
557
|
+
btn(checkingBalance === entry.provider ? t('checkingBalance') : ('💰 ' + t('balanceCheckBtn')), () => checkBalances(pIndex, entry.provider), {
|
|
558
|
+
disabled: Boolean(checkingBalance) || !entry.provider.trim(),
|
|
559
|
+
title: t('balanceCheckBtn'),
|
|
560
|
+
}),
|
|
561
|
+
btn(testAllProvider === entry.provider ? (testingProgress[entry.provider] ? t('testingAllProgress').replace('{n}', testingProgress[entry.provider].split('/')[0]).replace('{total}', testingProgress[entry.provider].split('/')[1]) : t('testing')) : t('testAll'), () => doTestAll(entry.provider), { disabled: Boolean(testing || testAllProvider), title: t('testAll') }),
|
|
562
|
+
exportCsv,
|
|
563
|
+
),
|
|
564
|
+
);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
const noProviders = catalog.status === 'error'
|
|
568
|
+
? h('div', { className: 'krot-hint', role: 'status' }, t('providerCatalogUnavailable'),
|
|
569
|
+
btn(t('retry'), catalog.reload))
|
|
570
|
+
: catalog.status === 'ready' && providers.length === 0
|
|
571
|
+
? h('div', { className: 'krot-empty', role: 'status', style: { display: 'flex', flexDirection: 'column', gap: '6px', padding: '16px 14px', border: '1px dashed var(--dsw-alias-border-l2)', borderRadius: 10, background: 'var(--dsw-alias-bg-layer-2)' } },
|
|
572
|
+
h('p', { style: { margin: 0, fontWeight: 600, fontSize: 14, color: 'var(--dsw-alias-label-primary)' } }, t('emptyTitle')),
|
|
573
|
+
h('p', { className: 'krot-hint', style: { margin: 0 } }, t('emptyDesc')),
|
|
574
|
+
h('p', { className: 'krot-hint', style: { margin: 0 } }, t('noProviders')),
|
|
575
|
+
)
|
|
576
|
+
: null;
|
|
577
|
+
|
|
578
|
+
const allConfiguredProviders = Array.isArray(val?.providers) ? val.providers : [];
|
|
579
|
+
const totalPoolsCount = allConfiguredProviders.length;
|
|
580
|
+
let totalKeysCount = 0;
|
|
581
|
+
let healthyKeysCount = 0;
|
|
582
|
+
for (const prov of allConfiguredProviders) {
|
|
583
|
+
const pKeys = Array.isArray(prov.keys) ? prov.keys : [];
|
|
584
|
+
totalKeysCount += pKeys.length;
|
|
585
|
+
for (const k of pKeys) {
|
|
586
|
+
const st = keyStatus(prov.provider, k);
|
|
587
|
+
if (st && st.ready) healthyKeysCount++;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|