@goodandready/dsh-subscriptions 0.6.5 → 0.6.6

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
@@ -186,6 +186,13 @@ const res = await ctx.subscriptions.request('codex', '/backend-api/codex/images/
186
186
  * **Frosted Panel**: Hovering reveals a `backdrop-filter: blur(28px)` panel listing every account with usage bars. Clicking the bubble refreshes quota data; data also refreshes every 60 s.
187
187
  * **Settings Card Polish (`v0.4.18`)**: Chevron switched to the core `IconChevronDownOutline14` primitive; explicit settings snapshot states (loading / unavailable + Retry) guard against phantom input.
188
188
 
189
+ ### 16. ⚡ ClineBot-Inspired UI & Hardened Telemetry (`v0.6.6`)
190
+ * **Live Status Badge Bar**: Instant latency check to the host (`Host online (XX ms)`), real-time connected account indicators, and pool size at a glance.
191
+ * **One-Click Smoke Test (Ping)**: Test live connectivity to the active upstream subscription provider and measure real round-trip latency.
192
+ * **Session Telemetry Dashboard**: Visual stat cards displaying successful/total requests, average latency, session success rate, and last request activity.
193
+ * **Curated Models Catalog**: Quick preview of supported model families with context windows and capability tags (Vision, Reasoning, Hybrid).
194
+ * **Non-Blocking Storage**: `HistoryStore` debounces disk persistence asynchronously to prevent blocking the Node.js event loop during high-throughput streaming.
195
+
189
196
  ---
190
197
 
191
198
  ## 📦 Quick Installation
package/README.ru.md CHANGED
@@ -162,6 +162,13 @@ const res = await ctx.subscriptions.request('codex', '/backend-api/codex/images/
162
162
  * **Reasoning vs Standard**: 429 на reasoning-модели (claude `*thinking*`, grok `*reasoning*`, все codex-модели) охлаждает только семейство reasoning этого аккаунта — standard-модели того же аккаунта продолжают работать. Legacy-кулдауны (от старых версий) по-прежнему блокируют весь аккаунт до истечения.
163
163
  * **Скрыть устаревшие модели (`hideDeprecatedModels`)**: убирает из нативного селектора id с `test`/`preview`/`dev`/`alpha`/`beta`/`legacy` (действует и на живые каталоги, и на статичный фолбэк).
164
164
 
165
+ ### 12. ⚡ Дизайн в стиле ClineBot и неблокирующая телеметрия (`v0.6.6`)
166
+ * **Строка статуса с бейджами**: Мгновенный замер задержки хоста (`Host online (XX ms)`), количество активных подключений и размер пула.
167
+ * **Интерактивный Smoke Test (Ping)**: Проверка связи с провайдером в один клик с измерением реального времени отклика в миллисекундах.
168
+ * **Панель телеметрии сессии**: 4 карточки метрик (успешных/всего запросов, средняя задержка, процент успешности, последняя активность).
169
+ * **Каталог моделей подписок**: Компактный список поддерживаемых моделей с размером контекста (128k, 200k, 1M) и тегами возможностей.
170
+ * **Асинхронная история**: Неблокирующий дебаунсинг записи `HistoryStore`, исключающий просадки event loop при интенсивном стриминге.
171
+
165
172
  ---
166
173
 
167
174
  ## 📦 Быстрая установка
package/lib/client.js CHANGED
@@ -111,6 +111,27 @@ window.__ModuleLoader__.load({
111
111
  '.dsub-details{margin-top:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 12px;background:var(--dsw-alias-bg-layer-2)}' +
112
112
  '.dsub-summary{cursor:pointer;font-weight:600;font-size:12.5px;color:var(--dsw-alias-label-secondary);user-select:none}' +
113
113
  '.dsub-summary:hover{color:var(--dsw-alias-label-primary)}' +
114
+ '.dsub-header-bar{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;padding:8px 0 12px;margin-bottom:8px;border-bottom:1px solid var(--dsw-alias-border-l2)}' +
115
+ '.dsub-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}' +
116
+ '.dsub-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}' +
117
+ '.dsub-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}' +
118
+ '.dsub-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08)}' +
119
+ '.dsub-badge-neutral{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2)}' +
120
+ '.dsub-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px;margin-top:12px}' +
121
+ '.dsub-section-title{font-size:14.5px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;gap:8px}' +
122
+ '.dsub-section-desc{font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:-4px;line-height:1.4}' +
123
+ '.dsub-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:10px}' +
124
+ '.dsub-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:3px}' +
125
+ '.dsub-stat-val{font-size:16.5px;font-weight:700;color:var(--dsw-alias-label-primary)}' +
126
+ '.dsub-stat-lbl{font-size:11.5px;color:var(--dsw-alias-label-secondary)}' +
127
+ '.dsub-alert-ok{padding:8px 12px;border-radius:8px;background:rgba(16,185,129,0.09);color:var(--dsw-alias-state-success-primary);font-size:12.5px;border:1px solid rgba(16,185,129,0.25)}' +
128
+ '.dsub-alert-bad{padding:8px 12px;border-radius:8px;background:rgba(239,68,68,0.09);color:var(--dsw-alias-state-error-primary);font-size:12.5px;border:1px solid rgba(239,68,68,0.25)}' +
129
+ '.dsub-btn-primary{background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;border-color:transparent !important;font-weight:600}' +
130
+ '.dsub-btn-primary:hover:not(:disabled){opacity:0.9}' +
131
+ '.dsub-model-grid{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}' +
132
+ '.dsub-model-chip{display:inline-flex;align-items:center;gap:6px;padding:3px 8px;border-radius:6px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);font-size:11.5px;color:var(--dsw-alias-label-primary)}' +
133
+ '.dsub-model-chip code{font-family:ui-monospace,monospace;font-weight:600}' +
134
+ '.dsub-model-tag{font-size:10px;padding:1px 4px;border-radius:3px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-weight:600}' +
114
135
  '.dsub-heroBars{display:flex;flex-direction:column;gap:8px;margin-top:10px}' +
115
136
  '.dsub-barLabelRow{display:flex;justify-content:space-between;font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-bottom:3px}' +
116
137
  '.dsub-barTrack{height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
@@ -353,6 +374,21 @@ const cssId = 'dsh-subscriptions/settings.module.css'
353
374
  'probeIntervalMinHint': 'Interval for background checks to refresh account status and quota balance.',
354
375
  'notifyLimits': 'Notify on rate limits',
355
376
  'notifyLimitsHint': 'Show notification alerts when account usage reaches quota ceiling.',
377
+ 'hostOnline': 'Host online',
378
+ 'hostOffline': 'Host unreachable',
379
+ 'statsTitle': 'Session Telemetry',
380
+ 'statsRequests': 'Successful / Total Requests',
381
+ 'statsLatency': 'Avg Response Latency',
382
+ 'statsSuccessRate': 'Session Success Rate',
383
+ 'statsLastActivity': 'Last Activity',
384
+ 'smokeTitle': 'Connection Smoke Test (Ping)',
385
+ 'smokeDesc': 'Test live upstream connectivity and measure round-trip latency to the subscription provider.',
386
+ 'runSmoke': '⚡ Run Smoke Test (Ping)',
387
+ 'smokeTesting': 'Pinging…',
388
+ 'smokeOk': 'Smoke test passed',
389
+ 'smokeFail': 'Smoke test failed',
390
+ 'modelsCatalogTitle': 'Curated Subscription Models',
391
+ 'modelsCatalogDesc': 'Available model catalog across active subscriptions in DeepSeek Harness.',
356
392
  'resetCredits': 'Reset credits',
357
393
  'resetCreditsHint': 'ChatGPT reset cards: emergency reset for the 5-hour window. Requires a 5s confirmation.',
358
394
  'resetCreditsAction': 'Reset ChatGPT Limit',
@@ -650,6 +686,32 @@ const cssId = 'dsh-subscriptions/settings.module.css'
650
686
  .catch((e) => setDiag('error: ' + String(e && e.message || e)))
651
687
  }
652
688
  const [checkRes, setCheckRes] = React.useState({})
689
+ const [hostPing, setHostPing] = React.useState({ ok: true, latencyMs: null })
690
+ const [telemetry, setTelemetry] = React.useState(null)
691
+ const [smoke, setSmoke] = React.useState({ testing: false, result: null })
692
+
693
+ const reloadTelemetry = () => {
694
+ fetch('/dsh-subscriptions/telemetry', { cache: 'no-store' })
695
+ .then((r) => r.json())
696
+ .then((d) => { if (d && d.ok && d.telemetry) setTelemetry(d.telemetry) })
697
+ .catch(() => {})
698
+ }
699
+
700
+ const runSmokeTest = async (provider, index) => {
701
+ setSmoke({ testing: true, result: null })
702
+ try {
703
+ const res = await fetch('/dsh-subscriptions/smoke', {
704
+ method: 'POST',
705
+ headers: { 'Content-Type': 'application/json' },
706
+ body: JSON.stringify({ provider, index }),
707
+ })
708
+ const d = await res.json()
709
+ setSmoke({ testing: false, result: d })
710
+ reloadTelemetry()
711
+ } catch (e) {
712
+ setSmoke({ testing: false, result: { ok: false, error: { message: String(e && e.message || e) } } })
713
+ }
714
+ }
653
715
  const [checking, setChecking] = React.useState({})
654
716
  const [saved, setSaved] = React.useState(false)
655
717
  const [tokenDraft, setTokenDraft] = React.useState({})
@@ -663,9 +725,21 @@ const cssId = 'dsh-subscriptions/settings.module.css'
663
725
  setProviders((data && data.providers) || [])
664
726
  }
665
727
 
666
- const reload = () => fetch('/dsh-subscriptions/config', { cache: 'no-store' })
667
- .then((res) => res.json())
668
- .then(applyPayload)
728
+ const reload = () => {
729
+ const t0 = Date.now()
730
+ return fetch('/dsh-subscriptions/config', { cache: 'no-store' })
731
+ .then((res) => res.json())
732
+ .then((data) => {
733
+ setHostPing({ ok: true, latencyMs: Date.now() - t0 })
734
+ applyPayload(data)
735
+ reloadTelemetry()
736
+ return data
737
+ })
738
+ .catch((err) => {
739
+ setHostPing({ ok: false, latencyMs: null })
740
+ throw err
741
+ })
742
+ }
669
743
 
670
744
  React.useEffect(() => {
671
745
  let alive = true
@@ -871,7 +945,59 @@ const cssId = 'dsh-subscriptions/settings.module.css'
871
945
  { id: 'antigravity', name: 'Antigravity' },
872
946
  ]
873
947
 
948
+ const connectedCount = accounts.filter((a) => a.configured).length
949
+
874
950
  return React.createElement('div', { className: 'dsub-wrap' },
951
+ React.createElement('div', { className: 'dsub-header-bar' },
952
+ React.createElement('div', { className: 'dsub-row', style: { marginTop: 0 } },
953
+ React.createElement('span', {
954
+ className: 'dsub-badge ' + (hostPing.ok ? 'dsub-badge-ok' : 'dsub-badge-bad')
955
+ }, hostPing.ok
956
+ ? (hostPing.latencyMs != null ? 'Host online (' + hostPing.latencyMs + ' ms)' : t('hostOnline'))
957
+ : t('hostOffline')
958
+ ),
959
+ React.createElement('span', {
960
+ className: 'dsub-badge ' + (connectedCount > 0 ? 'dsub-badge-ok' : 'dsub-badge-warn')
961
+ }, connectedCount > 0
962
+ ? (connectedCount + ' ' + t('subsLogged'))
963
+ : t('subsNotLogged')
964
+ ),
965
+ React.createElement('span', {
966
+ className: 'dsub-badge dsub-badge-neutral'
967
+ }, 'Pool: ' + accounts.length + ' accounts'),
968
+ ),
969
+ React.createElement('button', {
970
+ type: 'button',
971
+ className: 'dsub-mini dsub-btn-primary',
972
+ disabled: smoke.testing,
973
+ onClick: () => runSmokeTest(),
974
+ }, smoke.testing ? t('smokeTesting') : t('runSmoke')),
975
+ ),
976
+ smoke.result ? React.createElement('div', {
977
+ className: smoke.result.ok ? 'dsub-alert-ok' : 'dsub-alert-bad',
978
+ style: { marginBottom: '12px' }
979
+ }, smoke.result.ok
980
+ ? ('🟢 ' + t('smokeOk') + ': ' + (smoke.result.provider || 'Provider').toUpperCase() + ' · ' + (smoke.result.latencyMs != null ? smoke.result.latencyMs + ' ms' : 'OK') + (smoke.result.label ? ' (' + smoke.result.label + ')' : ''))
981
+ : ('❌ ' + t('smokeFail') + ': ' + (smoke.result.error && (smoke.result.error.message || smoke.result.error.code) || 'Unknown error'))
982
+ ) : null,
983
+ telemetry && telemetry.totalRequests > 0 ? React.createElement('div', { className: 'dsub-grid-2', style: { marginBottom: '14px' } },
984
+ React.createElement('div', { className: 'dsub-stat-box' },
985
+ React.createElement('div', { className: 'dsub-stat-val' }, telemetry.successRequests + ' / ' + telemetry.totalRequests),
986
+ React.createElement('div', { className: 'dsub-stat-lbl' }, t('statsRequests')),
987
+ ),
988
+ React.createElement('div', { className: 'dsub-stat-box' },
989
+ React.createElement('div', { className: 'dsub-stat-val' }, (telemetry.avgLatencyMs || 0) + ' ms'),
990
+ React.createElement('div', { className: 'dsub-stat-lbl' }, t('statsLatency')),
991
+ ),
992
+ React.createElement('div', { className: 'dsub-stat-box' },
993
+ React.createElement('div', { className: 'dsub-stat-val' }, (telemetry.successRate || 100) + '%'),
994
+ React.createElement('div', { className: 'dsub-stat-lbl' }, t('statsSuccessRate')),
995
+ ),
996
+ React.createElement('div', { className: 'dsub-stat-box' },
997
+ React.createElement('div', { className: 'dsub-stat-val' }, telemetry.lastRequestAt ? Math.round((Date.now() - telemetry.lastRequestAt) / 60000) + 'm ago' : '—'),
998
+ React.createElement('div', { className: 'dsub-stat-lbl' }, t('statsLastActivity')),
999
+ ),
1000
+ ) : null,
875
1001
  React.createElement('div', { className: 'dsub-block' },
876
1002
  React.createElement('div', { className: 'dsub-h' }, t('title')),
877
1003
  React.createElement('div', { className: 'dsub-sub' },
@@ -1011,6 +1137,20 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1011
1137
  }),
1012
1138
  ) : null,
1013
1139
  ),
1140
+ React.createElement('details', { className: 'dsub-group' },
1141
+ React.createElement('summary', { className: 'dsub-groupHead' }, '🎯 ' + t('modelsCatalogTitle')),
1142
+ React.createElement('div', { className: 'dsub-sub', style: { marginTop: '4px' } }, t('modelsCatalogDesc')),
1143
+ React.createElement('div', { className: 'dsub-model-grid' },
1144
+ React.createElement('div', { className: 'dsub-model-chip' }, React.createElement('code', null, 'gpt-4o'), React.createElement('span', { className: 'dsub-model-tag' }, '128k · Vision')),
1145
+ React.createElement('div', { className: 'dsub-model-chip' }, React.createElement('code', null, 'o1 / o3-mini'), React.createElement('span', { className: 'dsub-model-tag' }, '200k · Reasoning')),
1146
+ React.createElement('div', { className: 'dsub-model-chip' }, React.createElement('code', null, 'claude-3-7-sonnet'), React.createElement('span', { className: 'dsub-model-tag' }, '200k · Hybrid')),
1147
+ React.createElement('div', { className: 'dsub-model-chip' }, React.createElement('code', null, 'claude-3-5-sonnet'), React.createElement('span', { className: 'dsub-model-tag' }, '200k · Coding')),
1148
+ React.createElement('div', { className: 'dsub-model-chip' }, React.createElement('code', null, 'grok-beta'), React.createElement('span', { className: 'dsub-model-tag' }, '128k · Speed')),
1149
+ React.createElement('div', { className: 'dsub-model-chip' }, React.createElement('code', null, 'gemini-2.5-pro'), React.createElement('span', { className: 'dsub-model-tag' }, '1M · Context')),
1150
+ React.createElement('div', { className: 'dsub-model-chip' }, React.createElement('code', null, 'kimi-k1.5'), React.createElement('span', { className: 'dsub-model-tag' }, '128k · Math')),
1151
+ React.createElement('div', { className: 'dsub-model-chip' }, React.createElement('code', null, 'glm-4-plus'), React.createElement('span', { className: 'dsub-model-tag' }, '128k · Chinese')),
1152
+ ),
1153
+ ),
1014
1154
  React.createElement('details', { className: 'dsub-details' },
1015
1155
  React.createElement('summary', { className: 'dsub-summary' }, t('advancedSettings')),
1016
1156
  React.createElement('div', { className: 'dsub-block' },
package/lib/history.js CHANGED
@@ -4,17 +4,24 @@ import { homedir } from 'node:os'
4
4
 
5
5
  // #65: request and cost history. Stored in
6
6
  // ~/.dsh/storages/dsh-subscriptions/history.json (JSON array, newest first).
7
- // All IO is best-effort and synchronous: a write failure must never crash the harness.
7
+ // All IO is best-effort: a write failure must never crash the harness.
8
8
 
9
9
  export const DEFAULT_HISTORY_DIR = join(homedir(), '.dsh', 'storages', 'dsh-subscriptions')
10
10
 
11
11
  export class HistoryStore {
12
- constructor(dir = DEFAULT_HISTORY_DIR, ttlMs = 7 * 24 * 60 * 60 * 1000) {
12
+ constructor(dir = DEFAULT_HISTORY_DIR, ttlMs = 7 * 24 * 60 * 60 * 1000, debounceMs = 0) {
13
13
  this.dir = dir
14
14
  this.ttlMs = ttlMs
15
+ this.debounceMs = debounceMs
15
16
  this.path = join(dir, 'history.json')
16
17
  this.rows = []
18
+ this._persistTimer = null
17
19
  this._load()
20
+
21
+ if (typeof process !== 'undefined' && typeof process.on === 'function') {
22
+ this._exitHandler = () => this.flush()
23
+ process.on('beforeExit', this._exitHandler)
24
+ }
18
25
  }
19
26
 
20
27
  _load() {
@@ -36,6 +43,24 @@ export class HistoryStore {
36
43
  }
37
44
 
38
45
  _persist() {
46
+ if (this.debounceMs > 0) {
47
+ if (this._persistTimer) return
48
+ this._persistTimer = setTimeout(() => {
49
+ this._persistTimer = null
50
+ this.flush()
51
+ }, this.debounceMs)
52
+ if (typeof this._persistTimer.unref === 'function') this._persistTimer.unref()
53
+ return
54
+ }
55
+ this.flush()
56
+ }
57
+
58
+ /** Write pending updates to disk immediately. */
59
+ flush() {
60
+ if (this._persistTimer) {
61
+ clearTimeout(this._persistTimer)
62
+ this._persistTimer = null
63
+ }
39
64
  try {
40
65
  mkdirSync(this.dir, { recursive: true })
41
66
  writeFileSync(this.path, JSON.stringify(this.rows))
@@ -63,4 +88,55 @@ export class HistoryStore {
63
88
  size() {
64
89
  return this.rows.length
65
90
  }
91
+
92
+ /** Summarize session and 24-hour telemetry for UI stat cards. */
93
+ telemetrySummary() {
94
+ const total = this.rows.length
95
+ if (total === 0) {
96
+ return {
97
+ totalRequests: 0,
98
+ requests24h: 0,
99
+ successRequests: 0,
100
+ errorRequests: 0,
101
+ successRate: 100,
102
+ avgLatencyMs: 0,
103
+ lastLatencyMs: 0,
104
+ lastRequestAt: null,
105
+ lastProvider: null,
106
+ lastModel: null,
107
+ }
108
+ }
109
+ let success = 0
110
+ let error = 0
111
+ let totalMs = 0
112
+ let latencyCount = 0
113
+ const now = Date.now()
114
+ const last24h = now - 24 * 60 * 60 * 1000
115
+ let count24h = 0
116
+
117
+ for (const r of this.rows) {
118
+ if (r.ts >= last24h) count24h++
119
+ const st = Number(r.status || 0)
120
+ if (st >= 200 && st < 400) success++
121
+ else if (st >= 400) error++
122
+ if (typeof r.ms === 'number' && r.ms > 0) {
123
+ totalMs += r.ms
124
+ latencyCount++
125
+ }
126
+ }
127
+
128
+ const first = this.rows[0] // newest first
129
+ return {
130
+ totalRequests: total,
131
+ requests24h: count24h,
132
+ successRequests: success,
133
+ errorRequests: error,
134
+ successRate: total > 0 ? Math.round((success / total) * 100) : 100,
135
+ avgLatencyMs: latencyCount > 0 ? Math.round(totalMs / latencyCount) : 0,
136
+ lastLatencyMs: (first && typeof first.ms === 'number') ? first.ms : 0,
137
+ lastRequestAt: (first && first.ts) || null,
138
+ lastProvider: (first && first.provider) || null,
139
+ lastModel: (first && first.model) || null,
140
+ }
141
+ }
66
142
  }
@@ -32,7 +32,7 @@ export function registerStatusRoutes(ctx, state) {
32
32
  ctx.effect(() => ctx.webServer.register({
33
33
  kind: 'exact',
34
34
  path: '/dsh-subscriptions/config',
35
- handler: async (req, res) => {
35
+ handler: safeJsonHandler(async (req, res) => {
36
36
  if (req.method === 'GET') {
37
37
  writeJson(res, 200, await configResponse())
38
38
  return
@@ -67,10 +67,7 @@ export function registerStatusRoutes(ctx, state) {
67
67
  } catch (e) {
68
68
  writeJson(res, 400, { ok: false, error: { code: 'save', message: String(e && e.message || e) } })
69
69
  }
70
- },
71
-
72
-
73
-
70
+ }),
74
71
  }), 'dsh-subscriptions: /config')
75
72
 
76
73
 
@@ -239,7 +236,7 @@ export function registerStatusRoutes(ctx, state) {
239
236
  ctx.effect(() => ctx.webServer.register({
240
237
  kind: 'exact',
241
238
  path: '/dsh-subscriptions/history',
242
- handler: async (req, res) => {
239
+ handler: safeJsonHandler(async (req, res) => {
243
240
  if (req.method !== 'GET') {
244
241
  writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
245
242
  return
@@ -251,6 +248,127 @@ export function registerStatusRoutes(ctx, state) {
251
248
  }
252
249
  const limit = Math.min(Number(queryOf(req).get('limit') || '10'), 100)
253
250
  writeJson(res, 200, { ok: true, total: history.size(), items: history.recent(limit) })
254
- },
251
+ }),
255
252
  }), 'dsh-subscriptions: /history')
253
+
254
+ // Telemetry summary route for UI stat cards (requests, latency, success rate)
255
+ ctx.effect(() => ctx.webServer.register({
256
+ kind: 'exact',
257
+ path: '/dsh-subscriptions/telemetry',
258
+ handler: safeJsonHandler(async (req, res) => {
259
+ if (req.method !== 'GET') {
260
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
261
+ return
262
+ }
263
+ const summary = typeof history.telemetrySummary === 'function'
264
+ ? history.telemetrySummary()
265
+ : { totalRequests: history.size(), requests24h: 0, successRequests: 0, errorRequests: 0, successRate: 100, avgLatencyMs: 0 }
266
+ writeJson(res, 200, { ok: true, telemetry: summary })
267
+ }),
268
+ }), 'dsh-subscriptions: /telemetry')
269
+
270
+ // Live smoke ping test for active subscription providers
271
+ ctx.effect(() => ctx.webServer.register({
272
+ kind: 'exact',
273
+ path: '/dsh-subscriptions/smoke',
274
+ handler: safeJsonHandler(async (req, res) => {
275
+ if (req.method !== 'POST') {
276
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
277
+ return
278
+ }
279
+ let payload = {}
280
+ try { payload = JSON.parse((await readBody(req, 64 * 1024)).toString('utf8') || '{}') } catch {}
281
+
282
+ let provider = payload.provider
283
+ let index = payload.index || 1
284
+
285
+ if (!provider) {
286
+ const accounts = await accountsView()
287
+ const connected = accounts.find((a) => a.configured)
288
+ if (connected) {
289
+ provider = connected.provider
290
+ index = connected.index || 1
291
+ }
292
+ }
293
+
294
+ if (!provider || !isProvider(provider)) {
295
+ writeJson(res, 400, { ok: false, error: { code: 'no_connected_accounts', message: 'No connected subscription account found to test' } })
296
+ return
297
+ }
298
+
299
+ const ref = oauthRef(provider, index)
300
+ const info = await store.describeRef(ref)
301
+ if (!info.configured) {
302
+ writeJson(res, 400, { ok: false, provider, index, ref, error: { code: 'not_connected', message: 'Account is not connected' } })
303
+ return
304
+ }
305
+
306
+ const t0 = Date.now()
307
+ let blob
308
+ try {
309
+ blob = await store.loadBlob(ref)
310
+ blob = await store.ensureFresh(provider, blob, ref)
311
+ } catch (e) {
312
+ const latencyMs = Date.now() - t0
313
+ history.add({ provider, ref, path: "/smoke", status: 401, ms: latencyMs })
314
+ writeJson(res, 502, { ok: false, provider, index, ref, latencyMs, error: { code: 'auth_failed', message: String(e && e.message || e) } })
315
+ return
316
+ }
317
+
318
+ const vendor = getVendor(provider)
319
+ if (typeof vendor.check === 'function') {
320
+ let capturedQuota = null
321
+ const probeFetch = async (url, init) => {
322
+ const res2 = await fetch(url, init)
323
+ try {
324
+ const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
325
+ if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
326
+ } catch {}
327
+ return res2
328
+ }
329
+ try {
330
+ await vendor.check(blob, vendorConfig(provider, live()), probeFetch)
331
+ const latencyMs = Date.now() - t0
332
+ history.add({ provider, ref, path: "/smoke", status: 200, ms: latencyMs })
333
+ writeJson(res, 200, {
334
+ ok: true,
335
+ provider,
336
+ index,
337
+ ref,
338
+ latencyMs,
339
+ label: pmL(blob.label),
340
+ email: pmE(blob.email),
341
+ quota: capturedQuota || info.quota || null,
342
+ usagePercent: info.usagePercent ?? null,
343
+ })
344
+ return
345
+ } catch (e) {
346
+ const latencyMs = Date.now() - t0
347
+ history.add({ provider, ref, path: "/smoke", status: 502, ms: latencyMs })
348
+ writeJson(res, 502, {
349
+ ok: false,
350
+ provider,
351
+ index,
352
+ ref,
353
+ latencyMs,
354
+ error: { code: e && e.code ? e.code : 'VENDOR', message: String(e && e.message || e).slice(0, 300) },
355
+ })
356
+ return
357
+ }
358
+ }
359
+
360
+ const latencyMs = Date.now() - t0
361
+ history.add({ provider, ref, path: "/smoke", status: 200, ms: latencyMs })
362
+ writeJson(res, 200, {
363
+ ok: true,
364
+ provider,
365
+ index,
366
+ ref,
367
+ latencyMs,
368
+ label: pmL(blob.label),
369
+ email: pmE(blob.email),
370
+ quota: info.quota || null,
371
+ })
372
+ }),
373
+ }), 'dsh-subscriptions: /smoke')
256
374
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "description": "Use ChatGPT Codex, Claude, Grok, Antigravity, Kimi, GLM, Cursor, Kiro, Copilot, Qwen, ERNIE, Spark, JetBrains, Perplexity, Replit, and Cody subscriptions as DeepSeek Harness LLM providers via OAuth.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -34,10 +34,6 @@
34
34
  "bugs": {
35
35
  "url": "https://github.com/GooDAnDReaDY/dsh-subscriptions/issues"
36
36
  },
37
- "scripts": {
38
- "test": "eslint lib/ && node --test test/*.test.mjs",
39
- "typecheck": "tsc -p tsconfig.checkjs.json"
40
- },
41
37
  "dsh": {
42
38
  "bundle": {
43
39
  "patch": "./cordis.patch.yml"
@@ -66,5 +62,9 @@
66
62
  "@deepseek-ai/dsh-llm": "0.1.0-rc.8",
67
63
  "@types/node": "^22.20.2",
68
64
  "typescript": "^7.0.2"
65
+ },
66
+ "scripts": {
67
+ "test": "eslint lib/ && node --test test/*.test.mjs",
68
+ "typecheck": "tsc -p tsconfig.checkjs.json"
69
69
  }
70
- }
70
+ }
File without changes
package/lib/ui-styles.js DELETED
@@ -1,119 +0,0 @@
1
- export const CSS =
2
- '.dsub-card{list-style:none;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;transition:border-color .16s,background .16s}' +
3
- '.dsub-card:hover{border-color:var(--dsw-alias-label-dimmed)}' +
4
- '.dsub-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}' +
5
- '.dsub-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:18px 20px;display:flex}' +
6
- '.dsub-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}' +
7
- '.dsub-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}' +
8
- '.dsub-name{color:var(--dsw-alias-label-primary);font-size:16px;font-weight:600;line-height:1.4}' +
9
- '.dsub-description{color:var(--dsw-alias-label-secondary);font-size:13px}' +
10
- '.dsub-chev{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}' +
11
- '.dsub-chevOpen{transform:rotate(180deg)}' +
12
- '.dsub-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 20px;padding-bottom:12px}' +
13
- '.dsub-block{margin-top:10px}' +
14
- '.dsub-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:8px}' +
15
- '.dsub-grow{flex:1 1 180px;min-width:140px}' +
16
- '.dsub-helpBox{margin-top:8px;padding:12px 14px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);font-size:12px;line-height:1.6}' +
17
- '.dsub-helpHead{display:flex;align-items:center;justify-content:space-between;font-weight:600;margin-bottom:8px;color:var(--dsw-alias-brand-primary)}' +
18
- '.dsub-helpStep{margin-bottom:6px;color:var(--dsw-alias-label-primary)}' +
19
- '.dsub-btnActive{background:var(--dsw-alias-brand-primary);color:#fff;border-color:var(--dsw-alias-brand-primary)}' +
20
- '.dsub-h{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
21
- '.dsub-sub{font-size:12px;color:var(--dsw-alias-label-secondary)}' +
22
- '.dsub-dim{font-size:11.5px;color:var(--dsw-alias-label-tertiary)}' +
23
- '.dsub-mini{appearance:none;font:inherit;font-size:12px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:3px 8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);transition:all .15s}' +
24
- '.dsub-mini:hover{background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-dimmed)}' +
25
- '.dsub-ok{font-size:12px;color:var(--dsw-alias-state-success-primary)}' +
26
- '.dsub-bad{font-size:12px;color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;word-break:break-word;max-width:100%}' +
27
- '.dsub-hint{overflow-wrap:anywhere;word-break:break-word;max-width:100%}' +
28
- '.dsub-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 14px;font-size:13px;font-weight:500;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);transition:all .15s}' +
29
- '.dsub-btn:hover{background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-dimmed)}' +
30
- '.dsub-inp{height:36px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;min-width:180px}' +
31
- '.dsub-inp:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}' +
32
- '.dsub-box{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:12px 14px;margin-top:8px;background:var(--dsw-alias-bg-layer-2)}' +
33
- '.dsub-cooldown{display:inline-block;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-state-warning-primary);color:#000;font-size:11px;font-weight:600}' +
34
- '.dsub-bar{position:relative;height:10px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;flex:1;min-width:80px}' +
35
- '.dsub-barFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary);transition:width .2s}' +
36
- '.dsub-barWarn .dsub-barFill{background:var(--dsw-alias-state-warning-primary)}' +
37
- '.dsub-barFull .dsub-barFill{background:var(--dsw-alias-state-error-primary)}' +
38
- '.dsub-barRow{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary)}' +
39
- '.dsub-manual{display:flex;flex-direction:column;gap:6px;margin-top:4px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2)}' +
40
- '.dsub-verify{font-size:12px;color:var(--dsw-alias-state-warning-primary)}' +
41
- '.dsub-verify a{color:var(--dsw-alias-brand-primary)}' +
42
- '.dsub-pill{display:inline-flex;align-items:center;gap:7px;height:28px;padding:0 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:12px;font-weight:600;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,.15),inset 0 1px 0 rgba(255,255,255,.06);transition:all .16s ease;user-select:none}' +
43
- '.dsub-pill:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary);transform:translateY(-1px);box-shadow:0 3px 8px rgba(0,0,0,.25),inset 0 1px 0 rgba(255,255,255,.1)}' +
44
- '.dsub-pill:active{transform:translateY(0)}' +
45
- '.dsub-pillTag{padding:1px 5px;border-radius:4px;font-size:10.5px;font-weight:700;letter-spacing:.02em;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary)}' +
46
- '.dsub-led{width:7px;height:7px;border-radius:50%;flex:none;transition:all .2s ease}' +
47
- '.dsub-ledOk{background:var(--dsw-alias-state-success-primary,#10b981);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-success-primary) 70%,transparent)}' +
48
- '.dsub-ledWarn{background:var(--dsw-alias-state-warning-primary,#f59e0b);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-warning-primary) 70%,transparent)}' +
49
- '.dsub-ledBad{background:var(--dsw-alias-state-error-primary,#ef4444);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-error-primary) 70%,transparent)}' +
50
- '.dsub-ledOff{background:var(--dsw-alias-label-tertiary,#6b7280)}' +
51
- '.dsub-modalWrap{position:fixed;inset:0;z-index:10020;display:grid;place-items:center;background:rgba(0,0,0,.65);backdrop-filter:blur(10px);animation:dsubFadeIn .16s ease-out}' +
52
- '.dsub-modal{width:min(520px,94vw);max-height:min(85vh,720px);overflow-y:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:16px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);box-shadow:0 24px 64px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.07);padding:20px;animation:dsubScaleIn .18s cubic-bezier(.16,1,.3,1)}' +
53
- '@keyframes dsubFadeIn{from{opacity:0}to{opacity:1}}' +
54
- '@keyframes dsubScaleIn{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}' +
55
- '.dsub-modalHead{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-bottom:14px;border-bottom:1px solid var(--dsw-alias-border-l2)}' +
56
- '.dsub-modalTitleWrap{display:flex;align-items:center;gap:10px}' +
57
- '.dsub-modalIcon{width:30px;height:30px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);display:grid;place-items:center;color:var(--dsw-alias-brand-primary);font-size:14px}' +
58
- '.dsub-modalBadge{font-size:11px;font-weight:600;padding:2px 7px;border-radius:10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary)}' +
59
- '.dsub-heroCard{margin-top:14px;padding:14px 16px;border-radius:12px;background:linear-gradient(135deg,var(--dsw-alias-bg-layer-2) 0%,var(--dsw-alias-bg-layer-3) 100%);border:1px solid var(--dsw-alias-border-l1);box-shadow:inset 0 1px 0 rgba(255,255,255,.05)}' +
60
- '.dsub-heroHead{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}' +
61
- '.dsub-heroTitle{font-size:14px;font-weight:700;display:flex;align-items:center;gap:6px}' +
62
- '.dsub-heroModel{font-size:11.5px;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,monospace}' +
63
- '.dsub-heroBars{display:flex;flex-direction:column;gap:8px;margin-top:10px}' +
64
- '.dsub-barLabelRow{display:flex;justify-content:space-between;font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-bottom:3px}' +
65
- '.dsub-barTrack{height:10px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
66
- '.dsub-barFillGrad{height:100%;border-radius:3px;transition:width .3s ease}' +
67
- '.dsub-panel-box{margin-top:10px;padding:14px 16px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:8px}' +
68
- '.dsub-panel-warn{border-color:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 5%,transparent)}' +
69
- '.dsub-panel-title{font-size:16px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}' +
70
- '.dsub-panel-desc{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}' +
71
- '.dsub-usage-grid{display:flex;flex-direction:column;gap:8px;margin-top:4px}' +
72
- '.dsub-usage-card{display:flex;flex-direction:column;gap:5px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}' +
73
- '.dsub-usage-head{display:flex;justify-content:space-between;align-items:center;font-size:12px;font-weight:500;color:var(--dsw-alias-label-primary)}' +
74
- '.dsub-usage-track{width:100%;height:8px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);overflow:hidden}' +
75
- '.dsub-usage-fill{height:100%;border-radius:999px;transition:width .3s ease}' +
76
- '.dsub-usage-meta{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary)}' +
77
- '.dsub-tag-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 8%,transparent);padding:3px 10px;border-radius:999px;font-size:12px;font-weight:600;border-style:solid;border-width:1px}' +
78
- '.dsub-tag-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 8%,transparent);padding:3px 10px;border-radius:999px;font-size:12px;font-weight:600;border-style:solid;border-width:1px}' +
79
- '.dsub-tag-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,transparent);padding:3px 10px;border-radius:999px;font-size:12px;font-weight:600;border-style:solid;border-width:1px}' +
80
- '.dsub-slot-meta-row{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2);font-size:12px}' +
81
- '.dsub-ref-tag{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}' +
82
- '.dsub-ref-tag code{padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);font-family:ui-monospace,monospace;color:var(--dsw-alias-label-primary);font-size:11px}' +
83
- '.dsub-btn-danger{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent)!important;color:var(--dsw-alias-state-error-primary)!important}' +
84
- '.dsub-btn-danger:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)!important}' +
85
- '.dsub-poolTitle{margin-top:16px;margin-bottom:8px;font-size:11.5px;font-weight:700;color:var(--dsw-alias-label-secondary);text-transform:uppercase;letter-spacing:.05em}' +
86
- '.dsub-accountCard{display:flex;align-items:center;gap:12px;padding:10px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);margin-top:6px;transition:border-color .15s,background .15s}' +
87
- '.dsub-accountCard:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary)}' +
88
- '.dsub-brandBadge{width:32px;height:32px;border-radius:8px;display:grid;place-items:center;font-size:12px;font-weight:700;flex-shrink:0}' +
89
- '.dsub-brandCodex{background:rgba(16,185,129,.15);color:#10b981;border:1px solid rgba(16,185,129,.3)}' +
90
- '.dsub-brandClaude{background:rgba(245,158,11,.15);color:#f59e0b;border:1px solid rgba(245,158,11,.3)}' +
91
- '.dsub-brandGrok{background:rgba(168,85,247,.15);color:#c084fc;border:1px solid rgba(168,85,247,.3)}' +
92
- '.dsub-brandAgy{background:rgba(59,130,246,.15);color:#60a5fa;border:1px solid rgba(59,130,246,.3)}' +
93
- '.dsub-brandOllama{background:rgba(107,114,128,.15);color:#9ca3af;border:1px solid rgba(107,114,128,.3)}' +
94
- '.dsub-brandKimi{background:rgba(236,72,153,.15);color:#f472b6;border:1px solid rgba(236,72,153,.3)}' +
95
- '.dsub-brandGlm{background:rgba(20,184,166,.15);color:#2dd4bf;border:1px solid rgba(20,184,166,.3)}' +
96
- '.dsub-planBadge{display:inline-flex;align-items:center;padding:1px 6px;border-radius:4px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;background:rgba(99,102,241,.15);color:#818cf8;border:1px solid rgba(99,102,241,.3);margin-left:6px}' +
97
-
98
- '.dsub-accInfo{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}' +
99
- '.dsub-accNameRow{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
100
- '.dsub-accName{font-size:13px;font-weight:600}' +
101
- '.dsub-accStatusTag{font-size:10.5px;font-weight:600;padding:1px 6px;border-radius:4px}' +
102
- '.dsub-statusOk{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 15%,transparent);color:var(--dsw-alias-state-success-primary)}' +
103
- '.dsub-statusWarn{background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 15%,transparent);color:var(--dsw-alias-state-warning-primary)}' +
104
- '.dsub-statusBad{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 15%,transparent);color:var(--dsw-alias-state-error-primary)}' +
105
- '.dsub-statusOff{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-tertiary)}' +
106
- '.dsub-modalFoot{display:flex;align-items:center;justify-content:space-between;margin-top:16px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2)}' +
107
- '.dsub-btnPrimary{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;background:var(--dsw-alias-brand-primary,#3b82f6);color:#fff;border:0;cursor:pointer;transition:opacity .15s}' +
108
- '.dsub-btnPrimary:hover{opacity:.9}' +
109
- '.dsub-btnSec{display:inline-flex;align-items:center;gap:5px;padding:5px 10px;border-radius:7px;font-size:11.5px;font-weight:500;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;text-decoration:none;transition:all .15s}' +
110
- '.dsub-btnSec:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-tertiary)}' +
111
- '.dsub-cq{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary);padding:0 6px}' +
112
- '.dsub-cqBar{position:relative;width:40px;height:5px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
113
- '.dsub-cqBarFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary)}' +
114
- '.dsub-cqBarWarn .dsub-cqBarFill{background:var(--dsw-alias-state-warning-primary)}' +
115
- '.dsub-cqBarFull .dsub-cqBarFill{background:var(--dsw-alias-state-error-primary)}' +
116
- '.dsub-cqB{font-variant-numeric:tabular-nums;font-weight:600;color:var(--dsw-alias-label-primary)}' +
117
- '.dsub-diag{font:11px/1.5 ui-monospace,SFMono-Regular,monospace;max-height:260px;overflow:auto;white-space:pre-wrap;word-break:break-word;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}'
118
-
119
- const cssId = 'dsh-subscriptions/settings.module.css'