@linxin666/dsh-usage 0.4.4 → 0.4.6

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.
@@ -3,6 +3,8 @@ import type { Context } from '@deepseek-ai/cordis'
3
3
  import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
4
4
  import { isUsageAllowed } from './access.ts'
5
5
  import { writeJson } from './http.ts'
6
+ import { isLedgerDateKey } from '../core/ledger.ts'
7
+ import type { UsageDayResponse } from '../core/types.ts'
6
8
  import type { UsageService } from './usage-service.ts'
7
9
 
8
10
  export const USAGE_API_PREFIX = '/api/dsh-usage'
@@ -31,6 +33,35 @@ export function makeUsageOverviewRoute(ctx: Context, service: UsageService): Web
31
33
  }
32
34
  }
33
35
 
36
+ /**
37
+ * One retained day route: `GET /api/dsh-usage/day?date=YYYY-MM-DD` answers
38
+ * that local day's per-provider and per-model totals, so the section's day
39
+ * pickers can show a past day instead of only today. Same trust fence as the
40
+ * overview route, and a key that is not a real local date is refused with 400
41
+ * rather than folded into a bogus day.
42
+ * @param ctx - host context; may expose remoteWebUiPairing.
43
+ * @param service - the usage service.
44
+ * @returns the route.
45
+ */
46
+ export function makeUsageDayRoute(ctx: Context, service: UsageService): WebRoute {
47
+ return {
48
+ kind: 'exact',
49
+ path: USAGE_API_PREFIX + '/day',
50
+ handler: (req: IncomingMessage, res: ServerResponse): void => {
51
+ if (!isUsageAllowed(ctx, req)) {
52
+ writeJson(res, 403, { ok: false, error: 'forbidden: loopback-only' })
53
+ return
54
+ }
55
+ const date = new URL(req.url ?? '/', 'http://127.0.0.1').searchParams.get('date') ?? ''
56
+ if (!isLedgerDateKey(date)) {
57
+ writeJson(res, 400, { ok: false, error: 'date must be YYYY-MM-DD' })
58
+ return
59
+ }
60
+ writeJson(res, 200, { ok: true, day: service.day(date) } satisfies UsageDayResponse, { 'cache-control': 'no-store' })
61
+ },
62
+ }
63
+ }
64
+
34
65
  /**
35
66
  * Manual refresh: forces one probe cycle now and answers with the fresh
36
67
  * overview. Same trust fence as the overview route.
@@ -20,7 +20,7 @@ import type { BalanceParse, PlanParse } from '../core/adapters.ts'
20
20
  import { foldAliasRoutes } from '../core/provider-routes.ts'
21
21
  import { deepseekModelSpend } from '../core/pricing.ts'
22
22
  import { createLedgerDocument, deserializeLedger, foldUsage, ledgerDayKeys, localDateKey, pruneLedger, summarizeDays, totalTokens } from '../core/ledger.ts'
23
- import type { BalanceView, CredentialKind, ObservedSpendView, PlanView, ProviderSnapshotState, ProviderSnapshotView, UsageLedgerDocument, UsageOverviewView, UsageTokenTotals } from '../core/types.ts'
23
+ import type { BalanceView, CredentialKind, ObservedSpendView, PlanView, ProviderSnapshotState, ProviderSnapshotView, UsageDayView, UsageLedgerDocument, UsageOverviewView, UsageTokenTotals } from '../core/types.ts'
24
24
  import { addTotals, emptyTotals } from '../core/types.ts'
25
25
 
26
26
  /**
@@ -350,6 +350,9 @@ export class UsageService {
350
350
  totals: range.totals,
351
351
  providers: range.providers,
352
352
  },
353
+ // Every retained day, not just the trend window: the day picker's
354
+ // option list reaches back to the oldest day retention still keeps.
355
+ availableDays: allKeys,
353
356
  all: {
354
357
  from: allKeys[0] ?? todayKey,
355
358
  to: allKeys[allKeys.length - 1] ?? todayKey,
@@ -393,8 +396,19 @@ export class UsageService {
393
396
  }
394
397
  }
395
398
 
399
+ /**
400
+ * One retained local day aggregated per provider and model, as the day
401
+ * route serves it. A day the ledger never held — pruned by retention, or
402
+ * simply without usage — reads as a zeroed day rather than an error, so a
403
+ * picker selection that aged out degrades to "no usage" instead of a broken
404
+ * panel. The route gates the key's shape before it gets here.
405
+ */
406
+ day(dateKey: string): UsageDayView {
407
+ return this.daySummary(dateKey)
408
+ }
409
+
396
410
  /** One local day aggregated per provider. */
397
- private daySummary(dateKey: string): UsageOverviewView['usage']['today'] {
411
+ private daySummary(dateKey: string): UsageDayView {
398
412
  const day = this.ledger.days[dateKey] ?? {}
399
413
  const { totals, providers } = summarizeDays([day])
400
414
  return { date: dateKey, totals, providers }
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ import type { SettingsNamespace } from '@deepseek-ai/dsh-settings'
4
4
  import z from '@deepseek-ai/schemastery'
5
5
  import { mountOnce } from './mount-once.ts'
6
6
  import { UsageService, type UsageServiceOptions } from './host/usage-service.ts'
7
- import { makeUsageOverviewRoute, makeUsageRefreshRoute } from './host/routes.ts'
7
+ import { makeUsageDayRoute, makeUsageOverviewRoute, makeUsageRefreshRoute } from './host/routes.ts'
8
8
 
9
9
  declare module '@deepseek-ai/cordis' {
10
10
  interface Events {
@@ -126,9 +126,9 @@ export const apply = mountOnce('@linxin666/dsh-usage', (ctx: Context, config?: U
126
126
  /** True while this activation has a start queued behind a predecessor's flush. */
127
127
  let queued = false
128
128
 
129
- /** Register the service's two routes; the returned disposer unregisters them. */
129
+ /** Register the service's routes; the returned disposer unregisters them. */
130
130
  const mountRoutes = (next: UsageService): (() => void) => {
131
- const disposers = [makeUsageOverviewRoute(ctx, next), makeUsageRefreshRoute(ctx, next)]
131
+ const disposers = [makeUsageOverviewRoute(ctx, next), makeUsageDayRoute(ctx, next), makeUsageRefreshRoute(ctx, next)]
132
132
  .map((route) => ctx.webServer.register(route))
133
133
  return () => {
134
134
  for (const dispose of disposers) {