@goodandready/dsh-subscriptions 0.4.8 → 0.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/adapter.js +14 -0
- package/lib/client.js +73 -8
- package/lib/history.js +66 -0
- package/lib/index.js +88 -0
- package/lib/subscriptions.js +14 -0
- package/package.json +1 -1
package/lib/adapter.js
CHANGED
|
@@ -121,6 +121,20 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
121
121
|
})
|
|
122
122
|
// поток завершился успешно: снять кулдаун и здоровье-штрафы
|
|
123
123
|
if (typeof deps.recordSuccess === 'function') deps.recordSuccess(account.ref)
|
|
124
|
+
// #65: записать в историю запросов и стоимости.
|
|
125
|
+
if (typeof deps.recordHistory === 'function') {
|
|
126
|
+
try {
|
|
127
|
+
deps.recordHistory({
|
|
128
|
+
provider,
|
|
129
|
+
ref: account.ref,
|
|
130
|
+
model: (opts && opts.model) || null,
|
|
131
|
+
path: '/responses',
|
|
132
|
+
method: 'POST',
|
|
133
|
+
status: 200,
|
|
134
|
+
kind: 'stream',
|
|
135
|
+
})
|
|
136
|
+
} catch {}
|
|
137
|
+
}
|
|
124
138
|
} catch (err) {
|
|
125
139
|
if (err && err.code === 'VALIDATION_REQUIRED' && err.validationUrl) {
|
|
126
140
|
await deps.saveBlob(account.ref, {
|
package/lib/client.js
CHANGED
|
@@ -42,7 +42,15 @@ window.__ModuleLoader__.load({
|
|
|
42
42
|
'.dsub-badge-warn{color:var(--dsw-alias-state-warning-primary);border-color:currentColor}' +
|
|
43
43
|
'.dsub-chip{display:inline-flex;align-items:center;gap:5px;height:26px;padding:0 10px;border-radius:999px;font-size:12px;font-weight:600;line-height:1;white-space:nowrap;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2)}' +
|
|
44
44
|
'.dsub-chipOn{color:var(--dsw-alias-state-success-primary);border-color:currentColor}' +
|
|
45
|
+
'.dsub-chipWarn{color:var(--dsw-alias-state-warning-primary);border-color:currentColor}' +
|
|
46
|
+
'.dsub-chipDanger{color:var(--dsw-alias-state-error-primary);border-color:currentColor}' +
|
|
45
47
|
'.dsub-chipDot{width:6px;height:6px;border-radius:999px;background:currentColor;flex:none}' +
|
|
48
|
+
'.dsub-chipActive{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);font-size:12px;font-weight:600;line-height:1;white-space:nowrap;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-3)}' +
|
|
49
|
+
'.dsub-chipActiveWarn{color:var(--dsw-alias-state-warning-primary);border-color:currentColor}' +
|
|
50
|
+
'.dsub-chipActiveDanger{color:var(--dsw-alias-state-error-primary);border-color:currentColor}' +
|
|
51
|
+
'.dsub-chipActiveOk{color:var(--dsw-alias-state-success-primary);border-color:currentColor}' +
|
|
52
|
+
'.dsub-chipActiveBar{height:4px;width:48px;background:var(--dsw-alias-bg-layer-1);border-radius:2px;flex:none;overflow:hidden}' +
|
|
53
|
+
'.dsub-chipActiveFill{height:100%;border-radius:2px;background:currentColor}' +
|
|
46
54
|
|
|
47
55
|
'.dsub-link{background:none;border:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;padding:0}' +
|
|
48
56
|
'.dsub-bar{position:relative;height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;flex:1;min-width:80px}' +
|
|
@@ -98,6 +106,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
98
106
|
'storedAs': 'Stored as',
|
|
99
107
|
'switchLabel': 'Provider',
|
|
100
108
|
'chipActive': 'Subscriptions',
|
|
109
|
+
'expiryLabel': 'expires',
|
|
101
110
|
'slashLogin': 'Login to a subscription provider',
|
|
102
111
|
'slashLogout': 'Log out a subscription provider',
|
|
103
112
|
'slashStatus': 'Subscription status',
|
|
@@ -152,6 +161,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
152
161
|
'storedAs': 'Хранится как',
|
|
153
162
|
'switchLabel': 'Провайдер',
|
|
154
163
|
'chipActive': 'Подписки',
|
|
164
|
+
'expiryLabel': 'окончание',
|
|
155
165
|
'slashLogin': 'Вход в провайдера подписки',
|
|
156
166
|
'slashLogout': 'Выйти из провайдера подписки',
|
|
157
167
|
'slashStatus': 'Статус подписок',
|
|
@@ -617,31 +627,86 @@ function PluginCard(props) {
|
|
|
617
627
|
const res = await fetch('/dsh-subscriptions/status', { cache: 'no-store' })
|
|
618
628
|
const data = await res.json().catch(() => ({}))
|
|
619
629
|
const loggedIn = (data && data.loggedIn) || {}
|
|
620
|
-
|
|
621
|
-
|
|
630
|
+
const usage = (data && data.usagePercent) || {}
|
|
631
|
+
const logged = ORDER.filter((p) => loggedIn[p])
|
|
632
|
+
const maxUsage = logged.reduce((m, p) => Math.max(m, usage[p] || 0), 0)
|
|
633
|
+
return {
|
|
634
|
+
logged,
|
|
635
|
+
usage: maxUsage,
|
|
636
|
+
expiresAt: (data && data.expiresAt) || {},
|
|
637
|
+
labels: (data && data.labels) || {},
|
|
638
|
+
expiryNotifyDays: (data && data.expiryNotifyDays) || 7,
|
|
639
|
+
active: (data && data.active) || null,
|
|
640
|
+
}
|
|
641
|
+
} catch { return { logged: [], usage: 0, expiresAt: {}, labels: {}, expiryNotifyDays: 7, active: null } }
|
|
622
642
|
}
|
|
623
643
|
|
|
624
644
|
// #52: компактный чип в шапке сессии со статусом подключённых подписок.
|
|
625
645
|
function HeaderChip(props) {
|
|
626
|
-
const [state, setState] = React.useState({ loading: true, count: 0 })
|
|
646
|
+
const [state, setState] = React.useState({ loading: true, count: 0, usage: 0, expiry: null, active: null })
|
|
627
647
|
const t = (props && props.t) || ((k) => k)
|
|
628
648
|
React.useEffect(() => {
|
|
629
649
|
const pull = () => {
|
|
630
|
-
refreshLoggedIn().then((
|
|
631
|
-
|
|
632
|
-
|
|
650
|
+
refreshLoggedIn().then((r) => {
|
|
651
|
+
// #67: ближайшее окончание подписки в пределах expiryNotifyDays.
|
|
652
|
+
let expiry = null
|
|
653
|
+
const now = Date.now()
|
|
654
|
+
const days = r.expiryNotifyDays || 7
|
|
655
|
+
for (const p of r.logged) {
|
|
656
|
+
const at = r.expiresAt[p]
|
|
657
|
+
if (!at) continue
|
|
658
|
+
const msLeft = at - now
|
|
659
|
+
if (msLeft > 0 && msLeft <= days * 24 * 60 * 60 * 1000) {
|
|
660
|
+
if (!expiry || at < expiry.at) expiry = { at, label: r.labels[p] || p }
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
setState({ loading: false, count: r.logged.length, usage: r.usage, expiry, active: r.active })
|
|
664
|
+
}).catch(() => setState({ loading: false, count: 0, usage: 0, expiry: null, active: null }))
|
|
633
665
|
}
|
|
634
666
|
pull()
|
|
635
667
|
const id = setInterval(pull, 60 * 1000)
|
|
636
668
|
return () => clearInterval(id)
|
|
637
669
|
}, [])
|
|
670
|
+
// #69: если есть активная подписка — показываем её (провайдер + план + usage%).
|
|
671
|
+
if (state.active) {
|
|
672
|
+
const a = state.active
|
|
673
|
+
const u = typeof a.usagePercent === 'number' ? a.usagePercent : null
|
|
674
|
+
let acls = 'dsub-chipActive'
|
|
675
|
+
if (u != null) acls += u >= 100 ? ' dsub-chipActiveDanger' : (u >= 80 ? ' dsub-chipActiveDanger' : (u >= 50 ? ' dsub-chipActiveWarn' : ' dsub-chipActiveOk'))
|
|
676
|
+
const prov = t('provider' + a.provider.charAt(0).toUpperCase() + a.provider.slice(1)) || a.provider
|
|
677
|
+
const plan = a.plan || ''
|
|
678
|
+
const label = plan ? (prov + ' ' + plan) : prov
|
|
679
|
+
const pctText = u == null ? '' : Math.round(u) + '%'
|
|
680
|
+
return React.createElement('span', {
|
|
681
|
+
className: acls,
|
|
682
|
+
title: a.model ? (prov + ' · ' + a.model) : prov,
|
|
683
|
+
},
|
|
684
|
+
React.createElement('span', { className: 'dsub-chipDot' }),
|
|
685
|
+
label,
|
|
686
|
+
u != null
|
|
687
|
+
? React.createElement('span', { className: 'dsub-chipActiveBar' },
|
|
688
|
+
React.createElement('span', { className: 'dsub-chipActiveFill', style: { width: Math.min(100, Math.max(0, u)) + '%' } }))
|
|
689
|
+
: null,
|
|
690
|
+
pctText ? React.createElement('span', null, pctText) : null,
|
|
691
|
+
)
|
|
692
|
+
}
|
|
638
693
|
const on = state.count > 0
|
|
694
|
+
// #66: цвет чипа по максимальному usagePercent (50/80/100).
|
|
695
|
+
const u = state.usage
|
|
696
|
+
let cls = 'dsub-chip'
|
|
697
|
+
if (on) cls += u >= 100 ? ' dsub-chipDanger' : (u >= 80 ? ' dsub-chipDanger' : (u >= 50 ? ' dsub-chipWarn' : ' dsub-chipOn'))
|
|
698
|
+
// #67: надпись об окончании подписки.
|
|
699
|
+
let text = on ? String(state.count) : '—'
|
|
700
|
+
if (state.expiry) {
|
|
701
|
+
const d = new Date(state.expiry.at).toLocaleDateString()
|
|
702
|
+
text = t('expiryLabel') + ': ' + state.expiry.label + ' ' + d
|
|
703
|
+
}
|
|
639
704
|
return React.createElement('span', {
|
|
640
|
-
className:
|
|
705
|
+
className: cls,
|
|
641
706
|
title: on ? t('chipActive') + ': ' + state.count : t('none'),
|
|
642
707
|
},
|
|
643
708
|
React.createElement('span', { className: 'dsub-chipDot' }),
|
|
644
|
-
|
|
709
|
+
text,
|
|
645
710
|
)
|
|
646
711
|
}
|
|
647
712
|
|
package/lib/history.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
|
|
5
|
+
// #65: история запросов и стоимости. Хранится в
|
|
6
|
+
// ~/.dsh/storages/dsh-subscriptions/history.json (JSON-массив, новые сверху).
|
|
7
|
+
// Все IO best-effort и синхронные: сбой записи не должен ронять харнесс.
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_HISTORY_DIR = join(homedir(), '.dsh', 'storages', 'dsh-subscriptions')
|
|
10
|
+
|
|
11
|
+
export class HistoryStore {
|
|
12
|
+
constructor(dir = DEFAULT_HISTORY_DIR, ttlMs = 7 * 24 * 60 * 60 * 1000) {
|
|
13
|
+
this.dir = dir
|
|
14
|
+
this.ttlMs = ttlMs
|
|
15
|
+
this.path = join(dir, 'history.json')
|
|
16
|
+
this.rows = []
|
|
17
|
+
this._load()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_load() {
|
|
21
|
+
try {
|
|
22
|
+
mkdirSync(this.dir, { recursive: true })
|
|
23
|
+
} catch { /* best-effort */ }
|
|
24
|
+
try {
|
|
25
|
+
const raw = JSON.parse(readFileSync(this.path, 'utf8'))
|
|
26
|
+
if (Array.isArray(raw)) this.rows = raw
|
|
27
|
+
} catch { /* absent/corrupt history is normal */ }
|
|
28
|
+
this._prune()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_prune() {
|
|
32
|
+
const cutoff = Date.now() - this.ttlMs
|
|
33
|
+
const before = this.rows.length
|
|
34
|
+
this.rows = this.rows.filter((r) => r && r.ts && r.ts >= cutoff)
|
|
35
|
+
if (this.rows.length !== before) this._persist()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_persist() {
|
|
39
|
+
try {
|
|
40
|
+
mkdirSync(this.dir, { recursive: true })
|
|
41
|
+
writeFileSync(this.path, JSON.stringify(this.rows))
|
|
42
|
+
} catch { /* best-effort */ }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Добавить одну запись. Возвращает длину истории. */
|
|
46
|
+
add(entry) {
|
|
47
|
+
if (!entry || typeof entry !== 'object') return this.rows.length
|
|
48
|
+
this.rows.unshift({ ts: Date.now(), ...entry })
|
|
49
|
+
this._prune()
|
|
50
|
+
this._persist()
|
|
51
|
+
return this.rows.length
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Последние N записей (новые сверху). */
|
|
55
|
+
recent(n) {
|
|
56
|
+
return this.rows.slice(0, n)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
all() {
|
|
60
|
+
return this.rows
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
size() {
|
|
64
|
+
return this.rows.length
|
|
65
|
+
}
|
|
66
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { createSubscriptionsService } from './subscriptions.js'
|
|
|
13
13
|
import { encryptWithPassphrase, decryptWithPassphrase } from './crypto.js'
|
|
14
14
|
import { quotaSnapshot } from './ratelimit.js'
|
|
15
15
|
import { createAccountStore, normalizeSlots, vendorConfig } from './accounts.js'
|
|
16
|
+
import { HistoryStore } from './history.js'
|
|
16
17
|
import {
|
|
17
18
|
inspectGoogleAccount,
|
|
18
19
|
antigravityMetadata,
|
|
@@ -32,6 +33,8 @@ const Slot = z.object({
|
|
|
32
33
|
.description('Account slot number. Credential ref is <PROVIDER>_OAUTH_<index>.'),
|
|
33
34
|
label: z.string().default('')
|
|
34
35
|
.description('Optional display label. Empty uses the account email after login.'),
|
|
36
|
+
expiresAt: z.number().default(0)
|
|
37
|
+
.description('#67 Optional subscription expiry timestamp (ms). When set and within expiryNotifyDays, the header chip shows the account and expiry date.'),
|
|
35
38
|
})
|
|
36
39
|
|
|
37
40
|
const defaultSlots = PROVIDERS.map((provider) => ({ provider, index: 1, label: '' }))
|
|
@@ -49,6 +52,8 @@ export const Config = z.object({
|
|
|
49
52
|
.description('Background health-check interval in minutes. 0 disables.'),
|
|
50
53
|
notifyLimits: z.boolean().default(true)
|
|
51
54
|
.description('Emit log notices when usage crosses 70/90/100% of a window.'),
|
|
55
|
+
expiryNotifyDays: z.number().default(7)
|
|
56
|
+
.description('#67 Warn in the header chip this many days before a subscription expiry (expiresAt). 0 disables.'),
|
|
52
57
|
slots: z.array(Slot).default(defaultSlots)
|
|
53
58
|
.description('Account slots. Secrets are not stored here; only the credential ref names.'),
|
|
54
59
|
useWebCallback: z.boolean().default(false)
|
|
@@ -132,6 +137,9 @@ export function apply(ctx, config) {
|
|
|
132
137
|
try { ctx.emit && ctx.emit('subscriptions.limit-notice', { provider, ref, window: win.id, usedPercent: win.usedPercent, threshold }) } catch {}
|
|
133
138
|
},
|
|
134
139
|
})
|
|
140
|
+
const history = new HistoryStore()
|
|
141
|
+
const recordHistory = (entry) => history.add(entry)
|
|
142
|
+
|
|
135
143
|
const subscriptions = createSubscriptionsService({
|
|
136
144
|
listAccounts: (provider) => store.listAccounts(provider),
|
|
137
145
|
loadBlob: (ref) => store.loadBlob(ref),
|
|
@@ -149,6 +157,7 @@ export function apply(ctx, config) {
|
|
|
149
157
|
rememberQuota: (ref, snap) => store.rememberQuota(ref, snap),
|
|
150
158
|
rememberRequest: (ref) => store.rememberRequest(ref),
|
|
151
159
|
getRequestCount: (ref) => store.getRequestCount(ref),
|
|
160
|
+
recordHistory,
|
|
152
161
|
fetchImpl: fetch,
|
|
153
162
|
})
|
|
154
163
|
|
|
@@ -205,6 +214,7 @@ export function apply(ctx, config) {
|
|
|
205
214
|
getQuota: (ref) => store.getQuota(ref),
|
|
206
215
|
refreshUsage: (provider) => store.refreshUsage(provider),
|
|
207
216
|
saveBlob: (ref, blob) => store.saveBlob(ref, blob),
|
|
217
|
+
recordHistory,
|
|
208
218
|
fetchImpl: fetch,
|
|
209
219
|
})
|
|
210
220
|
|
|
@@ -508,9 +518,56 @@ export function apply(ctx, config) {
|
|
|
508
518
|
return
|
|
509
519
|
}
|
|
510
520
|
const logged = await store.loggedInProviders()
|
|
521
|
+
// #66/#67: usagePercent (максимум по аккаунтам) и expiresAt для чипа.
|
|
522
|
+
const usage = {}
|
|
523
|
+
const expires = {}
|
|
524
|
+
const labels = {}
|
|
525
|
+
for (const slot of normalizeSlots(live().slots)) {
|
|
526
|
+
try {
|
|
527
|
+
const info = await store.describeRef(slot.ref)
|
|
528
|
+
if (info.usagePercent != null) {
|
|
529
|
+
usage[slot.provider] = Math.max(usage[slot.provider] || 0, info.usagePercent)
|
|
530
|
+
}
|
|
531
|
+
// #67: дата окончания подписки берётся из слота (вводится в настройках).
|
|
532
|
+
if (slot.expiresAt) {
|
|
533
|
+
expires[slot.provider] = Math.max(expires[slot.provider] || 0, slot.expiresAt)
|
|
534
|
+
labels[slot.provider] = slot.label || info.label || slot.provider
|
|
535
|
+
}
|
|
536
|
+
} catch {}
|
|
537
|
+
}
|
|
538
|
+
// #69: активная подписка = последний успешный запрос (новые сверху).
|
|
539
|
+
let active = null
|
|
540
|
+
const last = history.recent(1)
|
|
541
|
+
if (last.length) {
|
|
542
|
+
const lastRow = last[0]
|
|
543
|
+
const accts = await store.listAccounts(lastRow.provider)
|
|
544
|
+
const acct = accts.find((a) => a.ref === lastRow.ref) || accts[0]
|
|
545
|
+
let plan = ''
|
|
546
|
+
let status = 'ok'
|
|
547
|
+
try {
|
|
548
|
+
const info = await store.describeRef(lastRow.ref)
|
|
549
|
+
plan = info.paidTierName || ''
|
|
550
|
+
if (info.validationUrl) status = 'verify'
|
|
551
|
+
else if (info.cooldownUntil && info.cooldownUntil > Date.now()) status = 'cooldown'
|
|
552
|
+
} catch {}
|
|
553
|
+
active = {
|
|
554
|
+
provider: lastRow.provider,
|
|
555
|
+
model: lastRow.model || null,
|
|
556
|
+
path: lastRow.path || null,
|
|
557
|
+
plan,
|
|
558
|
+
usagePercent: usage[lastRow.provider] != null ? usage[lastRow.provider] : null,
|
|
559
|
+
status,
|
|
560
|
+
at: lastRow.ts,
|
|
561
|
+
}
|
|
562
|
+
}
|
|
511
563
|
writeJson(res, 200, {
|
|
512
564
|
ok: true,
|
|
513
565
|
loggedIn: Object.fromEntries(PROVIDERS.map((id) => [id, logged.includes(id)])),
|
|
566
|
+
usagePercent: usage,
|
|
567
|
+
expiresAt: expires,
|
|
568
|
+
labels,
|
|
569
|
+
expiryNotifyDays: live().expiryNotifyDays,
|
|
570
|
+
active,
|
|
514
571
|
})
|
|
515
572
|
},
|
|
516
573
|
}), 'dsh-subscriptions: /status')
|
|
@@ -869,6 +926,25 @@ export function apply(ctx, config) {
|
|
|
869
926
|
}), 'dsh-subscriptions: /import-token')
|
|
870
927
|
|
|
871
928
|
// #50: сводная страница /subscriptions (localhost-only).
|
|
929
|
+
// #65: история запросов и стоимости (JSON).
|
|
930
|
+
ctx.effect(() => ctx.webServer.register({
|
|
931
|
+
kind: 'exact',
|
|
932
|
+
path: '/dsh-subscriptions/history',
|
|
933
|
+
handler: async (req, res) => {
|
|
934
|
+
if (req.method !== 'GET') {
|
|
935
|
+
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET only' } })
|
|
936
|
+
return
|
|
937
|
+
}
|
|
938
|
+
const host = (req.headers.host || '').split(':')[0]
|
|
939
|
+
if (host !== 'localhost' && host !== '127.0.0.1') {
|
|
940
|
+
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'localhost only' } })
|
|
941
|
+
return
|
|
942
|
+
}
|
|
943
|
+
const limit = Math.min(Number(queryOf(req).get('limit') || '10'), 100)
|
|
944
|
+
writeJson(res, 200, { ok: true, total: history.size(), items: history.recent(limit) })
|
|
945
|
+
},
|
|
946
|
+
}), 'dsh-subscriptions: /history')
|
|
947
|
+
|
|
872
948
|
ctx.effect(() => ctx.webServer.register({
|
|
873
949
|
kind: 'exact',
|
|
874
950
|
path: '/dsh-subscriptions/subscriptions',
|
|
@@ -925,6 +1001,18 @@ h1{font-size:20px} .dim{color:#8b949e;font-size:13px}
|
|
|
925
1001
|
<div class="dim">/subscriptions — localhost only</div>
|
|
926
1002
|
<div class="stats"><span><b>${connected}</b> connected</span><span><b>${accounts.length}</b> accounts</span><span><b>${slots.length}</b> slots</span></div>
|
|
927
1003
|
${body}
|
|
1004
|
+
<h2>History</h2>
|
|
1005
|
+
<div class="dim">last 10 · <a href="/dsh-subscriptions/history?limit=100">show 100</a></div>
|
|
1006
|
+
<div class="hist" id="hist"></div>
|
|
1007
|
+
<script>
|
|
1008
|
+
fetch('/dsh-subscriptions/history?limit=10').then(r=>r.json()).then(d=>{
|
|
1009
|
+
const el=document.getElementById('hist')
|
|
1010
|
+
if(!d||!d.items||!d.items.length){el.textContent='No requests yet.';return}
|
|
1011
|
+
el.innerHTML='<table class="grid"><tr><th>time</th><th>provider</th><th>model</th><th>path</th><th>status</th></tr>'+
|
|
1012
|
+
d.items.map(i=>'<tr><td>'+new Date(i.ts).toLocaleString()+'</td><td>'+esc(i.provider)+'</td><td>'+esc(i.model||'')+'</td><td>'+esc(i.path)+'</td><td>'+i.status+'</td></tr>').join('')+'</table>'
|
|
1013
|
+
}).catch(()=>{})
|
|
1014
|
+
function esc(x){return String(x==null?'':x).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}
|
|
1015
|
+
</script>
|
|
928
1016
|
</body></html>`)
|
|
929
1017
|
},
|
|
930
1018
|
}), 'dsh-subscriptions: /subscriptions')
|
package/lib/subscriptions.js
CHANGED
|
@@ -159,6 +159,20 @@ export function createSubscriptionsService(deps) {
|
|
|
159
159
|
else err.code = "VENDOR"
|
|
160
160
|
throw err
|
|
161
161
|
}
|
|
162
|
+
// #65: записать в историю запросов и стоимости.
|
|
163
|
+
if (typeof deps.recordHistory === "function") {
|
|
164
|
+
try {
|
|
165
|
+
deps.recordHistory({
|
|
166
|
+
provider,
|
|
167
|
+
ref: account.ref,
|
|
168
|
+
model: (body && typeof body === "object" && body.model) || null,
|
|
169
|
+
path,
|
|
170
|
+
method,
|
|
171
|
+
status: res.status || 200,
|
|
172
|
+
kind: "request",
|
|
173
|
+
})
|
|
174
|
+
} catch {}
|
|
175
|
+
}
|
|
162
176
|
return res
|
|
163
177
|
} catch (err) {
|
|
164
178
|
lastError = err
|
package/package.json
CHANGED