@goodandready/dsh-subscriptions 0.6.22 → 0.6.24

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
@@ -4,9 +4,6 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} }
5
5
  var exports = module.exports
6
6
  const React = require('react')
7
- const createPortal = (function () {
8
- try { return require('react-dom').createPortal } catch { return null }
9
- })()
10
7
 
11
8
  // Modular translator: shared by component subscriptions and helpers.
12
9
  let t = (key) => key
@@ -21,15 +18,15 @@ window.__ModuleLoader__.load({
21
18
  return { hasError: true, error }
22
19
  }
23
20
  componentDidCatch(error, info) {
24
- try { console.error("[dsh-subscriptions UI error]", error, info) } catch (err) { /* fallback if console fails */ }
21
+ try { console.error("[dsh-subscriptions UI error]", error, info) } catch { /* fallback if console fails */ }
25
22
  }
26
23
  render() {
27
24
  if (this.state.hasError) {
28
25
  return React.createElement(
29
26
  "div",
30
- { style: { padding: "12px", border: "1px solid var(--dsw-alias-state-error-primary, #e5534b)", borderRadius: "8px", background: "var(--dsw-alias-bg-layer-2, #1f1f1f)", margin: "8px 0", fontSize: "13px" } },
31
- React.createElement("div", { style: { fontWeight: 600, color: "var(--dsw-alias-state-error-primary, #e5534b)", marginBottom: "4px" } }, "Subscriptions UI error"),
32
- React.createElement("div", { style: { color: "var(--dsw-alias-label-secondary, #8b949e)", fontSize: "12px", marginBottom: "8px" } }, String(this.state.error && this.state.error.message || this.state.error || "Unknown error")),
27
+ { style: { padding: "12px", border: "1px solid var(--dsw-alias-state-error-primary)", borderRadius: "8px", background: "var(--dsw-alias-bg-layer-2)", margin: "8px 0", fontSize: "13px" } },
28
+ React.createElement("div", { style: { fontWeight: 600, color: "var(--dsw-alias-state-error-primary)", marginBottom: "4px" } }, "Subscriptions UI error"),
29
+ React.createElement("div", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "12px", marginBottom: "8px" } }, String(this.state.error && this.state.error.message || this.state.error || "Unknown error")),
33
30
  React.createElement("button", {
34
31
  className: "dsub-mini",
35
32
  onClick: () => this.setState({ hasError: false, error: null })
@@ -63,7 +60,7 @@ window.__ModuleLoader__.load({
63
60
  '.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}' +
64
61
  '.dsub-helpHead{display:flex;align-items:center;justify-content:space-between;font-weight:600;margin-bottom:8px;color:var(--dsw-alias-brand-primary)}' +
65
62
  '.dsub-helpStep{margin-bottom:6px;color:var(--dsw-alias-label-primary)}' +
66
- '.dsub-btnActive{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-text-contrast,#fff);border-color:var(--dsw-alias-brand-primary)}' +
63
+ '.dsub-btnActive{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-text-contrast);border-color:var(--dsw-alias-brand-primary)}' +
67
64
  '.dsub-h{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
68
65
  '.dsub-sub{font-size:12px;color:var(--dsw-alias-label-secondary)}' +
69
66
  '.dsub-dim{font-size:11.5px;color:var(--dsw-alias-label-tertiary)}' +
@@ -77,7 +74,7 @@ window.__ModuleLoader__.load({
77
74
  '.dsub-inp{height:30px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 8px;font-size:12px;min-width:180px}' +
78
75
  '.dsub-inp:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}' +
79
76
  '.dsub-box{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px;margin-top:8px;background:var(--dsw-alias-bg-layer-2)}' +
80
- '.dsub-cooldown{display:inline-block;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-bg-layer-1,#000);font-size:11px;font-weight:600}' +
77
+ '.dsub-cooldown{display:inline-block;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-bg-layer-1);font-size:11px;font-weight:600}' +
81
78
  '.dsub-bar{position:relative;height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;flex:1;min-width:80px}' +
82
79
  '.dsub-barFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary);transition:width .2s}' +
83
80
  '.dsub-barWarn .dsub-barFill{background:var(--dsw-alias-state-warning-primary)}' +
@@ -91,10 +88,10 @@ window.__ModuleLoader__.load({
91
88
  '.dsub-pill:active{transform:translateY(0)}' +
92
89
  '.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)}' +
93
90
  '.dsub-led{width:7px;height:7px;border-radius:50%;flex:none;transition:all .2s ease}' +
94
- '.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)}' +
95
- '.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)}' +
96
- '.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)}' +
97
- '.dsub-ledOff{background:var(--dsw-alias-label-tertiary,#6b7280)}' +
91
+ '.dsub-ledOk{background:var(--dsw-alias-state-success-primary);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-success-primary) 70%,transparent)}' +
92
+ '.dsub-ledWarn{background:var(--dsw-alias-state-warning-primary);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-warning-primary) 70%,transparent)}' +
93
+ '.dsub-ledBad{background:var(--dsw-alias-state-error-primary);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-error-primary) 70%,transparent)}' +
94
+ '.dsub-ledOff{background:var(--dsw-alias-label-tertiary)}' +
98
95
  '.dsub-modalWrap{position:fixed;inset:0;z-index:10020;display:grid;place-items:center;background:color-mix(in srgb, black 65%, transparent);backdrop-filter:blur(10px);animation:dsubFadeIn .16s ease-out}' +
99
96
  '.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 color-mix(in srgb, black 60%, transparent),0 0 0 1px color-mix(in srgb, white 7%, transparent);padding:20px;animation:dsubScaleIn .18s cubic-bezier(.16,1,.3,1)}' +
100
97
  '@keyframes dsubFadeIn{from{opacity:0}to{opacity:1}}' +
@@ -160,11 +157,11 @@ window.__ModuleLoader__.load({
160
157
  '.dsub-brandBadge{width:32px;height:32px;border-radius:8px;display:grid;place-items:center;font-size:12px;font-weight:700;flex-shrink:0}' +
161
158
  '.dsub-brandCodex{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 15%,transparent);color:var(--dsw-alias-state-success-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary) 30%,transparent)}' +
162
159
  '.dsub-brandClaude{background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 15%,transparent);color:var(--dsw-alias-state-warning-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-warning-primary) 30%,transparent)}' +
163
- '.dsub-brandGrok{background:color-mix(in srgb,#a855f7 15%,transparent);color:#c084fc;border:1px solid color-mix(in srgb,#a855f7 30%,transparent)}' +
160
+ '.dsub-brandGrok{background:color-mix(in srgb,var(--dsw-alias-accent-primary, var(--dsw-alias-brand-primary)) 15%,transparent);color:var(--dsw-alias-accent-primary, var(--dsw-alias-brand-primary));border:1px solid color-mix(in srgb,var(--dsw-alias-accent-primary, var(--dsw-alias-brand-primary)) 30%,transparent)}' +
164
161
  '.dsub-brandAgy{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 15%,transparent);color:var(--dsw-alias-brand-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 30%,transparent)}' +
165
162
  '.dsub-brandOllama{background:color-mix(in srgb,var(--dsw-alias-label-tertiary) 15%,transparent);color:var(--dsw-alias-label-secondary);border:1px solid color-mix(in srgb,var(--dsw-alias-label-tertiary) 30%,transparent)}' +
166
- '.dsub-brandKimi{background:color-mix(in srgb,#ec4899 15%,transparent);color:#f472b6;border:1px solid color-mix(in srgb,#ec4899 30%,transparent)}' +
167
- '.dsub-brandGlm{background:color-mix(in srgb,#14b8a6 15%,transparent);color:#2dd4bf;border:1px solid color-mix(in srgb,#14b8a6 30%,transparent)}' +
163
+ '.dsub-brandKimi{background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 15%,transparent);color:var(--dsw-alias-state-warning-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-warning-primary) 30%,transparent)}' +
164
+ '.dsub-brandGlm{background:color-mix(in srgb,var(--dsw-alias-state-info-primary, var(--dsw-alias-brand-primary)) 15%,transparent);color:var(--dsw-alias-state-info-primary, var(--dsw-alias-brand-primary));border:1px solid color-mix(in srgb,var(--dsw-alias-state-info-primary, var(--dsw-alias-brand-primary)) 30%,transparent)}' +
168
165
  '.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:color-mix(in srgb,var(--dsw-alias-brand-primary) 15%,transparent);color:var(--dsw-alias-brand-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 30%,transparent);margin-left:6px}' +
169
166
 
170
167
  '.dsub-accInfo{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}' +
@@ -176,7 +173,7 @@ window.__ModuleLoader__.load({
176
173
  '.dsub-statusBad{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 15%,transparent);color:var(--dsw-alias-state-error-primary)}' +
177
174
  '.dsub-statusOff{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-tertiary)}' +
178
175
  '.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)}' +
179
- '.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:var(--dsw-alias-text-contrast,#fff);border:0;cursor:pointer;transition:opacity .15s}' +
176
+ '.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);color:var(--dsw-alias-text-contrast);border:0;cursor:pointer;transition:opacity .15s}' +
180
177
  '.dsub-btnPrimary:hover{opacity:.9}' +
181
178
  '.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}' +
182
179
  '.dsub-btnSec:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-tertiary)}' +
@@ -696,7 +693,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
696
693
  if (parsed.error && parsed.error.message) text = parsed.error.message
697
694
  else if (parsed.message) text = parsed.message
698
695
  else if (parsed.code) text = parsed.code
699
- } catch (e) { /* ignore parse error */ }
696
+ } catch { /* ignore parse error */ }
700
697
  }
701
698
  var firstLine = text.split('\n')[0].trim()
702
699
  if (firstLine.length > 160) return firstLine.slice(0, 157) + '...'
@@ -767,7 +764,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
767
764
  function badgeFor(account, t) {
768
765
  if (!account || !account.configured) return t('notConnected')
769
766
  if (account.validationUrl) return t('verifyAccount')
770
- // #303: a quarantined account says why it is parked.
767
+ // Issue 303: a quarantined account says why it is parked.
771
768
  if (account.quarantineReason && account.quarantineUntil && account.quarantineUntil > Date.now()) {
772
769
  return account.quarantineReason === 'VERIFY' ? t('verifyAccount') : String(account.quarantineReason)
773
770
  }
@@ -783,7 +780,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
783
780
  try {
784
781
  const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
785
782
  ChevronIcon = primitives && primitives.IconChevronDownOutline14
786
- } catch (noPrimitives) {
783
+ } catch {
787
784
  ChevronIcon = null
788
785
  }
789
786
  function FallbackChevron(props) {
@@ -863,7 +860,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
863
860
  .then((d) => {
864
861
  const txt = d && d.ok ? JSON.stringify(d.report, null, 2) : ('error: ' + ((d && d.error && d.error.message) || 'unknown'))
865
862
  setDiag(txt)
866
- try { navigator.clipboard.writeText(txt) } catch (err) { /* clipboard write disallowed */ }
863
+ try { navigator.clipboard.writeText(txt) } catch { /* clipboard write disallowed */ }
867
864
  })
868
865
  .catch((e) => setDiag('error: ' + String(e && e.message || e)))
869
866
  }
@@ -937,8 +934,10 @@ const cssId = 'dsh-subscriptions/settings.module.css'
937
934
  const [openHelp, setOpenHelp] = React.useState({})
938
935
  const [err, setErr] = React.useState('')
939
936
  const [info, setInfo] = React.useState('')
937
+ const [revision, setRevision] = React.useState(null)
940
938
 
941
939
  const applyPayload = (data) => {
940
+ if (data && data.revision != null) setRevision(data.revision)
942
941
  setDraft(JSON.parse(JSON.stringify((data && data.config) || {})))
943
942
  setAccounts((data && data.accounts) || [])
944
943
  setProviders((data && data.providers) || [])
@@ -998,13 +997,20 @@ const cssId = 'dsh-subscriptions/settings.module.css'
998
997
 
999
998
  const save = async () => {
1000
999
  setErr(''); setSaved(false)
1000
+ const payload = revision != null ? Object.assign({}, draft, { revision }) : draft
1001
1001
  const res = await fetch('/dsh-subscriptions/config', {
1002
1002
  method: 'PUT',
1003
1003
  headers: { 'Content-Type': 'application/json' },
1004
- body: JSON.stringify(draft),
1004
+ body: JSON.stringify(payload),
1005
1005
  })
1006
1006
  const data = await res.json().catch(() => ({}))
1007
- if (!res.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
1007
+ if (!res.ok) {
1008
+ if (res.status === 409) {
1009
+ reload().catch(() => {})
1010
+ throw new Error((data.error && data.error.message) || 'Configuration modified concurrently; reloaded.')
1011
+ }
1012
+ throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
1013
+ }
1008
1014
  applyPayload(data)
1009
1015
  setSaved(true); setTimeout(() => setSaved(false), 2000)
1010
1016
  }
@@ -1251,7 +1257,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1251
1257
  React.createElement('div', { style: { padding: '8px 0', overflowX: 'auto' } },
1252
1258
  React.createElement('table', { style: { width: '100%', fontSize: '12px', borderCollapse: 'collapse' } },
1253
1259
  React.createElement('thead', null,
1254
- React.createElement('tr', { style: { borderBottom: '1px solid var(--dsw-alias-border-primary, rgba(255,255,255,0.1))', textAlign: 'left', opacity: 0.7 } },
1260
+ React.createElement('tr', { style: { borderBottom: '1px solid var(--dsw-alias-border-primary)', textAlign: 'left', opacity: 0.7 } },
1255
1261
  React.createElement('th', { style: { padding: '6px' } }, t('healthAccount')),
1256
1262
  React.createElement('th', { style: { padding: '6px' } }, t('healthScore')),
1257
1263
  React.createElement('th', { style: { padding: '6px' } }, t('healthLatency')),
@@ -1262,11 +1268,11 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1262
1268
  React.createElement('tbody', null,
1263
1269
  accounts.map((acc, idx) => {
1264
1270
  const sc = acc.healthScore != null ? acc.healthScore : 100
1265
- const healthColor = sc >= 80 ? '#10b981' : (sc >= 50 ? '#f59e0b' : '#ef4444')
1271
+ const healthColor = sc >= 80 ? 'var(--dsw-alias-state-success-primary)' : (sc >= 50 ? 'var(--dsw-alias-state-warning-primary)' : 'var(--dsw-alias-state-error-primary)')
1266
1272
  const isQ = acc.status === 'quarantine' || (acc.quarantineUntil && Number(acc.quarantineUntil) > Date.now())
1267
1273
  const statusLabel = isQ ? ('⛔ ' + t('healthQuarantine')) : (acc.status === 'probing' ? ('🟡 ' + t('healthWarmup')) : (acc.configured ? ('🟢 ' + t('healthActive')) : ('⚪ ' + t('healthIdle'))))
1268
1274
  const pct = acc.usagePercent != null ? Math.round(acc.usagePercent) : null
1269
- return React.createElement('tr', { key: (acc.ref || acc.provider + acc.index || idx), style: { borderBottom: '1px solid var(--dsw-alias-border-secondary, rgba(255,255,255,0.05))' } },
1275
+ return React.createElement('tr', { key: (acc.ref || acc.provider + acc.index || idx), style: { borderBottom: '1px solid var(--dsw-alias-border-secondary)' } },
1270
1276
  React.createElement('td', { style: { padding: '6px', fontWeight: 600 } }, (acc.provider ? acc.provider.toUpperCase() : 'LLM') + ' #' + (acc.index || 1) + (acc.label ? ' (' + acc.label + ')' : '')),
1271
1277
  React.createElement('td', { style: { padding: '6px', color: healthColor } }, sc + '%'),
1272
1278
  React.createElement('td', { style: { padding: '6px' } }, acc.latencyMs ? acc.latencyMs + ' ms' : '—'),
@@ -1481,7 +1487,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1481
1487
  diag ? React.createElement('div', { className: 'dsub-row' },
1482
1488
  React.createElement('button', {
1483
1489
  type: 'button', className: 'dsub-mini',
1484
- onClick: () => { try { navigator.clipboard.writeText(diag) } catch (err) { /* clipboard write error */ } },
1490
+ onClick: () => { try { navigator.clipboard.writeText(diag) } catch { /* clipboard write error */ } },
1485
1491
  }, t('diagCopy')),
1486
1492
  React.createElement('pre', { className: 'dsub-diag' }, diag),
1487
1493
  ) : null,
@@ -1650,7 +1656,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1650
1656
  React.createElement('div', { className: 'dsub-row' },
1651
1657
  React.createElement('button', {
1652
1658
  type: 'button', className: 'dsub-mini',
1653
- onClick: () => { try { navigator.clipboard.writeText(d.userCode) } catch (err) { /* clipboard write error */ } },
1659
+ onClick: () => { try { navigator.clipboard.writeText(d.userCode) } catch { /* clipboard write error */ } },
1654
1660
  }, t('deviceCopy')),
1655
1661
  React.createElement('a', { href: d.authUrl, target: '_blank', rel: 'noopener noreferrer', className: 'dsub-mini' }, t('verifyLink')),
1656
1662
  ),
@@ -1876,7 +1882,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1876
1882
  const addLocale = (locale, dictionary) => {
1877
1883
  try {
1878
1884
  return ctx.locale.register(NS, locale, dictionary)
1879
- } catch (alreadyTaken) {
1885
+ } catch {
1880
1886
  return () => {}
1881
1887
  }
1882
1888
  }
@@ -1889,51 +1895,59 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1889
1895
  // The canonical place is the Plugins tab: a card with its own header
1890
1896
  // and collapse instead of a row in the side list. The registration key must
1891
1897
  // equal the settings namespace, otherwise the tab silently hides the slot.
1892
- try {
1893
- // Plugin-list seat (plugins.item) first: the seat the current core
1894
- // (0.1.6-alpha.2) renders as the plugin's own page with its configuration. The
1895
- // label is a static string on purpose — it is resolved while the page renders,
1896
- // and a locale lookup there would take the whole client batch down with it.
1897
- ctx.slots.inject('plugins.item', () => ctx.slots.register(
1898
- {
1899
- name: 'plugins.item',
1900
- id: ROW_ID,
1901
- order: 60,
1902
- label: () => t('title') || 'Subscriptions',
1903
- locale: NS,
1904
- inject: () => ({ ctx: ctx }),
1905
- },
1906
- PluginCard,
1907
- ))
1908
- // Row seat and the legacy seat stay as fallbacks.
1909
- ctx.slots.inject('plugins.row.config', () => ctx.slots.register(
1910
- {
1911
- name: 'plugins.row.config',
1912
- key: ROW_CONFIG_KEY,
1913
- locale: NS,
1914
- inject: () => ({ ctx: ctx }),
1915
- },
1916
- PluginCard,
1917
- ))
1918
- ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
1919
- {
1920
- name: 'settings.plugin.item',
1921
- key: NS,
1922
- locale: NS,
1923
- inject: () => ({ ctx: ctx }),
1924
- },
1925
- PluginCard,
1926
- ))
1927
- } catch (e) {
1928
- // Slot system not available — expected on headless / non-web profiles.
1929
- }
1898
+ ctx.effect(() => {
1899
+ const cleanups = []
1900
+ try {
1901
+ if (ctx.slots && typeof ctx.slots.inject === 'function') {
1902
+ const un1 = ctx.slots.inject('plugins.item', () => ctx.slots.register(
1903
+ {
1904
+ name: 'plugins.item',
1905
+ id: ROW_ID,
1906
+ order: 60,
1907
+ label: () => t('title') || 'Subscriptions',
1908
+ locale: NS,
1909
+ inject: () => ({ ctx: ctx }),
1910
+ },
1911
+ PluginCard,
1912
+ ))
1913
+ if (typeof un1 === 'function') cleanups.push(un1)
1914
+
1915
+ const un2 = ctx.slots.inject('plugins.row.config', () => ctx.slots.register(
1916
+ {
1917
+ name: 'plugins.row.config',
1918
+ key: ROW_CONFIG_KEY,
1919
+ locale: NS,
1920
+ inject: () => ({ ctx: ctx }),
1921
+ },
1922
+ PluginCard,
1923
+ ))
1924
+ if (typeof un2 === 'function') cleanups.push(un2)
1925
+
1926
+ const un3 = ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
1927
+ {
1928
+ name: 'settings.plugin.item',
1929
+ key: NS,
1930
+ locale: NS,
1931
+ inject: () => ({ ctx: ctx }),
1932
+ },
1933
+ PluginCard,
1934
+ ))
1935
+ if (typeof un3 === 'function') cleanups.push(un3)
1936
+ }
1937
+ } catch {
1938
+ // Slot system not available — expected on headless / non-web profiles.
1939
+ }
1940
+ return () => {
1941
+ for (const off of cleanups) {
1942
+ try { off() } catch { /* best-effort cleanup */ }
1943
+ }
1944
+ }
1945
+ }, 'dsh-subscriptions: settings slots')
1930
1946
  }
1931
1947
 
1932
1948
  // Hot provider switcher in the composer row: a click cycles
1933
1949
  // the active subscription provider for the next request.
1934
- let activeIdx = 0
1935
- let labelEl = null
1936
- const ORDER = ['codex', 'claude', 'grok', 'antigravity']
1950
+ const ORDER = ['codex', 'claude', 'grok', 'antigravity']
1937
1951
 
1938
1952
  async function refreshLoggedIn() {
1939
1953
  try {
@@ -1970,7 +1984,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1970
1984
  windows[key] = { samples: samples.filter((x) => x.at >= now - 24 * 60 * 60 * 1000).slice(-192) }
1971
1985
  }
1972
1986
 
1973
- function fcEstimate(key, remaining, now) {
1987
+ function fcEstimate(key, remaining) {
1974
1988
  const rec = fcSamples.windows[key]
1975
1989
  if (!rec) return { status: 'calibrating' }
1976
1990
  const samples = rec.samples
@@ -2144,7 +2158,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2144
2158
  className: 'dsub-barFillGrad',
2145
2159
  style: {
2146
2160
  width: Math.min(100, Math.max(0, primPct)) + '%',
2147
- background: primPct >= 90 ? 'var(--dsw-alias-state-error-primary,#ef4444)' : (primPct >= 50 ? 'var(--dsw-alias-state-warning-primary,#f59e0b)' : 'var(--dsw-alias-state-success-primary,#10b981)'),
2161
+ background: primPct >= 90 ? 'var(--dsw-alias-state-error-primary)' : (primPct >= 50 ? 'var(--dsw-alias-state-warning-primary)' : 'var(--dsw-alias-state-success-primary)'),
2148
2162
  },
2149
2163
  }),
2150
2164
  ),
@@ -2271,7 +2285,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2271
2285
  try {
2272
2286
  const btn = Array.from(document.querySelectorAll('button')).find((b) => (b.textContent || '').includes(t('title')))
2273
2287
  if (btn) btn.click()
2274
- } catch (err) { /* best-effort */ }
2288
+ } catch { /* best-effort */ }
2275
2289
  },
2276
2290
  }, t('subsOpenSettings')),
2277
2291
  ),
@@ -2290,7 +2304,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2290
2304
  order: 15,
2291
2305
  locale: NS,
2292
2306
  },
2293
- (props) => React.createElement(ErrorBoundary, null, React.createElement(SubsPill, { t })),
2307
+ () => React.createElement(ErrorBoundary, null, React.createElement(SubsPill, { t })),
2294
2308
  ),
2295
2309
  )
2296
2310
  }, 'dsh-subscriptions: subs pill')
@@ -2306,7 +2320,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2306
2320
  order: 5,
2307
2321
  locale: NS,
2308
2322
  },
2309
- (props) => React.createElement(ErrorBoundary, null, React.createElement(ComposerQuota, { t })),
2323
+ () => React.createElement(ErrorBoundary, null, React.createElement(ComposerQuota, { t })),
2310
2324
  ),
2311
2325
  )
2312
2326
  }, 'dsh-subscriptions: composer quota')
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto'
2
2
  import { httpError, readJson } from './wire.js'
3
- import { validationFromLoadCodeAssist, validationFromHttpError, noticeFromLoadCodeAssist, validationRequiredError } from './google-validation.js'
3
+ import { validationFromLoadCodeAssist, noticeFromLoadCodeAssist, validationRequiredError } from './google-validation.js'
4
4
  import { asUsageSnapshot, usageWindows, deepestUsedPercent } from './usage.js'
5
5
 
6
6
  export const CODE_ASSIST_PROD = 'https://cloudcode-pa.googleapis.com/v1internal'
@@ -56,6 +56,8 @@ export const Config = z.object({
56
56
  .description('#84 Composer quota indicator mode: off, percent, bar or forecast (predictive runway from a sliding window).'),
57
57
  codexRedirectUri: z.string().default(''),
58
58
  codexBaseUrl: z.string().default(''),
59
+ codexClientVersion: z.string().default('')
60
+ .description('Codex CLI identity version for /models catalog gating (e.g. 0.157.1). Empty uses the built-in default.'),
59
61
  claudeClientId: z.string().default(''),
60
62
  claudeRedirectUri: z.string().default(''),
61
63
  grokClientId: z.string().default(''),
@@ -64,10 +66,8 @@ export const Config = z.object({
64
66
  grokClientVersion: z.string().default('')
65
67
  .description('Grok CLI identity version header. Empty uses the built-in default.'),
66
68
  antigravityClientId: z.string().default(''),
67
- antigravityClientSecret: z.string().default('')
68
- .description('Google Cloud OAuth Client Secret for Antigravity.'),
69
69
  antigravityRedirectUri: z.string().default(''),
70
- cascadingFallback: z.boolean().default(false)
70
+ cascadingFallback: z.boolean().default(false)
71
71
  .description('#349 Opt-in cross-vendor fallback when all accounts of a provider are exhausted or rate limited.'),
72
72
  cascadingChain: z.any().default({})
73
73
  .description('#349 Custom cross-vendor fallback chains (e.g. { claude: ["copilot", "cursor"] }).'),
@@ -82,11 +82,53 @@ export const Config = z.object({
82
82
  })
83
83
 
84
84
  export function publicConfig(cfg) {
85
+ if (!cfg || typeof cfg !== "object") return cfg
85
86
  const clone = { ...cfg }
86
- // #242: redact all *ClientSecret fields before exposing config via GET /config.
87
- // Secrets must never leave the server; the UI does not need them.
87
+ // #242, #252: redact secret fields before exposing config via GET /config.
88
88
  for (const key of Object.keys(clone)) {
89
- if (key.endsWith('ClientSecret')) clone[key] = clone[key] ? '••••••' : ''
89
+ if (key.endsWith("ClientSecret") || key.endsWith("Secret")) {
90
+ clone[key] = clone[key] ? "••••••" : ""
91
+ }
90
92
  }
93
+
94
+ // Redact credentials in slots (proxyUrl with user:pass)
95
+ if (Array.isArray(clone.slots)) {
96
+ clone.slots = clone.slots.map((s) => {
97
+ if (!s || typeof s !== "object" || !s.proxyUrl) return s
98
+ try {
99
+ const u = new URL(s.proxyUrl)
100
+ if (u.password || u.username) {
101
+ const auth = u.username ? (u.password ? `${u.username}:${u.password}` : u.username) : `:${u.password}`
102
+ const safeAuth = u.username ? `${u.username}:••••••` : `:••••••`
103
+ return { ...s, proxyUrl: s.proxyUrl.replace(auth + "@", safeAuth + "@") }
104
+ }
105
+ } catch { /* ignore non-standard url */ }
106
+ return s
107
+ })
108
+ }
109
+
110
+ // Redact credentials in customVendors (apiKey, token, secret, Authorization headers)
111
+ if (Array.isArray(clone.customVendors)) {
112
+ clone.customVendors = clone.customVendors.map((cv) => {
113
+ if (!cv || typeof cv !== "object") return cv
114
+ const copy = { ...cv }
115
+ if (copy.apiKey) copy.apiKey = "••••••"
116
+ if (copy.token) copy.token = "••••••"
117
+ if (copy.secret) copy.secret = "••••••"
118
+ if (copy.clientSecret) copy.clientSecret = "••••••"
119
+ if (copy.headers && typeof copy.headers === "object") {
120
+ const h = { ...copy.headers }
121
+ for (const hk of Object.keys(h)) {
122
+ const lower = hk.toLowerCase()
123
+ if (lower === "authorization" || lower.includes("key") || lower.includes("token") || lower.includes("secret")) {
124
+ h[hk] = "••••••"
125
+ }
126
+ }
127
+ copy.headers = h
128
+ }
129
+ return copy
130
+ })
131
+ }
132
+
91
133
  return clone
92
134
  }
@@ -0,0 +1,60 @@
1
+ import { maskText } from './mask.js'
2
+ import { normalizeSlots } from './accounts.js'
3
+ import { PROVIDERS } from './refs.js'
4
+
5
+ export function scrubReport(v) {
6
+ if (typeof v === 'string') return maskText(v)
7
+ if (Array.isArray(v)) return v.map(scrubReport)
8
+ if (v && typeof v === 'object') {
9
+ const o = {}
10
+ for (const k of Object.keys(v)) o[k] = scrubReport(v[k])
11
+ return o
12
+ }
13
+ return v
14
+ }
15
+
16
+ export async function createDiagnosticsReport({ live, store, history, NS, pkgVersion }) {
17
+ const cfg = live()
18
+ const slots = normalizeSlots(cfg.slots)
19
+ const mk = () => ({ loggedIn: false, slots: 0, configured: 0, cooldown: 0, proxy: 0, maxUsagePercent: null })
20
+ const providers = {}
21
+ for (const p of PROVIDERS) providers[p] = mk()
22
+ const logged = await store.loggedInProviders()
23
+ for (const slot of slots) {
24
+ const pv = providers[slot.provider] || (providers[slot.provider] = mk())
25
+ pv.slots++
26
+ if (slot.proxyUrl) pv.proxy++
27
+ try {
28
+ const info = await store.describeRef(slot.ref)
29
+ if (info.configured) pv.configured++
30
+ if (info.cooldownUntil && info.cooldownUntil > Date.now()) pv.cooldown++
31
+ if (info.usagePercent != null) pv.maxUsagePercent = Math.max(pv.maxUsagePercent || 0, Math.round(info.usagePercent))
32
+ } catch { /* best-effort */ }
33
+ }
34
+ for (const p of Object.keys(providers)) providers[p].loggedIn = logged.includes(p)
35
+ const rows = history.all()
36
+ const byStatus = {}
37
+ for (const r of rows) {
38
+ const k = (r.provider || '?') + ':' + (r.status || '?')
39
+ byStatus[k] = (byStatus[k] || 0) + 1
40
+ }
41
+ const lastErrors = rows.filter((r) => r.status && r.status >= 400).slice(0, 10)
42
+ .map((r) => ({ ts: r.ts, provider: r.provider, status: r.status, kind: r.kind || 'request', ms: r.ms || null }))
43
+ return scrubReport({
44
+ generatedAt: new Date().toISOString(),
45
+ plugin: NS + (pkgVersion ? ' v' + pkgVersion : ''),
46
+ runtime: { node: process.version, platform: process.platform, arch: process.arch, uptimeSec: Math.round(process.uptime()) },
47
+ providers,
48
+ requests: { total: rows.length, byStatus, lastErrors },
49
+ settings: {
50
+ cooldownMs: cfg.cooldownMs,
51
+ switchAtRemaining: cfg.switchAtRemaining,
52
+ probeIntervalMin: cfg.probeIntervalMin,
53
+ useWebCallback: !!cfg.useWebCallback,
54
+ autoLoopback: !!cfg.autoLoopback,
55
+ privacyMask: !!cfg.privacyMask,
56
+ customVendors: Array.isArray(cfg.customVendors) ? cfg.customVendors.length : 0,
57
+ proxySlots: slots.filter((s) => s.proxyUrl).length,
58
+ },
59
+ })
60
+ }
package/lib/history.js CHANGED
@@ -9,10 +9,11 @@ import { homedir } from 'node:os'
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, debounceMs = 0) {
12
+ constructor(dir = DEFAULT_HISTORY_DIR, ttlMs = 7 * 24 * 60 * 60 * 1000, debounceMs = 0, maxEntries = 1000) {
13
13
  this.dir = dir
14
14
  this.ttlMs = ttlMs
15
15
  this.debounceMs = debounceMs
16
+ this.maxEntries = maxEntries
16
17
  this.path = join(dir, 'history.json')
17
18
  this.rows = []
18
19
  this._persistTimer = null
@@ -39,6 +40,9 @@ export class HistoryStore {
39
40
  const cutoff = Date.now() - this.ttlMs
40
41
  const before = this.rows.length
41
42
  this.rows = this.rows.filter((r) => r && r.ts && r.ts >= cutoff)
43
+ if (this.rows.length > this.maxEntries) {
44
+ this.rows = this.rows.slice(0, this.maxEntries)
45
+ }
42
46
  if (this.rows.length !== before) this._persist()
43
47
  }
44
48
 
@@ -71,12 +75,28 @@ export class HistoryStore {
71
75
  add(entry) {
72
76
  if (!entry || typeof entry !== 'object') return this.rows.length
73
77
  this.rows.unshift({ ts: Date.now(), ...entry })
78
+ if (this.rows.length > this.maxEntries) {
79
+ this.rows = this.rows.slice(0, this.maxEntries)
80
+ }
74
81
  this._prune()
75
82
  this._persist()
76
83
  return this.rows.length
77
84
  }
78
85
 
79
86
  /** Last N entries (newest first). */
87
+ dispose() {
88
+ this.flush()
89
+ if (this._persistTimer) {
90
+ clearTimeout(this._persistTimer)
91
+ this._persistTimer = null
92
+ }
93
+ if (this._exitHandler && typeof process !== "undefined" && typeof process.removeListener === "function") {
94
+ process.removeListener("beforeExit", this._exitHandler)
95
+ this._exitHandler = null
96
+ }
97
+ this.rows = []
98
+ }
99
+
80
100
  recent(n) {
81
101
  return this.rows.slice(0, n)
82
102
  }