// Wire types mirror the moments-entities types serialised by the API. // Hand-maintained for now; if drift becomes a problem, generate them // from the Rust crate via ts-rs or specta. export type Source = 'github' | 'gitea' | 'hg' | 'bugzilla'; export type TitleSegment = | { kind: 'text'; text: string } | { kind: 'link'; text: string; url: string }; export interface CommitSummary { sha: string; short_sha: string; message: string; url: string; author: string | null; } export type TimelineBody = | { kind: 'markdown'; text: string } | { kind: 'commits'; commits: CommitSummary[] } | { kind: 'links'; items: TitleSegment[] }; export type TimelineIcon = | 'git-push' | 'git-commit' | 'git-merge' | 'git-fork' | 'git-branch-create' | 'git-branch-delete' | 'pull-request' | 'issue' | 'comment' | 'star' | 'release' | 'bug' | 'generic'; export interface TimelineItem { id: string; source: Source; action: string; occurred_at: string; icon: TimelineIcon; title: TitleSegment[]; subtitle: TitleSegment[] | null; body: TimelineBody | null; } export interface SourceSummary { source: Source; count: number; earliest: string | null; latest: string | null; } export interface ProjectSummary { repo: string; source: Source; host: string; commit_count: number; issue_count: number; pr_count: number; first_activity: string | null; last_activity: string | null; } export interface DailyCount { date: string; count: number; } export interface EventQuery { from?: Date; to?: Date; sources?: Source[]; repo?: string; limit?: number; } const API_BASE = '/api/v1'; /** Decode base64 content as UTF-8 (atob only handles Latin-1). */ function decodeBase64Utf8(b64: string): string { const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)); return new TextDecoder().decode(bytes); } export async function fetchEvents(q: EventQuery): Promise { const params = new URLSearchParams(); if (q.from) params.set('from', q.from.toISOString()); if (q.to) params.set('to', q.to.toISOString()); if (q.sources && q.sources.length > 0) { params.set('source', q.sources.join(',')); } if (q.repo) params.set('repo', q.repo); if (q.limit) params.set('limit', String(q.limit)); const resp = await fetch(`${API_BASE}/events?${params}`); if (!resp.ok) throw new Error(`events: HTTP ${resp.status}`); return resp.json(); } export async function fetchSources(): Promise { const resp = await fetch(`${API_BASE}/sources`); if (!resp.ok) throw new Error(`sources: HTTP ${resp.status}`); return resp.json(); } export async function fetchDailyCounts(from: string, to: string): Promise { const resp = await fetch(`${API_BASE}/activity/daily?from=${from}&to=${to}`); if (!resp.ok) throw new Error(`daily-counts: HTTP ${resp.status}`); return resp.json(); } export async function fetchProjects(): Promise { const resp = await fetch(`${API_BASE}/projects`); if (!resp.ok) throw new Error(`projects: HTTP ${resp.status}`); return resp.json(); } /** Fetch repo README as raw markdown via the forge proxy. */ export async function fetchReadme(source: Source, host: string, repo: string): Promise { if (source === 'github') { const resp = await fetch(`${API_BASE}/forge/github/repos/${repo}/readme`); if (!resp.ok) return null; const data = await resp.json(); if (data.encoding === 'base64' && data.content) { return decodeBase64Utf8(data.content); } return data.content ?? null; } if (source === 'gitea') { for (const name of ['README.md', 'readme.md', 'Readme.md']) { const resp = await fetch(`${API_BASE}/forge/gitea/repos/${repo}/contents/${name}?host=${encodeURIComponent(host)}`); if (!resp.ok) continue; const data = await resp.json(); if (data.encoding === 'base64' && data.content) { return decodeBase64Utf8(data.content); } if (data.content) return data.content; } return null; } return null; } /** Fetch repo languages as { language: bytes } map via the forge proxy. */ export async function fetchLanguages(source: Source, host: string, repo: string): Promise | null> { if (source !== 'github' && source !== 'gitea') return null; const hostParam = source === 'gitea' ? `?host=${encodeURIComponent(host)}` : ''; const resp = await fetch(`${API_BASE}/forge/${source}/repos/${repo}/languages${hostParam}`); if (!resp.ok) return null; return resp.json(); }