Files
traefik-llmgateway/webui/src/lib/burndown.ts
T
lukaszraczylo ee49467e64 feat(webui): redesign the admin dashboard around admin tasks
A sidebar replaces the flat tabs with seven pages, and one filter bar
sets the time range, comparison period and scope for all of them.

- Home: spend against budget, forecast, request rate, error rate,
  open breakers, top users and models, recent events and warnings.
- Spend: spend over time with comparison, budget burn-down, an
  attribution drill-down from group to user to model, pricing health
  and cost avoided by free models.
- Consumers: a directory with last seen and limit bars, an access
  matrix, and a detail page per user.
- Models: the catalog with billing prices, fleet latency and error
  rates, and provider health.
- Reliability: error, timeout, failover, 429 and 402 trends, events,
  and latency per provider.
- Config: the redacted config and a change helper that emits YAML or
  users.json snippets to paste into git.
- MCP & Agents: callers per target.

Page state lives in the URL hash, and old tab links map to the new
pages. The replaced views are removed. admin_assets_gen.go is
regenerated.

Claude-Session: https://claude.ai/code/session_01V5jYJNxpDFdJkyyExpADrv
2026-09-23 16:41:01 +01:00

94 lines
4.2 KiB
TypeScript

// Pure math behind BurnDownChart.vue (redesign-plan.md section 3.4): a
// cumulative month-to-date spend line against a flat budget reference, and
// the date that spend would cross the budget at the current pace. Kept out
// of the component the same way lib/forecast.ts keeps CostForecastCard.vue's
// own month-end projection math out of it — every function here takes `now`
// as an explicit argument (never reads Date.now() itself), so vitest's
// node-environment config exercises the real calendar-boundary cases
// deterministically.
//
// All month-boundary math is UTC, mirroring lib/forecast.ts's own
// monthProgress (costPerMonthMicroUsd/AdminSeriesResponse's day buckets
// both accumulate against a UTC calendar month — limits.go's windowKey
// "200601"/"20060102" — so projecting against the LOCAL calendar month
// would silently misalign on the first/last day of a month in any
// non-UTC timezone).
import { monthProgress } from '@/lib/forecast'
const MS_PER_DAY = 24 * 60 * 60 * 1000
/**
* daysInMonthUTC returns how many days are in `now`'s own UTC calendar
* month (28-31) — correct for every month length, including a leap
* February, via the same Date.UTC month-rollover normalization
* monthProgress (lib/forecast.ts) already relies on, never a hardcoded
* days-per-month table.
*/
function daysInMonthUTC(now: Date): number {
const year = now.getUTCFullYear()
const month = now.getUTCMonth()
return (Date.UTC(year, month + 1, 1) - Date.UTC(year, month, 1)) / MS_PER_DAY
}
/**
* currentMonthDaySpan is how many day buckets (1..31) `now`'s own UTC
* calendar month has elapsed so far, INCLUDING today — the `span` the
* spend store requests from GET /admin/api/usage/series for the burn-down
* line (redesign-plan.md section 3.3: "burn-down current-month day series
* span=dayOfMonth"), so a day-3-of-the-month poll asks for exactly 3
* buckets, never the whole month's worth of not-yet-existing future ones.
*/
export function currentMonthDaySpan(now: Date): number {
return now.getUTCDate()
}
/**
* cumulative turns a bucket-per-day points array (day-window
* AdminSeriesResponse series, oldest first) into a running total —
* BurnDownChart.vue's own "spend so far" line, since the series endpoint
* itself reports each day's OWN cost, not a running sum.
*/
export function cumulative(points: number[]): number[] {
let sum = 0
return points.map((p) => (sum += p))
}
/**
* MIN_ELAPSED_DAYS is runOutDate's own extrapolation floor, mirroring
* lib/forecast.ts's MIN_PROJECTION_FRACTION for the identical reason: the
* first day of a month is too little data for a daily-average run rate to
* mean anything, so runOutDate reports "not enough data yet" (null) below
* it rather than a wildly overstated projection from a single day's spend.
*/
export const MIN_ELAPSED_DAYS = 1
/**
* runOutDate projects the date month-to-date spend would cross
* `limitMicros`, extrapolating from the MTD average daily rate
* (mtdMicros / elapsed days so far) — redesign-plan.md section 3.4:
* "run-out date lib/burndown.ts runOutDate(mtdMicros, limitMicros, now) at
* MTD average daily rate". Mirrors lib/forecast.ts's headroom() null
* conventions: null when there is no budget to compare against
* (`limitMicros <= 0`), and null when there is too little of the month
* elapsed to extrapolate from (MIN_ELAPSED_DAYS) or the rate so far is 0
* (spend has not started — "never" is not representable as a date, so
* this reports "no forecast" rather than a fabricated far-future one).
* Already-exceeded budgets return `now` itself (the run-out already
* happened, as of this read) rather than a nonsensical past-vs-future date
* from continuing the division.
*/
export function runOutDate(mtdMicros: number, limitMicros: number, now: Date): Date | null {
if (limitMicros <= 0) return null
if (mtdMicros >= limitMicros) return new Date(now.getTime())
const elapsedDays = monthProgress(now) * daysInMonthUTC(now)
if (elapsedDays < MIN_ELAPSED_DAYS) return null
const dailyRateMicros = mtdMicros / elapsedDays
if (dailyRateMicros <= 0) return null
const remainingMicros = limitMicros - mtdMicros
const daysToExhaust = remainingMicros / dailyRateMicros
return new Date(now.getTime() + daysToExhaust * MS_PER_DAY)
}