@goodandready/dsh-key-rotation 0.8.77 → 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 +7 -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/index.js +3 -0
- package/lib/model-catalog.js +296 -0
- package/lib/pool-selection.js +36 -13
- package/lib/quota-runway.js +222 -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 +15 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.78 (2026-10-30)
|
|
4
|
+
|
|
5
|
+
### Network Transports, Model Catalog & Predictive Quota Runway (Block 6 Release)
|
|
6
|
+
- **Network Transports & Canonical Token Normalization (Issue #546)**: Introduced dedicated `lib/transports/` layer separating wire protocol streaming and usage normalization. Created `lib/transports/tokens.js` with `normalizeUsageTokens` mapping heterogeneous usage payloads across OpenAI, Anthropic, Google Gemini, Ollama, and DeepSeek into canonical `{ inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, totalTokens }`. Created `lib/transports/sse.js` providing zero-dependency streaming Server-Sent Events parser (`createSseDecoder`, `parseSseStream`) with cross-chunk buffering, comment filtering, `[DONE]` stripping, and clean `AbortSignal` handling. Added wire parsers for OpenAI chat streams (`openai.js`) and Anthropic Claude Messages (`anthropic.js`) with split-event usage accumulation. Integrated canonical usage into `lib/stream-metrics.js`.
|
|
7
|
+
- **Central ModelCatalog with Cache Stampede Protection (Issue #550)**: Implemented domain service `ModelCatalog` (`lib/model-catalog.js`) with TTL-based caching and in-flight promise deduplication (`inflight = new Map<string, Promise<ModelMeta[]>>()`). Eliminates Cache Stampede by guaranteeing only one upstream `/models` request executes when multiple concurrent sessions query model capabilities simultaneously. Provides model capability normalization (context window, max tokens, reasoning, tools, vision) and resilient offline fallback (`KNOWN_MODELS_FALLBACK`).
|
|
8
|
+
- **Predictive Quota Runway & Burn Rate Preemption (Issue #549)**: Introduced `QuotaRunway` (`lib/quota-runway.js`) replacing reactive 429 rate-limiting with conservative predictive preemption. Calculates window burn rates (`percentUsed / elapsed`) and projected time to exhaustion (`etaMs = (100 - percentUsed) / rate`). Identifies keys projected to exhaust before provider reset windows (`exhaustsBeforeReset: etaMs < timeToReset`). Integrated directly into `sortAttemptList` in `lib/pool-selection.js`: keys risking quota exhaustion are preemptively deprioritized in favor of healthy credentials before encountering HTTP 429 failures.
|
|
9
|
+
|
|
3
10
|
## 0.8.77 (2026-10-30)
|
|
4
11
|
|
|
5
12
|
### Session Routing, Typed Error Taxonomy & Pre-First-Token Failover (Block 5 Release)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// dsh-key-rotation — Settings card (Key Rotation).
|
|
2
|
+
// Renders in Settings → Plugins → Plugin settings via the settings.plugin.item slot and
|
|
3
|
+
// edits the plugin's `dsh-key-rotation` settings namespace through the
|
|
4
|
+
// loopback-fenced config bridge at /dsh-key-rotation/config.
|
|
5
|
+
//
|
|
6
|
+
// The config is a KEY POOL PER PROVIDER: a list of providers, each with a list
|
|
7
|
+
// of API-key env names. The provider is picked from the catalog of providers
|
|
8
|
+
// actually registered with ctx.llm (served by the host as data.providers), so no
|
|
9
|
+
// manual route typing is ever needed. The plugin derives the fallback chain and
|
|
10
|
+
// auto-creates clone routes from the key count.
|
|
11
|
+
//
|
|
12
|
+
// Localization: source strings are English; Chinese is a first-class locale (en + zh).
|
|
13
|
+
// Other languages come from the DSH core locale service / translation plugins
|
|
14
|
+
// via props.t (slot locale: NS). Active locale prefers ctx.locale.getSnapshot().active,
|
|
15
|
+
// else first navigator.languages entry, else 'en' — same fallback chain as DSH core.
|
|
16
|
+
window.__ModuleLoader__.load({
|
|
17
|
+
id: '@goodandready/dsh-key-rotation',
|
|
18
|
+
factory: (require) => {
|
|
19
|
+
var module = { exports: {} };
|
|
20
|
+
var exports = module.exports;
|
|
21
|
+
const React = require('react');
|
|
22
|
+
const h = React.createElement;
|
|
23
|
+
|
|
24
|
+
// Core primitives and chevron icon
|
|
25
|
+
let ChevronIcon = null;
|
|
26
|
+
try {
|
|
27
|
+
const primitives = require('@deepseek-ai/dsh-client-primitives') || require('@deepseek-ai/dsh-client-icons');
|
|
28
|
+
ChevronIcon = primitives && (primitives.IconChevronDownOutline14 || primitives.IconChevronDownOutline);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
ChevronIcon = null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const CONFIG_PATH = '/dsh-key-rotation/config';
|
|
34
|
+
const NS = 'dsh-key-rotation';
|
|
35
|
+
// Plugins page row seat (DSH 0.1.6-alpha.2): key = '<package name>#<row id>'.
|
|
36
|
+
const PKG = '@goodandready/dsh-key-rotation';
|
|
37
|
+
const ROW_ID = 'dsh-key-rotation';
|
|
38
|
+
const ROW_CONFIG_KEY = PKG + '#' + ROW_ID;
|
|
39
|
+
|
|
40
|
+
// -------------------------------------------------------------- i18n
|
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
const en = {
|
|
2
|
+
title: 'Key Rotation',
|
|
3
|
+
'header.title': 'Key Rotation & Failover',
|
|
4
|
+
'header.sub': 'Automated multi-key round-robin rotation, 429/5xx exponential backoff cooldowns, circuit breaker failover protection, and credential security (~/.dsh/.credentials.yaml).',
|
|
5
|
+
'header.pools_badge': 'pools active',
|
|
6
|
+
'header.keys_badge': 'keys configured',
|
|
7
|
+
simulatorTitle: 'Interactive Routing Simulator',
|
|
8
|
+
simulatorDesc: 'Test routing strategies and visualize failover trace across key pools without sending real upstream requests.',
|
|
9
|
+
simulatorRun: 'Run Simulation',
|
|
10
|
+
simulatorRunning: 'Simulating...',
|
|
11
|
+
simulatorSelectProvider: 'Provider',
|
|
12
|
+
simulatorSelectModel: 'Model (optional)',
|
|
13
|
+
simulatorMaxAttempts: 'Max Attempts',
|
|
14
|
+
simulatorEvent: 'Simulated Outcome',
|
|
15
|
+
simulatorEventSuccess: 'All keys succeed',
|
|
16
|
+
simulatorEventRateLimit: 'Key 1 Rate Limited (429)',
|
|
17
|
+
simulatorEventServer500: 'Key 1 Internal Error (500)',
|
|
18
|
+
simulatorResults: 'Simulation Trace',
|
|
19
|
+
simulatorNoResults: 'Run a simulation to view the candidate execution trace.',
|
|
20
|
+
sectionStats: 'Pool Health & Telemetry',
|
|
21
|
+
sectionStatsDesc: 'Real-time telemetry of active key pools, healthy credentials, and failover status.',
|
|
22
|
+
sectionFailover: 'Failover Triggers & Error Codes',
|
|
23
|
+
sectionFailoverDesc: 'Select error conditions that trigger automatic switching to the next available key.',
|
|
24
|
+
maxAttemptsLabel: 'Max Attempts (Budget)',
|
|
25
|
+
maxDurationLabel: 'Max Duration ms (Budget)',
|
|
26
|
+
traceTitle: 'Key Selection Traces',
|
|
27
|
+
traceDesc: 'Candidate evaluation history, skips and selection decisions for requests.',
|
|
28
|
+
traceRefresh: 'Refresh Traces',
|
|
29
|
+
traceClear: 'Clear History',
|
|
30
|
+
traceEmpty: 'No selection traces recorded yet.',
|
|
31
|
+
traceSelected: 'Selected Key',
|
|
32
|
+
traceOutcome: 'Outcome',
|
|
33
|
+
drainTitle: 'Managed Key Drain',
|
|
34
|
+
drainDesc: 'Safely transition a key out of rotation waiting for active requests to finish.',
|
|
35
|
+
drainKeyPlaceholder: 'Key ref (e.g. DEEPSEEK_API_KEY_2)',
|
|
36
|
+
drainStart: 'Start Drain',
|
|
37
|
+
drainCancel: 'Cancel Drain',
|
|
38
|
+
drainActiveStreams: 'Active requests',
|
|
39
|
+
drainCanComplete: 'Ready to remove',
|
|
40
|
+
drainDraining: 'Draining Keys',
|
|
41
|
+
drainEmpty: 'No keys currently draining.',
|
|
42
|
+
sectionTiming: 'Timing & Rotation Schedule',
|
|
43
|
+
sectionTimingDesc: 'Configure cooldown durations and scheduled periodic rotation windows.',
|
|
44
|
+
sectionBackup: 'Backup & Snapshots',
|
|
45
|
+
sectionBackupDesc: 'Export or import provider pools and create full plugin configuration snapshots.',
|
|
46
|
+
statPools: 'Configured Pools',
|
|
47
|
+
statKeys: 'Total Keys',
|
|
48
|
+
statHealthy: 'Healthy / Ready',
|
|
49
|
+
loadDistribution: 'Traffic Distribution',
|
|
50
|
+
statRequests: 'requests',
|
|
51
|
+
noTrafficYet: 'No traffic recorded yet',
|
|
52
|
+
confirm: 'Confirm',
|
|
53
|
+
cancel: 'Cancel',
|
|
54
|
+
confirmResetTitle: 'Reset cooldowns for {p}?',
|
|
55
|
+
confirmResetDesc: 'This will immediately clear all failure counts, cooldown timers, and reset the circuit breaker.',
|
|
56
|
+
confirmRemoveProvTitle: 'Remove provider {p}?',
|
|
57
|
+
confirmRemoveProvDesc: 'Are you sure you want to remove this provider and all configured keys from rotation?',
|
|
58
|
+
filterAll: 'All',
|
|
59
|
+
filterReady: 'Ready',
|
|
60
|
+
filterCooldown: 'In Cooldown',
|
|
61
|
+
filterErrors: 'With Errors',
|
|
62
|
+
subtitle: 'Per-provider API key rotation: pools of keys, automatic failover on quota/rate-limit errors, cooldown and recovery.',
|
|
63
|
+
cardDesc: 'Per-provider API key rotation: pools of keys, automatic failover on quota/rate-limit errors, cooldown and recovery.',
|
|
64
|
+
loading: 'Loading…',
|
|
65
|
+
notRegistered: 'not registered',
|
|
66
|
+
providerCatalogUnavailable: 'Provider list unavailable',
|
|
67
|
+
removeKey: 'Remove key',
|
|
68
|
+
removeProvider: 'Remove provider',
|
|
69
|
+
addKey: '+ Add key',
|
|
70
|
+
addKeyTitle: 'Add API key',
|
|
71
|
+
noProviders: 'No providers registered with DSH — nothing to pick from yet.',
|
|
72
|
+
bulkImport: 'Bulk Import',
|
|
73
|
+
bulkImportDesc: 'Paste API keys line-by-line, as .env (KEY=value), or CSV (KEY, weight):',
|
|
74
|
+
bulkImportBtn: 'Import Keys',
|
|
75
|
+
bulkImportCancel: 'Cancel',
|
|
76
|
+
bulkImportSuccess: 'Imported {n} keys',
|
|
77
|
+
keyPaused: 'Paused',
|
|
78
|
+
keyRevoked: 'Revoked (401)',
|
|
79
|
+
keyAuthPaused: 'Auth Paused (401/403)',
|
|
80
|
+
presetLimitsBtn: '⚡ Preset Limits',
|
|
81
|
+
presetApplied: 'Applied {prov} preset limits ({rpm} RPM / {tpm} TPM)',
|
|
82
|
+
presetNone: 'No canonical preset for this provider',
|
|
83
|
+
detectedLimitsLabel: 'Upstream: {rpm} RPM · {tpm} TPM',
|
|
84
|
+
trafficSmoothingLabel: 'Traffic Smoothing (Burst)',
|
|
85
|
+
balanceLabel: 'Balance: {amt} {curr}',
|
|
86
|
+
balanceEmpty: 'Balance Empty (Paused)',
|
|
87
|
+
balanceCheckBtn: '💰 Check Balance',
|
|
88
|
+
checkingBalance: 'Checking…',
|
|
89
|
+
upstreamResetIn: '⚡ Reset in {time} (header)',
|
|
90
|
+
pauseKey: 'Pause key from rotation',
|
|
91
|
+
resumeKey: 'Resume key rotation',
|
|
92
|
+
sparklineTraffic: 'Traffic Trend',
|
|
93
|
+
sparklineLatency: 'Latency Trend',
|
|
94
|
+
addProvider: '+ Add provider',
|
|
95
|
+
desc: 'Per-provider API key rotation. For each provider, list its API keys (env names, stored in DSH credentials). The plugin routes a model through that provider\u2019s keys in order and switches to the next on a quota/rate-limit failure.',
|
|
96
|
+
cooldown: 'Cooldown after failure (ms)',
|
|
97
|
+
scheduleDays: 'Rotation schedule (days, 0=off)',
|
|
98
|
+
switchCodes: 'Switch codes (comma-separated)',
|
|
99
|
+
providersTitle: 'Providers and their keys',
|
|
100
|
+
save: 'Save',
|
|
101
|
+
discard: 'Discard',
|
|
102
|
+
saving: 'Saving…',
|
|
103
|
+
moveUp: 'Move up',
|
|
104
|
+
moveDown: 'Move down',
|
|
105
|
+
keyActive: 'in use',
|
|
106
|
+
keyReady: 'ready',
|
|
107
|
+
keyCooling: 'cooling down, {s}s',
|
|
108
|
+
keyMissing: 'no such credential',
|
|
109
|
+
switchesNone: 'no switches yet',
|
|
110
|
+
switchesSome: 'switches: {n} · last: {reason}, {ago}',
|
|
111
|
+
justNow: 'just now',
|
|
112
|
+
minutesAgo: '{n} min ago',
|
|
113
|
+
hoursAgo: '{n} h ago',
|
|
114
|
+
codesTitle: 'Switch on these failures',
|
|
115
|
+
keyValuePlaceholder: 'paste the key, then Save',
|
|
116
|
+
keySave: 'Save key',
|
|
117
|
+
keySaved: 'saved',
|
|
118
|
+
keyFromEnv: 'from the environment, read-only here',
|
|
119
|
+
keyWriteFailed: 'could not store the key: {msg}',
|
|
120
|
+
notSecretShape: 'saved, but the value does not look like an API key - check for a typo',
|
|
121
|
+
rpmTitle: 'requests/min: {u} used, {r} remaining',
|
|
122
|
+
tpmTitle: 'tokens/min: {u} used, {r} remaining',
|
|
123
|
+
rpmLimitLabel: 'Rate Limit (RPM)',
|
|
124
|
+
tpmLimitLabel: 'Rate Limit (TPM)',
|
|
125
|
+
budgetLabel: 'budget:',
|
|
126
|
+
exportCsv: 'Export CSV',
|
|
127
|
+
weightHint: 'round-robin weight: how many times this key joins the cycle (1 = equal share)',
|
|
128
|
+
modelPoolsTitle: 'Per-model token quotas',
|
|
129
|
+
modelPoolsHint: 'Give one credential its own token budget for one model. A key that spends its budget is skipped for that model only — other models keep using it. Leave the limit empty for unlimited.',
|
|
130
|
+
modelPoolsEmpty: 'No model sub-pools configured. Add one to give a model its own keys and token budgets.',
|
|
131
|
+
addModel: '+ Add model',
|
|
132
|
+
modelName: 'Model id',
|
|
133
|
+
modelNamePlaceholder: 'e.g. claude-sonnet',
|
|
134
|
+
removeModel: 'Remove this model pool',
|
|
135
|
+
modelKeys: 'Credential references for this model',
|
|
136
|
+
tokenLimitLabel: 'Token limit',
|
|
137
|
+
tokenLimitPlaceholder: 'unlimited',
|
|
138
|
+
tokenLimitHint: 'Tokens this credential may spend on this model per quota window. Empty = unlimited.',
|
|
139
|
+
mtUnlimited: 'Unlimited',
|
|
140
|
+
mtExhausted: 'Exhausted',
|
|
141
|
+
mtRemaining: 'Remaining: {n}',
|
|
142
|
+
mtResetIn: 'Reset: {t}',
|
|
143
|
+
mtUsedOf: '{u} / {l}',
|
|
144
|
+
mtResetWindowHint: 'Window controlled by quotaResetWindow.',
|
|
145
|
+
mtLocalOnly: 'Tracked locally from provider-reported usage on successful responses.',
|
|
146
|
+
retestBroken: 'Re-test',
|
|
147
|
+
retestFail: 're-test failed - key still down',
|
|
148
|
+
snapshotExport: 'Snapshot ⬇',
|
|
149
|
+
snapshotImport: 'Restore',
|
|
150
|
+
keyHint: 'The value is stored in DSH credentials and never sent back to the browser — only its last 5 characters are shown. Names are generated for you; hover a key to see the one it uses.',
|
|
151
|
+
brokenKey: 'broken (3× AUTH)',
|
|
152
|
+
keyExpired: 'expired',
|
|
153
|
+
keyExpiringSoon: 'expires in {n} d',
|
|
154
|
+
exportPools: 'Export',
|
|
155
|
+
exportOne: '⬇',
|
|
156
|
+
usedAgo: '{ago} ago',
|
|
157
|
+
importPools: 'Import',
|
|
158
|
+
importEnv: 'Import .env',
|
|
159
|
+
resetCooldown: 'Reset cooldown',
|
|
160
|
+
testAll: 'Test all keys',
|
|
161
|
+
testing: 'Testing…',
|
|
162
|
+
testKey: 'Test',
|
|
163
|
+
testOk: 'OK',
|
|
164
|
+
testFail: 'FAIL',
|
|
165
|
+
poolExhausted: 'pool exhausted — all keys cooling',
|
|
166
|
+
resetting: 'Resetting…',
|
|
167
|
+
keyLabel: 'Key {n}',
|
|
168
|
+
undoProvider: 'Provider removed',
|
|
169
|
+
undoKey: 'Key removed',
|
|
170
|
+
undo: 'Undo',
|
|
171
|
+
// (issue 289) a11y / issue 290 states / issue 291 bulk / issue 292 locale
|
|
172
|
+
emptyTitle: 'No pools yet',
|
|
173
|
+
emptyDesc: 'Add a provider and at least one API key name to start rotating.',
|
|
174
|
+
errorTitle: 'Could not load settings',
|
|
175
|
+
retry: 'Retry',
|
|
176
|
+
chooseProvider: 'Choose a provider',
|
|
177
|
+
readOnly: 'Settings are read-only.',
|
|
178
|
+
settingsChanged: 'The settings service changed. Discard this draft and reload before saving.',
|
|
179
|
+
saveRefused: 'The host did not accept this save. Your draft is kept; discard it to load the latest settings.',
|
|
180
|
+
saveUnconfirmed: 'The save result could not be confirmed. Your draft is kept; reload before trying again.',
|
|
181
|
+
unavailableTitle: 'Settings bridge unavailable',
|
|
182
|
+
unavailableDesc: 'The host did not expose the dsh-key-rotation settings namespace. Reopen Settings or check the plugin is enabled.',
|
|
183
|
+
modalClose: 'Close dialog',
|
|
184
|
+
dialogLabel: 'Confirmation',
|
|
185
|
+
loadChartAria: 'Key traffic distribution',
|
|
186
|
+
bulkCooldownPlaceholder: 'Bulk cooldown (ms)',
|
|
187
|
+
bulkApply: 'Apply to selected',
|
|
188
|
+
bulkRemove: 'Remove selected',
|
|
189
|
+
confirmBulkRemoveTitle: 'Remove {n} provider(s)?',
|
|
190
|
+
confirmBulkRemoveDesc: 'Selected providers and all their keys will be removed from rotation. You can undo once immediately after.',
|
|
191
|
+
selectProvider: 'Select provider {p}',
|
|
192
|
+
activeBadge: 'active',
|
|
193
|
+
healthScoreTitle: 'health score',
|
|
194
|
+
totalRequestsTitle: 'total requests',
|
|
195
|
+
exportProvider: 'Export this provider',
|
|
196
|
+
recentFailures: 'Recent failures ({n})',
|
|
197
|
+
noActivePools: 'No active pools',
|
|
198
|
+
headerPoolsTitle: 'Key rotation pools',
|
|
199
|
+
pausedBudget: 'paused',
|
|
200
|
+
sortUsage: 'Sort by usage',
|
|
201
|
+
testingAllProgress: 'Testing {n}/{total}…',
|
|
202
|
+
liveEventStream: 'Live Event Stream',
|
|
203
|
+
liveEventStreamEmpty: 'No rotation events recorded',
|
|
204
|
+
eventAt: 'Time',
|
|
205
|
+
eventKey: 'Key',
|
|
206
|
+
eventReason: 'Reason',
|
|
207
|
+
eventCooldown: 'Cooldown',
|
|
208
|
+
quotaResetIn: 'Reset in {time}',
|
|
209
|
+
routingStrategyLabel: 'Routing Strategy',
|
|
210
|
+
routingStrategyRoundRobin: 'Round-Robin (default)',
|
|
211
|
+
routingStrategyLeastLoaded: 'Least-Loaded (concurrency)',
|
|
212
|
+
routingStrategyLowestLatency: 'Lowest-Latency (p95)',
|
|
213
|
+
routingStrategySticky: 'Sticky (preserves prompt cache)',
|
|
214
|
+
proactiveGuardLabel: 'Proactive Rate-Limit Guard',
|
|
215
|
+
proactiveGuardHint: 'Pre-emptively pause keys when remaining quota or Retry-After header indicates limit before hitting 429',
|
|
216
|
+
showChatHeaderChipLabel: 'Chat Header Status Chip',
|
|
217
|
+
showChatHeaderChipDesc: 'Show pool status chip in conversation header',
|
|
218
|
+
selfHealingLabel: 'Auto-Unbreak Interval (min)',
|
|
219
|
+
selfHealingHint: 'Periodic free model check to revive broken keys (0 to disable)',
|
|
220
|
+
updateCheck: 'Check for updates',
|
|
221
|
+
updateAvailable: 'Update available',
|
|
222
|
+
updateNone: 'Plugin is up to date',
|
|
223
|
+
updateRun: 'Update now',
|
|
224
|
+
updateRestart: 'Update installed. Restart DSH to load the new version.',
|
|
225
|
+
updateRunning: 'Updating…',
|
|
226
|
+
updateFailed: 'Update failed',
|
|
227
|
+
currentVersion: 'Current version',
|
|
228
|
+
latestVersion: 'Latest version',
|
|
229
|
+
testAllKeys: 'Test All Keys',
|
|
230
|
+
testingMatrix: 'Testing Keys…',
|
|
231
|
+
matrixModalTitle: '⚡ Key Test Matrix',
|
|
232
|
+
matrixModalDesc: 'Parallel probe of all configured credentials across all provider pools.',
|
|
233
|
+
matrixSummary: '{healthy}/{total} keys operational',
|
|
234
|
+
matrixLatency: 'Latency',
|
|
235
|
+
matrixStatus: 'Status',
|
|
236
|
+
matrixClose: 'Close',
|
|
237
|
+
discoverBannerTitle: '⚡ Unassigned API Keys Discovered',
|
|
238
|
+
discoverBannerDesc: 'Found {n} credentials in your environment (~/.dsh/.credentials.yaml / env) not yet assigned to any rotation pool.',
|
|
239
|
+
autoImportBtn: '1-Click Auto-Import',
|
|
240
|
+
autoImportSuccess: 'Imported {n} keys into rotation pools',
|
|
241
|
+
autoImporting: 'Importing…',
|
|
242
|
+
quickSkip: 'Skip',
|
|
243
|
+
quickSkipTitle: 'Skip to next key in pool',
|
|
244
|
+
quickReset: 'Reset',
|
|
245
|
+
quickResetTitle: 'Reset cooldowns and circuit breaker for active provider',
|
|
246
|
+
quickPause: 'Pause',
|
|
247
|
+
quickPauseTitle: 'Pause or resume active key',
|
|
248
|
+
exportConfig: 'Export Pools',
|
|
249
|
+
importConfig: 'Import Pools',
|
|
250
|
+
exportSafe: 'Masked (Safe for sharing)',
|
|
251
|
+
exportFull: 'Full Backup (Includes secrets)',
|
|
252
|
+
exportWithPassphrase: 'Encrypted with passphrase',
|
|
253
|
+
importMode: 'Import Mode',
|
|
254
|
+
importMerge: 'Merge with existing pools',
|
|
255
|
+
importReplace: 'Replace entire pool configuration',
|
|
256
|
+
importPreview: 'Preview Diff',
|
|
257
|
+
importApply: 'Apply Configuration',
|
|
258
|
+
importSuccess: 'Configuration successfully imported and applied',
|
|
259
|
+
exportSuccess: 'Configuration exported successfully',
|
|
260
|
+
passphraseLabel: 'Passphrase (optional)',
|
|
261
|
+
passphrasePlaceholder: 'Enter master passphrase for encryption/decryption',
|
|
262
|
+
jsonConfigPlaceholder: 'Paste JSON configuration here or choose a file...',
|
|
263
|
+
previewSummary: 'Diff: {created} new providers, {updated} updated providers, {keys} keys ({added} new).',
|
|
264
|
+
promptCacheTitle: 'Prompt Cache Savings',
|
|
265
|
+
promptCacheDesc: 'Tokens served from prefix cache across sessions and estimated cost savings.',
|
|
266
|
+
promptCacheHitRatio: 'Hit Ratio',
|
|
267
|
+
promptCacheSaved: 'saved',
|
|
268
|
+
analytics24hTitle: '24h Pool Health & Load Balance',
|
|
269
|
+
healthIndex: 'Health Index',
|
|
270
|
+
cleanRequests: 'clean',
|
|
271
|
+
errorBreakdown: '24h Errors',
|
|
272
|
+
avgRecovery: 'Avg Recovery',
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const zh = {
|
|
276
|
+
title: '密钥轮换',
|
|
277
|
+
'header.title': '密钥轮换与故障转移',
|
|
278
|
+
'header.sub': '自动化多密钥轮换、429/5xx指数退避冷却、断路器保护及凭证安全 (~/.dsh/.credentials.yaml)。',
|
|
279
|
+
'header.pools_badge': '个活跃密钥池',
|
|
280
|
+
'header.keys_badge': '个已配置密钥',
|
|
281
|
+
simulatorTitle: '交互式路由模拟器',
|
|
282
|
+
simulatorDesc: '测试路由策略并在不向上游发送实际请求的情况下可视化密钥池的故障转移链路。',
|
|
283
|
+
simulatorRun: '运行模拟',
|
|
284
|
+
simulatorRunning: '模拟中…',
|
|
285
|
+
simulatorSelectProvider: '提供商',
|
|
286
|
+
simulatorSelectModel: '模型(可选)',
|
|
287
|
+
simulatorMaxAttempts: '最大尝试次数',
|
|
288
|
+
simulatorEvent: '模拟场景',
|
|
289
|
+
simulatorEventSuccess: '所有密钥均成功',
|
|
290
|
+
simulatorEventRateLimit: '密钥 1 触发速率限制 (429)',
|
|
291
|
+
simulatorEventServer500: '密钥 1 内部服务错误 (500)',
|
|
292
|
+
simulatorResults: '模拟追踪结果',
|
|
293
|
+
simulatorNoResults: '运行模拟以查看候选密钥执行链路。',
|
|
294
|
+
sectionStats: '密钥池健康与遥测',
|
|
295
|
+
sectionStatsDesc: '活跃密钥池、健康凭证和故障转移状态的实时遥测。',
|
|
296
|
+
sectionFailover: '故障转移触发器与错误代码',
|
|
297
|
+
sectionFailoverDesc: '选择触发自动切换至下一个可用密钥的错误条件。',
|
|
298
|
+
maxAttemptsLabel: '最大重试次数 (预算)',
|
|
299
|
+
maxDurationLabel: '最大超时毫秒 (预算)',
|
|
300
|
+
traceTitle: '密钥选择追踪 (Trace)',
|
|
301
|
+
traceDesc: '查看请求的候选密钥评估、跳过原因和最终选择决策。',
|
|
302
|
+
traceRefresh: '刷新追踪',
|
|
303
|
+
traceClear: '清空历史',
|
|
304
|
+
traceEmpty: '暂无选择追踪记录。',
|
|
305
|
+
traceSelected: '所选密钥',
|
|
306
|
+
traceOutcome: '结果',
|
|
307
|
+
drainTitle: '安全下线密钥 (Drain)',
|
|
308
|
+
drainDesc: '平滑下线密钥并等待当前在途请求完成,避免中途断流。',
|
|
309
|
+
drainKeyPlaceholder: '密钥标识 (例如 DEEPSEEK_API_KEY_2)',
|
|
310
|
+
drainStart: '开始下线',
|
|
311
|
+
drainCancel: '取消下线',
|
|
312
|
+
drainActiveStreams: '在途请求数',
|
|
313
|
+
drainCanComplete: '可安全移除',
|
|
314
|
+
drainDraining: '下线中的密钥',
|
|
315
|
+
drainEmpty: '当前没有正在下线的密钥。',
|
|
316
|
+
sectionTiming: '时间与轮换计划',
|
|
317
|
+
sectionTimingDesc: '配置冷却持续时间和定期轮换计划窗口。',
|
|
318
|
+
sectionBackup: '备份与快照',
|
|
319
|
+
sectionBackupDesc: '导出或导入提供商密钥池,并创建完整插件配置快照。',
|
|
320
|
+
statPools: '已配置密钥池',
|
|
321
|
+
statKeys: '总密钥数',
|
|
322
|
+
statHealthy: '健康 / 就绪',
|
|
323
|
+
loadDistribution: '流量分布',
|
|
324
|
+
statRequests: '请求',
|
|
325
|
+
noTrafficYet: '暂无流量记录',
|
|
326
|
+
confirm: '确认',
|
|
327
|
+
cancel: '取消',
|
|
328
|
+
confirmResetTitle: '重置 {p} 的冷却状态?',
|
|
329
|
+
confirmResetDesc: '这将立即清除所有故障计数、冷却计时器并重置断路器。',
|
|
330
|
+
confirmRemoveProvTitle: '移除提供商 {p}?',
|
|
331
|
+
confirmRemoveProvDesc: '确定要从轮换中移除该提供商及其所有已配置密钥吗?',
|
|
332
|
+
filterAll: '全部',
|
|
333
|
+
filterReady: '就绪',
|
|
334
|
+
filterCooldown: '冷却中',
|
|
335
|
+
filterErrors: '存在错误',
|
|
336
|
+
subtitle: '按提供商 API 密钥轮换:密钥池、配额/限流错误自动故障转移、冷却与恢复。',
|
|
337
|
+
cardDesc: '按提供商 API 密钥轮换:密钥池、配额/限流错误自动故障转移、冷却与恢复。',
|
|
338
|
+
loading: '加载中…',
|
|
339
|
+
notRegistered: '未注册',
|
|
340
|
+
providerCatalogUnavailable: '提供商列表暂不可用',
|
|
341
|
+
removeKey: '移除密钥',
|
|
342
|
+
removeProvider: '移除提供商',
|
|
343
|
+
addKey: '+ 添加密钥',
|
|
344
|
+
addKeyTitle: '添加 API 密钥',
|
|
345
|
+
noProviders: 'DSH 尚未注册任何提供商 — 暂无可选项目。',
|
|
346
|
+
bulkImport: '批量导入',
|
|
347
|
+
bulkImportDesc: '按行粘贴 API 密钥,支持 .env (KEY=value) 或 CSV (KEY, 权重) 格式:',
|
|
348
|
+
bulkImportBtn: '导入密钥',
|
|
349
|
+
bulkImportCancel: '取消',
|
|
350
|
+
bulkImportSuccess: '已导入 {n} 个密钥',
|
|
351
|
+
keyPaused: '已暂停',
|
|
352
|
+
keyRevoked: '已撤销 (401)',
|
|
353
|
+
keyAuthPaused: '身份验证失败已暂停 (401/403)',
|
|
354
|
+
presetLimitsBtn: '⚡ 预设限制',
|
|
355
|
+
presetApplied: '已应用 {prov} 预设限制 ({rpm} RPM / {tpm} TPM)',
|
|
356
|
+
presetNone: '未找到该提供商预设规则',
|
|
357
|
+
detectedLimitsLabel: '上游限制:{rpm} RPM · {tpm} TPM',
|
|
358
|
+
trafficSmoothingLabel: '流量平滑 (突发并发)',
|
|
359
|
+
balanceLabel: '余额:{amt} {curr}',
|
|
360
|
+
balanceEmpty: '余额不足已暂停',
|
|
361
|
+
balanceCheckBtn: '💰 检查余额',
|
|
362
|
+
checkingBalance: '查询中…',
|
|
363
|
+
upstreamResetIn: '⚡ {time}后重置 (上游头信息)',
|
|
364
|
+
pauseKey: '暂停此密钥轮换',
|
|
365
|
+
resumeKey: '恢复此密钥轮换',
|
|
366
|
+
sparklineTraffic: '流量趋势',
|
|
367
|
+
sparklineLatency: '延迟趋势',
|
|
368
|
+
addProvider: '+ 添加提供商',
|
|
369
|
+
desc: '按提供商的 API 密钥轮换。为每个提供商列出其 API 密钥(环境变量名,存储在 DSH 凭据中)。插件按顺序通过提供商的密钥路由模型,并在配额/速率限制失败时切换到下一个。',
|
|
370
|
+
cooldown: '故障后冷却时间 (毫秒)',
|
|
371
|
+
scheduleDays: '轮换计划 (天数,0=关闭)',
|
|
372
|
+
switchCodes: '切换代码 (逗号分隔)',
|
|
373
|
+
providersTitle: '提供商与其密钥',
|
|
374
|
+
save: '保存',
|
|
375
|
+
discard: '放弃',
|
|
376
|
+
saving: '保存中…',
|
|
377
|
+
moveUp: '上移',
|
|
378
|
+
moveDown: '下移',
|
|
379
|
+
keyActive: '使用中',
|
|
380
|
+
keyReady: '就绪',
|
|
381
|
+
keyCooling: '冷却中,{s}秒',
|
|
382
|
+
keyMissing: '凭证不存在',
|
|
383
|
+
switchesNone: '暂无切换',
|
|
384
|
+
switchesSome: '切换次数: {n} · 上次: {reason}, {ago}',
|
|
385
|
+
justNow: '刚刚',
|
|
386
|
+
minutesAgo: '{n} 分钟前',
|
|
387
|
+
hoursAgo: '{n} 小时前',
|
|
388
|
+
codesTitle: '在这些故障时切换',
|
|
389
|
+
keyValuePlaceholder: '粘贴密钥,然后保存',
|
|
390
|
+
keySave: '保存密钥',
|
|
391
|
+
keySaved: '已保存',
|
|
392
|
+
keyFromEnv: '来自环境变量,此处只读',
|
|
393
|
+
keyWriteFailed: '无法存储密钥: {msg}',
|
|
394
|
+
notSecretShape: '已保存,但该值看起来不像 API 密钥 - 请检查是否有拼写错误',
|
|
395
|
+
rpmTitle: '请求/分钟: 已用 {u}, 剩余 {r}',
|
|
396
|
+
tpmTitle: 'Token/分钟: 已用 {u}, 剩余 {r}',
|
|
397
|
+
rpmLimitLabel: '速率限制 (RPM)',
|
|
398
|
+
tpmLimitLabel: '速率限制 (TPM)',
|
|
399
|
+
budgetLabel: '预算:',
|
|
400
|
+
exportCsv: '导出 CSV',
|
|
401
|
+
weightHint: '轮换权重:该密钥参与循环的次数 (1 = 等分)',
|
|
402
|
+
modelPoolsTitle: '按模型设置 Token 额度',
|
|
403
|
+
modelPoolsHint: '为某个凭据在某个模型上单独设置 Token 额度。额度用尽的密钥只会在该模型上被跳过,其他模型仍可继续使用。留空表示不限额。',
|
|
404
|
+
modelPoolsEmpty: '尚未配置模型子池。添加一个即可为该模型单独指定密钥与 Token 额度。',
|
|
405
|
+
addModel: '+ 添加模型',
|
|
406
|
+
modelName: '模型 ID',
|
|
407
|
+
modelNamePlaceholder: '例如 claude-sonnet',
|
|
408
|
+
removeModel: '移除该模型子池',
|
|
409
|
+
modelKeys: '该模型使用的凭据引用',
|
|
410
|
+
tokenLimitLabel: 'Token 额度',
|
|
411
|
+
tokenLimitPlaceholder: '不限额',
|
|
412
|
+
tokenLimitHint: '该凭据在此模型的每个额度周期内可消耗的 Token 数。留空表示不限额。',
|
|
413
|
+
mtUnlimited: '不限额',
|
|
414
|
+
mtExhausted: '已耗尽',
|
|
415
|
+
mtRemaining: '剩余:{n}',
|
|
416
|
+
mtResetIn: '重置:{t}',
|
|
417
|
+
mtUsedOf: '{u} / {l}',
|
|
418
|
+
mtResetWindowHint: '周期由 quotaResetWindow 控制。',
|
|
419
|
+
mtLocalOnly: '仅在成功响应后根据提供商返回的 usage 本地记账。',
|
|
420
|
+
retestBroken: '重新测试',
|
|
421
|
+
retestFail: '重新测试失败 - 密钥仍然不可用',
|
|
422
|
+
snapshotExport: '快照 ⬇',
|
|
423
|
+
snapshotImport: '恢复',
|
|
424
|
+
keyHint: '该值存储在 DSH 凭据中,绝不会发送回浏览器 — 仅显示其最后 5 个字符。名称已自动生成;悬停在密钥上可查看其使用的名称。',
|
|
425
|
+
brokenKey: '已损坏 (3× 认证失败)',
|
|
426
|
+
keyExpired: '已过期',
|
|
427
|
+
keyExpiringSoon: '{n} 天后过期',
|
|
428
|
+
exportPools: '导出',
|
|
429
|
+
exportOne: '⬇',
|
|
430
|
+
usedAgo: '{ago}前使用',
|
|
431
|
+
importPools: '导入',
|
|
432
|
+
importEnv: '导入 .env',
|
|
433
|
+
resetCooldown: '重置冷却',
|
|
434
|
+
testAll: '测试所有密钥',
|
|
435
|
+
testing: '测试中…',
|
|
436
|
+
testKey: '测试',
|
|
437
|
+
testOk: '正常',
|
|
438
|
+
testFail: '失败',
|
|
439
|
+
poolExhausted: '密钥池耗尽 — 所有密钥冷却中',
|
|
440
|
+
resetting: '重置中…',
|
|
441
|
+
keyLabel: '密钥 {n}',
|
|
442
|
+
undoProvider: '提供商已移除',
|
|
443
|
+
undoKey: '密钥已移除',
|
|
444
|
+
undo: '撤销',
|
|
445
|
+
emptyTitle: '暂无密钥池',
|
|
446
|
+
emptyDesc: '添加提供商及至少一个 API 密钥名称以开始轮换。',
|
|
447
|
+
errorTitle: '无法加载设置',
|
|
448
|
+
retry: '重试',
|
|
449
|
+
chooseProvider: '请选择提供商',
|
|
450
|
+
readOnly: '当前设置为只读。',
|
|
451
|
+
settingsChanged: '设置服务已变更。请放弃当前草稿并重新加载后再保存。',
|
|
452
|
+
saveRefused: '宿主未接受此次保存。草稿已保留;放弃草稿可加载最新设置。',
|
|
453
|
+
saveUnconfirmed: '无法确认保存结果。草稿已保留,请重新加载后再试。',
|
|
454
|
+
unavailableTitle: '设置桥接不可用',
|
|
455
|
+
unavailableDesc: '宿主未暴露 dsh-key-rotation 设置命名空间。请重新打开设置或检查插件是否已启用。',
|
|
456
|
+
modalClose: '关闭对话框',
|
|
457
|
+
dialogLabel: '确认',
|
|
458
|
+
loadChartAria: '密钥流量分布',
|
|
459
|
+
bulkCooldownPlaceholder: '批量冷却时间 (毫秒)',
|
|
460
|
+
bulkApply: '应用到所选项',
|
|
461
|
+
bulkRemove: '删除所选项',
|
|
462
|
+
confirmBulkRemoveTitle: '删除 {n} 个提供商?',
|
|
463
|
+
confirmBulkRemoveDesc: '所选提供商及其所有密钥将从轮换中移除。操作后可立即撤销一次。',
|
|
464
|
+
selectProvider: '选择提供商 {p}',
|
|
465
|
+
activeBadge: '活跃',
|
|
466
|
+
healthScoreTitle: '健康评分',
|
|
467
|
+
totalRequestsTitle: '总请求数',
|
|
468
|
+
exportProvider: '导出此提供商',
|
|
469
|
+
recentFailures: '近期故障 ({n})',
|
|
470
|
+
noActivePools: '无活跃密钥池',
|
|
471
|
+
headerPoolsTitle: '密钥轮换池',
|
|
472
|
+
pausedBudget: '已暂停',
|
|
473
|
+
sortUsage: '按使用量排序',
|
|
474
|
+
testingAllProgress: '正在测试 {n}/{total}…',
|
|
475
|
+
liveEventStream: '实时事件流',
|
|
476
|
+
liveEventStreamEmpty: '暂无轮换事件记录',
|
|
477
|
+
eventAt: '时间',
|
|
478
|
+
eventKey: '密钥',
|
|
479
|
+
eventReason: '原因',
|
|
480
|
+
eventCooldown: '冷却',
|
|
481
|
+
quotaResetIn: '{time} 后重置',
|
|
482
|
+
routingStrategyLabel: '路由策略',
|
|
483
|
+
routingStrategyRoundRobin: '轮询 (默认)',
|
|
484
|
+
routingStrategyLeastLoaded: '最低负载 (并发)',
|
|
485
|
+
routingStrategyLowestLatency: '最低延迟 (p95)',
|
|
486
|
+
routingStrategySticky: '粘性模式 (优先保持提示词缓存)',
|
|
487
|
+
proactiveGuardLabel: '前瞻性限流防护',
|
|
488
|
+
proactiveGuardHint: '当响应中的剩余配额或 Retry-After 头提示即将超限时,预先冷却密钥以防触发 429',
|
|
489
|
+
showChatHeaderChipLabel: '聊天头部状态按钮',
|
|
490
|
+
showChatHeaderChipDesc: '在会话界面顶部工具栏显示密钥池状态按钮',
|
|
491
|
+
selfHealingLabel: '自愈检查间隔 (分钟)',
|
|
492
|
+
selfHealingHint: '定期轻量探测以自动恢复损坏密钥 (0 为禁用)',
|
|
493
|
+
updateCheck: '检查更新',
|
|
494
|
+
updateAvailable: '有可用更新',
|
|
495
|
+
updateNone: '插件已是最新版本',
|
|
496
|
+
updateRun: '立即更新',
|
|
497
|
+
updateRestart: '更新已安装。请重启 DSH 以加载新版本。',
|
|
498
|
+
updateRunning: '更新中…',
|
|
499
|
+
updateFailed: '更新失败',
|
|
500
|
+
currentVersion: '当前版本',
|
|
501
|
+
latestVersion: '最新版本',
|
|
502
|
+
testAllKeys: '批量测试所有密钥',
|
|
503
|
+
testingMatrix: '正在测试密钥…',
|
|
504
|
+
matrixModalTitle: '⚡ 密钥测试矩阵',
|
|
505
|
+
matrixModalDesc: '并行探测所有提供商密钥池中配置的凭据。',
|
|
506
|
+
matrixSummary: '{healthy}/{total} 个密钥正常可用',
|
|
507
|
+
matrixLatency: '延迟',
|
|
508
|
+
matrixStatus: '状态',
|
|
509
|
+
matrixClose: '关闭',
|
|
510
|
+
discoverBannerTitle: '⚡ 发现未分配的 API 密钥',
|
|
511
|
+
discoverBannerDesc: '在当前环境 (~/.dsh/.credentials.yaml / env) 中发现 {n} 个未分配给轮换池的密钥。',
|
|
512
|
+
autoImportBtn: '一键自动导入',
|
|
513
|
+
autoImportSuccess: '成功导入 {n} 个密钥到轮换池',
|
|
514
|
+
autoImporting: '正在导入…',
|
|
515
|
+
quickSkip: '跳过',
|
|
516
|
+
quickSkipTitle: '跳过并轮换到池中下一个密钥',
|
|
517
|
+
quickReset: '重置',
|
|
518
|
+
quickResetTitle: '重置当前提供商的冷却时间与断路器',
|
|
519
|
+
quickPause: '暂停',
|
|
520
|
+
quickPauseTitle: '暂停或恢复当前正在使用的密钥',
|
|
521
|
+
exportConfig: '导出密钥池',
|
|
522
|
+
importConfig: '导入密钥池',
|
|
523
|
+
exportSafe: '脱敏导出 (安全分享)',
|
|
524
|
+
exportFull: '完整备份 (包含密钥)',
|
|
525
|
+
exportWithPassphrase: '使用密码加密导出',
|
|
526
|
+
importMode: '导入模式',
|
|
527
|
+
importMerge: '与现有密钥池合并',
|
|
528
|
+
importReplace: '覆盖现有全部密钥池配置',
|
|
529
|
+
importPreview: '预览差异',
|
|
530
|
+
importApply: '应用配置',
|
|
531
|
+
importSuccess: '配置已成功导入并生效',
|
|
532
|
+
exportSuccess: '配置导出成功',
|
|
533
|
+
passphraseLabel: '加密密码 (可选)',
|
|
534
|
+
passphrasePlaceholder: '输入用于加密/解密的主密码',
|
|
535
|
+
jsonConfigPlaceholder: '在此粘贴 JSON 配置或选择文件...',
|
|
536
|
+
previewSummary: '差异: 新增 {created} 个提供商, 更新 {updated} 个提供商, 包含 {keys} 个密钥 (新增 {added} 个)。',
|
|
537
|
+
promptCacheTitle: 'Prompt 缓存节省',
|
|
538
|
+
promptCacheDesc: '会话间前缀缓存命中 Token 及预估节省成本。',
|
|
539
|
+
promptCacheHitRatio: '缓存命中率',
|
|
540
|
+
promptCacheSaved: '已节省',
|
|
541
|
+
analytics24hTitle: '24小时池健康度与负载分布',
|
|
542
|
+
healthIndex: '健康指数',
|
|
543
|
+
cleanRequests: '无重试',
|
|
544
|
+
errorBreakdown: '24小时错误',
|
|
545
|
+
avgRecovery: '平均恢复',
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
// Codes worth switching on. Host list is DEFAULT_SWITCH_CODES; config may
|
|
549
|
+
// add extras — those render as separate checked boxes so a custom rule is
|
|
550
|
+
// not dropped on first save.
|
|
551
|
+
const KNOWN_CODES = ['QUOTA', 'RATE_LIMIT', 'SERVER', 'TIMEOUT', 'TRANSPORT', 'EMPTY_RESPONSE', 'UNKNOWN_MODEL', 'AUTH'];
|
|
552
|
+
|
|
553
|
+
// (issue 273): keep the settings list item mounted if the section throws.
|
|
554
|
+
const bestEffort = (label, fn) => {
|
|
555
|
+
try { return fn(); } catch (err) {
|
|
556
|
+
try { console.debug('[dsh-key-rotation] best-effort ' + label, err); } catch { /* console unavailable */ }
|
|
557
|
+
return undefined;
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
class KeyRotationErrorBoundary extends React.Component {
|
|
561
|
+
constructor(props) {
|
|
562
|
+
super(props);
|
|
563
|
+
this.state = { error: null };
|
|
564
|
+
}
|
|
565
|
+
static getDerivedStateFromError(error) {
|
|
566
|
+
return { error };
|
|
567
|
+
}
|
|
568
|
+
componentDidCatch(error, info) {
|
|
569
|
+
bestEffort('error-boundary.log', () => { console.error('[dsh-key-rotation] settings card crashed', error, info); });
|
|
570
|
+
}
|
|
571
|
+
render() {
|
|
572
|
+
if (this.state.error) {
|
|
573
|
+
return React.createElement('div', { className: 'krot krot-err', style: { padding: 12 } },
|
|
574
|
+
React.createElement('p', null, 'Key Rotation settings failed to render.'),
|
|
575
|
+
React.createElement('p', { style: { fontSize: 11, opacity: 0.8 } }, String(this.state.error?.message || this.state.error)),
|
|
576
|
+
React.createElement('button', {
|
|
577
|
+
type: 'button',
|
|
578
|
+
className: 'krot-btn',
|
|
579
|
+
onClick: () => this.setState({ error: null }),
|
|
580
|
+
}, 'Retry'));
|
|
581
|
+
}
|
|
582
|
+
return this.props.children;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|