@goodandready/dsh-cost-meter 0.7.7 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/design/DESIGN.md +1 -1
- package/docs/plans/findings.md +7 -13
- package/docs/plans/progress.md +12 -10
- package/docs/plans/task_plan.md +22 -23
- package/lib/client.js +24 -5
- package/lib/index.js +12 -10
- package/package.json +1 -1
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.8):
|
|
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,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Findings & Architecture Decisions (v0.7.8)
|
|
2
2
|
|
|
3
|
-
## 1.
|
|
4
|
-
|
|
5
|
-
DeepSeek Harness core projection machinery requires `.parse(view)` method on projection schemas for runtime boundary contract validation. Schemastery does not provide `.parse()`, so `zod` is strictly required here and is not overengineering.
|
|
3
|
+
## 1. Browser Request Race Conditions
|
|
4
|
+
In `CostMeter`, rapid model switches or toggling the popover calls `load(usedKeys)`. Since network latency varies, an older request might resolve after a newer one. By tracking an active request counter or cancellation token per effect invocation, we ensure only the most up-to-date state response mutates the component state.
|
|
6
5
|
|
|
7
|
-
## 2.
|
|
8
|
-
|
|
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.
|
|
6
|
+
## 2. IANA TimeZone Validation
|
|
7
|
+
`Intl.DateTimeFormat(undefined, { timeZone })` throws a `RangeError` if the timezone string is invalid. We can safely leverage this in `CostMeterCard` to reject typos (e.g. `Europe/Moskow` instead of `Europe/Moscow`) on the client before writing to `settingsScope`.
|
|
10
8
|
|
|
11
|
-
## 3.
|
|
12
|
-
-
|
|
13
|
-
Removed `setTimeout(..., 500)` fallback registering `settings.section`. The standard `settings.plugin.item` slot cleanly renders the settings card inside the Harness Settings view.
|
|
14
|
-
|
|
15
|
-
## 4. Style Isolation & Deduplication
|
|
16
|
-
- Added `data-dsh-plugin="dsh-cost-meter"` attribute to injected `<style>` tags to adhere to DSH UI design guidelines and prevent duplicate stylesheets.
|
|
9
|
+
## 3. Query Parameter Sanitization
|
|
10
|
+
The endpoint `GET /dsh-cost-meter/state?models=...` accepts comma-separated model keys. Adding a length bound (e.g. <= 128 chars) and trimming ensures no malformed payloads cause unexpected overhead.
|
package/docs/plans/progress.md
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
# Implementation Progress (v0.7.
|
|
1
|
+
# Implementation Progress (v0.7.8)
|
|
2
2
|
|
|
3
|
-
- [x] Create Gitea Issue #
|
|
4
|
-
- [x] Create isolated worktree `
|
|
5
|
-
- [x] Bump version to 0.7.
|
|
6
|
-
- [x]
|
|
7
|
-
- [x]
|
|
8
|
-
- [
|
|
3
|
+
- [x] Create Gitea Issue #19
|
|
4
|
+
- [x] Create isolated worktree `stability-v078`
|
|
5
|
+
- [x] Bump version to 0.7.8 in `package.json`
|
|
6
|
+
- [x] Add monotonic request sequence guard in `lib/client.js` `CostMeter`
|
|
7
|
+
- [x] Add IANA time zone validation in `lib/client.js` `CostMeterCard`
|
|
8
|
+
- [x] Sanitize query parameters in `lib/index.js`
|
|
9
|
+
- [x] Add automated unit tests (`test/timezone-sanitization.test.mjs`, 11/11 passing)
|
|
10
|
+
- [x] Update design documentation in `docs/design/DESIGN.md`
|
|
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.8 to npm and GitHub Release
|
|
16
|
+
- [ ] Install v0.7.8 from npm on MiniAI and verify live health
|
|
17
|
+
- [ ] Close Gitea Issue #19 and clean up worktree
|
package/docs/plans/task_plan.md
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
|
-
# Task Plan:
|
|
1
|
+
# Task Plan: Stability, Race Condition Prevention & TimeZone Validation (v0.7.8)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
-
|
|
3
|
+
## 1. Goal
|
|
4
|
+
Strengthen `@goodandready/dsh-cost-meter` runtime resilience:
|
|
5
|
+
- Prevent race condition response overwrites in browser `CostMeter` component.
|
|
6
|
+
- Validate IANA TimeZone inputs in `CostMeterCard` settings before saving.
|
|
7
|
+
- Sanitize `models` query parameter keys in `lib/index.js` route handler.
|
|
8
|
+
- Maintain strict backward compatibility, zero regression in automated tests.
|
|
6
9
|
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
- [ ]
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- [ ]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- [ ]
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- [ ]
|
|
20
|
-
|
|
21
|
-
|
|
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`).
|
|
10
|
+
## 2. Checklist
|
|
11
|
+
- [ ] Bump version to `0.7.8` in `package.json`
|
|
12
|
+
- [ ] Implement cancellation flag / abort protection in `lib/client.js` `CostMeter`
|
|
13
|
+
- [ ] Implement `isValidTimeZone` helper and validation check in `CostMeterCard`
|
|
14
|
+
- [ ] Implement key sanitization (length <= 128 chars, alphanumeric/dash/slash) in `lib/index.js`
|
|
15
|
+
- [ ] Add unit tests for timezone check and query sanitization
|
|
16
|
+
- [ ] Run test suite (`npm test`)
|
|
17
|
+
- [ ] Update `docs/design/DESIGN.md`
|
|
18
|
+
- [ ] Conventional Commit & Push
|
|
19
|
+
- [ ] Gitea PR & Merge to `main`
|
|
20
|
+
- [ ] Test on MiniPC test server (`192.168.1.123`)
|
|
21
|
+
- [ ] Production Smoke Verification on MiniAI (`192.168.1.111`)
|
|
22
|
+
- [ ] Publish Tag `v0.7.8`, GitHub Release & npm
|
|
23
|
+
- [ ] Update production profile from npm
|
|
24
|
+
- [ ] Close Gitea Issue #19 and clean up worktree
|
package/lib/client.js
CHANGED
|
@@ -209,9 +209,14 @@ window.__ModuleLoader__.load({
|
|
|
209
209
|
} catch (e) { broken.push('usdRate: ' + (e && e.message || String(e))) }
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
const tzVal = String(draft.displayTimeZone || 'Europe/Moscow').trim()
|
|
213
|
+
if (!isValidTimeZone(tzVal)) {
|
|
214
|
+
broken.push('displayTimeZone: invalid IANA time zone')
|
|
215
|
+
} else {
|
|
216
|
+
try {
|
|
217
|
+
await scope.set('displayTimeZone', tzVal)
|
|
218
|
+
} catch (e) { broken.push('displayTimeZone: ' + (e && e.message || String(e))) }
|
|
219
|
+
}
|
|
215
220
|
|
|
216
221
|
setSaving(false)
|
|
217
222
|
if (broken.length) {
|
|
@@ -385,6 +390,16 @@ window.__ModuleLoader__.load({
|
|
|
385
390
|
return String(Math.floor(minute / 60)).padStart(2, '0') + ':' + String(minute % 60).padStart(2, '0')
|
|
386
391
|
}
|
|
387
392
|
|
|
393
|
+
function isValidTimeZone(tz) {
|
|
394
|
+
if (!tz || typeof tz !== 'string') return false
|
|
395
|
+
try {
|
|
396
|
+
new Intl.DateTimeFormat(undefined, { timeZone: tz })
|
|
397
|
+
return true
|
|
398
|
+
} catch {
|
|
399
|
+
return false
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
388
403
|
/**
|
|
389
404
|
* Render a UTC minute-of-day in the display zone. Built from a real Date so
|
|
390
405
|
* the zone's current offset (and any DST) comes from Intl, not arithmetic.
|
|
@@ -491,14 +506,18 @@ window.__ModuleLoader__.load({
|
|
|
491
506
|
m.usage.cacheReadTokens || m.usage.cacheWriteTokens)
|
|
492
507
|
const usedKeys = used.map((m) => m.provider + '/' + m.model).sort().join(',')
|
|
493
508
|
|
|
509
|
+
const reqSeq = React.useRef(0)
|
|
510
|
+
|
|
494
511
|
const load = React.useCallback((keys) => {
|
|
512
|
+
const seq = ++reqSeq.current
|
|
495
513
|
const query = keys ? '?models=' + encodeURIComponent(keys) : ''
|
|
496
514
|
fetch(STATE_PATH + query, { headers: { accept: 'application/json' } })
|
|
497
515
|
.then((r) => (r.ok ? r.json() : null))
|
|
498
|
-
.then((data) => {
|
|
516
|
+
.then((data) => {
|
|
517
|
+
if (data && seq === reqSeq.current) setState(data)
|
|
518
|
+
})
|
|
499
519
|
.catch(() => {})
|
|
500
520
|
}, [])
|
|
501
|
-
|
|
502
521
|
// Refetch when the set of billed models changes — a new model needs its
|
|
503
522
|
// own tariff before its tokens can be priced.
|
|
504
523
|
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.8",
|
|
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",
|