@goodandready/dsh-usage-guard 0.1.7 → 0.1.10

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/lib/client.js CHANGED
@@ -22,38 +22,91 @@ window.__ModuleLoader__.load({
22
22
  repairDesc: 'Replace missing, non-numeric, or malformed counters with 0 before summing to prevent NaN history lockups.',
23
23
  report: 'Log diagnostic warnings',
24
24
  reportDesc: 'Log a detailed warning naming the session turn, step, and raw sample whenever damaged usage arrives.',
25
+ maxStepTokens: 'Token Spike Clamping Limit',
26
+ maxStepTokensDesc: 'Clamp abnormally huge step usage counters (e.g., > 1,000,000) before projection calculation to prevent integer overflow and corrupted session summaries. Set 0 to disable.',
25
27
  stats_title: 'Protection Status',
26
28
  stats_mode: 'Current Mode',
27
29
  stats_mode_repair: 'Active Repair (Safe)',
28
30
  stats_mode_report: 'Audit Only (Unsafe)',
29
31
  stats_scope: 'Scope Target',
30
32
  stats_scope_val: 'sessionProjections',
33
+ telemetry_title: 'Live Telemetry',
34
+ telemetry_rescued: 'Rescued Events',
35
+ telemetry_tokens: 'Fixed Tokens',
36
+ telemetry_clamped: 'Clamped Spikes',
37
+ telemetry_last_incident: 'Last Incident',
38
+ telemetry_no_incidents: 'No incidents detected (System healthy)',
39
+ telemetry_refresh: 'Refresh Telemetry',
40
+ telemetry_refreshing: 'Refreshing…',
41
+ updater_title: 'Plugin Version & Updates',
42
+ updater_current: 'Current Version',
43
+ updater_latest: 'Latest Version',
44
+ updater_checking: 'Checking…',
45
+ updater_check: 'Check for Updates',
46
+ updater_updating: 'Updating…',
47
+ updater_update_now: 'Update to {version}',
48
+ updater_up_to_date: 'Plugin is up to date',
49
+ updater_failed: 'Update check failed',
50
+ updater_success: 'Update successful. Please reload page.',
31
51
  };
32
52
 
33
- const ru = {
34
- title: 'Usage Guard',
35
- subtitle: 'Санитайзер расхода токенов и защита от сбоев сессий',
36
- header_desc: 'Защищает проекции истории сессий от повреждённых или отсутствующих счётчиков токенов всех LLM-провайдеров.',
37
- badge_active: 'Защита активна',
38
- badge_report_only: 'Только аудит',
39
- 'settings.loading': 'Загрузка настроек Usage Guard…',
40
- 'settings.unavailable': 'Настройки недоступны (пространство хоста ещё не готово).',
41
- 'settings.retry': 'Повторить попытку',
42
- saved: 'Настройки успешно сохранены',
43
- save: 'Сохранить изменения',
44
- saving: 'Сохранение…',
45
- repair: 'Автоматическая починка счётчиков',
46
- repairDesc: 'Подставлять 0 вместо пропущенных или нечисловых счётчиков до сложения ядра, предотвращая зависание истории с ошибкой NaN.',
47
- report: 'Диагностические предупреждения',
48
- reportDesc: 'Выводить в системный журнал подробную строку с указанием сессии, хода, шага и сырой порции при обнаружении повреждений.',
49
- stats_title: 'Статус защиты',
50
- stats_mode: 'Текущий режим',
51
- stats_mode_repair: 'Активная починка (Безопасно)',
52
- stats_mode_report: 'Только аудит (Небезопасно)',
53
- stats_scope: 'Целевая служба',
53
+ const zh = {
54
+ title: 'Token 用量防护',
55
+ subtitle: 'Token 用量清洗与会话防崩溃保护',
56
+ header_desc: '保护会话历史投影免受所有 LLM 提供商损坏或缺失的 Token 计数器影响。',
57
+ badge_active: '保护生效中',
58
+ badge_report_only: '仅审计',
59
+ 'settings.loading': '正在加载 Usage Guard 设置…',
60
+ 'settings.unavailable': '设置不可用(宿主命名空间尚未就绪)。',
61
+ 'settings.unmonitored': '配置监听不可用。当前使用默认配置运行;变更需重启 DSH 生效。',
62
+ 'settings.retry': '重试',
63
+ saved: '设置保存成功',
64
+ save: '保存更改',
65
+ saving: '正在保存…',
66
+ repair: '自动修复 Token 计数器',
67
+ repairDesc: '在求和前将缺失、非数值或格式错误的计数器替换为 0,防止 NaN 导致会话历史崩溃锁死。',
68
+ report: '记录诊断告警',
69
+ reportDesc: '每当收到损坏的用量数据时,记录详细警告并指出会话轮次、步骤和原始样本。',
70
+ maxStepTokens: '单步 Token 突增削峰阈值',
71
+ maxStepTokensDesc: '在进行投影计算前,对异常巨大的单步用量计数器(例如 > 1,000,000)进行削峰限制,防止整数溢出与会话汇总异常。设为 0 表示禁用。',
72
+ stats_title: '保护状态',
73
+ stats_mode: '当前模式',
74
+ stats_mode_repair: '主动修复(安全)',
75
+ stats_mode_report: '仅审计(不安全)',
76
+ stats_scope: '作用范围',
54
77
  stats_scope_val: 'sessionProjections',
78
+ telemetry_title: '实时拦截指标',
79
+ telemetry_rescued: '已挽救异常事件',
80
+ telemetry_tokens: '已修复 Token 数',
81
+ telemetry_clamped: '削峰拦截次数',
82
+ telemetry_last_incident: '最近一次拦截事件',
83
+ telemetry_no_incidents: '未检测到拦截事件(系统运行健康)',
84
+ telemetry_refresh: '刷新指标',
85
+ telemetry_refreshing: '正在刷新…',
86
+ updater_title: '插件版本与在线更新',
87
+ updater_current: '当前版本',
88
+ updater_latest: '最新版本',
89
+ updater_checking: '正在检查…',
90
+ updater_check: '检查更新',
91
+ updater_updating: '正在更新…',
92
+ updater_update_now: '立即更新至 {version}',
93
+ updater_up_to_date: '已是最新版本',
94
+ updater_failed: '检查更新失败',
95
+ updater_success: '更新成功,请刷新页面。',
55
96
  };
56
97
 
98
+ function getActiveLocale(c) {
99
+ try {
100
+ const snap = (typeof c?.locale?.getLocale === 'function' ? c.locale.getLocale() : null)
101
+ || (typeof c?.locale?.getSnapshot === 'function' ? c.locale.getSnapshot() : null);
102
+ const cur = snap?.active || snap?.locale || '';
103
+ if (typeof cur === 'string' && cur.toLowerCase().startsWith('zh')) return 'zh';
104
+ } catch (err) {
105
+ void err;
106
+ }
107
+ return 'en';
108
+ }
109
+
57
110
  function makeT(dict, fallback) {
58
111
  return function t(key, vars) {
59
112
  let val = (dict && dict[key]) || (fallback && fallback[key]) || key;
@@ -79,7 +132,15 @@ window.__ModuleLoader__.load({
79
132
  })
80
133
  );
81
134
  }
82
- const Chevron = FallbackChevron;
135
+
136
+ let ChevronIcon = null;
137
+ try {
138
+ const primitives = require('@deepseek-ai/dsh-client-ui-primitives');
139
+ ChevronIcon = primitives && primitives.IconChevronDownOutline14;
140
+ } catch (err) {
141
+ void err;
142
+ }
143
+ const Chevron = ChevronIcon || FallbackChevron;
83
144
 
84
145
  function ensureCss() {
85
146
  if (typeof document === 'undefined') return;
@@ -94,9 +155,9 @@ window.__ModuleLoader__.load({
94
155
  .ug-head-sub{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:2px}
95
156
 
96
157
  .ug-badge{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500}
97
- .ug-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}
98
- .ug-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}
99
- .ug-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08)}
158
+ .ug-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-state-success-bg, var(--dsw-alias-bg-layer-2))}
159
+ .ug-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:var(--dsw-alias-state-warning-bg, var(--dsw-alias-bg-layer-2))}
160
+ .ug-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-state-error-bg, var(--dsw-alias-bg-layer-2))}
100
161
 
101
162
  .ug-body{display:flex;flex-direction:column;gap:14px;border-top:1px solid var(--dsw-alias-border-l2);padding-top:16px;margin-top:4px}
102
163
  .ug-field-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:8px;padding:12px 14px;display:flex;flex-direction:column;gap:6px}
@@ -104,21 +165,29 @@ window.__ModuleLoader__.load({
104
165
  .ug-row-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}
105
166
  .ug-field-desc{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.4}
106
167
 
107
- .ug-checkbox{width:18px;height:18px;accent-color:var(--dsw-alias-state-brand-primary, #6366f1);cursor:pointer}
168
+ .ug-checkbox{width:18px;height:18px;accent-color:var(--dsw-alias-state-brand-primary);cursor:pointer}
169
+ .ug-number-input{background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:6px 10px;color:var(--dsw-alias-label-primary);font-size:13px;font-family:inherit;width:140px}
170
+ .ug-number-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}
108
171
 
109
- .ug-stats-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:10px}
172
+ .ug-stats-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(180px, 1fr));gap:10px}
110
173
  .ug-stat-box{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:2px}
111
174
  .ug-stat-val{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}
175
+ .ug-stat-num{font-size:18px;font-weight:700;color:var(--dsw-alias-label-primary);letter-spacing:-0.02em}
112
176
  .ug-stat-lbl{font-size:11px;color:var(--dsw-alias-label-secondary)}
113
177
 
114
178
  .ug-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 16px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}
115
179
  .ug-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}
180
+ .ug-btn-sm{padding:4px 10px;font-size:12px;border-radius:6px}
116
181
  .ug-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
117
182
  .ug-btn-primary:hover:not(:disabled){opacity:0.9}
183
+ .ug-btn-accent{background:var(--dsw-alias-state-brand-primary);color:var(--dsw-alias-label-invert, var(--dsw-alias-bg-layer-3));border-color:transparent}
184
+ .ug-btn-accent:hover:not(:disabled){opacity:0.9}
118
185
  .ug-btn:disabled{opacity:0.5;cursor:not-allowed}
119
186
 
120
- .ug-alert-ok{padding:10px 14px;border-radius:8px;background:rgba(16,185,129,0.1);color:var(--dsw-alias-state-success-primary);font-size:13px;border:1px solid rgba(16,185,129,0.2)}
121
- .ug-alert-bad{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px;border:1px solid rgba(239,68,68,0.2)}
187
+ .ug-alert-ok{padding:10px 14px;border-radius:8px;background:var(--dsw-alias-state-success-bg, var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-success-primary);font-size:13px;border:1px solid var(--dsw-alias-state-success-border, var(--dsw-alias-border-l2))}
188
+ .ug-alert-bad{padding:10px 14px;border-radius:8px;background:var(--dsw-alias-state-error-bg, var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-error-primary);font-size:13px;border:1px solid var(--dsw-alias-state-error-border, var(--dsw-alias-border-l2))}
189
+ .ug-update-banner{margin-top:8px;display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-radius:6px;background:var(--dsw-alias-state-warning-bg, var(--dsw-alias-bg-layer-2));border:1px solid var(--dsw-alias-state-warning-border, var(--dsw-alias-border-l2))}
190
+ .ug-incident-box{margin-top:6px;padding:8px 10px;border-radius:6px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);font-size:12px;font-family:monospace;color:var(--dsw-alias-label-secondary);overflow-x:auto}
122
191
  `;
123
192
  document.head.appendChild(style);
124
193
  }
@@ -149,8 +218,8 @@ window.__ModuleLoader__.load({
149
218
  'button',
150
219
  {
151
220
  type: 'button',
152
- className: 'ug-btn',
153
- style: { marginTop: '8px', fontSize: '12px', padding: '4px 10px' },
221
+ className: 'ug-btn ug-btn-sm',
222
+ style: { marginTop: '8px' },
154
223
  onClick: () => this.setState({ hasError: false, error: null }),
155
224
  },
156
225
  'Retry'
@@ -163,9 +232,222 @@ window.__ModuleLoader__.load({
163
232
  }
164
233
  const ErrorBoundary = createErrorBoundary();
165
234
 
235
+ function TelemetrySection({ t }) {
236
+ const [telemetry, setTelemetry] = React.useState(null);
237
+ const [loading, setLoading] = React.useState(false);
238
+ const [err, setErr] = React.useState(null);
239
+
240
+ const fetchTelemetry = React.useCallback(async () => {
241
+ setLoading(true);
242
+ setErr(null);
243
+ try {
244
+ const res = await fetch('/api/dsh-usage-guard/telemetry', {
245
+ headers: { accept: 'application/json' },
246
+ cache: 'no-store',
247
+ });
248
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
249
+ const data = await res.json();
250
+ setTelemetry(data);
251
+ } catch (e) {
252
+ setErr(e?.message || String(e));
253
+ } finally {
254
+ setLoading(false);
255
+ }
256
+ }, []);
257
+
258
+ React.useEffect(() => {
259
+ fetchTelemetry();
260
+ }, [fetchTelemetry]);
261
+
262
+ const recent = telemetry?.recentIncidents || [];
263
+ const lastIncident = recent.length > 0 ? recent[recent.length - 1] : null;
264
+
265
+ return React.createElement(
266
+ 'div',
267
+ { className: 'ug-field-card' },
268
+ React.createElement(
269
+ 'div',
270
+ { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
271
+ React.createElement('span', { className: 'ug-row-title' }, t('telemetry_title')),
272
+ React.createElement(
273
+ 'button',
274
+ {
275
+ type: 'button',
276
+ className: 'ug-btn ug-btn-sm',
277
+ onClick: fetchTelemetry,
278
+ disabled: loading,
279
+ },
280
+ loading ? t('telemetry_refreshing') : t('telemetry_refresh')
281
+ )
282
+ ),
283
+ err
284
+ ? React.createElement('div', { className: 'ug-alert-bad', style: { padding: '6px 10px', fontSize: '12px' } }, err)
285
+ : null,
286
+ React.createElement(
287
+ 'div',
288
+ { className: 'ug-stats-grid', style: { marginTop: '8px' } },
289
+ React.createElement(
290
+ 'div',
291
+ { className: 'ug-stat-box' },
292
+ React.createElement('span', { className: 'ug-stat-lbl' }, t('telemetry_rescued')),
293
+ React.createElement('span', { className: 'ug-stat-num', style: { color: 'var(--dsw-alias-state-success-primary)' } }, String(telemetry?.rescuedEvents ?? 0))
294
+ ),
295
+ React.createElement(
296
+ 'div',
297
+ { className: 'ug-stat-box' },
298
+ React.createElement('span', { className: 'ug-stat-lbl' }, t('telemetry_tokens')),
299
+ React.createElement('span', { className: 'ug-stat-num', style: { color: 'var(--dsw-alias-state-brand-primary)' } }, Number(telemetry?.fixedTokens ?? 0).toLocaleString())
300
+ ),
301
+ React.createElement(
302
+ 'div',
303
+ { className: 'ug-stat-box' },
304
+ React.createElement('span', { className: 'ug-stat-lbl' }, t('telemetry_clamped')),
305
+ React.createElement('span', { className: 'ug-stat-num', style: { color: 'var(--dsw-alias-state-warning-primary)' } }, String(telemetry?.clampedSpikes ?? 0))
306
+ )
307
+ ),
308
+ React.createElement(
309
+ 'div',
310
+ { style: { marginTop: '8px' } },
311
+ React.createElement('div', { className: 'ug-stat-lbl' }, t('telemetry_last_incident')),
312
+ lastIncident
313
+ ? React.createElement(
314
+ 'div',
315
+ { className: 'ug-incident-box' },
316
+ `[${lastIncident.timestamp}] ${lastIncident.kind || 'incident'} | Turn: ${lastIncident.turn ?? '-'} Step: ${lastIncident.step ?? '-'} | Target: ${lastIncident.target || '-'}`
317
+ )
318
+ : React.createElement(
319
+ 'div',
320
+ { style: { fontSize: '12px', color: 'var(--dsw-alias-state-success-primary)', marginTop: '4px' } },
321
+ `✓ ${t('telemetry_no_incidents')}`
322
+ )
323
+ )
324
+ );
325
+ }
326
+
327
+ function UpdaterSection({ t }) {
328
+ const [status, setStatus] = React.useState(null);
329
+ const [loading, setLoading] = React.useState(false);
330
+ const [updating, setUpdating] = React.useState(false);
331
+ const [feedback, setFeedback] = React.useState({ text: '', ok: true });
332
+
333
+ const checkUpdate = React.useCallback(async () => {
334
+ setLoading(true);
335
+ setFeedback({ text: '', ok: true });
336
+ try {
337
+ const res = await fetch('/api/dsh-usage-guard/update', {
338
+ headers: { accept: 'application/json' },
339
+ cache: 'no-store',
340
+ });
341
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
342
+ const data = await res.json();
343
+ setStatus(data);
344
+ } catch (e) {
345
+ setFeedback({ text: `${t('updater_failed')}: ${e?.message || String(e)}`, ok: false });
346
+ } finally {
347
+ setLoading(false);
348
+ }
349
+ }, [t]);
350
+
351
+ const onUpdateNow = async () => {
352
+ setUpdating(true);
353
+ setFeedback({ text: '', ok: true });
354
+ try {
355
+ const res = await fetch('/api/dsh-usage-guard/update', {
356
+ method: 'POST',
357
+ headers: {
358
+ 'x-dsh-plugin-update': '1',
359
+ 'content-type': 'application/json',
360
+ },
361
+ });
362
+ const data = await res.json();
363
+ if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`);
364
+ setStatus(data);
365
+ setFeedback({ text: t('updater_success'), ok: true });
366
+ } catch (e) {
367
+ setFeedback({ text: e?.message || String(e), ok: false });
368
+ } finally {
369
+ setUpdating(false);
370
+ }
371
+ };
372
+
373
+ React.useEffect(() => {
374
+ checkUpdate();
375
+ }, [checkUpdate]);
376
+
377
+ const currentVer = status?.currentVersion || '0.1.9';
378
+ const latestVer = status?.latestVersion;
379
+ const updateAvailable = !!status?.updateAvailable && !!latestVer && latestVer !== currentVer;
380
+
381
+ return React.createElement(
382
+ 'div',
383
+ { className: 'ug-field-card' },
384
+ React.createElement(
385
+ 'div',
386
+ { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
387
+ React.createElement('span', { className: 'ug-row-title' }, t('updater_title')),
388
+ React.createElement(
389
+ 'button',
390
+ {
391
+ type: 'button',
392
+ className: 'ug-btn ug-btn-sm',
393
+ onClick: checkUpdate,
394
+ disabled: loading || updating,
395
+ },
396
+ loading ? t('updater_checking') : t('updater_check')
397
+ )
398
+ ),
399
+ React.createElement(
400
+ 'div',
401
+ { style: { display: 'flex', alignItems: 'center', gap: '16px', marginTop: '6px', fontSize: '13px' } },
402
+ React.createElement(
403
+ 'div',
404
+ null,
405
+ React.createElement('span', { className: 'ug-stat-lbl' }, t('updater_current') + ': '),
406
+ React.createElement('strong', { style: { color: 'var(--dsw-alias-label-primary)' } }, `v${currentVer}`)
407
+ ),
408
+ latestVer
409
+ ? React.createElement(
410
+ 'div',
411
+ null,
412
+ React.createElement('span', { className: 'ug-stat-lbl' }, t('updater_latest') + ': '),
413
+ React.createElement(
414
+ 'strong',
415
+ { style: { color: updateAvailable ? 'var(--dsw-alias-state-warning-primary)' : 'var(--dsw-alias-state-success-primary)' } },
416
+ `v${latestVer}`
417
+ )
418
+ )
419
+ : null
420
+ ),
421
+ updateAvailable
422
+ ? React.createElement(
423
+ 'div',
424
+ { className: 'ug-update-banner' },
425
+ React.createElement('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-state-warning-primary)', fontWeight: 500 } }, `New release available: v${latestVer}`),
426
+ React.createElement(
427
+ 'button',
428
+ {
429
+ type: 'button',
430
+ className: 'ug-btn ug-btn-sm ug-btn-accent',
431
+ onClick: onUpdateNow,
432
+ disabled: updating,
433
+ },
434
+ updating ? t('updater_updating') : t('updater_update_now', { version: latestVer })
435
+ )
436
+ )
437
+ : null,
438
+ feedback.text
439
+ ? React.createElement(
440
+ 'div',
441
+ { className: feedback.ok ? 'ug-alert-ok' : 'ug-alert-bad', style: { marginTop: '8px', padding: '6px 10px', fontSize: '12px' } },
442
+ feedback.text
443
+ )
444
+ : null
445
+ );
446
+ }
447
+
166
448
  function SettingsPage({ ctx, t }) {
167
449
  const [snap, setSnap] = React.useState({ status: 'loading', value: {} });
168
- const [draft, setDraft] = React.useState({ repair: true, report: true });
450
+ const [draft, setDraft] = React.useState({ repair: true, report: true, maxStepTokens: 1000000 });
169
451
  const [saving, setSaving] = React.useState(false);
170
452
  const [msg, setMsg] = React.useState({ text: '', ok: true });
171
453
  const isDirtyRef = React.useRef(false);
@@ -190,6 +472,7 @@ window.__ModuleLoader__.load({
190
472
  setDraft({
191
473
  repair: res.repair !== false,
192
474
  report: res.report !== false,
475
+ maxStepTokens: typeof res.maxStepTokens === 'number' ? res.maxStepTokens : 1000000,
193
476
  });
194
477
  }
195
478
  }
@@ -198,6 +481,9 @@ window.__ModuleLoader__.load({
198
481
  }
199
482
  };
200
483
  read();
484
+ if (typeof scope.watch !== 'function' && active) {
485
+ setSnap((prev) => ({ ...prev, unmonitored: true }));
486
+ }
201
487
  const unwatch = typeof scope.watch === 'function'
202
488
  ? scope.watch((next) => {
203
489
  if (!active || !next || typeof next !== 'object') return;
@@ -206,13 +492,16 @@ window.__ModuleLoader__.load({
206
492
  setDraft({
207
493
  repair: next.repair !== false,
208
494
  report: next.report !== false,
495
+ maxStepTokens: typeof next.maxStepTokens === 'number' ? next.maxStepTokens : 1000000,
209
496
  });
210
497
  }
211
498
  })
212
499
  : () => {};
213
500
  return () => {
214
501
  active = false;
215
- try { unwatch(); } catch (_) {}
502
+ try { unwatch(); } catch (_) {
503
+ // Intentional cleanup catch: ignore errors if subscription was already disposed
504
+ }
216
505
  };
217
506
  } catch (_) {
218
507
  setSnap({ status: 'unavailable', value: {} });
@@ -243,13 +532,23 @@ window.__ModuleLoader__.load({
243
532
  setDraft((prev) => ({ ...prev, [field]: checked }));
244
533
  };
245
534
 
535
+ const handleNumberChange = (field, rawVal) => {
536
+ isDirtyRef.current = true;
537
+ if (msg.text) {
538
+ if (msgTimerRef.current) clearTimeout(msgTimerRef.current);
539
+ setMsg({ text: '', ok: true });
540
+ }
541
+ const val = rawVal === '' ? 0 : Number(rawVal);
542
+ setDraft((prev) => ({ ...prev, [field]: isNaN(val) ? 0 : val }));
543
+ };
544
+
246
545
  const onSave = async () => {
247
546
  if (!ctx?.settingsScope || typeof ctx.settingsScope.bind !== 'function') return;
248
547
  setSaving(true);
249
548
  try {
250
549
  const scope = ctx.settingsScope.bind({ namespace: NS });
251
550
  const errs = [];
252
- for (const k of ['repair', 'report']) {
551
+ for (const k of ['repair', 'report', 'maxStepTokens']) {
253
552
  try {
254
553
  await scope.set(k, draft[k]);
255
554
  } catch (e) {
@@ -290,6 +589,14 @@ window.__ModuleLoader__.load({
290
589
  'div',
291
590
  { className: 'ug-body' },
292
591
 
592
+ snap.unmonitored
593
+ ? React.createElement(
594
+ 'div',
595
+ { className: 'ug-alert-bad', style: { marginBottom: '12px' } },
596
+ t('settings.unmonitored')
597
+ )
598
+ : null,
599
+
293
600
  // Telemetry / Protection status banner
294
601
  React.createElement(
295
602
  'div',
@@ -353,6 +660,27 @@ window.__ModuleLoader__.load({
353
660
  React.createElement('div', { className: 'ug-field-desc' }, t('reportDesc'))
354
661
  ),
355
662
 
663
+ // Field 3: maxStepTokens
664
+ React.createElement(
665
+ 'div',
666
+ { className: 'ug-field-card' },
667
+ React.createElement(
668
+ 'div',
669
+ { className: 'ug-row' },
670
+ React.createElement('label', { className: 'ug-row-title', htmlFor: 'ug-tokens-input' }, t('maxStepTokens')),
671
+ React.createElement('input', {
672
+ id: 'ug-tokens-input',
673
+ type: 'number',
674
+ className: 'ug-number-input',
675
+ min: 0,
676
+ step: 10000,
677
+ value: draft.maxStepTokens ?? 1000000,
678
+ onChange: (e) => handleNumberChange('maxStepTokens', e.target.value),
679
+ })
680
+ ),
681
+ React.createElement('div', { className: 'ug-field-desc' }, t('maxStepTokensDesc'))
682
+ ),
683
+
356
684
  // Feedback message
357
685
  msg.text
358
686
  ? React.createElement(
@@ -362,7 +690,7 @@ window.__ModuleLoader__.load({
362
690
  )
363
691
  : null,
364
692
 
365
- // Actions
693
+ // Save action
366
694
  React.createElement(
367
695
  'div',
368
696
  { style: { display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '4px' } },
@@ -376,14 +704,23 @@ window.__ModuleLoader__.load({
376
704
  },
377
705
  saving ? t('saving') : t('save')
378
706
  )
379
- )
707
+ ),
708
+
709
+ // Live Telemetry section
710
+ React.createElement(TelemetrySection, { t }),
711
+
712
+ // Updater section
713
+ React.createElement(UpdaterSection, { t })
380
714
  );
381
715
  }
382
716
 
383
717
  // Accordion PluginCard for settings.plugin.item
384
718
  function PluginCard(props) {
385
719
  const [open, setOpen] = React.useState(false);
386
- const t = props?.t || makeT(ru, en);
720
+ const hostCtx = (props && props.ctx) || ctx;
721
+ const locale = getActiveLocale(hostCtx);
722
+ const dict = locale === 'zh' ? zh : en;
723
+ const t = props?.t || makeT(dict, en);
387
724
 
388
725
  React.useEffect(() => {
389
726
  ensureCss();
@@ -430,7 +767,7 @@ window.__ModuleLoader__.load({
430
767
  null,
431
768
  React.createElement(SettingsPage, {
432
769
  ...props,
433
- ctx: (props && props.ctx) || ctx,
770
+ ctx: hostCtx,
434
771
  t,
435
772
  })
436
773
  )
@@ -438,10 +775,10 @@ window.__ModuleLoader__.load({
438
775
  );
439
776
  }
440
777
 
441
- function refreshMirrorUntilVisible(ctx) {
778
+ function refreshMirrorUntilVisible(c) {
442
779
  const visible = () => {
443
780
  try {
444
- const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope;
781
+ const s = (c?.get && c.get('lanSettings')) || c?.settingsScope;
445
782
  const view = s?.describe?.()?.getSnapshot?.()?.view;
446
783
  return !!view && Array.isArray(view.namespaces) && view.namespaces.some((row) => row.ns === NS);
447
784
  } catch (_) {
@@ -457,9 +794,13 @@ window.__ModuleLoader__.load({
457
794
  }
458
795
  tries += 1;
459
796
  try {
460
- const s = (ctx?.get && ctx.get('lanSettings')) || ctx?.settingsScope;
797
+ const s = (c?.get && c.get('lanSettings')) || c?.settingsScope;
461
798
  s?.describe?.()?.load?.();
462
- } catch (_) {}
799
+ } catch (err) {
800
+ if (typeof console !== 'undefined' && console.warn) {
801
+ console.warn('[dsh-usage-guard] settings mirror load attempt failed:', err?.message || err);
802
+ }
803
+ }
463
804
  }, 1000);
464
805
  if (timer && typeof timer.unref === 'function') {
465
806
  timer.unref();
@@ -482,14 +823,14 @@ window.__ModuleLoader__.load({
482
823
  if (ctx.locale && typeof ctx.locale.register === 'function') {
483
824
  if (typeof ctx.effect === 'function') {
484
825
  ctx.effect(() => {
485
- const undo = [addLocale('en', en), addLocale('ru', ru)];
826
+ const undo = [addLocale('en', en), addLocale('zh', zh)];
486
827
  return () => {
487
828
  for (const off of undo) off();
488
829
  };
489
830
  }, 'dsh-usage-guard: dictionaries');
490
831
  } else {
491
832
  addLocale('en', en);
492
- addLocale('ru', ru);
833
+ addLocale('zh', zh);
493
834
  }
494
835
  }
495
836
 
@@ -549,6 +890,7 @@ window.__ModuleLoader__.load({
549
890
  makeT,
550
891
  ensureCss,
551
892
  createErrorBoundary,
893
+ getActiveLocale,
552
894
  };
553
895
  return module.exports;
554
896
  },