| 1 | import type { DeliveryMetrics, UsageReport } from "@g1t/contracts"; |
| 2 | |
| 3 | export function deliveryTotals(rows: DeliveryMetrics[]) { |
| 4 | return rows.reduce((total, row) => ({ |
| 5 | opened: total.opened + row.opened, merged: total.merged + row.merged, |
| 6 | active: total.active + row.active, unreviewed: total.unreviewed + row.unreviewed, |
| 7 | queued: total.queued + row.queued, checking: total.checking + row.checking, |
| 8 | failed: total.failed + row.failed, leadSeconds: total.leadSeconds + row.leadSeconds, |
| 9 | reviewSeconds: total.reviewSeconds + row.reviewSeconds, reviewed: total.reviewed + row.reviewed, |
| 10 | }), { opened: 0, merged: 0, active: 0, unreviewed: 0, queued: 0, checking: 0, failed: 0, leadSeconds: 0, reviewSeconds: 0, reviewed: 0 }); |
| 11 | } |
| 12 | |
| 13 | /** How billing names a project's usage: its repository, `owner/name`, as Usage filters it. */ |
| 14 | export function usageKey(repo: { namespace: string; name: string }): string { |
| 15 | return `${repo.namespace}/${repo.name}`.toLowerCase(); |
| 16 | } |
| 17 | |
| 18 | export function projectCosts(report: UsageReport | null): Map<string, number> { |
| 19 | const costs = new Map<string, number>(); |
| 20 | for (const product of report?.products ?? []) for (const meter of product.meters) { |
| 21 | for (const row of meter.byProject) costs.set(row.project, (costs.get(row.project) ?? 0) + row.micros); |
| 22 | } |
| 23 | return costs; |
| 24 | } |
| 25 | |
| 26 | export function duration(seconds: number | null): string { |
| 27 | if (seconds == null || !Number.isFinite(seconds)) return "—"; |
| 28 | if (seconds < 60) return "<1m"; |
| 29 | if (seconds < 3600) return `${Math.round(seconds / 60)}m`; |
| 30 | if (seconds < 86400) return `${(seconds / 3600).toFixed(1)}h`; |
| 31 | return `${(seconds / 86400).toFixed(1)}d`; |
| 32 | } |
| 33 | |
| 34 | /** Merge service batches and fill quiet UTC days so gaps never disappear. */ |
| 35 | export function deliveryDays(from: string, until: string, rows: { day: string; merged: number }[]) { |
| 36 | const counts = new Map<string, number>(); |
| 37 | for (const row of rows) counts.set(row.day, (counts.get(row.day) ?? 0) + row.merged); |
| 38 | const days = []; |
| 39 | for (let at = Date.parse(`${from}T00:00:00Z`); at <= Date.parse(`${until}T00:00:00Z`); at += 86_400_000) { |
| 40 | const day = new Date(at).toISOString().slice(0, 10); |
| 41 | days.push({ day, merged: counts.get(day) ?? 0 }); |
| 42 | } |
| 43 | return days; |
| 44 | } |