@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.
@@ -0,0 +1,568 @@
1
+ const statsSection = h('div', { className: 'krot-section-card' },
2
+ h('div', { className: 'krot-section-title' },
3
+ h('span', null, '📊 ' + t('sectionStats')),
4
+ h('span', { className: 'krot-badge ' + (totalPoolsCount > 0 && healthyKeysCount === totalKeysCount ? 'krot-badge-ok' : totalPoolsCount > 0 ? 'krot-badge-warn' : 'krot-badge-bad') },
5
+ totalPoolsCount > 0 ? (healthyKeysCount + '/' + totalKeysCount + ' ' + t('keyReady')) : t('notRegistered')
6
+ )
7
+ ),
8
+ h('div', { className: 'krot-section-desc' }, t('sectionStatsDesc')),
9
+ h('div', { className: 'krot-grid-3' },
10
+ h('div', { className: 'krot-stat-box' },
11
+ h('span', { className: 'krot-stat-val' }, String(totalPoolsCount)),
12
+ h('span', { className: 'krot-stat-lbl' }, t('statPools'))
13
+ ),
14
+ h('div', { className: 'krot-stat-box' },
15
+ h('span', { className: 'krot-stat-val' }, String(totalKeysCount)),
16
+ h('span', { className: 'krot-stat-lbl' }, t('statKeys'))
17
+ ),
18
+ h('div', { className: 'krot-stat-box' },
19
+ h('span', { className: 'krot-stat-val', style: { color: healthyKeysCount > 0 ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-primary)' } }, String(healthyKeysCount)),
20
+ h('span', { className: 'krot-stat-lbl' }, t('statHealthy'))
21
+ )
22
+ )
23
+ );
24
+
25
+ const poolsSection = h('div', { className: 'krot-section-card' },
26
+ h('div', { className: 'krot-section-title' },
27
+ h('span', null, '🔑 ' + t('providersTitle')),
28
+ h('span', { className: 'krot-badge krot-badge-ok' }, totalPoolsCount + ' ' + t('statPools'))
29
+ ),
30
+ h('div', { className: 'krot-section-desc' }, t('desc')),
31
+ searchInput,
32
+ h('div', { className: 'krot-foot', style: { marginBottom: '8px' } },
33
+ h('input', {
34
+ className: 'krot-in',
35
+ placeholder: t('bulkCooldownPlaceholder'),
36
+ value: bulkCooldown,
37
+ onChange: (e) => setBulkCooldown(e.target.value),
38
+ style: { maxWidth: '160px' },
39
+ }),
40
+ btn(t('bulkApply'), () => {
41
+ const v = Number(bulkCooldown); if (!v) return;
42
+ setField((cur) => {
43
+ const next = [...(cur.providers ?? [])];
44
+ for (let i = 0; i < next.length; i++) if (selected.has(next[i].provider)) next[i] = { ...next[i], cooldownMs: v };
45
+ return { ...cur, providers: next };
46
+ });
47
+ }, { disabled: selected.size === 0 || !bulkCooldown }),
48
+ btn(t('bulkRemove'), () => {
49
+ const ids = new Set(selected);
50
+ if (!ids.size) return;
51
+ setConfirmModal({
52
+ title: t('confirmBulkRemoveTitle').replace('{n}', String(ids.size)),
53
+ desc: t('confirmBulkRemoveDesc'),
54
+ actionLabel: t('bulkRemove'),
55
+ danger: true,
56
+ onConfirm: () => {
57
+ setField((cur) => {
58
+ const next = (cur.providers ?? []).filter((p) => !ids.has(p.provider));
59
+ return { ...cur, providers: next };
60
+ });
61
+ setSelected(new Set());
62
+ },
63
+ });
64
+ }, { disabled: selected.size === 0, className: 'krot-btn-danger' })
65
+ ),
66
+ h('div', { className: 'krot-keys' }, providerRows),
67
+ h('div', { className: 'krot-foot', style: { marginTop: '10px' } },
68
+ btn(t('addProvider'), addProvider, { primary: false }),
69
+ btn('⚡ ' + (testMatrixRunning ? t('testingMatrix') : t('testAllKeys')), runTestMatrix, { disabled: testMatrixRunning || totalKeysCount === 0 }),
70
+ noProviders
71
+ ),
72
+ h(UpdaterSection, { t })
73
+ );
74
+
75
+ const failoverSection = h('div', { className: 'krot-section-card' },
76
+ h('div', { className: 'krot-section-title' },
77
+ h('span', null, '⚡ ' + t('codesTitle')),
78
+ h('span', { className: 'krot-badge krot-badge-warn' }, selectedCodes.size + ' ' + t('activeBadge'))
79
+ ),
80
+ h('div', { className: 'krot-section-desc' }, t('sectionFailoverDesc')),
81
+ h('div', { className: 'krot-codes' }, codeList.map((code) => h('label', { key: code, className: 'krot-code' },
82
+ h('input', {
83
+ type: 'checkbox',
84
+ checked: selectedCodes.has(code),
85
+ onChange: (e) => toggleCode(code, e.target.checked),
86
+ }),
87
+ code,
88
+ )))
89
+ );
90
+
91
+ const timingSection = h('div', { className: 'krot-section-card' },
92
+ h('div', { className: 'krot-section-title' },
93
+ h('span', null, '⏱ ' + t('sectionTiming')),
94
+ ),
95
+ h('div', { className: 'krot-section-desc' }, t('sectionTimingDesc')),
96
+ h('div', { className: 'krot-grid-2' },
97
+ field(t('cooldown'), textInput(String(val.cooldownMs ?? 60000), (v) => setField((cur) => ({ ...cur, cooldownMs: Number(v) || 0 })))),
98
+ field(t('scheduleDays'), textInput(String(val.rotationScheduleDays ?? 0), (v) => setField((cur) => ({ ...cur, rotationScheduleDays: Number(v) || 0 }))))
99
+ ),
100
+ h('div', { className: 'krot-grid-2', style: { marginTop: '10px' } },
101
+ field(t('rpmLimitLabel'), textInput(String(val.rpmLimit ?? 0), (v) => setField((cur) => ({ ...cur, rpmLimit: Number(v) || 0 })))),
102
+ field(t('tpmLimitLabel'), textInput(String(val.tpmLimit ?? 0), (v) => setField((cur) => ({ ...cur, tpmLimit: Number(v) || 0 }))))
103
+ ),
104
+ h('div', { className: 'krot-grid-2', style: { marginTop: '10px' } },
105
+ field(t('maxAttemptsLabel'), textInput(String(val.maxAttempts ?? 0), (v) => setField((cur) => ({ ...cur, maxAttempts: Number(v) || 0 })))),
106
+ field(t('maxDurationLabel'), textInput(String(val.maxDurationMs ?? 0), (v) => setField((cur) => ({ ...cur, maxDurationMs: Number(v) || 0 }))))
107
+ ),
108
+ h('div', { className: 'krot-grid-3', style: { marginTop: '10px' } },
109
+ field(t('routingStrategyLabel'), h('select', {
110
+ className: 'krot-in',
111
+ value: val.routingStrategy ?? 'round-robin',
112
+ onChange: (e) => setField((cur) => ({ ...cur, routingStrategy: e.target.value })),
113
+ }, [
114
+ h('option', { key: 'round-robin', value: 'round-robin' }, t('routingStrategyRoundRobin')),
115
+ h('option', { key: 'least-loaded', value: 'least-loaded' }, t('routingStrategyLeastLoaded')),
116
+ h('option', { key: 'lowest-latency', value: 'lowest-latency' }, t('routingStrategyLowestLatency')),
117
+ h('option', { key: 'sticky', value: 'sticky' }, t('routingStrategySticky')),
118
+ ])),
119
+ field(t('selfHealingLabel'), textInput(String(val.selfHealingIntervalMinutes ?? 30), (v) => setField((cur) => ({ ...cur, selfHealingIntervalMinutes: Number(v) || 0 })))),
120
+ field(t('proactiveGuardLabel'), h('label', { style: { display: 'flex', alignItems: 'center', gap: '8px', height: '36px', cursor: 'pointer' } }, [
121
+ h('input', {
122
+ key: 'input', type: 'checkbox',
123
+ checked: val.proactiveRateLimitGuard ?? true,
124
+ onChange: (e) => setField((cur) => ({ ...cur, proactiveRateLimitGuard: e.target.checked })),
125
+ }),
126
+ h('span', { key: 'label', style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('proactiveGuardLabel')),
127
+ ]))
128
+ ),
129
+ h('div', { className: 'krot-grid-2', style: { marginTop: '10px' } },
130
+ field(t('showChatHeaderChipLabel'), h('label', { style: { display: 'flex', alignItems: 'center', gap: '8px', height: '36px', cursor: 'pointer' } }, [
131
+ h('input', {
132
+ key: 'input', type: 'checkbox',
133
+ checked: val.showChatHeaderChip ?? true,
134
+ onChange: (e) => setField((cur) => ({ ...cur, showChatHeaderChip: e.target.checked })),
135
+ }),
136
+ h('span', { key: 'label', style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('showChatHeaderChipDesc')),
137
+ ]))
138
+ )
139
+ );
140
+
141
+
142
+
143
+ const configuredProviders = (draft?.providers ?? state.value?.providers ?? []);
144
+ const simulatorSection = h('div', { className: 'krot-section', style: { marginTop: '14px', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '8px', padding: '14px' } },
145
+ h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' } },
146
+ h('div', null,
147
+ h('span', { style: { fontWeight: 600, fontSize: '13px' } }, '🧪 ' + t('simulatorTitle')),
148
+ h('div', { className: 'krot-hint', style: { fontSize: '11px', marginTop: '2px' } }, t('simulatorDesc'))
149
+ )
150
+ ),
151
+ h('div', { style: { display: 'flex', gap: '8px', flexWrap: 'wrap', alignItems: 'center', marginTop: '10px' } },
152
+ h('select', {
153
+ className: 'krot-input',
154
+ style: { width: '140px' },
155
+ value: simProvider || (configuredProviders[0]?.provider || ''),
156
+ onChange: (e) => setSimProvider(e.target.value),
157
+ }, configuredProviders.map((p) => h('option', { key: p.provider, value: p.provider }, p.provider))),
158
+ h('input', {
159
+ type: 'text',
160
+ className: 'krot-input',
161
+ style: { width: '120px' },
162
+ placeholder: t('simulatorSelectModel'),
163
+ value: simModel,
164
+ onChange: (e) => setSimModel(e.target.value),
165
+ }),
166
+ h('select', {
167
+ className: 'krot-input',
168
+ style: { width: '180px' },
169
+ value: simScenario,
170
+ onChange: (e) => setSimScenario(e.target.value),
171
+ },
172
+ h('option', { value: 'success' }, t('simulatorEventSuccess')),
173
+ h('option', { value: 'rate_limit' }, t('simulatorEventRateLimit')),
174
+ h('option', { value: 'error_500' }, t('simulatorEventServer500')),
175
+ ),
176
+ btn(simRunning ? t('simulatorRunning') : t('simulatorRun'), runSimulator, {
177
+ primary: true,
178
+ disabled: simRunning,
179
+ })
180
+ ),
181
+ simError ? h('div', { className: 'krot-warn', style: { marginTop: '8px', color: 'var(--dsw-alias-error)' } }, simError) : null,
182
+ simResult ? h('div', { style: { marginTop: '12px', background: 'var(--dsw-alias-bg-hover)', padding: '10px', borderRadius: '6px' } },
183
+ h('div', { style: { fontWeight: 600, fontSize: '12px', marginBottom: '6px' } }, t('simulatorResults') + ': ' + (simResult.status || 'DONE')),
184
+ h('div', { style: { fontSize: '11px', display: 'flex', flexDirection: 'column', gap: '4px' } },
185
+ Array.isArray(simResult.steps) ? simResult.steps.map((st, idx) =>
186
+ h('div', { key: idx, style: { display: 'flex', gap: '8px', fontFamily: 'monospace' } },
187
+ h('span', { style: { color: 'var(--dsw-alias-text-sub)' } }, '[' + (idx + 1) + ']'),
188
+ h('span', { style: { fontWeight: 500 } }, st.ref || '-'),
189
+ h('span', { style: { color: st.status === 'OK' ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-secondary)' } }, st.status),
190
+ h('span', { style: { opacity: 0.8 } }, st.reason || st.error || '')
191
+ )
192
+ ) : null
193
+ )
194
+ ) : null
195
+ );
196
+
197
+ const drainSection = h('div', { className: 'krot-section-card' },
198
+ h('div', { className: 'krot-section-title' },
199
+ h('span', null, '⏳ ' + t('drainTitle')),
200
+ h('span', { className: 'krot-badge ' + (drainingKeys.length > 0 ? 'krot-badge-warn' : 'krot-badge-ok') },
201
+ drainingKeys.length > 0 ? (drainingKeys.length + ' ' + t('drainDraining')) : t('drainEmpty')
202
+ )
203
+ ),
204
+ h('div', { className: 'krot-section-desc' }, t('drainDesc')),
205
+ h('div', { style: { display: 'flex', gap: '8px', flexWrap: 'wrap', alignItems: 'center', marginTop: '10px' } },
206
+ h('input', {
207
+ className: 'krot-in',
208
+ placeholder: t('drainKeyPlaceholder'),
209
+ value: drainRefInput,
210
+ onChange: (e) => setDrainRefInput(e.target.value),
211
+ style: { maxWidth: '240px' },
212
+ }),
213
+ btn(t('drainStart'), () => startKeyDrain(drainRefInput), { primary: true, disabled: !drainRefInput.trim() }),
214
+ btn(t('traceRefresh'), loadDrainStatus, { disabled: loadingDrain })
215
+ ),
216
+ drainActionError ? h('div', { className: 'krot-warn', style: { marginTop: '8px' } }, '⚠ ' + drainActionError) : null,
217
+ drainingKeys.length > 0 ? h('div', { style: { marginTop: '10px', display: 'flex', flexDirection: 'column', gap: '6px' } },
218
+ drainingKeys.map((dk) => h('div', { key: dk.ref, className: 'krot-stat-box', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '6px 12px' } },
219
+ h('span', { style: { fontWeight: 600 } }, dk.ref),
220
+ h('span', { className: 'krot-badge ' + (dk.canComplete ? 'krot-badge-ok' : 'krot-badge-warn') },
221
+ dk.canComplete ? t('drainCanComplete') : (t('drainActiveStreams') + ': ' + dk.inFlight)
222
+ ),
223
+ btn(t('drainCancel'), () => cancelKeyDrain(dk.ref), { danger: true })
224
+ ))
225
+ ) : null
226
+ );
227
+
228
+ const traceSection = h('div', { className: 'krot-section-card' },
229
+ h('div', { className: 'krot-section-title' },
230
+ h('span', null, '🧭 ' + t('traceTitle')),
231
+ h('span', { className: 'krot-badge krot-badge-ok' }, selectionTraces.length + ' traces')
232
+ ),
233
+ h('div', { className: 'krot-section-desc' }, t('traceDesc')),
234
+ h('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', marginTop: '10px' } },
235
+ btn(loadingTraces ? '...' : t('traceRefresh'), loadTraces, { disabled: loadingTraces }),
236
+ btn(t('traceClear'), clearTraces, { disabled: selectionTraces.length === 0 })
237
+ ),
238
+ selectionTraces.length > 0 ? h('div', { style: { marginTop: '10px', display: 'flex', flexDirection: 'column', gap: '8px', maxHeight: '280px', overflowY: 'auto' } },
239
+ selectionTraces.slice().reverse().map((tr) => h('div', { key: tr.requestId, style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '6px', padding: '8px', fontSize: '11px' } },
240
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', marginBottom: '4px' } },
241
+ h('span', { style: { fontWeight: 600 } }, (tr.provider || 'default') + (tr.model ? ' / ' + tr.model : '')),
242
+ h('span', { className: 'krot-badge ' + (tr.outcome === 'SUCCESS' ? 'krot-badge-ok' : 'krot-badge-bad') }, tr.outcome)
243
+ ),
244
+ h('div', { style: { color: 'var(--dsw-alias-label-secondary)', marginBottom: '4px' } },
245
+ tr.selectedRef ? (t('traceSelected') + ': ' + tr.selectedRef) : ''
246
+ ),
247
+ h('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px', paddingLeft: '8px', borderLeft: '2px solid var(--dsw-alias-border-l2)' } },
248
+ (tr.steps || []).map((st, i) => h('div', { key: i, style: { display: 'flex', gap: '6px' } },
249
+ h('span', { style: { color: st.decision === 'SELECTED' ? 'var(--dsw-alias-state-success-primary)' : st.decision === 'SKIPPED' ? 'var(--dsw-alias-label-tertiary)' : 'var(--dsw-alias-state-danger-primary)', fontWeight: 600 } }, st.decision),
250
+ h('span', { style: { fontWeight: 600 } }, st.ref || '-'),
251
+ h('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, st.reasonCode ? ('[' + st.reasonCode + ']') : ''),
252
+ h('span', null, st.details || '')
253
+ ))
254
+ )
255
+ ))
256
+ ) : h('div', { className: 'krot-hint', style: { marginTop: '8px' } }, t('traceEmpty'))
257
+ );
258
+
259
+ const backupSection = h('div', { className: 'krot-section-card' },
260
+ h('div', { className: 'krot-section-title' },
261
+ h('span', null, '💾 ' + t('sectionBackup')),
262
+ ),
263
+ h('div', { className: 'krot-section-desc' }, t('sectionBackupDesc')),
264
+ exportModal ? h('div', { className: 'krot-box', style: { marginBottom: '12px', padding: '12px', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '6px', background: 'var(--dsw-alias-bg-hover)' } },
265
+ h('div', { style: { fontWeight: 600, marginBottom: '8px' } }, '📥 ' + (t('exportConfig') || 'Export Pools')),
266
+ h('label', { style: { display: 'flex', alignItems: 'center', gap: '6px', fontSize: '13px', cursor: 'pointer', marginBottom: '8px' } },
267
+ h('input', { type: 'checkbox', checked: exportMasked, onChange: (e) => setExportMasked(e.target.checked) }),
268
+ t('exportSafe') || 'Masked (Safe for sharing)'
269
+ ),
270
+ h('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px', marginBottom: '10px' } },
271
+ h('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, t('passphraseLabel') || 'Passphrase (optional)'),
272
+ h('input', {
273
+ type: 'password',
274
+ className: 'krot-input',
275
+ value: exportPassphrase,
276
+ onChange: (e) => setExportPassphrase(e.target.value),
277
+ placeholder: t('passphrasePlaceholder') || 'Enter passphrase for AES-256-GCM encryption',
278
+ style: { maxWidth: '360px' }
279
+ })
280
+ ),
281
+ exportError ? h('div', { className: 'krot-alert krot-alert-bad', style: { marginBottom: '8px' } }, exportError) : null,
282
+ h('div', { style: { display: 'flex', gap: '8px' } },
283
+ btn('⬇️ ' + (t('exportConfig') || 'Download JSON'), runExport, { primary: true }),
284
+ btn(t('cancel'), () => setExportModal(false))
285
+ )
286
+ ) : null,
287
+ importModal ? h('div', { className: 'krot-box', style: { marginBottom: '12px', padding: '12px', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '6px', background: 'var(--dsw-alias-bg-hover)' } },
288
+ h('div', { style: { fontWeight: 600, marginBottom: '8px' } }, '📤 ' + (t('importConfig') || 'Import Pools')),
289
+ h('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', marginBottom: '8px' } },
290
+ h('label', { className: 'krot-btn', style: { cursor: 'pointer', fontSize: '12px' } },
291
+ '📁 Choose File',
292
+ h('input', {
293
+ type: 'file',
294
+ accept: '.json',
295
+ style: { display: 'none' },
296
+ onChange: (e) => {
297
+ const file = e.target.files?.[0];
298
+ if (!file) return;
299
+ const reader = new FileReader();
300
+ reader.onload = (ev) => { setImportText(String(ev.target?.result || '')); };
301
+ reader.readAsText(file);
302
+ }
303
+ })
304
+ ),
305
+ h('span', { style: { fontSize: '12px', opacity: 0.7 } }, 'or paste JSON below')
306
+ ),
307
+ h('textarea', {
308
+ className: 'krot-input',
309
+ rows: 6,
310
+ value: importText,
311
+ onChange: (e) => { setImportText(e.target.value); setImportPreview(null); },
312
+ placeholder: t('jsonConfigPlaceholder') || 'Paste JSON configuration here...',
313
+ style: { width: '100%', fontFamily: 'monospace', fontSize: '11px', marginBottom: '8px' }
314
+ }),
315
+ h('div', { style: { display: 'flex', gap: '16px', alignItems: 'center', marginBottom: '8px' } },
316
+ h('label', { style: { display: 'flex', alignItems: 'center', gap: '4px', fontSize: '12px' } },
317
+ h('input', { type: 'radio', name: 'importMode', checked: importMode === 'merge', onChange: () => setImportMode('merge') }),
318
+ t('importMerge') || 'Merge'
319
+ ),
320
+ h('label', { style: { display: 'flex', alignItems: 'center', gap: '4px', fontSize: '12px' } },
321
+ h('input', { type: 'radio', name: 'importMode', checked: importMode === 'replace', onChange: () => setImportMode('replace') }),
322
+ t('importReplace') || 'Replace'
323
+ )
324
+ ),
325
+ h('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px', marginBottom: '10px' } },
326
+ h('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, t('passphraseLabel') || 'Passphrase (if encrypted)'),
327
+ h('input', {
328
+ type: 'password',
329
+ className: 'krot-input',
330
+ value: importPassphrase,
331
+ onChange: (e) => setImportPassphrase(e.target.value),
332
+ placeholder: t('passphrasePlaceholder') || 'Enter passphrase for decryption',
333
+ style: { maxWidth: '360px' }
334
+ })
335
+ ),
336
+ importError ? h('div', { className: 'krot-alert krot-alert-bad', style: { marginBottom: '8px' } }, importError) : null,
337
+ importPreview ? h('div', { className: 'krot-alert krot-alert-ok', style: { marginBottom: '8px', fontSize: '12px' } },
338
+ `Preview: ${importPreview.createdProviders?.length || 0} created, ${importPreview.updatedProviders?.length || 0} updated, ${importPreview.keysAdded || 0} keys added, ${importPreview.totalKeys || 0} total keys.`
339
+ ) : null,
340
+ h('div', { style: { display: 'flex', gap: '8px' } },
341
+ btn('🔍 ' + (t('importPreview') || 'Preview Diff'), runImportPreview, { disabled: !importText.trim() || importRunning }),
342
+ btn('✅ ' + (t('importApply') || 'Apply Configuration'), runImportApply, { primary: true, disabled: !importText.trim() || importRunning }),
343
+ btn(t('cancel'), () => { setImportModal(false); setImportPreview(null); })
344
+ )
345
+ ) : null,
346
+ h('div', { className: 'krot-row', style: { display: 'flex', gap: '8px', flexWrap: 'wrap' } },
347
+ btn('📥 ' + (t('exportConfig') || 'Export Pools'), () => { setExportModal((v) => !v); setImportModal(false); }, { primary: exportModal }),
348
+ btn('📤 ' + (t('importConfig') || 'Import Pools'), () => { setImportModal((v) => !v); setExportModal(false); }, { primary: importModal }),
349
+ btn(t('exportPools'), () => {
350
+ const data = JSON.stringify(val.providers ?? [], null, 2);
351
+ const blob = new Blob([data], { type: 'application/json' });
352
+ const url = URL.createObjectURL(blob);
353
+ const a = document.createElement('a'); a.href = url; a.download = 'pools.json'; a.click(); URL.revokeObjectURL(url);
354
+ }, {}),
355
+ h('label', { className: 'krot-btn', style: { cursor: 'pointer' } }, t('importPools'), h('input', { type: 'file', accept: '.json', style: { display: 'none' }, onChange: (e) => {
356
+ const file = e.target.files[0]; e.target.value = '';
357
+ readImport(file, (imported) => {
358
+ validatePoolDraft(imported);
359
+ setField((cur) => {
360
+ const map = new Map((cur.providers ?? []).map((p) => [p.provider, p]));
361
+ for (const p of imported) map.set(p.provider, p);
362
+ return { ...cur, providers: [...map.values()] };
363
+ }, true);
364
+ });
365
+ } })),
366
+ btn(t('snapshotExport'), () => {
367
+ fetch('/dsh-key-rotation/snapshot', { headers: { accept: 'application/json' }, signal: (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') ? AbortSignal.timeout(8000) : undefined })
368
+ .then((r) => r.json())
369
+ .then((data) => {
370
+ const blob = new Blob([JSON.stringify(data.snapshot ?? {}, null, 2)], { type: 'application/json' });
371
+ const url = URL.createObjectURL(blob);
372
+ const a = document.createElement('a'); a.href = url; a.download = 'dsh-key-rotation-snapshot.json'; a.click(); URL.revokeObjectURL(url);
373
+ })
374
+ .catch((e) => setSecretError(String(e?.message ?? e)));
375
+ }, {}),
376
+ h('label', { className: 'krot-btn', style: { cursor: 'pointer' } }, t('snapshotImport'), h('input', { type: 'file', accept: '.json', style: { display: 'none' }, onChange: (e) => {
377
+ const file = e.target.files[0]; e.target.value = '';
378
+ readImport(file, (imported) => {
379
+ const snap = imported?.snapshot ?? imported;
380
+ if (!snap || typeof snap !== 'object' || Array.isArray(snap)) throw new Error('Expected a settings snapshot');
381
+ if (Object.prototype.hasOwnProperty.call(snap, 'providers')) validatePoolDraft(snap.providers);
382
+ const patch = { ...snap };
383
+ // Exported empty secret fields mean "not included", not "erase".
384
+ if (!patch.webhookActionToken) delete patch.webhookActionToken;
385
+ setField((cur) => ({ ...cur, ...patch }), true);
386
+ });
387
+ } }))
388
+ ),
389
+ h('p', { className: 'krot-hint', style: { marginTop: '4px' } }, t('keyHint'))
390
+ );
391
+
392
+ const alerts = [
393
+ secretError ? h('div', { key: 'se', className: 'krot-alert-bad' }, secretError) : null,
394
+ state.error ? h('div', { key: 'st', role: 'alert', className: 'krot-alert-bad' }, state.error) : null,
395
+ undo ? h('div', { key: 'un', className: 'krot-alert-ok' },
396
+ h('span', null, undo.type === 'provider' ? t('undoProvider') : t('undoKey')),
397
+ btn(t('undo'), doUndo, { className: 'krot-btn-sm' })
398
+ ) : null,
399
+ ].filter(Boolean);
400
+
401
+ const confirmModalEl = confirmModal ? h('div', {
402
+ className: 'krot-modal-backdrop',
403
+ onClick: () => setConfirmModal(null),
404
+ },
405
+ h('div', {
406
+ className: 'krot-modal-card',
407
+ ref: modalCardRef,
408
+ role: 'dialog',
409
+ 'aria-modal': 'true',
410
+ 'aria-labelledby': 'krot-modal-title',
411
+ 'aria-describedby': 'krot-modal-desc',
412
+ onClick: (e) => e.stopPropagation(),
413
+ },
414
+ h('div', { className: 'krot-modal-title', id: 'krot-modal-title' }, confirmModal.title),
415
+ h('div', { className: 'krot-modal-desc', id: 'krot-modal-desc' }, confirmModal.desc),
416
+ h('div', { className: 'krot-modal-actions' },
417
+ btn(t('cancel'), () => setConfirmModal(null), { 'data-krot-modal-cancel': '1' }),
418
+ btn(confirmModal.actionLabel || t('confirm'), () => {
419
+ const fn = confirmModal.onConfirm;
420
+ setConfirmModal(null);
421
+ if (fn) fn();
422
+ }, { primary: !confirmModal.danger, className: confirmModal.danger ? 'krot-btn-danger' : undefined })
423
+ )
424
+ )
425
+ ) : null;
426
+
427
+ const headerSection = h('div', { className: 'krot-header' },
428
+ h('div', { className: 'krot-page-title' },
429
+ '🔄 ' + t('header.title'),
430
+ h('span', { className: 'krot-badge ' + (totalPoolsCount > 0 ? 'krot-badge-ok' : 'krot-badge-warn') },
431
+ totalPoolsCount > 0 ? (totalPoolsCount + ' ' + t('header.pools_badge')) : t('noActivePools')
432
+ ),
433
+ h('span', { className: 'krot-badge ' + (totalKeysCount > 0 ? 'krot-badge-ok' : 'krot-badge-warn') },
434
+ totalKeysCount + ' ' + t('header.keys_badge')
435
+ ),
436
+ h('span', { className: 'krot-badge ' + (healthyKeysCount === totalKeysCount && totalKeysCount > 0 ? 'krot-badge-ok' : 'krot-badge-warn') },
437
+ healthyKeysCount + '/' + totalKeysCount + ' ' + t('keyReady')
438
+ )
439
+ ),
440
+ h('div', { className: 'krot-page-sub' }, t('header.sub'))
441
+ );
442
+
443
+ const discoveryBanner = (discoveredKeys.length > 0 || autoImportMsg) ? h('div', {
444
+ className: 'krot-section-card',
445
+ style: {
446
+ background: 'var(--dsw-alias-bg-hover)',
447
+ border: '1px solid var(--dsw-alias-border-l2)',
448
+ marginBottom: '12px',
449
+ padding: '12px 14px',
450
+ display: 'flex',
451
+ flexDirection: 'column',
452
+ gap: '8px',
453
+ }
454
+ },
455
+ h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
456
+ h('div', null,
457
+ h('span', { style: { fontWeight: 600, fontSize: '13px' } }, t('discoverBannerTitle')),
458
+ h('div', { className: 'krot-hint', style: { fontSize: '11px', marginTop: '2px' } },
459
+ autoImportMsg || t('discoverBannerDesc').replace('{n}', String(discoveredKeys.length))
460
+ ),
461
+ ),
462
+ discoveredKeys.length > 0 ? btn(autoImporting ? t('autoImporting') : t('autoImportBtn'), runAutoImport, {
463
+ primary: true,
464
+ disabled: autoImporting,
465
+ }) : null
466
+ ),
467
+ discoveredKeys.length > 0 ? h('div', { style: { display: 'flex', gap: '6px', flexWrap: 'wrap', marginTop: '4px' } },
468
+ discoveredKeys.slice(0, 10).map((d) => h('span', {
469
+ key: d.ref,
470
+ className: 'krot-badge',
471
+ style: { fontSize: '10px', background: 'var(--dsw-alias-bg-layer-3)' }
472
+ }, `${d.provider}: ${d.ref} ${d.keyTail ? '(' + d.keyTail + ')' : ''}`))
473
+ ) : null
474
+ ) : null;
475
+
476
+ const testMatrixDialog = testMatrixModal ? h('div', {
477
+ className: 'krot-modal-backdrop',
478
+ onClick: () => setTestMatrixModal(false),
479
+ },
480
+ h('div', {
481
+ className: 'krot-modal-card',
482
+ role: 'dialog',
483
+ 'aria-modal': 'true',
484
+ onClick: (e) => e.stopPropagation(),
485
+ style: {
486
+ maxWidth: '560px',
487
+ width: '90vw',
488
+ maxHeight: '80vh',
489
+ display: 'flex',
490
+ flexDirection: 'column',
491
+ gap: '12px',
492
+ }
493
+ },
494
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
495
+ h('span', { style: { fontWeight: 600, fontSize: '15px' } }, t('matrixModalTitle')),
496
+ btn('✕', () => setTestMatrixModal(false), { className: 'krot-btn-sub' })
497
+ ),
498
+ h('div', { className: 'krot-hint', style: { fontSize: '12px' } }, t('matrixModalDesc')),
499
+ testMatrixRunning ? h('div', { style: { textAlign: 'center', padding: '20px' } }, t('testingMatrix')) : null,
500
+ testMatrixResults ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: '10px', overflowY: 'auto', maxHeight: '50vh' } },
501
+ h('div', { style: { fontWeight: 600, fontSize: '13px' } },
502
+ t('matrixSummary').replace('{healthy}', String(testMatrixResults.healthy)).replace('{total}', String(testMatrixResults.total))
503
+ ),
504
+ h('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px' } },
505
+ (testMatrixResults.results || []).map((r, i) => h('div', {
506
+ key: i,
507
+ style: {
508
+ display: 'flex',
509
+ justifyContent: 'space-between',
510
+ alignItems: 'center',
511
+ padding: '6px 10px',
512
+ borderRadius: '6px',
513
+ background: 'var(--dsw-alias-bg-hover)',
514
+ fontSize: '12px',
515
+ }
516
+ },
517
+ h('div', { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
518
+ h('span', { style: { fontWeight: 500 } }, r.provider),
519
+ h('span', { style: { fontFamily: 'monospace' } }, r.ref),
520
+ r.keyTail ? h('span', { style: { color: 'var(--dsw-alias-text-sub)', fontSize: '11px' } }, `(${r.keyTail})`) : null
521
+ ),
522
+ h('div', { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
523
+ h('span', { style: { color: 'var(--dsw-alias-text-sub)' } }, `${r.latencyMs || 0}ms`),
524
+ h('span', {
525
+ className: 'krot-badge ' + (r.ok ? 'krot-badge-ok' : 'krot-badge-bad')
526
+ }, r.ok ? 'OK' : (r.code || 'FAIL'))
527
+ )
528
+ ))
529
+ )
530
+ ) : null,
531
+ h('div', { style: { display: 'flex', justifyContent: 'flex-end', marginTop: '8px' } },
532
+ btn(t('matrixClose'), () => setTestMatrixModal(false), { primary: true })
533
+ )
534
+ )
535
+ ) : null;
536
+ return h('div', { className: 'krot', 'aria-busy': saving || importing },
537
+ confirmModalEl,
538
+ testMatrixDialog,
539
+ headerSection,
540
+ statsSection,
541
+ discoveryBanner,
542
+ h('fieldset', { disabled: saving || importing || !state.writable,
543
+ style: { border: 0, padding: 0, margin: 0, minWidth: 0, display: 'flex', flexDirection: 'column', gap: '14px' } },
544
+ poolsSection,
545
+ failoverSection,
546
+ timingSection,
547
+ drainSection,
548
+ traceSection,
549
+ backupSection,
550
+ simulatorSection,
551
+ ),
552
+ alerts.length > 0 ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px' } }, alerts) : null,
553
+ h('div', { className: 'krot-foot', style: { marginTop: '6px', paddingTop: '14px', borderTop: '1px solid var(--dsw-alias-border-l2)' } },
554
+ btn(t('save'), save, { primary: true, disabled: saving || importing || !state.writable || !draft }),
555
+ btn(t('discard'), () => {
556
+ if (saveInFlightRef.current || importRef.current) return;
557
+ clearDraft();
558
+ setSecretError('');
559
+ void load();
560
+ }, { disabled: saving || importing }),
561
+ saving || importing ? h('span', { className: 'krot-hint' }, t(saving ? 'saving' : 'loading')) : null,
562
+ !state.writable ? h('span', { className: 'krot-hint' }, t('readOnly')) : null,
563
+ )
564
+ );
565
+ }
566
+
567
+
568
+