@goodandready/dsh-cost-meter 0.7.7 → 0.7.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -8
- package/docs/README.ru.md +14 -8
- package/docs/design/DESIGN.md +1 -1
- package/docs/plans/findings.md +19 -13
- package/docs/plans/progress.md +12 -10
- package/docs/plans/task_plan.md +30 -23
- package/lib/client.js +77 -9
- package/lib/index.js +12 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -108,14 +108,20 @@ dsh-cost-meter:
|
|
|
108
108
|
|
|
109
109
|
### Configuration Parameters
|
|
110
110
|
|
|
111
|
-
| Parameter | Type | Default | Description |
|
|
112
|
-
|
|
113
|
-
| `currency` | `string` | `"$"` | Display currency symbol
|
|
114
|
-
| `usdRate` | `number` | `1.0` | Exchange rate multiplier
|
|
115
|
-
| `displayTimeZone` | `string` | `"
|
|
116
|
-
| `useOpenRouter` | `boolean` | `true` |
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
111
|
+
| Parameter | Scope / Location | Type | Default | Description |
|
|
112
|
+
|:---|:---|:---|:---|:---|
|
|
113
|
+
| `currency` | GUI / `settings.yaml` | `string` | `"$"` | Display currency symbol (e.g. `$`, `₽`, `€`) |
|
|
114
|
+
| `usdRate` | GUI / `settings.yaml` | `number` | `1.0` | Exchange rate multiplier: units of currency per 1 USD |
|
|
115
|
+
| `displayTimeZone` | GUI / `settings.yaml` | `string` | `"Europe/Moscow"` | IANA timezone for peak/off-peak windows formatting |
|
|
116
|
+
| `useOpenRouter` | GUI / `settings.yaml` | `boolean` | `true` | Automatically fetch rates & discount windows from OpenRouter |
|
|
117
|
+
| `refreshHours` | GUI / `settings.yaml` | `number` | `24` | Hours between periodic OpenRouter catalog updates |
|
|
118
|
+
| `deepseekPeakPrices` | `settings.yaml` only | `object` | `{}` | Override built-in DeepSeek peak rates by model id |
|
|
119
|
+
| `prices` | `settings.yaml` only | `object` | `{}` | Manual rates per 1M tokens `{ input, output, cacheHit?, cacheWrite? }` |
|
|
120
|
+
| `modelMap` | `settings.yaml` only | `object` | `{}` | Route override: `"provider/model"` -> OpenRouter model id |
|
|
121
|
+
| `manualPeakWindowsUtc` | `settings.yaml` only | `array` | `[]` | Manual peak windows HH:MM-HH:MM UTC used with manual prices |
|
|
122
|
+
| `manualOffPeakMultiplier` | `settings.yaml` only | `number` | `1.0` | Multiplier applied outside manual peak windows |
|
|
123
|
+
|
|
124
|
+
> **Note on GUI vs YAML**: Primary scalar parameters (`currency`, `usdRate`, `displayTimeZone`, `useOpenRouter`, `refreshHours`) are directly editable in the GUI Settings Card under **Settings → Plugins → Plugin Settings → Cost Meter**. Advanced structured rules (`prices`, `modelMap`, `deepseekPeakPrices`, `manualPeakWindowsUtc`, `manualOffPeakMultiplier`) are configured in `settings.yaml` due to their complex dictionary/array schema.
|
|
119
125
|
|
|
120
126
|
---
|
|
121
127
|
|
package/docs/README.ru.md
CHANGED
|
@@ -108,14 +108,20 @@ dsh-cost-meter:
|
|
|
108
108
|
|
|
109
109
|
### Параметры конфигурации
|
|
110
110
|
|
|
111
|
-
| Параметр | Тип | По умолчанию | Описание |
|
|
112
|
-
|
|
113
|
-
| `currency` | `string` | `"₽"` | Символ
|
|
114
|
-
| `usdRate` | `number` | `
|
|
115
|
-
| `displayTimeZone` | `string` | `"
|
|
116
|
-
| `useOpenRouter` | `boolean` | `true` | Загружать ли каталог
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
111
|
+
| Параметр | Доступность | Тип | По умолчанию | Описание |
|
|
112
|
+
|:---|:---|:---|:---|:---|
|
|
113
|
+
| `currency` | GUI / `settings.yaml` | `string` | `"₽"` | Символ валюты в интерфейсе (например, `₽`, `$`, `€`) |
|
|
114
|
+
| `usdRate` | GUI / `settings.yaml` | `number` | `80.0` | Курс обмена: единиц валюты за 1 USD |
|
|
115
|
+
| `displayTimeZone` | GUI / `settings.yaml` | `string` | `"Europe/Moscow"` | IANA часовой пояс для отображения окон пика и не-пика |
|
|
116
|
+
| `useOpenRouter` | GUI / `settings.yaml` | `boolean` | `true` | Загружать ли каталог тарифов и скидочных окон OpenRouter |
|
|
117
|
+
| `refreshHours` | GUI / `settings.yaml` | `number` | `24` | Частота обновления каталога OpenRouter (в часах) |
|
|
118
|
+
| `deepseekPeakPrices` | Только `settings.yaml` | `object` | `{}` | Переопределение встроенных пиковых ставок DeepSeek по id модели |
|
|
119
|
+
| `prices` | Только `settings.yaml` | `object` | `{}` | Ручные ставки за 1M токенов `{ input, output, cacheHit?, cacheWrite? }` |
|
|
120
|
+
| `modelMap` | Только `settings.yaml` | `object` | `{}` | Сопоставление маршрута `"provider/model"` на id в OpenRouter |
|
|
121
|
+
| `manualPeakWindowsUtc` | Только `settings.yaml` | `array` | `[]` | Ручные окна пика HH:MM-HH:MM UTC для ручных ставок |
|
|
122
|
+
| `manualOffPeakMultiplier` | Только `settings.yaml` | `number` | `1.0` | Коэффициент тарифа вне ручных окон пика |
|
|
123
|
+
|
|
124
|
+
> **Интерфейс настроек (GUI) vs settings.yaml**: Основные параметры (`currency`, `usdRate`, `displayTimeZone`, `useOpenRouter`, `refreshHours`) настраиваются прямо в карточке интерфейса DSH (**Настройки → Плагины → Настройки плагинов → Cost Meter**). Расширенные структурированные правила сопоставления и ручных тарифов задаются через `settings.yaml` из-за сложной древовидной структуры.
|
|
119
125
|
|
|
120
126
|
---
|
|
121
127
|
|
package/docs/design/DESIGN.md
CHANGED
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
- Не допускать утечек локальных путей и инфраструктурных данных в коде или документации.
|
|
83
83
|
|
|
84
84
|
## History & Decisions
|
|
85
|
-
- 2026-09-05 (v0.7.
|
|
85
|
+
- 2026-09-05 (v0.7.9):
|
|
86
86
|
- Hotfix: исправлен ReferenceError (t is not defined) в объекте словаря ru (lib/client.js) (#15).
|
|
87
87
|
- Добавлен автоматический тест на чистое исполнение фабрики client loader (`test/public-route.test.mjs`).
|
|
88
88
|
- 2026-09-05 (v0.7.4):
|
package/docs/plans/findings.md
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
# Technical Findings
|
|
1
|
+
# Technical Findings (Issue #21)
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
## 1. Config Fields Breakdown
|
|
4
|
+
The plugin `Config` defines 10 fields:
|
|
5
|
+
1. `currency` (string, default '$') -> Present in UI
|
|
6
|
+
2. `usdRate` (number, default 1) -> Present in UI
|
|
7
|
+
3. `displayTimeZone` (string, default 'Europe/Moscow') -> Present in UI
|
|
8
|
+
4. `useOpenRouter` (boolean, default true) -> Missing in UI -> To be added as a checkbox in `CostMeterCard`
|
|
9
|
+
5. `refreshHours` (number, default 24) -> Missing in UI -> To be added as a numeric input in `CostMeterCard`
|
|
10
|
+
6. `modelMap` (dict of string -> string) -> Advanced JSON/YAML route mapping
|
|
11
|
+
7. `prices` (dict of PriceRow) -> Advanced manual pricing rules
|
|
12
|
+
8. `deepseekPeakPrices` (dict of PriceRow) -> Advanced DeepSeek peak overrides
|
|
13
|
+
9. `manualPeakWindowsUtc` (array of string) -> Advanced manual time windows
|
|
14
|
+
10. `manualOffPeakMultiplier` (number) -> Advanced discount multiplier for manual rates
|
|
6
15
|
|
|
7
|
-
|
|
8
|
-
- Reactivity in browser UI (`lib/client.js`):
|
|
9
|
-
Using `React.useSyncExternalStore` connected to `ctx.settingsScope` allows instant updates of `currency` and `usdRate` in the header chip and popover without needing a page refresh or roundtrip state reload.
|
|
16
|
+
By adding `useOpenRouter` and `refreshHours` to `CostMeterCard`, all 5 primary scalar/control settings are directly configurable in the DSH Settings GUI. The 5 advanced structured override rules (`prices`, `modelMap`, `deepseekPeakPrices`, `manualPeakWindowsUtc`, `manualOffPeakMultiplier`) are explicitly documented in README.md and DESIGN.md as YAML configuration rows.
|
|
10
17
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- Added `data-dsh-plugin="dsh-cost-meter"` attribute to injected `<style>` tags to adhere to DSH UI design guidelines and prevent duplicate stylesheets.
|
|
18
|
+
## 2. Service Access Pattern
|
|
19
|
+
In `lib/index.js`, we have:
|
|
20
|
+
`const settings = ctx.get('settings')` (canonical).
|
|
21
|
+
In `lib/client.js`, `ctx.settingsScope` is accessed directly. We should safely check `(ctx.get && ctx.get('settingsScope')) || ctx.settingsScope || (ctx.services && ctx.services.settingsScope)`.
|
|
22
|
+
Same for `ctx.locale` and `ctx.slots`.
|
package/docs/plans/progress.md
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Progress Log (Issue #21 / v0.7.9)
|
|
2
2
|
|
|
3
|
-
- [x]
|
|
4
|
-
- [x] Create
|
|
5
|
-
- [x]
|
|
6
|
-
- [x]
|
|
7
|
-
- [x]
|
|
8
|
-
- [
|
|
3
|
+
- [x] Analyze Issue #21
|
|
4
|
+
- [x] Create worktree `.worktrees/issue-21`
|
|
5
|
+
- [x] Initialize plans (`task_plan.md`, `findings.md`, `progress.md`)
|
|
6
|
+
- [x] Bump version to 0.7.9 in `package.json`
|
|
7
|
+
- [x] Implement settings card additions (`useOpenRouter`, `refreshHours`) in `lib/client.js`
|
|
8
|
+
- [x] Safe service resolution with `ctx.get('...')` in `lib/client.js`
|
|
9
|
+
- [x] Update documentation with full 10-field matrix (`README.md`, `docs/README.ru.md`, `docs/design/DESIGN.md`)
|
|
10
|
+
- [x] Add regression test `test/settings-card-fields.test.mjs` (12/12 passing)
|
|
9
11
|
- [ ] Commit changes with conventional commits (`git-antigravity`)
|
|
10
12
|
- [ ] Create Pull Request and merge into `main`
|
|
11
13
|
- [ ] Test package on MiniPC isolated test server (`192.168.1.123:3082`)
|
|
12
14
|
- [ ] Smoke-test package on production candidate (`192.168.1.111:3080`)
|
|
13
|
-
- [ ] Publish v0.7.
|
|
14
|
-
- [ ] Install v0.7.
|
|
15
|
-
- [ ] Close Gitea Issue #
|
|
15
|
+
- [ ] Publish v0.7.9 to npm and GitHub Release
|
|
16
|
+
- [ ] Install v0.7.9 from npm on MiniAI and verify live health
|
|
17
|
+
- [ ] Close Gitea Issue #21 and clean up worktree
|
package/docs/plans/task_plan.md
CHANGED
|
@@ -1,25 +1,32 @@
|
|
|
1
|
-
# Task Plan:
|
|
1
|
+
# Task Plan: Address Issue #21 (Settings Card Schema Fields & Service Access)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
## 1. Goal
|
|
4
|
+
Fulfill Gitea Issue #21 requirements:
|
|
5
|
+
1. **Schema Fields Availability in Settings Card**:
|
|
6
|
+
- Audit all 10 fields of `Config`:
|
|
7
|
+
- UI fields: `currency`, `usdRate`, `displayTimeZone`.
|
|
8
|
+
- Boolean toggle: `useOpenRouter` (checkbox / toggle in settings card).
|
|
9
|
+
- Numeric: `refreshHours` (catalog refresh frequency, e.g. 24 hours).
|
|
10
|
+
- Complex structured overrides: `prices` (dict), `modelMap` (dict), `deepseekPeakPrices` (dict), `manualPeakWindowsUtc` (array), `manualOffPeakMultiplier` (number).
|
|
11
|
+
- Add `useOpenRouter` and `refreshHours` directly to the `CostMeterCard` form so all scalar/toggle configuration is editable in GUI.
|
|
12
|
+
- Document structured dict/array fields (`prices`, `modelMap`, `deepseekPeakPrices`, `manualPeakWindowsUtc`, `manualOffPeakMultiplier`) in documentation, READMEs and DESIGN.md as YAML-only/advanced configuration rows.
|
|
13
|
+
2. **Context Service Access Hygiene**:
|
|
14
|
+
- Inspect all `ctx.settings` / `ctx.settingsScope` / `ctx.locale` / `ctx.webServer` calls.
|
|
15
|
+
- Use `ctx.get('...')` or inject where appropriate, or guard against undefined proxiable properties.
|
|
16
|
+
3. **Locale & Translations**:
|
|
17
|
+
- Provide clean English locale strings as canonical with ru dictionaries, and hint descriptions for all new fields.
|
|
6
18
|
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
- [ ]
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- [ ]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- [ ]
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- [ ]
|
|
20
|
-
|
|
21
|
-
- Update `docs/design/DESIGN.md` and release notes for v0.7.7.
|
|
22
|
-
- [ ] **Phase 5: Release Gate & Production Rollout**
|
|
23
|
-
- Test on MiniPC test server (`192.168.1.123:3082`).
|
|
24
|
-
- Merge PR, tag v0.7.7, publish to npm & GitHub release.
|
|
25
|
-
- Deploy to production web profile on MiniAI (`192.168.1.111:3080`).
|
|
19
|
+
## 2. Checklist
|
|
20
|
+
- [ ] Bump version to 0.7.9 in `package.json`
|
|
21
|
+
- [ ] Add `useOpenRouter` and `refreshHours` fields to `CostMeterCard` in `lib/client.js`
|
|
22
|
+
- [ ] Update `en` and `ru` dictionaries for new fields
|
|
23
|
+
- [ ] Audit and refine service accesses in `lib/index.js` and `lib/client.js`
|
|
24
|
+
- [ ] Update `README.md`, `docs/README.ru.md`, `docs/README.zh.md`, and `docs/design/DESIGN.md`
|
|
25
|
+
- [ ] Add unit test verifying settings card field contract
|
|
26
|
+
- [ ] Run `npm test` (all tests passing)
|
|
27
|
+
- [ ] Conventional commit & push
|
|
28
|
+
- [ ] Gitea PR & merge
|
|
29
|
+
- [ ] Test on MiniPC test server (`192.168.1.123`)
|
|
30
|
+
- [ ] Install on production MiniAI (`192.168.1.111`) & verify
|
|
31
|
+
- [ ] Release v0.7.9 on GitHub & npm
|
|
32
|
+
- [ ] Close Issue #21 & cleanup worktree
|
package/lib/client.js
CHANGED
|
@@ -49,6 +49,10 @@ window.__ModuleLoader__.load({
|
|
|
49
49
|
'hint.currency': 'Currency symbol shown in the widget (e.g. $, ₽, €).',
|
|
50
50
|
'hint.usdRate': 'Exchange rate: units of currency per 1 USD (e.g. 80 for ₽).',
|
|
51
51
|
'hint.displayTimeZone': 'IANA timezone used for tariff peak/off-peak schedule.',
|
|
52
|
+
'field.useOpenRouter': 'Use OpenRouter Catalog',
|
|
53
|
+
'hint.useOpenRouter': 'Pull rates and discount windows automatically from the OpenRouter catalog.',
|
|
54
|
+
'field.refreshHours': 'Catalog Refresh Interval (hours)',
|
|
55
|
+
'hint.refreshHours': 'How often to fetch fresh model rates from OpenRouter (default: 24).',
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
const ru = {
|
|
@@ -79,6 +83,10 @@ window.__ModuleLoader__.load({
|
|
|
79
83
|
'hint.currency': 'Символ валюты, отображаемый в виджете (например, ₽, $, €).',
|
|
80
84
|
'hint.usdRate': 'Сколько единиц валюты в 1 долларе США (например, 80 для ₽).',
|
|
81
85
|
'hint.displayTimeZone': 'IANA таймзона для расчёта часов пика и не-пика (например, Europe/Moscow).',
|
|
86
|
+
'field.useOpenRouter': 'Каталог OpenRouter',
|
|
87
|
+
'hint.useOpenRouter': 'Автоматически загружать тарифы и скидочные окна из каталога OpenRouter.',
|
|
88
|
+
'field.refreshHours': 'Интервал обновления каталога (часы)',
|
|
89
|
+
'hint.refreshHours': 'Как часто обновлять ставки из OpenRouter (по умолчанию: 24).',
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
let ChevronIcon = null
|
|
@@ -118,6 +126,8 @@ window.__ModuleLoader__.load({
|
|
|
118
126
|
'.dcm-sub { color:var(--dsw-alias-label-secondary); font-size:13px } ' +
|
|
119
127
|
'.dcm-body { border-top:1px solid var(--dsw-alias-border-l2); margin:0 16px; padding-bottom:8px } ' +
|
|
120
128
|
'.dcm-field { display:flex; flex-direction:column; gap:6px; padding:12px 0 } ' +
|
|
129
|
+
'.dcm-row-check { display:flex; align-items:center; gap:8px; padding:12px 0 } ' +
|
|
130
|
+
'.dcm-check { width:16px; height:16px; accent-color:var(--dsw-alias-brand-primary, #6366f1); cursor:pointer } ' +
|
|
121
131
|
'.dcm-label { color:var(--dsw-alias-label-primary); font-size:13px; font-weight:500 } ' +
|
|
122
132
|
'.dcm-hint { color:var(--dsw-alias-label-secondary); font-size:12px } ' +
|
|
123
133
|
'.dcm-input { height:34px; border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-3); color:var(--dsw-alias-label-primary); border-radius:8px; padding:0 12px; font-size:13px } ' +
|
|
@@ -149,9 +159,10 @@ window.__ModuleLoader__.load({
|
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
function useActiveLocale(ctx) {
|
|
162
|
+
const localeService = (ctx && typeof ctx.get === 'function' ? ctx.get('locale') : null) || (ctx && ctx.locale) || (ctx && ctx.services && ctx.services.locale)
|
|
152
163
|
return React.useSyncExternalStore(
|
|
153
|
-
React.useMemo(() => (cb) => (
|
|
154
|
-
React.useCallback(() => (
|
|
164
|
+
React.useMemo(() => (cb) => (localeService && typeof localeService.subscribe === 'function' ? localeService.subscribe(cb) : () => {}), [localeService]),
|
|
165
|
+
React.useCallback(() => (localeService && typeof localeService.getSnapshot === 'function' ? localeService.getSnapshot().active : 'ru'), [localeService]),
|
|
155
166
|
React.useCallback(() => 'ru', []),
|
|
156
167
|
)
|
|
157
168
|
}
|
|
@@ -166,9 +177,10 @@ window.__ModuleLoader__.load({
|
|
|
166
177
|
const [err, setErr] = React.useState('')
|
|
167
178
|
const [saved, setSaved] = React.useState(false)
|
|
168
179
|
|
|
180
|
+
const settingsScope = (ctx && typeof ctx.get === 'function' ? ctx.get('settingsScope') : null) || (ctx && ctx.settingsScope) || (ctx && ctx.services && ctx.services.settingsScope)
|
|
169
181
|
const scope = React.useMemo(
|
|
170
|
-
() => (
|
|
171
|
-
[
|
|
182
|
+
() => (settingsScope ? settingsScope.bind({ namespace: NS }) : undefined),
|
|
183
|
+
[settingsScope],
|
|
172
184
|
)
|
|
173
185
|
|
|
174
186
|
const snapshot = React.useSyncExternalStore(
|
|
@@ -188,6 +200,8 @@ window.__ModuleLoader__.load({
|
|
|
188
200
|
currency: stored.currency !== undefined ? String(stored.currency) : '$',
|
|
189
201
|
usdRate: stored.usdRate !== undefined ? String(stored.usdRate) : '1',
|
|
190
202
|
displayTimeZone: stored.displayTimeZone !== undefined ? String(stored.displayTimeZone) : 'Europe/Moscow',
|
|
203
|
+
useOpenRouter: stored.useOpenRouter !== undefined ? Boolean(stored.useOpenRouter) : true,
|
|
204
|
+
refreshHours: stored.refreshHours !== undefined ? String(stored.refreshHours) : '24',
|
|
191
205
|
})
|
|
192
206
|
}
|
|
193
207
|
}, [status, stored, draft])
|
|
@@ -209,9 +223,27 @@ window.__ModuleLoader__.load({
|
|
|
209
223
|
} catch (e) { broken.push('usdRate: ' + (e && e.message || String(e))) }
|
|
210
224
|
}
|
|
211
225
|
|
|
226
|
+
const tzVal = String(draft.displayTimeZone || 'Europe/Moscow').trim()
|
|
227
|
+
if (!isValidTimeZone(tzVal)) {
|
|
228
|
+
broken.push('displayTimeZone: invalid IANA time zone')
|
|
229
|
+
} else {
|
|
230
|
+
try {
|
|
231
|
+
await scope.set('displayTimeZone', tzVal)
|
|
232
|
+
} catch (e) { broken.push('displayTimeZone: ' + (e && e.message || String(e))) }
|
|
233
|
+
}
|
|
234
|
+
|
|
212
235
|
try {
|
|
213
|
-
await scope.set('
|
|
214
|
-
} catch (e) { broken.push('
|
|
236
|
+
await scope.set('useOpenRouter', Boolean(draft.useOpenRouter))
|
|
237
|
+
} catch (e) { broken.push('useOpenRouter: ' + (e && e.message || String(e))) }
|
|
238
|
+
|
|
239
|
+
const hoursNum = Number(String(draft.refreshHours).trim())
|
|
240
|
+
if (!Number.isFinite(hoursNum) || hoursNum <= 0) {
|
|
241
|
+
broken.push('refreshHours: must be a positive number')
|
|
242
|
+
} else {
|
|
243
|
+
try {
|
|
244
|
+
await scope.set('refreshHours', hoursNum)
|
|
245
|
+
} catch (e) { broken.push('refreshHours: ' + (e && e.message || String(e))) }
|
|
246
|
+
}
|
|
215
247
|
|
|
216
248
|
setSaving(false)
|
|
217
249
|
if (broken.length) {
|
|
@@ -268,6 +300,28 @@ window.__ModuleLoader__.load({
|
|
|
268
300
|
}),
|
|
269
301
|
React.createElement('span', { className: 'dcm-hint' }, t('hint.displayTimeZone')),
|
|
270
302
|
),
|
|
303
|
+
React.createElement('div', { className: 'dcm-row-check' },
|
|
304
|
+
React.createElement('input', {
|
|
305
|
+
id: 'dcm-use-openrouter',
|
|
306
|
+
type: 'checkbox',
|
|
307
|
+
className: 'dcm-check',
|
|
308
|
+
checked: draft ? Boolean(draft.useOpenRouter) : true,
|
|
309
|
+
onChange: (e) => setDraft({ ...draft, useOpenRouter: e.target.checked }),
|
|
310
|
+
}),
|
|
311
|
+
React.createElement('label', { htmlFor: 'dcm-use-openrouter', className: 'dcm-label', style: { cursor: 'pointer' } }, t('field.useOpenRouter')),
|
|
312
|
+
),
|
|
313
|
+
React.createElement('span', { className: 'dcm-hint', style: { marginTop: -4, display: 'block', paddingBottom: 10 } }, t('hint.useOpenRouter')),
|
|
314
|
+
React.createElement('div', { className: 'dcm-field' },
|
|
315
|
+
React.createElement('label', { className: 'dcm-label' }, t('field.refreshHours')),
|
|
316
|
+
React.createElement('input', {
|
|
317
|
+
className: 'dcm-input',
|
|
318
|
+
type: 'number',
|
|
319
|
+
step: 'any',
|
|
320
|
+
value: (draft && draft.refreshHours) !== undefined ? draft.refreshHours : '',
|
|
321
|
+
onChange: (e) => setDraft({ ...draft, refreshHours: e.target.value }),
|
|
322
|
+
}),
|
|
323
|
+
React.createElement('span', { className: 'dcm-hint' }, t('hint.refreshHours')),
|
|
324
|
+
),
|
|
271
325
|
err ? React.createElement('div', { className: 'dcm-msg-err', style: { padding: '4px 0' } }, err) : null,
|
|
272
326
|
saved ? React.createElement('div', { className: 'dcm-msg-ok', style: { padding: '4px 0' } }, t('settings.saved')) : null,
|
|
273
327
|
React.createElement('div', { className: 'dcm-foot' },
|
|
@@ -385,6 +439,16 @@ window.__ModuleLoader__.load({
|
|
|
385
439
|
return String(Math.floor(minute / 60)).padStart(2, '0') + ':' + String(minute % 60).padStart(2, '0')
|
|
386
440
|
}
|
|
387
441
|
|
|
442
|
+
function isValidTimeZone(tz) {
|
|
443
|
+
if (!tz || typeof tz !== 'string') return false
|
|
444
|
+
try {
|
|
445
|
+
new Intl.DateTimeFormat(undefined, { timeZone: tz })
|
|
446
|
+
return true
|
|
447
|
+
} catch {
|
|
448
|
+
return false
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
388
452
|
/**
|
|
389
453
|
* Render a UTC minute-of-day in the display zone. Built from a real Date so
|
|
390
454
|
* the zone's current offset (and any DST) comes from Intl, not arithmetic.
|
|
@@ -472,7 +536,7 @@ window.__ModuleLoader__.load({
|
|
|
472
536
|
const [open, setOpen] = React.useState(false)
|
|
473
537
|
const [now, setNow] = React.useState(() => new Date())
|
|
474
538
|
const box = React.useRef(null)
|
|
475
|
-
const settingsScope = ctx.settingsScope || (ctx.services && ctx.services.settingsScope)
|
|
539
|
+
const settingsScope = (ctx && typeof ctx.get === 'function' ? ctx.get('settingsScope') : null) || (ctx && ctx.settingsScope) || (ctx && ctx.services && ctx.services.settingsScope)
|
|
476
540
|
const settingsSnap = React.useSyncExternalStore
|
|
477
541
|
? React.useSyncExternalStore(
|
|
478
542
|
(cb) => (settingsScope && typeof settingsScope.subscribe === 'function' ? settingsScope.subscribe(cb) : () => {}),
|
|
@@ -491,14 +555,18 @@ window.__ModuleLoader__.load({
|
|
|
491
555
|
m.usage.cacheReadTokens || m.usage.cacheWriteTokens)
|
|
492
556
|
const usedKeys = used.map((m) => m.provider + '/' + m.model).sort().join(',')
|
|
493
557
|
|
|
558
|
+
const reqSeq = React.useRef(0)
|
|
559
|
+
|
|
494
560
|
const load = React.useCallback((keys) => {
|
|
561
|
+
const seq = ++reqSeq.current
|
|
495
562
|
const query = keys ? '?models=' + encodeURIComponent(keys) : ''
|
|
496
563
|
fetch(STATE_PATH + query, { headers: { accept: 'application/json' } })
|
|
497
564
|
.then((r) => (r.ok ? r.json() : null))
|
|
498
|
-
.then((data) => {
|
|
565
|
+
.then((data) => {
|
|
566
|
+
if (data && seq === reqSeq.current) setState(data)
|
|
567
|
+
})
|
|
499
568
|
.catch(() => {})
|
|
500
569
|
}, [])
|
|
501
|
-
|
|
502
570
|
// Refetch when the set of billed models changes — a new model needs its
|
|
503
571
|
// own tariff before its tokens can be priced.
|
|
504
572
|
React.useEffect(() => { load(usedKeys) }, [load, usedKeys])
|
package/lib/index.js
CHANGED
|
@@ -507,16 +507,18 @@ export function apply(ctx, config) {
|
|
|
507
507
|
// session that switched models can price each part on its own tariff.
|
|
508
508
|
const schedules = {}
|
|
509
509
|
const asked = new URL(req.url, 'http://localhost').searchParams.get('models')
|
|
510
|
-
for (const key of (asked ? asked.split(',') : []).slice(0, 24)) {
|
|
511
|
-
const
|
|
512
|
-
if (!
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
510
|
+
for (const key of (asked ? asked.split(',') : []).slice(0, 24)) {
|
|
511
|
+
const trimmed = String(key || '').trim()
|
|
512
|
+
if (!trimmed || trimmed.length > 128) continue
|
|
513
|
+
const parsed = splitRoute(trimmed)
|
|
514
|
+
if (!parsed) continue
|
|
515
|
+
const resolved = resolveTariff(cfg, catalog.index, parsed)
|
|
516
|
+
schedules[trimmed] = {
|
|
517
|
+
schedule: resolved.schedule,
|
|
518
|
+
source: resolved.source,
|
|
519
|
+
matchedId: resolved.matchedId,
|
|
520
|
+
}
|
|
521
|
+
}
|
|
520
522
|
|
|
521
523
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
|
|
522
524
|
res.end(JSON.stringify({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cost-meter",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
4
4
|
"description": "DeepSeek Harness plugin: a session cost chip in the conversation header - live token usage by bucket, peak/off-peak tariff, and a countdown to the next tariff switch.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|