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