@goodandready/dsh-subscriptions 0.7.6 → 0.7.8

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.
@@ -6,7 +6,7 @@ import { readFile } from 'node:fs/promises'
6
6
  export function parseArgs(argv) {
7
7
  const args = Array.isArray(argv) ? argv.slice() : []
8
8
  const command = args.shift() || 'status'
9
- const options = { command, csv: false, json: false, type: 'accounts', sessionId: '', mode: 'fast', help: false, setKey: '', setValue: '', packAction: '', packFile: '' }
9
+ const options = { command, csv: false, json: false, type: 'accounts', sessionId: '', mode: 'fast', help: false, setKey: '', setValue: '', packAction: '', packFile: '', vaultAction: '', vaultArg: '' }
10
10
  for (let i = 0; i < args.length; i += 1) {
11
11
  const arg = args[i]
12
12
  if (arg === '--csv') options.csv = true
@@ -18,6 +18,8 @@ export function parseArgs(argv) {
18
18
  else if (command === 'set') options.setValue = arg
19
19
  else if (command === 'packs' && arg === 'add') options.packAction = 'add'
20
20
  else if (command === 'packs' && options.packAction === 'add' && !options.packFile) options.packFile = arg
21
+ else if (command === 'vault' && !options.vaultAction) options.vaultAction = arg
22
+ else if (command === 'vault' && !options.vaultArg) options.vaultArg = arg
21
23
  else if (command === 'fast' && !options.sessionId) options.sessionId = arg
22
24
  else if (command === 'fast') options.mode = arg === 'standard' || arg === 'off' ? 'standard' : 'fast'
23
25
  }
@@ -72,6 +74,15 @@ export function buildRequest(options, env = process.env) {
72
74
  if (options.command === 'sources') return { url: base + '/sources', init: { method: 'GET', headers } }
73
75
  if (options.command === 'packs') return { url: base + '/config', init: { method: 'GET', headers } }
74
76
  if (options.command === 'diagnostics') return { url: base + '/diagnostics', init: { method: 'GET', headers } }
77
+ if (options.command === 'vault') {
78
+ if (options.vaultAction === 'restore') {
79
+ return { url: base + '/vault/snapshots/restore', init: { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ id: options.vaultArg }) } }
80
+ }
81
+ if (options.vaultAction === 'snapshot') {
82
+ return { url: base + '/vault/snapshots', init: { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ label: options.vaultArg || 'manual' }) } }
83
+ }
84
+ return { url: base + '/vault/snapshots', init: { method: 'GET', headers } }
85
+ }
75
86
  throw new Error('unknown command: ' + options.command)
76
87
  }
77
88
 
@@ -112,6 +123,12 @@ export function formatPacks(packs) {
112
123
  return JSON.stringify((Array.isArray(packs) ? packs : []).map(sanitizePack), null, 2) + '\\n'
113
124
  }
114
125
 
126
+ export function formatSnapshots(payload) {
127
+ const rows = (payload && payload.snapshots) || []
128
+ if (!rows.length) return 'no vault snapshots'
129
+ return rows.map((s) => s.id + ' ' + s.createdAt + ' ' + (s.label || '-') + ' ' + s.slotCount + ' slots').join('\n')
130
+ }
131
+
115
132
  export function mergeProviderPack(packs, pack) {
116
133
  const list = (Array.isArray(packs) ? packs : []).filter((p) => p && p.id !== pack.id)
117
134
  return [...list, pack]
@@ -197,6 +214,14 @@ export async function run(argv = process.argv.slice(2), env = process.env, fetch
197
214
  const res = await fetchImpl(request.url, request.init)
198
215
  const text = await res.text()
199
216
  if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + text.slice(0, 500))
217
+ if (options.command === 'vault' && !options.json) {
218
+ try {
219
+ const payload = JSON.parse(text || '{}')
220
+ if (options.vaultAction === 'snapshots' || !options.vaultAction) return { text: formatSnapshots(payload), contentType: 'text/plain' }
221
+ if (options.vaultAction === 'snapshot') return { text: 'created ' + (payload.snapshot && payload.snapshot.id), contentType: 'text/plain' }
222
+ if (options.vaultAction === 'restore') return { text: 'restored ' + (payload.slotCount || 0) + ' slots', contentType: 'text/plain' }
223
+ } catch { /* fall back to raw */ }
224
+ }
200
225
  if (options.command === 'packs' && options.packAction !== 'add') {
201
226
  try {
202
227
  const payload = JSON.parse(text || '{}')
@@ -212,7 +237,7 @@ export async function run(argv = process.argv.slice(2), env = process.env, fetch
212
237
  async function main() {
213
238
  const result = await run()
214
239
  if (result.help) {
215
- process.stdout.write('Usage: dsh-subscriptions <status|usage|fast|config|set|sources|packs|doctor> [options]\n usage --csv --type accounts|calls\n fast <session> [fast|standard]\n config\n set <dot.path> <value>\n sources [--json]\n packs\n packs add <file.json>\n doctor [--json]\n')
240
+ process.stdout.write('Usage: dsh-subscriptions <status|usage|fast|config|set|sources|packs|vault|doctor> [options]\n usage --csv --type accounts|calls\n fast <session> [fast|standard]\n config\n set <dot.path> <value>\n sources [--json]\n packs\n packs add <file.json>\n vault snapshots | vault snapshot [label] | vault restore <id>\n doctor [--json]\n')
216
241
  return
217
242
  }
218
243
  process.stdout.write(result.text.endsWith('\n') ? result.text : result.text + '\n')
package/lib/alerts.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from 'node:fs'
2
2
  import { createHmac } from 'node:crypto'
3
+ import { validateWebhookUrl } from './webhook-url.js'
3
4
 
4
5
  let pkgVersion = ''
5
6
  try {
@@ -14,10 +15,15 @@ try {
14
15
 
15
16
  const MAX_ALERTS = 100
16
17
  let defaultWebhookSecret = ''
18
+ let defaultAllowPrivate = false
17
19
 
18
20
  export function setWebhookSecret(secret) {
19
21
  defaultWebhookSecret = String(secret || '')
20
22
  }
23
+
24
+ export function setWebhookAllowPrivate(allow) {
25
+ defaultAllowPrivate = Boolean(allow)
26
+ }
21
27
  const DEDUP_WINDOW_MS = 10 * 60 * 1000 // 10 minutes cooldown per (ref + type)
22
28
 
23
29
  const alertBuffer = []
@@ -67,9 +73,7 @@ export function getRecentAlerts(limit = 50) {
67
73
 
68
74
  // #696: signed, retrying webhook delivery plus a digest endpoint payload.
69
75
  export async function dispatchWebhookAlert(alert, webhookUrl, fetchImpl = fetch, options = {}) {
70
- if (!alert || !webhookUrl || typeof webhookUrl !== 'string' || !webhookUrl.startsWith('http')) {
71
- return false
72
- }
76
+ if (!alert || !webhookUrl) return false
73
77
  return dispatchNotification({ event: 'subscription_alert', alert }, webhookUrl, fetchImpl, options)
74
78
  }
75
79
 
@@ -101,6 +105,10 @@ async function dispatchNotification(body, webhookUrl, fetchImpl, options) {
101
105
  const opts = options || {}
102
106
  const { retries = 1, timeoutMs = 5000, sleep } = opts
103
107
  const secret = opts.secret == null ? defaultWebhookSecret : opts.secret
108
+ const allowPrivate = opts.allowPrivate == null ? defaultAllowPrivate : Boolean(opts.allowPrivate)
109
+ const checked = validateWebhookUrl(webhookUrl, { allowPrivate })
110
+ if (!checked.ok) return false
111
+ webhookUrl = checked.url
104
112
  const payload = { ...body, source: 'dsh-subscriptions', sentAt: new Date().toISOString() }
105
113
  const raw = JSON.stringify(payload)
106
114
  const headers = {
package/lib/client.js CHANGED
@@ -40,7 +40,7 @@ window.__ModuleLoader__.load({
40
40
 
41
41
 
42
42
  const CSS =
43
- ".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}.dsub-card:hover{border-color:var(--dsw-alias-label-dimmed)}.dsub-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.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:14px 16px;display:flex}.dsub-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.dsub-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.dsub-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.dsub-description{color:var(--dsw-alias-label-secondary);font-size:13px}.dsub-chev{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.dsub-chevOpen{transform:rotate(180deg)}.dsub-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}.dsub-block{margin-top:10px}.dsub-group{margin-top:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:0 14px}.dsub-group[open]{padding-bottom:6px}.dsub-groupHead{cursor:pointer;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);padding:10px 0;list-style:none}.dsub-groupHead::-webkit-details-marker{display:none}.dsub-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:8px}.dsub-grow{flex:1 1 180px;min-width:140px}.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}.dsub-helpHead{display:flex;align-items:center;justify-content:space-between;font-weight:600;margin-bottom:8px;color:var(--dsw-alias-brand-primary)}.dsub-helpStep{margin-bottom:6px;color:var(--dsw-alias-label-primary)}.dsub-btnActive{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-foreground);border-color:var(--dsw-alias-brand-primary)}.dsub-h{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary)}.dsub-sub{font-size:12px;color:var(--dsw-alias-label-secondary)}.dsub-dim{font-size:11.5px;color:var(--dsw-alias-label-tertiary)}.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}.dsub-mini:hover{background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-dimmed)}.dsub-ok{font-size:12px;color:var(--dsw-alias-state-success-primary)}.dsub-bad{font-size:12px;color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;word-break:break-word;max-width:100%}.dsub-hint{overflow-wrap:anywhere;word-break:break-word;max-width:100%}.dsub-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:5px 12px;font-size:12.5px;font-weight:500;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);transition:all .15s}.dsub-btn:hover{background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-dimmed)}.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}.dsub-inp:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}.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)}.dsub-cooldown{display:inline-block;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-bg-layer-1);font-size:11px;font-weight:600}.dsub-bar{position:relative;height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;flex:1;min-width:80px}.dsub-barFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary);transition:width .2s}.dsub-barWarn .dsub-barFill{background:var(--dsw-alias-state-warn-primary)}.dsub-barFull .dsub-barFill{background:var(--dsw-alias-state-error-primary)}.dsub-barRow{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary)}.dsub-manual{display:flex;flex-direction:column;gap:6px;margin-top:4px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2)}.dsub-verify{font-size:12px;color:var(--dsw-alias-state-warn-primary)}.dsub-verify a{color:var(--dsw-alias-brand-primary)}.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 color-mix(in srgb,black 15%,transparent),inset 0 1px 0 color-mix(in srgb,white 6%,transparent);transition:all .16s ease;user-select:none}.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 color-mix(in srgb,black 25%,transparent),inset 0 1px 0 color-mix(in srgb,white 10%,transparent)}.dsub-pill:active{transform:translateY(0)}.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)}.dsub-led{width:7px;height:7px;border-radius:50%;flex:none;transition:all .2s ease}.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)}.dsub-ledWarn{background:var(--dsw-alias-state-warn-primary);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-warn-primary) 70%,transparent)}.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)}.dsub-ledOff{background:var(--dsw-alias-label-tertiary)}.dsub-toast-container{position:fixed;top:16px;right:16px;z-index:10030;display:flex;flex-direction:column;gap:8px;pointer-events:none}.dsub-toast{pointer-events:auto;min-width:280px;max-width:400px;padding:10px 14px;border-radius:10px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);box-shadow:0 8px 24px color-mix(in srgb,black 40%,transparent);display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:12px;line-height:1.4;animation:dsubFadeIn .2s ease-out}.dsub-toast-warn{border-color:var(--dsw-alias-state-warn-primary)}.dsub-toast-bad{border-color:var(--dsw-alias-state-error-primary)}.dsub-toast-ok{border-color:var(--dsw-alias-state-success-primary)}.dsub-toast-close{background:none;border:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:14px;padding:0 2px}.dsub-toast-close:hover{color:var(--dsw-alias-label-primary)}.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}.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)}@keyframes dsubFadeIn{from{opacity:0}to{opacity:1}}@keyframes dsubScaleIn{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}.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)}.dsub-modalTitleWrap{display:flex;align-items:center;gap:10px}.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}.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)}.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 color-mix(in srgb,white 5%,transparent)}.dsub-heroHead{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.dsub-heroTitle{font-size:14px;font-weight:700;display:flex;align-items:center;gap:6px}.dsub-heroModel{font-size:11.5px;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,monospace}.dsub-select{appearance:none;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 24px 0 8px;font-size:12px;cursor:pointer}.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)}.dsub-summary{cursor:pointer;font-weight:600;font-size:12.5px;color:var(--dsw-alias-label-secondary);user-select:none}.dsub-summary:hover{color:var(--dsw-alias-label-primary)}.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)}.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}.dsub-badge-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) 10%,transparent)}.dsub-badge-warn{border-color:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 10%,transparent)}.dsub-badge-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) 10%,transparent)}.dsub-badge-neutral{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2)}.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}.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}.dsub-section-desc{font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:-4px;line-height:1.4}.dsub-grid-2{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px}.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}.dsub-stat-val{font-size:16.5px;font-weight:700;color:var(--dsw-alias-label-primary)}.dsub-stat-lbl{font-size:11.5px;color:var(--dsw-alias-label-secondary)}.dsub-chart-wrap{display:flex;align-items:flex-end;gap:3px;height:52px;padding:6px 0;margin-top:8px;border-bottom:1px solid var(--dsw-alias-border-l3)}.dsub-chart-bar{flex:1;background:var(--dsw-alias-brand-primary);border-radius:2px 2px 0 0;min-height:3px;transition:height .2s ease;opacity:0.85;cursor:pointer}.dsub-chart-bar:hover{opacity:1;background:var(--dsw-alias-state-success-primary)}.dsub-chart-labels{display:flex;justify-content:space-between;font-size:10px;color:var(--dsw-alias-label-secondary);margin-top:3px}.dsub-alert-ok{padding:8px 12px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent);color:var(--dsw-alias-state-success-primary);font-size:12.5px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary) 25%,transparent)}.dsub-alert-bad{padding:8px 12px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary);font-size:12.5px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 25%,transparent)}.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}.dsub-btn-primary:hover:not(:disabled){opacity:0.9}.dsub-model-grid{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}.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)}.dsub-model-chip code{font-family:ui-monospace,monospace;font-weight:600}.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}.dsub-trace-box{padding:8px 12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);margin-top:8px;font-size:12px}.dsub-trace-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:4px 0;border-bottom:1px solid var(--dsw-alias-border-l3)}.dsub-trace-row:last-child{border-bottom:none}.dsub-model-status-available{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent)}.dsub-model-status-degraded{color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 12%,transparent)}.dsub-model-status-quarantined{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)}.dsub-model-status-cooldown{color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent)}.dsub-model-status-exhausted{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)}.dsub-model-status-unconfigured{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-3)}.dsub-heroBars{display:flex;flex-direction:column;gap:8px;margin-top:10px}.dsub-barLabelRow{display:flex;justify-content:space-between;font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-bottom:3px}.dsub-barTrack{height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}.dsub-barFillGrad{height:100%;border-radius:3px;transition:width .3s ease}.dsub-panel-box{margin-top:10px;padding:12px 14px;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}.dsub-panel-warn{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 5%,transparent)}.dsub-panel-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}.dsub-panel-desc{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}.dsub-usage-grid{display:flex;flex-direction:column;gap:8px;margin-top:4px}.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)}.dsub-usage-head{display:flex;justify-content:space-between;align-items:center;font-size:12px;font-weight:500;color:var(--dsw-alias-label-primary)}.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}.dsub-usage-fill{height:100%;border-radius:999px;transition:width .3s ease}.dsub-usage-meta{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary)}.dsub-tag-ok{border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 30%,transparent);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}.dsub-tag-warn{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 40%,transparent);color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}.dsub-tag-bad{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}.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}.dsub-ref-tag{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}.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}.dsub-speed-tag{display:inline-flex;align-items:center;gap:4px;font-size:11.5px;color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent);padding:2px 8px;border-radius:6px;font-weight:500;border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 20%,transparent)}.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}.dsub-btn-danger:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)!important}.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}.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}.dsub-accountCard:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary)}.dsub-brandBadge{width:32px;height:32px;border-radius:8px;display:grid;place-items:center;font-size:12px;font-weight:700;flex-shrink:0}.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)}.dsub-brandClaude{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 15%,transparent);color:var(--dsw-alias-state-warn-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary) 30%,transparent)}.dsub-brandGrok{background:color-mix(in srgb,var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary)) 15%,transparent);color:var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary));border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary)) 30%,transparent)}.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)}.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)}.dsub-brandKimi{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 15%,transparent);color:var(--dsw-alias-state-warn-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary) 30%,transparent)}.dsub-brandGlm{background:color-mix(in srgb,var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary)) 15%,transparent);color:var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary));border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary)) 30%,transparent)}.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}.dsub-accInfo{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}.dsub-accNameRow{display:flex;align-items:center;justify-content:space-between;gap:8px}.dsub-accName{font-size:13px;font-weight:600}.dsub-accStatusTag{font-size:10.5px;font-weight:600;padding:1px 6px;border-radius:4px}.dsub-statusOk{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 15%,transparent);color:var(--dsw-alias-state-success-primary)}.dsub-statusWarn{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 15%,transparent);color:var(--dsw-alias-state-warn-primary)}.dsub-statusBad{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 15%,transparent);color:var(--dsw-alias-state-error-primary)}.dsub-statusOff{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-tertiary)}.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)}.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-label-primary-foreground);border:0;cursor:pointer;transition:opacity .15s}.dsub-btnPrimary:hover{opacity:.9}.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}.dsub-btnSec:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-tertiary)}.dsub-cq{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary);padding:0 6px}.dsub-cqBar{position:relative;width:40px;height:5px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}.dsub-cqBarFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary)}.dsub-cqBarWarn .dsub-cqBarFill{background:var(--dsw-alias-state-warn-primary)}.dsub-cqBarFull .dsub-cqBarFill{background:var(--dsw-alias-state-error-primary)}.dsub-cqB{font-variant-numeric:tabular-nums;font-weight:600;color:var(--dsw-alias-label-primary)}.dsub-qw{position:fixed;z-index:10010;display:flex;align-items:center;gap:6px;height:30px;padding:0 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);box-shadow:0 4px 14px color-mix(in srgb,black 28%,transparent);cursor:grab;user-select:none;font-size:11.5px;font-weight:600;touch-action:none}.dsub-qw:active{cursor:grabbing}.dsub-qwDocked{right:16px !important;bottom:16px !important;cursor:default}.dsub-qwPct{font-variant-numeric:tabular-nums}.dsub-qwBar{width:28px;height:4px;border-radius:2px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}.dsub-qwBarFill{height:100%;border-radius:2px;background:var(--dsw-alias-state-success-primary)}.dsub-qwWarn .dsub-qwBarFill{background:var(--dsw-alias-state-warn-primary)}.dsub-qwFull .dsub-qwBarFill{background:var(--dsw-alias-state-error-primary)}.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)}";
43
+ ".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}.dsub-card:hover{border-color:var(--dsw-alias-label-dimmed)}.dsub-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.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:14px 16px;display:flex}.dsub-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.dsub-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.dsub-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.dsub-description{color:var(--dsw-alias-label-secondary);font-size:13px}.dsub-chev{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.dsub-chevOpen{transform:rotate(180deg)}.dsub-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}.dsub-block{margin-top:10px}.dsub-group{margin-top:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:0 14px}.dsub-group[open]{padding-bottom:6px}.dsub-groupHead{cursor:pointer;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);padding:10px 0;list-style:none}.dsub-groupHead::-webkit-details-marker{display:none}.dsub-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-top:8px}.dsub-grow{flex:1 1 180px;min-width:140px}.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}.dsub-helpHead{display:flex;align-items:center;justify-content:space-between;font-weight:600;margin-bottom:8px;color:var(--dsw-alias-brand-primary)}.dsub-helpStep{margin-bottom:6px;color:var(--dsw-alias-label-primary)}.dsub-btnActive{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-foreground);border-color:var(--dsw-alias-brand-primary)}.dsub-h{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary)}.dsub-sub{font-size:12px;color:var(--dsw-alias-label-secondary)}.dsub-dim{font-size:11.5px;color:var(--dsw-alias-label-tertiary)}.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}.dsub-mini:hover{background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-dimmed)}.dsub-ok{font-size:12px;color:var(--dsw-alias-state-success-primary)}.dsub-bad{font-size:12px;color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;word-break:break-word;max-width:100%}.dsub-hint{overflow-wrap:anywhere;word-break:break-word;max-width:100%}.dsub-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:5px 12px;font-size:12.5px;font-weight:500;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);transition:all .15s}.dsub-btn:hover{background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-dimmed)}.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}.dsub-inp:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}.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)}.dsub-cooldown{display:inline-block;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-bg-layer-1);font-size:11px;font-weight:600}.dsub-bar{position:relative;height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;flex:1;min-width:80px}.dsub-barFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary);transition:width .2s}.dsub-barWarn .dsub-barFill{background:var(--dsw-alias-state-warn-primary)}.dsub-barFull .dsub-barFill{background:var(--dsw-alias-state-error-primary)}.dsub-barRow{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary)}.dsub-manual{display:flex;flex-direction:column;gap:6px;margin-top:4px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2)}.dsub-verify{font-size:12px;color:var(--dsw-alias-state-warn-primary)}.dsub-verify a{color:var(--dsw-alias-brand-primary)}.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 color-mix(in srgb,black 15%,transparent),inset 0 1px 0 color-mix(in srgb,white 6%,transparent);transition:all .16s ease;user-select:none}.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 color-mix(in srgb,black 25%,transparent),inset 0 1px 0 color-mix(in srgb,white 10%,transparent)}.dsub-pill:active{transform:translateY(0)}.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)}.dsub-led{width:7px;height:7px;border-radius:50%;flex:none;transition:all .2s ease}.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)}.dsub-ledWarn{background:var(--dsw-alias-state-warn-primary);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-warn-primary) 70%,transparent)}.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)}.dsub-ledOff{background:var(--dsw-alias-label-tertiary)}.dsub-toast-container{position:fixed;top:16px;right:16px;z-index:10030;display:flex;flex-direction:column;gap:8px;pointer-events:none}.dsub-toast{pointer-events:auto;min-width:280px;max-width:400px;padding:10px 14px;border-radius:10px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);box-shadow:0 8px 24px color-mix(in srgb,black 40%,transparent);display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:12px;line-height:1.4;animation:dsubFadeIn .2s ease-out}.dsub-toast-warn{border-color:var(--dsw-alias-state-warn-primary)}.dsub-toast-bad{border-color:var(--dsw-alias-state-error-primary)}.dsub-toast-ok{border-color:var(--dsw-alias-state-success-primary)}.dsub-toast-close{background:none;border:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:14px;padding:0 2px}.dsub-toast-close:hover{color:var(--dsw-alias-label-primary)}.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}.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)}@keyframes dsubFadeIn{from{opacity:0}to{opacity:1}}@keyframes dsubScaleIn{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}.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)}.dsub-modalTitleWrap{display:flex;align-items:center;gap:10px}.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}.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)}.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 color-mix(in srgb,white 5%,transparent)}.dsub-heroHead{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.dsub-heroTitle{font-size:14px;font-weight:700;display:flex;align-items:center;gap:6px}.dsub-heroModel{font-size:11.5px;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,monospace}.dsub-select{appearance:none;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 24px 0 8px;font-size:12px;cursor:pointer}.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)}.dsub-summary{cursor:pointer;font-weight:600;font-size:12.5px;color:var(--dsw-alias-label-secondary);user-select:none}.dsub-summary:hover{color:var(--dsw-alias-label-primary)}.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)}.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}.dsub-badge-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) 10%,transparent)}.dsub-badge-warn{border-color:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 10%,transparent)}.dsub-badge-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) 10%,transparent)}.dsub-badge-neutral{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2)}.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}.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}.dsub-section-desc{font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:-4px;line-height:1.4}.dsub-grid-2{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px}.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}.dsub-stat-val{font-size:16.5px;font-weight:700;color:var(--dsw-alias-label-primary)}.dsub-stat-lbl{font-size:11.5px;color:var(--dsw-alias-label-secondary)}.dsub-chart-wrap{display:flex;align-items:flex-end;gap:3px;height:52px;padding:6px 0;margin-top:8px;border-bottom:1px solid var(--dsw-alias-border-l3)}.dsub-chart-bar{flex:1;background:var(--dsw-alias-brand-primary);border-radius:2px 2px 0 0;min-height:3px;transition:height .2s ease;opacity:0.85;cursor:pointer}.dsub-chart-bar:hover{opacity:1;background:var(--dsw-alias-state-success-primary)}.dsub-chart-labels{display:flex;justify-content:space-between;font-size:10px;color:var(--dsw-alias-label-secondary);margin-top:3px}.dsub-alert-ok{padding:8px 12px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent);color:var(--dsw-alias-state-success-primary);font-size:12.5px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary) 25%,transparent)}.dsub-alert-bad{padding:8px 12px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary);font-size:12.5px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 25%,transparent)}.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}.dsub-btn-primary:hover:not(:disabled){opacity:0.9}.dsub-model-grid{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}.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)}.dsub-model-chip code{font-family:ui-monospace,monospace;font-weight:600}.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}.dsub-trace-box{padding:8px 12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);margin-top:8px;font-size:12px}.dsub-trace-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:4px 0;border-bottom:1px solid var(--dsw-alias-border-l3)}.dsub-trace-row:last-child{border-bottom:none}.dsub-model-status-available{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent)}.dsub-model-status-degraded{color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 12%,transparent)}.dsub-model-status-quarantined{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)}.dsub-model-status-cooldown{color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent)}.dsub-model-status-exhausted{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)}.dsub-model-status-unconfigured{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-3)}.dsub-heroBars{display:flex;flex-direction:column;gap:8px;margin-top:10px}.dsub-barLabelRow{display:flex;justify-content:space-between;font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-bottom:3px}.dsub-barTrack{height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}.dsub-barFillGrad{height:100%;border-radius:3px;transition:width .3s ease}.dsub-panel-box{margin-top:10px;padding:12px 14px;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}.dsub-panel-warn{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 5%,transparent)}.dsub-panel-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}.dsub-panel-desc{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}.dsub-usage-grid{display:flex;flex-direction:column;gap:8px;margin-top:4px}.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)}.dsub-usage-head{display:flex;justify-content:space-between;align-items:center;font-size:12px;font-weight:500;color:var(--dsw-alias-label-primary)}.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}.dsub-usage-fill{height:100%;border-radius:999px;transition:width .3s ease}.dsub-usage-meta{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary)}.dsub-tag-ok{border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 30%,transparent);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}.dsub-tag-warn{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 40%,transparent);color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}.dsub-tag-bad{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}.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}.dsub-ref-tag{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}.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}.dsub-speed-tag{display:inline-flex;align-items:center;gap:4px;font-size:11.5px;color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent);padding:2px 8px;border-radius:6px;font-weight:500;border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 20%,transparent)}.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}.dsub-btn-danger:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)!important}.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}.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}.dsub-accountCard:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary)}.dsub-brandBadge{width:32px;height:32px;border-radius:8px;display:grid;place-items:center;font-size:12px;font-weight:700;flex-shrink:0}.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)}.dsub-brandClaude{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 15%,transparent);color:var(--dsw-alias-state-warn-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary) 30%,transparent)}.dsub-brandGrok{background:color-mix(in srgb,var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary)) 15%,transparent);color:var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary));border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary)) 30%,transparent)}.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)}.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)}.dsub-brandKimi{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 15%,transparent);color:var(--dsw-alias-state-warn-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary) 30%,transparent)}.dsub-brandGlm{background:color-mix(in srgb,var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary)) 15%,transparent);color:var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary));border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary)) 30%,transparent)}.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}.dsub-accInfo{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}.dsub-accNameRow{display:flex;align-items:center;justify-content:space-between;gap:8px}.dsub-accName{font-size:13px;font-weight:600}.dsub-accStatusTag{font-size:10.5px;font-weight:600;padding:1px 6px;border-radius:4px}.dsub-statusOk{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 15%,transparent);color:var(--dsw-alias-state-success-primary)}.dsub-statusWarn{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 15%,transparent);color:var(--dsw-alias-state-warn-primary)}.dsub-statusBad{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 15%,transparent);color:var(--dsw-alias-state-error-primary)}.dsub-statusOff{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-tertiary)}.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)}.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-label-primary-foreground);border:0;cursor:pointer;transition:opacity .15s}.dsub-btnPrimary:hover{opacity:.9}.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}.dsub-btnSec:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-tertiary)}.dsub-cq{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary);padding:0 6px}.dsub-cqBar{position:relative;width:40px;height:5px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}.dsub-cqBarFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary)}.dsub-cqBarWarn .dsub-cqBarFill{background:var(--dsw-alias-state-warn-primary)}.dsub-cqBarFull .dsub-cqBarFill{background:var(--dsw-alias-state-error-primary)}.dsub-cqB{font-variant-numeric:tabular-nums;font-weight:600;color:var(--dsw-alias-label-primary)}.dsub-qw{position:fixed;z-index:10010;display:flex;align-items:center;gap:6px;height:30px;padding:0 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);box-shadow:0 4px 14px color-mix(in srgb,black 28%,transparent);cursor:grab;user-select:none;font-size:11.5px;font-weight:600;touch-action:none}.dsub-qw:active{cursor:grabbing}.dsub-qwDocked{right:16px !important;bottom:16px !important;cursor:default}.dsub-qwPct{font-variant-numeric:tabular-nums}.dsub-qwBar{width:28px;height:4px;border-radius:2px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}.dsub-qwBarFill{height:100%;border-radius:2px;background:var(--dsw-alias-state-success-primary)}.dsub-qwPop{position:fixed;z-index:10011;width:280px;display:flex;flex-direction:column;gap:6px;padding:10px 12px;border-radius:10px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-3);box-shadow:0 12px 32px color-mix(in srgb,black 40%,transparent);font-size:12px}.dsub-qwWarn .dsub-qwBarFill{background:var(--dsw-alias-state-warn-primary)}.dsub-qwFull .dsub-qwBarFill{background:var(--dsw-alias-state-error-primary)}.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)}";
44
44
  const cssId = 'dsh-subscriptions/settings.module.css'
45
45
  if (typeof document !== 'undefined' && !document.querySelector('style[data-plugin-css="' + cssId + '"]')) {
46
46
  const tag = document.createElement('style')
@@ -1843,6 +1843,7 @@ window.__ModuleLoader__.load({
1843
1843
  h('div', { className: 'dsub-sub' }, 'Floating capsule can be dragged; docked stays bottom-right.'),
1844
1844
  h(SourcesPanel, { t }),
1845
1845
  h(ProviderPacksPanel),
1846
+ h(VaultSnapshotsPanel),
1846
1847
 
1847
1848
  h('div', { className: 'dsub-row' },
1848
1849
  h('span', { className: 'dsub-h' }, t('composerQuota')),
@@ -1907,6 +1908,7 @@ window.__ModuleLoader__.load({
1907
1908
  }),
1908
1909
  ),
1909
1910
  h('div', { className: 'dsub-sub' }, '0 disables digests. Requires a Webhook URL.'),
1911
+ h(WebhookTestButton),
1910
1912
  ),
1911
1913
  h('details', { className: 'dsub-group', open: true },
1912
1914
  h('summary', { className: 'dsub-groupHead' }, '🚦 ' + t('groupQueue')),
@@ -3690,6 +3692,69 @@ window.__ModuleLoader__.load({
3690
3692
  )
3691
3693
  }
3692
3694
 
3695
+ function WebhookTestButton() {
3696
+ const [state, setState] = React.useState({ busy: false, message: '', error: null })
3697
+ const send = () => {
3698
+ setState({ busy: true, message: '', error: null })
3699
+ fetch('/dsh-subscriptions/webhook/test', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })
3700
+ .then(async (res) => {
3701
+ const data = await res.json().catch(() => ({}))
3702
+ if (!res.ok || !data.delivered) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
3703
+ setState({ busy: false, message: 'delivered', error: null })
3704
+ })
3705
+ .catch((err) => setState({ busy: false, message: '', error: String(err && err.message || err) }))
3706
+ }
3707
+ return h('div', { className: 'dsub-row' },
3708
+ h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: send }, state.busy ? '…' : 'Send test'),
3709
+ state.message ? h('span', { className: 'dsub-ok' }, state.message) : null,
3710
+ state.error ? h('span', { className: 'dsub-bad' }, state.error) : null,
3711
+ )
3712
+ }
3713
+
3714
+ // #736: vault snapshot list with create/restore. Payload stays server-side.
3715
+ function VaultSnapshotsPanel() {
3716
+ const [state, setState] = React.useState({ snapshots: [], message: '', error: null, busy: false })
3717
+ const pull = () => {
3718
+ fetch('/dsh-subscriptions/vault/snapshots', { cache: 'no-store' })
3719
+ .then((res) => res.json())
3720
+ .then((data) => setState((st) => Object.assign({}, st, { snapshots: data.snapshots || [], error: null })))
3721
+ .catch((err) => setState((st) => Object.assign({}, st, { error: String(err && err.message || err) })))
3722
+ }
3723
+ React.useEffect(() => { pull() }, [])
3724
+ const call = (url, body) => fetch(url, {
3725
+ method: 'POST',
3726
+ headers: { 'content-type': 'application/json' },
3727
+ body: JSON.stringify(body || {}),
3728
+ }).then(async (res) => {
3729
+ const data = await res.json().catch(() => ({}))
3730
+ if (!res.ok || !data.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
3731
+ return data
3732
+ })
3733
+ const create = () => {
3734
+ setState((st) => Object.assign({}, st, { busy: true, message: '', error: null }))
3735
+ call('/dsh-subscriptions/vault/snapshots', { label: 'manual' })
3736
+ .then((data) => { setState((st) => Object.assign({}, st, { busy: false, message: 'created ' + data.snapshot.id })); pull() })
3737
+ .catch((err) => setState((st) => Object.assign({}, st, { busy: false, error: String(err && err.message || err) })))
3738
+ }
3739
+ const restore = (id) => {
3740
+ setState((st) => Object.assign({}, st, { busy: true, message: '', error: null }))
3741
+ call('/dsh-subscriptions/vault/snapshots/restore', { id })
3742
+ .then((data) => { setState((st) => Object.assign({}, st, { busy: false, message: 'restored ' + data.slotCount + ' slots' })); pull() })
3743
+ .catch((err) => setState((st) => Object.assign({}, st, { busy: false, error: String(err && err.message || err) })))
3744
+ }
3745
+ return h('div', { className: 'dsub-section-card' },
3746
+ h('div', { className: 'dsub-section-title' }, 'Vault snapshots', h('button', { type: 'button', className: 'dsub-mini', onClick: create, disabled: state.busy }, state.busy ? '…' : 'Create snapshot')),
3747
+ h('div', { className: 'dsub-section-desc' }, 'Encrypted slots snapshots. A pre-pull snapshot is created automatically. Payload stays on the host.'),
3748
+ state.snapshots.length ? state.snapshots.map((snap) => h('div', { className: 'dsub-row', key: snap.id },
3749
+ h('span', { className: 'dsub-h' }, snap.id),
3750
+ h('span', { className: 'dsub-dim' }, (snap.label || '-') + ' · ' + snap.slotCount + ' slots · ' + snap.createdAt),
3751
+ h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: () => restore(snap.id) }, 'Restore'),
3752
+ )) : h('div', { className: 'dsub-dim' }, 'No snapshots yet.'),
3753
+ state.message ? h('div', { className: 'dsub-ok' }, state.message) : null,
3754
+ state.error ? h('div', { className: 'dsub-bad' }, state.error) : null,
3755
+ )
3756
+ }
3757
+
3693
3758
  // #724: provider pack manager. Displays ids/models only; headers are masked.
3694
3759
  function ProviderPacksPanel() {
3695
3760
  const [state, setState] = React.useState({ packs: [], text: '', message: '', error: null, busy: false })
@@ -3800,8 +3865,10 @@ window.__ModuleLoader__.load({
3800
3865
  // #688: floating/docked quota capsule. Draggable unless docked; opens settings.
3801
3866
  function QuotaCapsule(props) {
3802
3867
  const t = (props && props.t) || ((k) => k)
3803
- const [state, setState] = React.useState({ mode: 'off', active: null, remaining: null, source: null })
3868
+ const [state, setState] = React.useState({ mode: 'off', active: null, remaining: null, source: null, candidates: [] })
3869
+ const [open, setOpen] = React.useState(false)
3804
3870
  const [pos, setPos] = React.useState({ right: 16, bottom: 16 })
3871
+ const popRef = React.useRef(null)
3805
3872
  const posRef = React.useRef({ right: 16, bottom: 16 })
3806
3873
  const drag = React.useRef(null)
3807
3874
  const suppressClick = React.useRef(false)
@@ -3829,7 +3896,7 @@ window.__ModuleLoader__.load({
3829
3896
  refreshLoggedIn().then(async (r) => {
3830
3897
  if (!alive) return
3831
3898
  const mode = r.quotaWidget || 'docked'
3832
- if (mode === 'off') { setState({ mode, active: null, remaining: null, source: null }); return }
3899
+ if (mode === 'off') { setState({ mode, active: null, remaining: null, source: null, candidates: [] }); return }
3833
3900
  const sources = await fetch('/dsh-subscriptions/sources', { cache: 'no-store' }).then((res) => res.json()).catch(() => null)
3834
3901
  let a = r.active
3835
3902
  const current = currentModelProvider(props)
@@ -3838,9 +3905,10 @@ window.__ModuleLoader__.load({
3838
3905
  const rows = (cfg && cfg.accounts) || []
3839
3906
  a = rows.find((row) => row && row.provider === current && row.usagePercent != null) || null
3840
3907
  }
3841
- if (!a || a.provider === 'ollama' || a.usagePercent == null) { setState({ mode, active: null, remaining: null, source: null }); return }
3908
+ if (!a || a.provider === 'ollama' || a.usagePercent == null) { setState({ mode, active: null, remaining: null, source: null, candidates: [] }); return }
3842
3909
  const sourceRow = (sources && Array.isArray(sources.providers) ? sources.providers : []).find((row) => row && row.provider === a.provider)
3843
3910
  const source = sourceRow ? sourceRow.selected : null
3911
+ const candidates = sourceRow && Array.isArray(sourceRow.candidates) ? sourceRow.candidates : []
3844
3912
  if (r.quotaWidgetPosition && typeof window !== 'undefined') {
3845
3913
  const clamp = (v, max) => Math.max(4, Math.min(max, Number(v) || 0))
3846
3914
  const next = {
@@ -3850,7 +3918,7 @@ window.__ModuleLoader__.load({
3850
3918
  posRef.current = next
3851
3919
  setPos(next)
3852
3920
  }
3853
- setState({ mode, active: a, remaining: Math.max(0, 100 - a.usagePercent), source })
3921
+ setState({ mode, active: a, remaining: Math.max(0, 100 - a.usagePercent), source, candidates })
3854
3922
  }).catch(() => {})
3855
3923
  }
3856
3924
  pull()
@@ -3871,6 +3939,16 @@ window.__ModuleLoader__.load({
3871
3939
  if (layout && typeof layout.selectPanel === 'function') layout.selectPanel('plugins')
3872
3940
  } catch { /* bestEffort */ }
3873
3941
  }
3942
+ React.useEffect(() => {
3943
+ if (!open) return undefined
3944
+ const onDoc = (e) => {
3945
+ if (popRef.current && !popRef.current.contains(e.target) && !(e.target.closest && e.target.closest('.dsub-qw'))) setOpen(false)
3946
+ }
3947
+ const onKey = (e) => { if (e.key === 'Escape') setOpen(false) }
3948
+ document.addEventListener('pointerdown', onDoc)
3949
+ document.addEventListener('keydown', onKey)
3950
+ return () => { document.removeEventListener('pointerdown', onDoc); document.removeEventListener('keydown', onKey) }
3951
+ }, [open])
3874
3952
  const onPointerDown = (e) => {
3875
3953
  if (docked) return
3876
3954
  drag.current = { x: e.clientX, y: e.clientY, right: pos.right, bottom: pos.bottom, moved: false }
@@ -3894,7 +3972,27 @@ window.__ModuleLoader__.load({
3894
3972
  drag.current = null
3895
3973
  if (!docked && wasDrag && wasDrag.moved) savePosition(posRef.current)
3896
3974
  }
3897
- return h('div', {
3975
+ const popover = open ? h('div', {
3976
+ ref: popRef,
3977
+ className: 'dsub-qwPop',
3978
+ style: { right: pos.right + 'px', bottom: (pos.bottom + 38) + 'px' },
3979
+ role: 'dialog',
3980
+ 'aria-label': 'Quota sources',
3981
+ },
3982
+ h('div', { className: 'dsub-h' }, String(state.active.provider || '').toUpperCase() + ' sources'),
3983
+ (state.candidates || []).map((c, i) => {
3984
+ const label = c.source === 'api-key' ? 'API key' : (c.source === 'oauth' ? 'OAuth' : 'Ollama')
3985
+ const cd = c.detail && Array.isArray(c.detail.keys) ? Math.max(0, ...c.detail.keys.map((k) => Number(k.cooldownMsLeft) || 0)) : 0
3986
+ return h('div', { className: 'dsub-row', key: i },
3987
+ h('span', { className: 'dsub-h' }, label),
3988
+ h('span', { className: c.available ? 'dsub-ok' : 'dsub-dim' }, c.available ? 'available' : 'unavailable'),
3989
+ cd > 0 ? h('span', { className: 'dsub-dim' }, 'cooldown ' + Math.ceil(cd / 1000) + 's') : null,
3990
+ c.detail && c.detail.healthyKeys != null ? h('span', { className: 'dsub-dim' }, c.detail.healthyKeys + '/' + c.detail.totalKeys + ' keys') : null,
3991
+ )
3992
+ }),
3993
+ h('button', { type: 'button', className: 'dsub-mini', onClick: () => { setOpen(false); openSettings() } }, 'Open settings'),
3994
+ ) : null
3995
+ return h(React.Fragment, null, h('div', {
3898
3996
  className: 'dsub-qw' + (docked ? ' dsub-qwDocked' : '') + tone,
3899
3997
  style: { right: pos.right + 'px', bottom: pos.bottom + 'px' },
3900
3998
  role: 'button',
@@ -3905,15 +4003,15 @@ window.__ModuleLoader__.load({
3905
4003
  onPointerMove,
3906
4004
  onPointerUp,
3907
4005
  onPointerCancel: () => { drag.current = null },
3908
- onClick: () => { if (suppressClick.current) { suppressClick.current = false; return } openSettings() },
3909
- onKeyDown: (e) => { if (e.key === 'Enter' || e.key === ' ') openSettings() },
4006
+ onClick: () => { if (suppressClick.current) { suppressClick.current = false; return } setOpen((o) => !o) },
4007
+ onKeyDown: (e) => { if (e.key === 'Enter' || e.key === ' ') setOpen((o) => !o) },
3910
4008
  },
3911
4009
  h('span', { className: 'dsub-led ' + ledClass(state.active.usagePercent) }),
3912
4010
  h('span', { className: 'dsub-qwBrand' }, brandBadge(state.active.provider).icon + ' ' + String(state.active.provider || '').toUpperCase()),
3913
4011
  state.source ? h('span', { className: 'dsub-pillTag' }, state.source === 'api-key' ? 'KEY' : (state.source === 'oauth' ? 'OAuth' : 'OLLAMA')) : null,
3914
4012
  h('span', { className: 'dsub-qwBar' }, h('span', { className: 'dsub-qwBarFill', style: { width: state.remaining + '%', display: 'block' } })),
3915
4013
  h('span', { className: 'dsub-qwPct' }, Math.round(state.remaining) + '%'),
3916
- )
4014
+ ), popover)
3917
4015
  }
3918
4016
 
3919
4017
  function registerSubsPill(ctx) {
@@ -159,6 +159,8 @@ export const Config = z.object({
159
159
  .description('#351 External webhook URL to receive quota and session alerts (JSON POST).'),
160
160
  webhookSecret: z.string().role('secret').default('')
161
161
  .description('#720 HMAC-SHA256 secret used to sign webhook payloads.'),
162
+ webhookAllowPrivate: z.boolean().volatile().default(false)
163
+ .description('#729 Allow webhook URLs that resolve to private or loopback hosts.'),
162
164
  digestIntervalHours: z.number().volatile().default(0)
163
165
  .description('#720 Send an alert digest every N hours (0 disables digests).'),
164
166
  alertThresholds: z.array(z.number()).volatile().default([80, 90, 95])
package/lib/index.js CHANGED
@@ -28,7 +28,7 @@ import { createAccountsView } from './accounts-view.js'
28
28
  import { inspectScopes } from './scope-inspector.js'
29
29
  import { createAdapterManager } from './adapter-manager.js'
30
30
  import { pickAccount, isAuthError } from './rotate.js'
31
- import { notifySessionExpired, getRecentAlerts, dispatchNotificationDigest, setWebhookSecret } from './alerts.js'
31
+ import { notifySessionExpired, getRecentAlerts, dispatchNotificationDigest, setWebhookSecret, setWebhookAllowPrivate } from './alerts.js'
32
32
  import { isDigestDue } from './notification-scheduler.js'
33
33
 
34
34
  export { observeForecast, estimateForecast } from './forecast.js'
@@ -94,6 +94,7 @@ export function apply(ctx, rawConfig) {
94
94
  let cachedConfig = resolveEntry()
95
95
  const live = () => {
96
96
  setWebhookSecret(cachedConfig.webhookSecret)
97
+ setWebhookAllowPrivate(cachedConfig.webhookAllowPrivate)
97
98
  return cachedConfig
98
99
  }
99
100
 
@@ -615,7 +616,7 @@ export function apply(ctx, rawConfig) {
615
616
  const cfg = live()
616
617
  const alerts = getRecentAlerts(100)
617
618
  if (!isDigestDue({ lastDigestAt, nowMs: Date.now(), intervalHours: cfg.digestIntervalHours, alertCount: alerts.length })) return
618
- dispatchNotificationDigest(alerts, cfg.webhookUrl, fetch, { secret: cfg.webhookSecret })
619
+ dispatchNotificationDigest(alerts, cfg.webhookUrl, fetch, { secret: cfg.webhookSecret, allowPrivate: cfg.webhookAllowPrivate })
619
620
  .then((ok) => { if (ok) lastDigestAt = Date.now() })
620
621
  .catch(() => {})
621
622
  }
@@ -0,0 +1,88 @@
1
+ import { Config, plainConfig, revisionOf } from '../config-schema.js'
2
+ import { isTrustedSettingsRequest, readBody, safeJsonHandler, writeJson } from '../http.js'
3
+ import { capturePrePullSnapshot, restoreVaultSnapshot } from '../vault-sync.js'
4
+ import { defaultSnapshotStore } from '../vault-snapshots.js'
5
+
6
+ export function registerVaultRoutes(ctx, state) {
7
+ const { live, getSettingsApi, syncAdapter, stripLegacySlots } = state
8
+
9
+ ctx.effect(() => ctx.webServer.register({
10
+ kind: 'exact',
11
+ path: '/dsh-subscriptions/vault/snapshots',
12
+ handler: safeJsonHandler(async (req, res) => {
13
+ if (!isTrustedSettingsRequest(req, ctx)) {
14
+ writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
15
+ return
16
+ }
17
+ if (req.method === 'GET') {
18
+ writeJson(res, 200, { ok: true, snapshots: defaultSnapshotStore.list() })
19
+ return
20
+ }
21
+ if (req.method !== 'POST') {
22
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET or POST' } })
23
+ return
24
+ }
25
+ let body = {}
26
+ try { body = JSON.parse((await readBody(req, 4096)).toString('utf8') || '{}') } catch {
27
+ writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
28
+ return
29
+ }
30
+ const passphrase = String(live().vaultSyncPassphrase || '').trim()
31
+ if (!passphrase) {
32
+ writeJson(res, 400, { ok: false, error: { code: 'vault', message: 'vaultSyncPassphrase is required' } })
33
+ return
34
+ }
35
+ try {
36
+ const snapshot = capturePrePullSnapshot(live().slots || [], passphrase, String(body.label || 'manual'))
37
+ writeJson(res, 200, { ok: true, snapshot })
38
+ } catch (err) {
39
+ writeJson(res, 400, { ok: false, error: { code: 'vault', message: String(err && err.message || err) } })
40
+ }
41
+ }),
42
+ }), 'dsh-subscriptions: /vault/snapshots')
43
+
44
+ ctx.effect(() => ctx.webServer.register({
45
+ kind: 'exact',
46
+ path: '/dsh-subscriptions/vault/snapshots/restore',
47
+ handler: safeJsonHandler(async (req, res) => {
48
+ if (req.method !== 'POST') {
49
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
50
+ return
51
+ }
52
+ if (!isTrustedSettingsRequest(req, ctx)) {
53
+ writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
54
+ return
55
+ }
56
+ let body = {}
57
+ try { body = JSON.parse((await readBody(req, 4096)).toString('utf8') || '{}') } catch {
58
+ writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
59
+ return
60
+ }
61
+ const snapshot = defaultSnapshotStore.get(String(body.id || ''))
62
+ if (!snapshot) {
63
+ writeJson(res, 404, { ok: false, error: { code: 'snapshot', message: 'snapshot not found' } })
64
+ return
65
+ }
66
+ const passphrase = String(live().vaultSyncPassphrase || '').trim()
67
+ if (!passphrase) {
68
+ writeJson(res, 400, { ok: false, error: { code: 'vault', message: 'vaultSyncPassphrase is required' } })
69
+ return
70
+ }
71
+ const api = getSettingsApi()
72
+ if (!api) {
73
+ writeJson(res, 503, { ok: false, error: { code: 'settings', message: 'settings not ready' } })
74
+ return
75
+ }
76
+ try {
77
+ const restored = restoreVaultSnapshot(snapshot, passphrase)
78
+ const cfg = live()
79
+ const slots = stripLegacySlots(restored.slots || [])
80
+ await api.replace(plainConfig(Config({ ...cfg, slots })), revisionOf(api))
81
+ if (typeof syncAdapter === 'function') await syncAdapter()
82
+ writeJson(res, 200, { ok: true, id: snapshot.id, slotCount: slots.length })
83
+ } catch (err) {
84
+ writeJson(res, 400, { ok: false, error: { code: 'vault', message: String(err && err.message || err) } })
85
+ }
86
+ }),
87
+ }), 'dsh-subscriptions: /vault/snapshots/restore')
88
+ }
@@ -0,0 +1,47 @@
1
+ import { dispatchWebhookAlert } from '../alerts.js'
2
+ import { isTrustedSettingsRequest, safeJsonHandler, writeJson } from '../http.js'
3
+ import { validateWebhookUrl } from '../webhook-url.js'
4
+
5
+ export function registerWebhookRoutes(ctx, state) {
6
+ const { live } = state
7
+ ctx.effect(() => ctx.webServer.register({
8
+ kind: 'exact',
9
+ path: '/dsh-subscriptions/webhook/test',
10
+ handler: safeJsonHandler(async (req, res) => {
11
+ if (req.method !== 'POST') {
12
+ writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
13
+ return
14
+ }
15
+ if (!isTrustedSettingsRequest(req, ctx)) {
16
+ writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
17
+ return
18
+ }
19
+ const cfg = live()
20
+ if (!cfg.webhookUrl) {
21
+ writeJson(res, 400, { ok: false, delivered: false, error: { code: 'webhook', message: 'webhookUrl is not configured' } })
22
+ return
23
+ }
24
+ const checked = validateWebhookUrl(cfg.webhookUrl, { allowPrivate: cfg.webhookAllowPrivate })
25
+ if (!checked.ok) {
26
+ writeJson(res, 400, { ok: false, delivered: false, error: { code: 'webhook', message: checked.reason } })
27
+ return
28
+ }
29
+ const alert = {
30
+ id: 'wh_test_' + Date.now().toString(36),
31
+ type: 'webhook_test',
32
+ ref: null,
33
+ provider: 'dsh-subscriptions',
34
+ message: 'DSH Subscriptions webhook test',
35
+ severity: 'info',
36
+ details: null,
37
+ timestamp: Date.now(),
38
+ }
39
+ const delivered = await dispatchWebhookAlert(alert, cfg.webhookUrl, fetch, {
40
+ secret: cfg.webhookSecret,
41
+ allowPrivate: cfg.webhookAllowPrivate,
42
+ retries: 1,
43
+ })
44
+ writeJson(res, delivered ? 200 : 502, { ok: delivered, delivered })
45
+ }),
46
+ }), 'dsh-subscriptions: /webhook/test')
47
+ }
package/lib/routes.js CHANGED
@@ -7,6 +7,8 @@ import { registerAccountsRoutes } from './routes/accounts.js'
7
7
  import { registerProxyRoutes } from './routes/proxy.js'
8
8
  import { registerSourcesRoutes } from './routes/sources.js'
9
9
  import { registerPacksRoutes } from './routes/packs.js'
10
+ import { registerWebhookRoutes } from './routes/webhook.js'
11
+ import { registerVaultRoutes } from './routes/vault.js'
10
12
  import { registerPluginUpdater } from './updater.js'
11
13
 
12
14
  export function registerRoutes(ctx, state) {
@@ -113,4 +115,6 @@ function esc(x){return String(x==null?'':x).replace(/&/g,'&amp;').replace(/</g,'
113
115
  registerProxyRoutes(ctx, state)
114
116
  registerSourcesRoutes(ctx, state)
115
117
  registerPacksRoutes(ctx)
118
+ registerWebhookRoutes(ctx, state)
119
+ registerVaultRoutes(ctx, state)
116
120
  }
@@ -0,0 +1,43 @@
1
+ // #734: bounded in-memory snapshot ring. Only metadata leaves list(); the
2
+ // encrypted payload is available through get(id) for an explicit restore.
3
+ let seq = 0
4
+
5
+ function meta(snapshot) {
6
+ return {
7
+ id: snapshot.id,
8
+ label: snapshot.label || '',
9
+ createdAt: snapshot.createdAt,
10
+ version: snapshot.version || 2,
11
+ slotCount: Number(snapshot.slotCount) || 0,
12
+ blobCount: Number(snapshot.blobCount) || 0,
13
+ }
14
+ }
15
+
16
+ export function createSnapshotStore({ max = 10, now = () => Date.now() } = {}) {
17
+ let items = []
18
+ return {
19
+ add(snapshot, label = '') {
20
+ const at = now()
21
+ const entry = {
22
+ ...snapshot,
23
+ id: 'vs_' + at.toString(36) + '_' + (++seq).toString(36),
24
+ label: String(label || snapshot.label || ''),
25
+ createdAt: snapshot.createdAt || new Date(at).toISOString(),
26
+ }
27
+ items.unshift(entry)
28
+ if (items.length > Math.max(1, max)) items = items.slice(0, Math.max(1, max))
29
+ return meta(entry)
30
+ },
31
+ list() {
32
+ return items.map(meta)
33
+ },
34
+ get(id) {
35
+ return items.find((item) => item.id === id) || null
36
+ },
37
+ clear() {
38
+ items = []
39
+ },
40
+ }
41
+ }
42
+
43
+ export const defaultSnapshotStore = createSnapshotStore({ max: 10 })
package/lib/vault-sync.js CHANGED
@@ -6,6 +6,7 @@ import { exportVault, importVault } from './vault.js'
6
6
  import { normalizeSlots } from './accounts.js'
7
7
  import { Config, plainConfig, revisionOf } from './config-schema.js'
8
8
  import { validateCredentialBlob } from './blob.js'
9
+ import { defaultSnapshotStore } from './vault-snapshots.js'
9
10
 
10
11
  let syncStatus = {
11
12
  enabled: false,
@@ -273,6 +274,10 @@ export function createVaultSnapshot({ slots, blobs = {} }, passphrase, label = '
273
274
  return { version: 2, label: String(label || ''), createdAt: new Date().toISOString(), vault, slotCount, blobCount }
274
275
  }
275
276
 
277
+ export function capturePrePullSnapshot(slots, passphrase, label = 'pre-pull') {
278
+ return defaultSnapshotStore.add(createVaultSnapshot({ slots: slots || [], blobs: {} }, passphrase, label))
279
+ }
280
+
276
281
  export function restoreVaultSnapshot(snapshot, passphrase) {
277
282
  if (!snapshot || typeof snapshot.vault !== 'string' || !snapshot.vault) throw new Error('snapshot is missing its encrypted vault')
278
283
  return importVault(snapshot.vault, passphrase)
@@ -401,6 +406,7 @@ export async function syncVaultPull({ live, store, settingsApi, syncAdapter, ref
401
406
  throw new Error(`unsupported vault sync backend: ${backend}`)
402
407
  }
403
408
 
409
+ try { capturePrePullSnapshot(cfg.slots || [], passphrase) } catch { /* snapshot is best-effort */ }
404
410
  const result = importVault(encryptedData, passphrase)
405
411
  const providerFilter = new Set((cfg.vaultSyncProviders || []).map((p) => String(p)))
406
412
  const remoteSlots = providerFilter.size
@@ -0,0 +1,27 @@
1
+ // #729: webhook URLs are user-controlled and fetched by the host, so reject
2
+ // obvious SSRF targets unless the operator explicitly opts into private hosts.
3
+ export function isPrivateHostname(hostname) {
4
+ const host = String(hostname || '').toLowerCase().replace(/^\[|\]$/g, '')
5
+ if (!host) return true
6
+ if (host === 'localhost' || host.endsWith('.localhost')) return true
7
+ if (host === '::1' || host === '0:0:0:0:0:0:0:1') return true
8
+ if (/^(fc|fd|fe8|fe9|fea|feb)/.test(host)) return true
9
+ const m = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)
10
+ if (!m) return false
11
+ const parts = m.slice(1).map(Number)
12
+ if (parts.some((n) => n > 255)) return true
13
+ const [a, b] = parts
14
+ return a === 0 || a === 10 || a === 127 || (a === 169 && b === 254)
15
+ || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168)
16
+ || (a === 100 && b >= 64 && b <= 127)
17
+ }
18
+
19
+ export function validateWebhookUrl(raw, { allowPrivate = false } = {}) {
20
+ if (typeof raw !== 'string' || !raw.trim()) return { ok: false, reason: 'webhook URL is empty' }
21
+ let url
22
+ try { url = new URL(raw.trim()) } catch { return { ok: false, reason: 'webhook URL is invalid' } }
23
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') return { ok: false, reason: 'webhook URL must use http(s)' }
24
+ if (url.username || url.password) return { ok: false, reason: 'webhook URL must not contain userinfo' }
25
+ if (!allowPrivate && isPrivateHostname(url.hostname)) return { ok: false, reason: 'webhook URL points to a private/loopback host' }
26
+ return { ok: true, url: url.toString() }
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
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",