diff --git a/dashboard/src/lib/api.ts b/dashboard/src/lib/api.ts index 9d44474..4083ffb 100644 --- a/dashboard/src/lib/api.ts +++ b/dashboard/src/lib/api.ts @@ -1,4 +1,4 @@ -import type { ProjectRow, TrendResponse, GroupedTrendResponse } from './types'; +import type { ProjectRow, GroupedTrendResponse, RangeKey } from './types'; export async function fetchProjects(fetchFn: typeof fetch = fetch): Promise { const res = await fetchFn('/api/projects', { redirect: 'manual' }); @@ -6,20 +6,13 @@ export async function fetchProjects(fetchFn: typeof fetch = fetch): Promise; } -export async function fetchTrend( - owner: string, - repo: string, - metric: string, - branch: string, - limit: number, - fetchFn: typeof fetch = fetch, -): Promise { - const params = new URLSearchParams({ metric, branch, limit: String(limit) }); - const res = await fetchFn(`/api/projects/${owner}/${repo}/metrics?${params}`, { - redirect: 'manual', - }); - if (!res.ok) throw new Error(`Failed to fetch trend: HTTP ${res.status}`); - return res.json() as Promise; +export interface TrendOptions { + /** Row-count cap for the legacy (unwindowed) fetch — ignored when `range` is set. */ + limit?: number; + /** Relative time window; when set, the backend returns an edge-anchored, windowed series. */ + range?: RangeKey; + /** Align all categories to a shared right edge, carrying stale series forward. Requires `range`. */ + align?: boolean; } export async function fetchTrendByCategory( @@ -27,10 +20,16 @@ export async function fetchTrendByCategory( repo: string, metric: string, branch: string, - limit: number, + options: TrendOptions, fetchFn: typeof fetch = fetch, ): Promise { - const params = new URLSearchParams({ metric, branch, limit: String(limit) }); + const params = new URLSearchParams({ metric, branch }); + if (options.range) { + params.set('range', options.range); + if (options.align) params.set('align', 'true'); + } else { + params.set('limit', String(options.limit ?? 100)); + } const res = await fetchFn(`/api/projects/${owner}/${repo}/metrics/categories?${params}`, { redirect: 'manual', }); diff --git a/dashboard/src/lib/chartFill.ts b/dashboard/src/lib/chartFill.ts new file mode 100644 index 0000000..b26306e --- /dev/null +++ b/dashboard/src/lib/chartFill.ts @@ -0,0 +1,18 @@ +import type uPlot from 'uplot'; + +export function hexAlpha(hex: string, alpha: number): string { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return `rgba(${r},${g},${b},${alpha})`; +} + +/** Top-to-bottom fading fill, used by every uPlot line series in the dashboard. */ +export function gradientFill(color: string, topAlpha = 0.28, bottomAlpha = 0.02) { + return (u: uPlot) => { + const grad = u.ctx.createLinearGradient(0, u.bbox.top, 0, u.bbox.top + u.bbox.height); + grad.addColorStop(0, hexAlpha(color, topAlpha)); + grad.addColorStop(1, hexAlpha(color, bottomAlpha)); + return grad; + }; +} diff --git a/dashboard/src/lib/components/BadgeModal.svelte b/dashboard/src/lib/components/BadgeModal.svelte index f1b7c9e..241d127 100644 --- a/dashboard/src/lib/components/BadgeModal.svelte +++ b/dashboard/src/lib/components/BadgeModal.svelte @@ -1,18 +1,21 @@ + +
+ + diff --git a/dashboard/src/lib/components/SparkLine.svelte b/dashboard/src/lib/components/SparkLine.svelte index 5e65cb4..086b043 100644 --- a/dashboard/src/lib/components/SparkLine.svelte +++ b/dashboard/src/lib/components/SparkLine.svelte @@ -1,6 +1,7 @@