mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-14 05:18:54 +00:00
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
94 lines
4.2 KiB
TypeScript
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)
|
|
}
|