@goodandready/dsh-subscriptions 0.7.2 → 0.7.3
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/bin/dsh-subscriptions.mjs +65 -0
- package/lib/alerts.js +54 -22
- package/lib/client.js +114 -2
- package/lib/config-schema.js +6 -0
- package/lib/index.js +12 -0
- package/lib/routes/status.js +1 -0
- package/lib/vault-sync.js +53 -15
- package/lib/vendor-pack.js +36 -0
- package/overlay.example.yml +8 -0
- package/package.json +33 -2
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
|
|
5
|
+
export function parseArgs(argv) {
|
|
6
|
+
const args = Array.isArray(argv) ? argv.slice() : []
|
|
7
|
+
const command = args.shift() || 'status'
|
|
8
|
+
const options = { command, csv: false, type: 'accounts', sessionId: '', mode: 'fast', help: false }
|
|
9
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
10
|
+
const arg = args[i]
|
|
11
|
+
if (arg === '--csv') options.csv = true
|
|
12
|
+
else if (arg === '--help' || arg === '-h') options.help = true
|
|
13
|
+
else if (arg === '--type') options.type = args[++i] || 'accounts'
|
|
14
|
+
else if (arg === '--session') options.sessionId = args[++i] || ''
|
|
15
|
+
else if (command === 'fast' && !options.sessionId) options.sessionId = arg
|
|
16
|
+
else if (command === 'fast') options.mode = arg === 'standard' || arg === 'off' ? 'standard' : 'fast'
|
|
17
|
+
}
|
|
18
|
+
return options
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildRequest(options, env = process.env) {
|
|
22
|
+
const base = String(env.DSH_SUBS_URL || 'http://127.0.0.1:3080/dsh-subscriptions').replace(/\/$/, '')
|
|
23
|
+
const headers = { Accept: 'application/json' }
|
|
24
|
+
if (env.DSH_AUTH_TOKEN) headers.Authorization = 'Bearer ' + env.DSH_AUTH_TOKEN
|
|
25
|
+
if (options.command === 'status') return { url: base + '/status', init: { method: 'GET', headers } }
|
|
26
|
+
if (options.command === 'usage') {
|
|
27
|
+
const params = new URLSearchParams({ type: options.type || 'accounts', format: options.csv ? 'csv' : 'json' })
|
|
28
|
+
return { url: base + '/usage?' + params.toString(), init: { method: 'GET', headers } }
|
|
29
|
+
}
|
|
30
|
+
if (options.command === 'fast') {
|
|
31
|
+
if (!options.sessionId) throw new Error('fast requires a session id: dsh-subscriptions fast <session> [fast|standard]')
|
|
32
|
+
headers['Content-Type'] = 'application/json'
|
|
33
|
+
return {
|
|
34
|
+
url: base + '/speed',
|
|
35
|
+
init: { method: 'POST', headers, body: JSON.stringify({ sessionId: options.sessionId, mode: options.mode || 'fast' }) },
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
throw new Error('unknown command: ' + options.command)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function run(argv = process.argv.slice(2), env = process.env, fetchImpl = fetch) {
|
|
42
|
+
const options = parseArgs(argv)
|
|
43
|
+
if (options.help) return { help: true }
|
|
44
|
+
const request = buildRequest(options, env)
|
|
45
|
+
const res = await fetchImpl(request.url, request.init)
|
|
46
|
+
const text = await res.text()
|
|
47
|
+
if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + text.slice(0, 500))
|
|
48
|
+
return { text, contentType: res.headers.get('content-type') || '' }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function main() {
|
|
52
|
+
const result = await run()
|
|
53
|
+
if (result.help) {
|
|
54
|
+
process.stdout.write('Usage: dsh-subscriptions <status|usage|fast> [options]\n usage --csv --type accounts|calls\n fast <session> [fast|standard]\n')
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
process.stdout.write(result.text.endsWith('\n') ? result.text : result.text + '\n')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
61
|
+
main().catch((err) => {
|
|
62
|
+
process.stderr.write(String(err && err.message || err) + '\n')
|
|
63
|
+
process.exitCode = 1
|
|
64
|
+
})
|
|
65
|
+
}
|
package/lib/alerts.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { createHmac } from 'node:crypto'
|
|
2
3
|
|
|
3
4
|
let pkgVersion = ''
|
|
4
5
|
try {
|
|
@@ -59,35 +60,66 @@ export function getRecentAlerts(limit = 50) {
|
|
|
59
60
|
return alertBuffer.slice(-Math.max(1, Math.min(limit, MAX_ALERTS))).reverse()
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
*/
|
|
65
|
-
export async function dispatchWebhookAlert(alert, webhookUrl, fetchImpl = fetch) {
|
|
63
|
+
// #696: signed, retrying webhook delivery plus a digest endpoint payload.
|
|
64
|
+
export async function dispatchWebhookAlert(alert, webhookUrl, fetchImpl = fetch, options = {}) {
|
|
66
65
|
if (!alert || !webhookUrl || typeof webhookUrl !== 'string' || !webhookUrl.startsWith('http')) {
|
|
67
66
|
return false
|
|
68
67
|
}
|
|
68
|
+
return dispatchNotification({ event: 'subscription_alert', alert }, webhookUrl, fetchImpl, options)
|
|
69
|
+
}
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
export function buildNotificationDigest(alerts, { windowMs = 24 * 60 * 60 * 1000, nowMs = Date.now() } = {}) {
|
|
72
|
+
const since = nowMs - Math.max(0, Number(windowMs) || 0)
|
|
73
|
+
const recent = (Array.isArray(alerts) ? alerts : []).filter((a) => a && Number(a.timestamp) >= since)
|
|
74
|
+
const bySeverity = {}
|
|
75
|
+
const byType = {}
|
|
76
|
+
for (const a of recent) {
|
|
77
|
+
bySeverity[a.severity || 'warn'] = (bySeverity[a.severity || 'warn'] || 0) + 1
|
|
78
|
+
byType[a.type || 'unknown'] = (byType[a.type || 'unknown'] || 0) + 1
|
|
75
79
|
}
|
|
80
|
+
return {
|
|
81
|
+
total: recent.length,
|
|
82
|
+
bySeverity,
|
|
83
|
+
byType,
|
|
84
|
+
since: new Date(since).toISOString(),
|
|
85
|
+
until: new Date(nowMs).toISOString(),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
76
88
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
export async function dispatchNotificationDigest(alerts, webhookUrl, fetchImpl = fetch, options = {}) {
|
|
90
|
+
if (!webhookUrl) return false
|
|
91
|
+
const digest = buildNotificationDigest(alerts, options)
|
|
92
|
+
return dispatchNotification({ event: 'subscription_digest', digest }, webhookUrl, fetchImpl, options)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function dispatchNotification(body, webhookUrl, fetchImpl, options) {
|
|
96
|
+
const { secret = '', retries = 1, timeoutMs = 5000, sleep } = options || {}
|
|
97
|
+
const payload = { ...body, source: 'dsh-subscriptions', sentAt: new Date().toISOString() }
|
|
98
|
+
const raw = JSON.stringify(payload)
|
|
99
|
+
const headers = {
|
|
100
|
+
'content-type': 'application/json',
|
|
101
|
+
'user-agent': pkgVersion ? `dsh-subscriptions/${pkgVersion}` : 'dsh-subscriptions',
|
|
102
|
+
}
|
|
103
|
+
if (secret) headers['x-dsh-signature'] = 'sha256=' + createHmac('sha256', String(secret)).update(raw).digest('hex')
|
|
104
|
+
headers['x-dsh-timestamp'] = payload.sentAt
|
|
105
|
+
const wait = typeof sleep === 'function' ? sleep : (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
106
|
+
const attempts = Math.max(0, Number(retries) || 0) + 1
|
|
107
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
108
|
+
try {
|
|
109
|
+
const init = { method: 'POST', headers, body: raw }
|
|
110
|
+
if (timeoutMs > 0 && typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
111
|
+
init.signal = AbortSignal.timeout(timeoutMs)
|
|
112
|
+
}
|
|
113
|
+
const res = await fetchImpl(webhookUrl, init)
|
|
114
|
+
if (res && res.ok) return true
|
|
115
|
+
const status = Number(res && res.status) || 0
|
|
116
|
+
if (status && status < 500) return false
|
|
117
|
+
} catch {
|
|
118
|
+
// retry network errors
|
|
119
|
+
}
|
|
120
|
+
if (attempt + 1 < attempts) await wait(250 * (2 ** attempt))
|
|
90
121
|
}
|
|
122
|
+
return false
|
|
91
123
|
}
|
|
92
124
|
|
|
93
125
|
/**
|
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-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-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')
|
|
@@ -1828,7 +1828,20 @@ window.__ModuleLoader__.load({
|
|
|
1828
1828
|
h('option', { value: 'hidden' }, t('composerQuotaDisplayHidden')),
|
|
1829
1829
|
),
|
|
1830
1830
|
),
|
|
1831
|
-
h('div', { className: 'dsub-sub' }, t('composerQuotaDisplayHint')),
|
|
1831
|
+
h('div', { className: 'dsub-sub' }, t('composerQuotaDisplayHint')), h('div', { className: 'dsub-row' },
|
|
1832
|
+
h('span', { className: 'dsub-h' }, 'Quota widget'),
|
|
1833
|
+
h('select', {
|
|
1834
|
+
className: 'dsub-select',
|
|
1835
|
+
value: draft.quotaWidget || 'docked',
|
|
1836
|
+
onChange: (e) => setDraft((d) => Object.assign({}, d, { quotaWidget: e.target.value })),
|
|
1837
|
+
},
|
|
1838
|
+
h('option', { value: 'off' }, 'Off'),
|
|
1839
|
+
h('option', { value: 'docked' }, 'Docked'),
|
|
1840
|
+
h('option', { value: 'floating' }, 'Floating (draggable)'),
|
|
1841
|
+
),
|
|
1842
|
+
),
|
|
1843
|
+
h('div', { className: 'dsub-sub' }, 'Floating capsule can be dragged; docked stays bottom-right.'),
|
|
1844
|
+
|
|
1832
1845
|
h('div', { className: 'dsub-row' },
|
|
1833
1846
|
h('span', { className: 'dsub-h' }, t('composerQuota')),
|
|
1834
1847
|
h('select', {
|
|
@@ -3225,6 +3238,7 @@ window.__ModuleLoader__.load({
|
|
|
3225
3238
|
expiryNotifyDays: (data && data.expiryNotifyDays) || 7,
|
|
3226
3239
|
composerQuota: (data && data.composerQuota) || 'off',
|
|
3227
3240
|
composerQuotaDisplay: (data && data.composerQuotaDisplay) || 'current',
|
|
3241
|
+
quotaWidget: (data && data.quotaWidget) || 'docked',
|
|
3228
3242
|
active: (data && data.active) || null,
|
|
3229
3243
|
error: null,
|
|
3230
3244
|
}
|
|
@@ -3639,6 +3653,87 @@ window.__ModuleLoader__.load({
|
|
|
3639
3653
|
)
|
|
3640
3654
|
}
|
|
3641
3655
|
|
|
3656
|
+
// #688: floating/docked quota capsule. Draggable unless docked; opens settings.
|
|
3657
|
+
function QuotaCapsule(props) {
|
|
3658
|
+
const t = (props && props.t) || ((k) => k)
|
|
3659
|
+
const [state, setState] = React.useState({ mode: 'off', active: null, remaining: null })
|
|
3660
|
+
const [pos, setPos] = React.useState({ right: 16, bottom: 16 })
|
|
3661
|
+
const drag = React.useRef(null)
|
|
3662
|
+
const suppressClick = React.useRef(false)
|
|
3663
|
+
React.useEffect(() => {
|
|
3664
|
+
let alive = true
|
|
3665
|
+
const pull = () => {
|
|
3666
|
+
refreshLoggedIn().then(async (r) => {
|
|
3667
|
+
if (!alive) return
|
|
3668
|
+
const mode = r.quotaWidget || 'docked'
|
|
3669
|
+
if (mode === 'off') { setState({ mode, active: null, remaining: null }); return }
|
|
3670
|
+
let a = r.active
|
|
3671
|
+
const current = currentModelProvider(props)
|
|
3672
|
+
if (current && (!a || a.provider !== current)) {
|
|
3673
|
+
const cfg = await fetch('/dsh-subscriptions/config', { cache: 'no-store' }).then((res) => res.json()).catch(() => null)
|
|
3674
|
+
const rows = (cfg && cfg.accounts) || []
|
|
3675
|
+
a = rows.find((row) => row && row.provider === current && row.usagePercent != null) || null
|
|
3676
|
+
}
|
|
3677
|
+
if (!a || a.provider === 'ollama' || a.usagePercent == null) { setState({ mode, active: null, remaining: null }); return }
|
|
3678
|
+
setState({ mode, active: a, remaining: Math.max(0, 100 - a.usagePercent) })
|
|
3679
|
+
}).catch(() => {})
|
|
3680
|
+
}
|
|
3681
|
+
pull()
|
|
3682
|
+
const id = setInterval(pull, 60 * 1000)
|
|
3683
|
+
return () => { alive = false; clearInterval(id) }
|
|
3684
|
+
}, [])
|
|
3685
|
+
if (state.mode === 'off' || !state.active || state.remaining == null) return null
|
|
3686
|
+
const docked = state.mode !== 'floating'
|
|
3687
|
+
const tone = state.remaining <= 10 ? ' dsub-qwFull' : (state.remaining <= 30 ? ' dsub-qwWarn' : '')
|
|
3688
|
+
const openSettings = () => {
|
|
3689
|
+
const clientCtx = props.ctx || window.__dshCtx
|
|
3690
|
+
try {
|
|
3691
|
+
const nav = (clientCtx && clientCtx.pluginNavigation) || (window.__dshCtx && window.__dshCtx.pluginNavigation)
|
|
3692
|
+
if (nav && typeof nav.openBundle === 'function') nav.openBundle(PKG)
|
|
3693
|
+
} catch { /* bestEffort */ }
|
|
3694
|
+
try {
|
|
3695
|
+
const layout = (clientCtx && clientCtx.layout) || (window.__dshCtx && window.__dshCtx.layout)
|
|
3696
|
+
if (layout && typeof layout.selectPanel === 'function') layout.selectPanel('plugins')
|
|
3697
|
+
} catch { /* bestEffort */ }
|
|
3698
|
+
}
|
|
3699
|
+
const onPointerDown = (e) => {
|
|
3700
|
+
if (docked) return
|
|
3701
|
+
drag.current = { x: e.clientX, y: e.clientY, right: pos.right, bottom: pos.bottom, moved: false }
|
|
3702
|
+
try { e.currentTarget.setPointerCapture(e.pointerId) } catch { /* optional */ }
|
|
3703
|
+
}
|
|
3704
|
+
const onPointerMove = (e) => {
|
|
3705
|
+
const d = drag.current
|
|
3706
|
+
if (!d) return
|
|
3707
|
+
const dx = e.clientX - d.x
|
|
3708
|
+
const dy = e.clientY - d.y
|
|
3709
|
+
if (Math.abs(dx) + Math.abs(dy) > 3) { d.moved = true; suppressClick.current = true }
|
|
3710
|
+
setPos({
|
|
3711
|
+
right: Math.max(4, Math.min(window.innerWidth - 40, d.right - dx)),
|
|
3712
|
+
bottom: Math.max(4, Math.min(window.innerHeight - 40, d.bottom - dy)),
|
|
3713
|
+
})
|
|
3714
|
+
}
|
|
3715
|
+
const onPointerUp = () => { drag.current = null }
|
|
3716
|
+
return h('div', {
|
|
3717
|
+
className: 'dsub-qw' + (docked ? ' dsub-qwDocked' : '') + tone,
|
|
3718
|
+
style: { right: pos.right + 'px', bottom: pos.bottom + 'px' },
|
|
3719
|
+
role: 'button',
|
|
3720
|
+
tabIndex: 0,
|
|
3721
|
+
title: (state.active.provider || 'subscription') + ' · ' + Math.round(state.remaining) + '% ' + t('subsFree'),
|
|
3722
|
+
'aria-label': 'Subscription quota ' + Math.round(state.remaining) + '%',
|
|
3723
|
+
onPointerDown,
|
|
3724
|
+
onPointerMove,
|
|
3725
|
+
onPointerUp,
|
|
3726
|
+
onPointerCancel: onPointerUp,
|
|
3727
|
+
onClick: () => { if (suppressClick.current) { suppressClick.current = false; return } openSettings() },
|
|
3728
|
+
onKeyDown: (e) => { if (e.key === 'Enter' || e.key === ' ') openSettings() },
|
|
3729
|
+
},
|
|
3730
|
+
h('span', { className: 'dsub-led ' + ledClass(state.active.usagePercent) }),
|
|
3731
|
+
h('span', { className: 'dsub-qwBrand' }, brandBadge(state.active.provider).icon + ' ' + String(state.active.provider || '').toUpperCase()),
|
|
3732
|
+
h('span', { className: 'dsub-qwBar' }, h('span', { className: 'dsub-qwBarFill', style: { width: state.remaining + '%', display: 'block' } })),
|
|
3733
|
+
h('span', { className: 'dsub-qwPct' }, Math.round(state.remaining) + '%'),
|
|
3734
|
+
)
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3642
3737
|
function registerSubsPill(ctx) {
|
|
3643
3738
|
ctx.effect(() => {
|
|
3644
3739
|
ctx.slots.inject('conversation.session.header.actions', () =>
|
|
@@ -3671,6 +3766,22 @@ window.__ModuleLoader__.load({
|
|
|
3671
3766
|
}, 'dsh-subscriptions: composer quota')
|
|
3672
3767
|
}
|
|
3673
3768
|
|
|
3769
|
+
function registerQuotaCapsule(ctx) {
|
|
3770
|
+
ctx.effect(() => {
|
|
3771
|
+
ctx.slots.inject('conversation.session.header.utilities', () =>
|
|
3772
|
+
ctx.slots.register(
|
|
3773
|
+
{
|
|
3774
|
+
name: 'conversation.session.header.utilities',
|
|
3775
|
+
id: 'dsh-subscriptions-quota-capsule',
|
|
3776
|
+
order: 30,
|
|
3777
|
+
locale: NS,
|
|
3778
|
+
},
|
|
3779
|
+
() => h(ErrorBoundary, null, h(QuotaCapsule, { t, ctx })),
|
|
3780
|
+
),
|
|
3781
|
+
)
|
|
3782
|
+
}, 'dsh-subscriptions: quota capsule')
|
|
3783
|
+
}
|
|
3784
|
+
|
|
3674
3785
|
function registerSlashCommands(ctx) {
|
|
3675
3786
|
ctx.effect(() => {
|
|
3676
3787
|
const triggers = ctx.get('inputTriggers')
|
|
@@ -3769,6 +3880,7 @@ window.__ModuleLoader__.load({
|
|
|
3769
3880
|
registerSettings(ctx)
|
|
3770
3881
|
registerSubsPill(ctx)
|
|
3771
3882
|
registerComposerQuota(ctx)
|
|
3883
|
+
registerQuotaCapsule(ctx)
|
|
3772
3884
|
registerSlashCommands(ctx)
|
|
3773
3885
|
}
|
|
3774
3886
|
return module.exports
|
package/lib/config-schema.js
CHANGED
|
@@ -97,6 +97,8 @@ export const Config = z.object({
|
|
|
97
97
|
.description('#84 Composer quota indicator mode: off, percent, bar or forecast (predictive runway from a sliding window).'),
|
|
98
98
|
composerQuotaDisplay: z.string().volatile().default('current')
|
|
99
99
|
.description('#682 Status-bar quota scope: current (model provider) or hidden.'),
|
|
100
|
+
quotaWidget: z.string().volatile().default('docked')
|
|
101
|
+
.description('#688 Quota widget placement: off, floating (draggable) or docked.'),
|
|
100
102
|
rateLimitWait: z.boolean().volatile().default(true)
|
|
101
103
|
.description('#684 Report the disclosed reset to dsh-llm-retry instead of parking the account on a guess.'),
|
|
102
104
|
rateLimitMaxWaitMs: z.number().volatile().default(21600000)
|
|
@@ -178,6 +180,8 @@ export const Config = z.object({
|
|
|
178
180
|
.description('#562 Remote storage backend: webdav, gist, or s3.'),
|
|
179
181
|
vaultSyncPassphrase: z.string().role('secret').default('')
|
|
180
182
|
.description('#562 Passphrase used to encrypt/decrypt the remote vault archive.'),
|
|
183
|
+
vaultSyncProviders: z.array(z.string()).volatile().default([])
|
|
184
|
+
.description('#691 Optional provider allowlist for selective vault sync. Empty means all providers.'),
|
|
181
185
|
vaultSyncIntervalHours: z.number().volatile().default(24)
|
|
182
186
|
.description('#562 Automated periodic sync interval in hours (0 disables periodic pull).'),
|
|
183
187
|
vaultSyncAutoPush: z.boolean().volatile().default(true)
|
|
@@ -208,6 +212,8 @@ export const Config = z.object({
|
|
|
208
212
|
.description('#562 S3 Secret Access Key.'),
|
|
209
213
|
customVendors: z.array(z.any()).volatile().default([])
|
|
210
214
|
.description('Declarative OpenAI-Responses-compatible providers. See README.'),
|
|
215
|
+
providerPacks: z.array(z.any()).volatile().default([])
|
|
216
|
+
.description('#694 Data-only vendor packs registered through the provider SDK.'),
|
|
211
217
|
})
|
|
212
218
|
|
|
213
219
|
export function publicConfig(cfg) {
|
package/lib/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { getVendor, registerCustomVendor, clearCustomVendors } from './vendors/i
|
|
|
12
12
|
import { generateOnce, SIZES as IMAGE_SIZES } from './images.js'
|
|
13
13
|
import { parseBlob } from './blob.js'
|
|
14
14
|
import { createVendorFromProfile, validateProfiles } from './vendor-factory.js'
|
|
15
|
+
import { registerVendorPack } from './vendor-pack.js'
|
|
15
16
|
import { registerCustomProviderIds, registerDisplayName } from './refs.js'
|
|
16
17
|
import { createSubscriptionsService } from './subscriptions.js'
|
|
17
18
|
import { registerSubscriptionTools } from './native-tools.js'
|
|
@@ -105,6 +106,17 @@ export function apply(ctx, rawConfig) {
|
|
|
105
106
|
if (logger && logger.warn) logger.warn('[dsh-subscriptions] customVendors[' + profile.id + ']: ' + String(e && e.message || e))
|
|
106
107
|
}
|
|
107
108
|
}
|
|
109
|
+
for (const pack of (live().providerPacks || [])) {
|
|
110
|
+
try {
|
|
111
|
+
registerVendorPack(pack, {
|
|
112
|
+
registerVendor: registerCustomVendor,
|
|
113
|
+
registerProviderId: registerCustomProviderIds,
|
|
114
|
+
registerDisplayName,
|
|
115
|
+
})
|
|
116
|
+
} catch (e) {
|
|
117
|
+
if (logger && logger.warn) logger.warn('[dsh-subscriptions] providerPacks[' + (pack && pack.id) + ']: ' + String(e && e.message || e))
|
|
118
|
+
}
|
|
119
|
+
}
|
|
108
120
|
}
|
|
109
121
|
syncCustomVendors()
|
|
110
122
|
|
package/lib/routes/status.js
CHANGED
|
@@ -259,6 +259,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
259
259
|
expiryNotifyDays: live().expiryNotifyDays,
|
|
260
260
|
fastMode: !!live().codexFastMode,
|
|
261
261
|
composerQuota: String(live().composerQuota || 'off'), composerQuotaDisplay: String(live().composerQuotaDisplay || 'current'),
|
|
262
|
+
quotaWidget: String(live().quotaWidget || 'docked'),
|
|
262
263
|
active,
|
|
263
264
|
})
|
|
264
265
|
}),
|
package/lib/vault-sync.js
CHANGED
|
@@ -244,6 +244,40 @@ export async function pullFromS3({ endpoint, bucket, key = 'dsh-subscriptions-va
|
|
|
244
244
|
return res.text()
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
// #691: v2 sync primitives — deterministic slot merge, encrypted snapshots and
|
|
248
|
+
// provider-level selection. Remote wins on conflicts, but conflicting rows are
|
|
249
|
+
// reported so the caller can surface them instead of silently dropping local state.
|
|
250
|
+
export function mergeVaultSlots(localSlots, remoteSlots) {
|
|
251
|
+
const out = (Array.isArray(localSlots) ? localSlots : []).map((slot) => ({ ...slot }))
|
|
252
|
+
const keyOf = (slot) => slot && (slot.ref || (slot.provider + ':' + (slot.index == null ? 1 : slot.index)))
|
|
253
|
+
const indexByKey = new Map(out.map((slot, idx) => [keyOf(slot), idx]))
|
|
254
|
+
const conflicts = []
|
|
255
|
+
for (const remote of (Array.isArray(remoteSlots) ? remoteSlots : [])) {
|
|
256
|
+
if (!remote) continue
|
|
257
|
+
const key = keyOf(remote)
|
|
258
|
+
const idx = indexByKey.get(key)
|
|
259
|
+
if (idx == null) {
|
|
260
|
+
out.push({ ...remote })
|
|
261
|
+
indexByKey.set(key, out.length - 1)
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
264
|
+
if (JSON.stringify(out[idx]) !== JSON.stringify(remote)) conflicts.push({ key, local: out[idx], remote })
|
|
265
|
+
out[idx] = { ...out[idx], ...remote }
|
|
266
|
+
}
|
|
267
|
+
return { slots: out, conflicts }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function createVaultSnapshot({ slots, blobs = {} }, passphrase, label = '') {
|
|
271
|
+
if (!passphrase) throw new Error('passphrase is required for a vault snapshot')
|
|
272
|
+
const { vault, slotCount, blobCount } = exportVault({ slots: slots || [], blobs }, passphrase)
|
|
273
|
+
return { version: 2, label: String(label || ''), createdAt: new Date().toISOString(), vault, slotCount, blobCount }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function restoreVaultSnapshot(snapshot, passphrase) {
|
|
277
|
+
if (!snapshot || typeof snapshot.vault !== 'string' || !snapshot.vault) throw new Error('snapshot is missing its encrypted vault')
|
|
278
|
+
return importVault(snapshot.vault, passphrase)
|
|
279
|
+
}
|
|
280
|
+
|
|
247
281
|
// --- Sync Coordination: Push & Pull ---
|
|
248
282
|
|
|
249
283
|
export async function syncVaultPush({ live, store, fetchFn = fetch }) {
|
|
@@ -258,7 +292,9 @@ export async function syncVaultPush({ live, store, fetchFn = fetch }) {
|
|
|
258
292
|
syncStatus.lastPushStatus = 'running'
|
|
259
293
|
|
|
260
294
|
try {
|
|
261
|
-
const
|
|
295
|
+
const providerFilter = new Set((cfg.vaultSyncProviders || []).map((p) => String(p)))
|
|
296
|
+
const allSlots = cfg.slots || []
|
|
297
|
+
const slots = providerFilter.size ? allSlots.filter((slot) => providerFilter.has(String(slot && slot.provider))) : allSlots
|
|
262
298
|
const blobs = {}
|
|
263
299
|
for (const slot of normalizeSlots(slots)) {
|
|
264
300
|
try {
|
|
@@ -366,26 +402,27 @@ export async function syncVaultPull({ live, store, settingsApi, syncAdapter, ref
|
|
|
366
402
|
}
|
|
367
403
|
|
|
368
404
|
const result = importVault(encryptedData, passphrase)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
405
|
+
const providerFilter = new Set((cfg.vaultSyncProviders || []).map((p) => String(p)))
|
|
406
|
+
const remoteSlots = providerFilter.size
|
|
407
|
+
? result.slots.filter((slot) => providerFilter.has(String(slot && slot.provider)))
|
|
408
|
+
: result.slots
|
|
409
|
+
const allowedRefs = new Set(remoteSlots.map((slot) => slot && slot.ref))
|
|
410
|
+
const remoteBlobs = Object.fromEntries(Object.entries(result.blobs || {}).filter(([ref]) =>
|
|
411
|
+
!providerFilter.size || allowedRefs.has(ref)))
|
|
412
|
+
|
|
413
|
+
// Merge slots into live configuration; keep local-only rows and report conflicts.
|
|
414
|
+
let conflicts = []
|
|
415
|
+
if (remoteSlots.length && settingsApi) {
|
|
372
416
|
const revision = revisionOf(settingsApi)
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (idx >= 0) {
|
|
377
|
-
curSlots[idx] = { ...curSlots[idx], ...s }
|
|
378
|
-
} else {
|
|
379
|
-
curSlots.push(s)
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
const parsed = plainConfig(Config({ ...cfg, slots: curSlots }))
|
|
417
|
+
const merged = mergeVaultSlots(Array.isArray(cfg.slots) ? cfg.slots.slice() : [], remoteSlots)
|
|
418
|
+
conflicts = merged.conflicts
|
|
419
|
+
const parsed = plainConfig(Config({ ...cfg, slots: merged.slots }))
|
|
383
420
|
await settingsApi.replace(parsed, revision)
|
|
384
421
|
}
|
|
385
422
|
|
|
386
423
|
// Save blobs into store
|
|
387
424
|
let blobCount = 0
|
|
388
|
-
for (const [ref, blob] of Object.entries(
|
|
425
|
+
for (const [ref, blob] of Object.entries(remoteBlobs)) {
|
|
389
426
|
await store.saveBlob(ref, validateCredentialBlob(blob))
|
|
390
427
|
blobCount++
|
|
391
428
|
}
|
|
@@ -403,6 +440,7 @@ export async function syncVaultPull({ live, store, settingsApi, syncAdapter, ref
|
|
|
403
440
|
backend,
|
|
404
441
|
slotCount: result.slots.length,
|
|
405
442
|
blobCount,
|
|
443
|
+
conflicts: conflicts.length,
|
|
406
444
|
exportedAt: result.exportedAt,
|
|
407
445
|
timestamp: syncStatus.lastPullAt,
|
|
408
446
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { validateProfile, createVendorFromProfile } from './vendor-factory.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A vendor pack is a data-only provider declaration. It reuses the declarative
|
|
5
|
+
* profile contract but also carries the built vendor object, so a pack can be
|
|
6
|
+
* registered by another plugin without touching providers/vendors/cascade.
|
|
7
|
+
*/
|
|
8
|
+
export function normalizeVendorPack(raw) {
|
|
9
|
+
const pack = raw && typeof raw === 'object' ? raw : {}
|
|
10
|
+
const profile = validateProfile({
|
|
11
|
+
id: pack.id,
|
|
12
|
+
displayName: pack.displayName,
|
|
13
|
+
authUrl: pack.authUrl,
|
|
14
|
+
tokenUrl: pack.tokenUrl,
|
|
15
|
+
baseUrl: pack.baseUrl,
|
|
16
|
+
scope: pack.scope,
|
|
17
|
+
tokenStyle: pack.tokenStyle,
|
|
18
|
+
clientId: pack.clientId,
|
|
19
|
+
redirectUri: pack.redirectUri,
|
|
20
|
+
modelsPath: pack.modelsPath,
|
|
21
|
+
models: pack.models,
|
|
22
|
+
headers: pack.headers,
|
|
23
|
+
})
|
|
24
|
+
return { ...profile, vendor: createVendorFromProfile(profile) }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function registerVendorPack(raw, registry = {}) {
|
|
28
|
+
const pack = normalizeVendorPack(raw)
|
|
29
|
+
if (typeof registry.registerVendor !== 'function') throw new Error('vendor pack registry is unavailable')
|
|
30
|
+
registry.registerVendor(pack.vendor)
|
|
31
|
+
if (typeof registry.registerProviderId === 'function') registry.registerProviderId([pack.id])
|
|
32
|
+
if (typeof registry.registerDisplayName === 'function' && pack.displayName) {
|
|
33
|
+
registry.registerDisplayName(pack.id, pack.displayName)
|
|
34
|
+
}
|
|
35
|
+
return pack
|
|
36
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Headless/TUI overlay for a profile that has no web UI.
|
|
2
|
+
# Usage: dsh --profile headless --patch /absolute/path/overlay.example.yml "your task"
|
|
3
|
+
- id: dsh-subscriptions
|
|
4
|
+
name: '@goodandready/dsh-subscriptions'
|
|
5
|
+
config:
|
|
6
|
+
composerQuota: percent
|
|
7
|
+
composerQuotaDisplay: current
|
|
8
|
+
rotationStrategy: least-loaded
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
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",
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"README.md",
|
|
18
18
|
"README.ru.md",
|
|
19
19
|
"README.zh.md",
|
|
20
|
-
"LICENSE"
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"bin/",
|
|
22
|
+
"overlay.example.yml"
|
|
21
23
|
],
|
|
22
24
|
"keywords": [
|
|
23
25
|
"dsh",
|
|
@@ -50,6 +52,32 @@
|
|
|
50
52
|
"@deepseek-ai/dsh-client-locale",
|
|
51
53
|
"@deepseek-ai/dsh-client-ui-slots"
|
|
52
54
|
]
|
|
55
|
+
},
|
|
56
|
+
"market": {
|
|
57
|
+
"name": {
|
|
58
|
+
"en": "Subscriptions",
|
|
59
|
+
"zh": "订阅管理"
|
|
60
|
+
},
|
|
61
|
+
"description": {
|
|
62
|
+
"en": "Use ChatGPT, Claude, Grok, Antigravity and 12 more subscriptions as DSH providers with account pools, quota tools and OAuth.",
|
|
63
|
+
"zh": "将 ChatGPT、Claude、Grok、Antigravity 等 16 种订阅接入 DSH,支持账户池、额度工具和 OAuth。"
|
|
64
|
+
},
|
|
65
|
+
"category": "providers",
|
|
66
|
+
"icon": "subscriptions",
|
|
67
|
+
"tags": [
|
|
68
|
+
"subscriptions",
|
|
69
|
+
"oauth",
|
|
70
|
+
"rotation",
|
|
71
|
+
"quota",
|
|
72
|
+
"providers",
|
|
73
|
+
"media"
|
|
74
|
+
],
|
|
75
|
+
"defaultProviders": [
|
|
76
|
+
"codex",
|
|
77
|
+
"claude",
|
|
78
|
+
"grok",
|
|
79
|
+
"antigravity"
|
|
80
|
+
]
|
|
53
81
|
}
|
|
54
82
|
},
|
|
55
83
|
"peerDependencies": {
|
|
@@ -77,5 +105,8 @@
|
|
|
77
105
|
"@types/node": "^22.20.2",
|
|
78
106
|
"eslint": "^10.4.0",
|
|
79
107
|
"typescript": "^7.0.2"
|
|
108
|
+
},
|
|
109
|
+
"bin": {
|
|
110
|
+
"dsh-subscriptions": "./bin/dsh-subscriptions.mjs"
|
|
80
111
|
}
|
|
81
112
|
}
|