@goodandready/dsh-key-rotation 0.7.34 → 0.7.36

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/README.md CHANGED
@@ -251,4 +251,19 @@ All management routes require loopback authentication (`127.0.0.1` / `::1`) with
251
251
 
252
252
  ## 📄 License
253
253
 
254
- MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
254
+ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
255
+
256
+ ### v0.7.36
257
+ - **Architecture de-bloat & hardening**: Removed 6 unused/overengineered modules (`shadow`, `incident`, `agent-budget`, `region`, `canary`, `maintenance`) and dead route registrations.
258
+ - **High-throughput buildRuntime memoization**: Eliminates per-token deep-cloning and schema validation on every streaming chunk.
259
+ - **Atomic round-robin pointer rotation**: Concurrent requests advance pointer immediately on candidate selection, eliminating race conditions on simultaneous tool calls.
260
+ - **Enhanced switchable error detection**: Direct parsing of HTTP status codes (`429`, `401`, `403`, `5xx`) and gRPC codes (`RESOURCE_EXHAUSTED`, `UNAVAILABLE`) alongside regex fallback.
261
+ - **Informative user exhaustion messaging**: Clear countdown notice with next key recovery ETA when all keys in a pool are cooling down.
262
+ - **Smart polling**: Client background polling paused when browser tab is inactive (`document.visibilityState`).
263
+
264
+ ### v0.7.35
265
+ - **Lifecycle Cleanups**: Wrapped `credentials.resolve` patch and `ctx.on` event handlers (`llm/stream`, `agent/request-error`) in `ctx.effect` scopes with guaranteed unmount cleanup (#238, #239).
266
+ - **Settings & Secret Roles**: Added `.role('secret')` to `incidentGitHubToken` and `webhookActionToken` in `Config` schema for automatic UI masking (#237).
267
+ - **Settings Architecture & UI**: Added native `settingsScope` snapshot reading/saving in settings card with graceful bridge fallback (#235).
268
+ - **Localization**: Localized `settings.section` sidebar fallback label via `t('title')` and registered `zh` dictionary in `ctx.locale` alongside `en` and `ru` (#236).
269
+ - **Dead Code Purge**: Removed obsolete `mountDashboard` routine after header-chip migration (#240).
package/README.ru.md CHANGED
@@ -251,4 +251,19 @@ dsh-key-rotation:
251
251
 
252
252
  ## 📄 Лицензия
253
253
 
254
- MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
254
+ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
255
+
256
+ ### v0.7.36
257
+ - **Глубокий рефакторинг и деблоатинг**: Удалены 6 неиспользуемых модулей оверинжиниринга (`shadow`, `incident`, `agent-budget`, `region`, `canary`, `maintenance`) и сопутствующие устаревшие эндпоинты.
258
+ - **Мемоизация buildRuntime**: Устранено глубокое клонирование и повторный парсинг схемы на каждом токене/чанге стрима.
259
+ - **Атомарная ротация указателя**: Устранено состояние гонки при параллельных вызовах инструментов — указатель смещается сразу при выборе кандидата.
260
+ - **Точная детекция ошибок**: Прямая проверка кодов HTTP (`429`, `401`, `403`, `5xx`) и gRPC (`RESOURCE_EXHAUSTED`, `UNAVAILABLE`) с фоллбеком на регулярные выражения.
261
+ - **Понятные сообщения при исчерпании пула**: Информативное уведомление с таймером восстановления ближайшего ключа.
262
+ - **Энергоэффективный опрос (Smart Polling)**: Автоматическая приостановка фонового опроса при неактивной вкладке браузера (`document.visibilityState`).
263
+
264
+ ### v0.7.35
265
+ - **Очистка жизненного цикла**: Патч `credentials.resolve` и слушатели событий `ctx.on` (`llm/stream`, `agent/request-error`) переведены в скоупы `ctx.effect` с автоматическим восстановлением функций и отпиской при выгрузке плагина (#238, #239).
266
+ - **Роли секретов в схеме**: Полям `incidentGitHubToken` и `webhookActionToken` в схеме `Config` присвоена роль `.role('secret')` для маскирования в UI (#237).
267
+ - **Архитектура настроек**: Добавлена нативная интеграция со снимками `settingsScope` в карточке настроек с безопасным фоллбеком на HTTP-мост (#235).
268
+ - **Локализация**: Фоллбек секции настроек `settings.section` переведён на локализованную метку `t('title')` со слотом `locale: NS`, а китайский словарь `zh` зарегистрирован в `ctx.locale` наряду с `en` и `ru` (#236).
269
+ - **Удаление мёртвого кода**: Удалена неиспользуемая функция `mountDashboard` после перехода на header chip (#240).
package/README.zh.md CHANGED
@@ -212,4 +212,19 @@ dsh-key-rotation:
212
212
 
213
213
  ## 📄 开源许可
214
214
 
215
- MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
215
+ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
216
+
217
+ ### v0.7.36
218
+ - **架构精简与稳定性加固**:移除 6 个过度设计的模块(`shadow`、`incident`、`agent-budget`、`region`、`canary`、`maintenance`)与废弃端点。
219
+ - **buildRuntime 高性能记忆化**:消除每个流式 token/chunk 上的深拷贝与模式重解析开销。
220
+ - **原子轮询指针推进**:并发请求在选定候选密钥时立即推进指针,消除并发工具调用中的竞争条件。
221
+ - **增强的可切换错误检测**:直接解析 HTTP 状态码(`429`, `401`, `403`, `5xx`)与 gRPC 状态码(`RESOURCE_EXHAUSTED`, `UNAVAILABLE`)。
222
+ - **用户友好的耗尽提示**:密钥池耗尽时返回带恢复倒计时的清晰通知。
223
+ - **智能轮询(Smart Polling)**:标签页不活动时暂停客户端后台轮询。
224
+
225
+ ### v0.7.35
226
+ - **生命周期清理**: 将 `credentials.resolve` 猴子补丁和 `ctx.on` 事件监听器 (`llm/stream`, `agent/request-error`) 封装在 `ctx.effect` 作用域内,确保卸载时自动注销并恢复原始方法 (#238, #239)。
227
+ - **配置密钥角色**: 在 `Config` Schema 中为 `incidentGitHubToken` 和 `webhookActionToken` 增加 `.role('secret')`,避免明文泄露并在 UI 中掩码显示 (#237)。
228
+ - **设置架构与状态**: 在设置卡片中增加原生 `settingsScope` 绑定支持,保留 HTTP 桥接安全回退机制 (#235)。
229
+ - **本地化与文案**: 侧边栏备用项 `settings.section` 标签支持本地化 `t('title')` 并配置 `locale: NS`,并在 `ctx.locale` 中注册 `zh` 中文字典 (#236)。
230
+ - **死代码清理**: 移除 header-chip 迁移后残留的废弃 `mountDashboard` 函数 (#240)。
package/lib/client.js CHANGED
@@ -240,12 +240,13 @@ window.__ModuleLoader__.load({
240
240
  // отдельными отмеченными галочками, чтобы правило нельзя было потерять.
241
241
  const KNOWN_CODES = ['QUOTA', 'RATE_LIMIT', 'SERVER', 'TIMEOUT', 'TRANSPORT', 'EMPTY_RESPONSE', 'UNKNOWN_MODEL', 'AUTH'];
242
242
 
243
- /** Опрос статуса ротации, пока раздел настроек открыт. */
243
+ /** Опрос статуса ротации, пока раздел настроек открыт (smart polling). */
244
244
  function useRotationStatus() {
245
245
  const [byProvider, setByProvider] = React.useState({});
246
246
  React.useEffect(() => {
247
247
  let alive = true;
248
248
  const pull = () => {
249
+ if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;
249
250
  fetch('/dsh-key-rotation/status', { headers: { accept: 'application/json' } })
250
251
  .then((r) => (r.ok ? r.json() : null))
251
252
  .then((data) => {
@@ -258,17 +259,24 @@ window.__ModuleLoader__.load({
258
259
  };
259
260
  pull();
260
261
  const id = setInterval(pull, 4000);
261
- return () => { alive = false; clearInterval(id); };
262
+ const onVis = () => { if (typeof document !== 'undefined' && document.visibilityState === 'visible') pull(); };
263
+ if (typeof document !== 'undefined') document.addEventListener('visibilitychange', onVis);
264
+ return () => {
265
+ alive = false;
266
+ clearInterval(id);
267
+ if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', onVis);
268
+ };
262
269
  }, []);
263
270
  return byProvider;
264
271
  }
265
272
 
266
- /** Последний probe-результат по каждому ключу (#219): /sandbox-cache. */
273
+ /** Последний probe-результат по каждому ключу (#219): /sandbox-cache (smart polling). */
267
274
  function useProbeCache() {
268
275
  const [cache, setCache] = React.useState({});
269
276
  React.useEffect(() => {
270
277
  let alive = true;
271
278
  const pull = () => {
279
+ if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;
272
280
  fetch('/dsh-key-rotation/sandbox-cache', { headers: { accept: 'application/json' } })
273
281
  .then((r) => (r.ok ? r.json() : null))
274
282
  .then((data) => { if (alive && data) setCache(data); })
@@ -276,7 +284,13 @@ window.__ModuleLoader__.load({
276
284
  };
277
285
  pull();
278
286
  const id = setInterval(pull, 4000);
279
- return () => { alive = false; clearInterval(id); };
287
+ const onVis = () => { if (typeof document !== 'undefined' && document.visibilityState === 'visible') pull(); };
288
+ if (typeof document !== 'undefined') document.addEventListener('visibilitychange', onVis);
289
+ return () => {
290
+ alive = false;
291
+ clearInterval(id);
292
+ if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', onVis);
293
+ };
280
294
  }, []);
281
295
  return cache;
282
296
  }
@@ -390,6 +404,20 @@ window.__ModuleLoader__.load({
390
404
  function KeyRotationSection(props) {
391
405
  const DICT = props.locale === 'ru' ? ru : (props.locale === 'zh' ? zh : en);
392
406
  const t = makeT(DICT, en);
407
+ const settingsScope = React.useMemo(() => {
408
+ if (props.ctx && props.ctx.settingsScope) {
409
+ try {
410
+ return props.ctx.settingsScope.bind({ namespace: NS });
411
+ } catch (_) {
412
+ return null;
413
+ }
414
+ }
415
+ return null;
416
+ }, [props.ctx]);
417
+ const scopeSnapshot = React.useSyncExternalStore(
418
+ React.useMemo(() => (cb) => (settingsScope ? settingsScope.subscribe(cb) : () => {}), [settingsScope]),
419
+ React.useCallback(() => (settingsScope ? settingsScope.getSnapshot() : null), [settingsScope])
420
+ );
393
421
  const [state, setState] = React.useState({ status: 'loading', value: null, revision: 0, error: '', providers: [] });
394
422
  const [draft, setDraft] = React.useState(null);
395
423
  // ── all hooks live ABOVE any early return (React error 310 otherwise) ──
@@ -455,7 +483,18 @@ window.__ModuleLoader__.load({
455
483
  .catch((e) => setState((s) => ({ ...s, status: 'error', error: String(e) })));
456
484
  }, []);
457
485
 
458
- React.useEffect(() => { load(); }, [load]);
486
+ React.useEffect(() => {
487
+ if (scopeSnapshot && scopeSnapshot.status === 'ready' && scopeSnapshot.value) {
488
+ setState((s) => ({
489
+ ...s,
490
+ status: 'ready',
491
+ value: scopeSnapshot.value,
492
+ revision: scopeSnapshot.revision ?? s.revision,
493
+ }));
494
+ } else {
495
+ load();
496
+ }
497
+ }, [load, scopeSnapshot]);
459
498
 
460
499
  const val = draft ?? state.value;
461
500
  const status = useRotationStatus();
@@ -650,21 +689,38 @@ window.__ModuleLoader__.load({
650
689
  const save = () => {
651
690
  if (!draft) return;
652
691
  setState((s) => ({ ...s, status: 'saving', error: '' }));
653
- fetch(CONFIG_PATH, {
654
- method: 'PUT',
655
- headers: { 'content-type': 'application/json' },
656
- body: JSON.stringify({ section: draft, expectedRevision: state.revision }),
657
- })
658
- .then((r) => r.json())
659
- .then((data) => {
660
- if (data.error) {
661
- setState((s) => ({ ...s, status: 'error', error: data.error.message }));
662
- return;
663
- }
664
- setState((s) => ({ status: 'ready', value: data.value ?? draft, revision: data.revision ?? s.revision, error: '', providers: s.providers }));
665
- setDraft(null);
692
+ const saveViaBridge = () => {
693
+ fetch(CONFIG_PATH, {
694
+ method: 'PUT',
695
+ headers: { 'content-type': 'application/json' },
696
+ body: JSON.stringify({ section: draft, expectedRevision: state.revision }),
666
697
  })
667
- .catch((e) => setState((s) => ({ ...s, status: 'error', error: String(e) })));
698
+ .then((r) => r.json())
699
+ .then((data) => {
700
+ if (data.error) {
701
+ setState((s) => ({ ...s, status: 'error', error: data.error.message }));
702
+ return;
703
+ }
704
+ setState((s) => ({ status: 'ready', value: data.value ?? draft, revision: data.revision ?? s.revision, error: '', providers: s.providers }));
705
+ setDraft(null);
706
+ })
707
+ .catch((e) => setState((s) => ({ ...s, status: 'error', error: String(e) })));
708
+ };
709
+ if (settingsScope && typeof settingsScope.set === 'function') {
710
+ (async () => {
711
+ try {
712
+ for (const [k, v] of Object.entries(draft)) {
713
+ await settingsScope.set(k, v);
714
+ }
715
+ setState((s) => ({ ...s, status: 'ready', value: draft, error: '' }));
716
+ setDraft(null);
717
+ } catch (_) {
718
+ saveViaBridge();
719
+ }
720
+ })();
721
+ return;
722
+ }
723
+ saveViaBridge();
668
724
  };
669
725
 
670
726
 
@@ -1021,13 +1077,7 @@ window.__ModuleLoader__.load({
1021
1077
  );
1022
1078
  }
1023
1079
 
1024
- function mountDashboard() {
1025
- // Floating dashboard disabled in favor of pinned header chip popover
1026
- if (typeof document !== 'undefined') {
1027
- var el = document.getElementById('krot-dash');
1028
- if (el) el.remove();
1029
- }
1030
- }
1080
+
1031
1081
 
1032
1082
  // #201 header chip: one dot + counts for all pools. Green = all healthy,
1033
1083
  // amber = some keys cooling, red = a pool fully exhausted. Click opens the
@@ -1040,14 +1090,21 @@ window.__ModuleLoader__.load({
1040
1090
  React.useEffect(() => {
1041
1091
  let alive = true;
1042
1092
  const load = () => {
1093
+ if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;
1043
1094
  fetch('/dsh-key-rotation/health', { headers: { accept: 'application/json' }, credentials: 'same-origin' })
1044
1095
  .then((r) => (r.ok ? r.json() : null))
1045
1096
  .then((d) => { if (alive) setSnap(d); })
1046
1097
  .catch(() => {});
1047
1098
  };
1048
1099
  load();
1049
- const id = setInterval(load, 4000);
1050
- return () => { alive = false; clearInterval(id); };
1100
+ const id = setInterval(load, 5000);
1101
+ const onVis = () => { if (typeof document !== 'undefined' && document.visibilityState === 'visible') load(); };
1102
+ if (typeof document !== 'undefined') document.addEventListener('visibilitychange', onVis);
1103
+ return () => {
1104
+ alive = false;
1105
+ clearInterval(id);
1106
+ if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', onVis);
1107
+ };
1051
1108
  }, []);
1052
1109
 
1053
1110
  React.useEffect(() => {
@@ -1096,9 +1153,7 @@ window.__ModuleLoader__.load({
1096
1153
  }
1097
1154
 
1098
1155
  function apply(ctx) {
1099
- ctx.effect(() => ctx.locale.register(NS, { en, ru }), 'dsh-key-rotation: dictionaries');
1100
- // Dashboard widget: lives on every page, polls /health (#152).
1101
- ctx.effect(() => mountDashboard(), 'dsh-key-rotation: dashboard widget');
1156
+ ctx.effect(() => ctx.locale.register(NS, { en, ru, zh }), 'dsh-key-rotation: dictionaries');
1102
1157
  // Header chip (#201): status dot in the session header utilities slot,
1103
1158
  // same slot dsh-gitea / dsh-subscriptions use for their header widgets.
1104
1159
  ctx.effect(() => {
@@ -1144,8 +1199,16 @@ window.__ModuleLoader__.load({
1144
1199
  };
1145
1200
  if (!tryPluginItem()) {
1146
1201
  // Fallback for builds without the Plugins tab slot: keep the sidebar section.
1202
+ const t = ctx.locale ? ctx.locale.bind(NS) : null;
1147
1203
  ctx.slots.inject('settings.section', () => ctx.slots.register(
1148
- { name: 'settings.section', id: 'dsh-key-rotation', order: 20, label: () => 'Key Rotation' },
1204
+ {
1205
+ name: 'settings.section',
1206
+ id: 'dsh-key-rotation',
1207
+ order: 20,
1208
+ locale: NS,
1209
+ label: () => (t ? t('title') : 'Key Rotation') || 'Key Rotation',
1210
+ inject: () => ({ ctx }),
1211
+ },
1149
1212
  (props) => h(KeyRotationSection, { ...props, locale: useLocale() }),
1150
1213
  ));
1151
1214
  }