1Branch0Tags
GL
glucryptosync: align production template with local master
2518e512 days ago3Commits
typescript
import type { ImportantDatesRoundupConfig } from "./config"; type GatewayImportantDate = { id: string; series: string; category: string; referencePeriod: string; releaseAt: string; source: string; sourceUrl: string; status: string; updatedAt: string; }; type ImportantDatesGatewayResponse = { events?: GatewayImportantDate[]; refreshedAt?: string | null; }; export type ImportantDateEvent = { id: string; series: string; category: string; referencePeriod: string; releaseAt: string; source: string; sourceUrl: string; status: string; updatedAt: string; }; export type ImportantDatesSummary = { title: string; overallSummary: string; topEvents: Array<ImportantDateEvent & { reason: string }>; }; export type ImportantDatesRoundupResult = { ok: true; title: string; generatedAt: string; refreshedAt: string | null; daysAhead: number; limit: number; category?: string; series?: string[]; summary: ImportantDatesSummary; events: ImportantDateEvent[]; }; function resolveGatewayBase() { const baseUrl = process.env.OPENPOND_GATEWAY_URL; if (!baseUrl) { throw new Error("OPENPOND_GATEWAY_URL is required for Important Dates Roundup."); } return baseUrl.replace(/\/$/, ""); } function isRecord(value: unknown): value is Record<string, unknown> { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function normalizeImportantDate(value: unknown): ImportantDateEvent | null { if (!isRecord(value)) return null; const id = typeof value.id === "string" ? value.id : ""; const series = typeof value.series === "string" ? value.series : ""; const category = typeof value.category === "string" ? value.category : ""; const referencePeriod = typeof value.referencePeriod === "string" ? value.referencePeriod : ""; const releaseAt = typeof value.releaseAt === "string" ? value.releaseAt : ""; const source = typeof value.source === "string" ? value.source : ""; const sourceUrl = typeof value.sourceUrl === "string" ? value.sourceUrl : ""; const status = typeof value.status === "string" ? value.status : ""; const updatedAt = typeof value.updatedAt === "string" ? value.updatedAt : ""; if (!id || !series || !releaseAt) { return null; } return { id, series, category, referencePeriod, releaseAt, source, sourceUrl, status, updatedAt, }; } function formatReleaseAt(value: string): string { const timestamp = Date.parse(value); if (!Number.isFinite(timestamp)) return value; return new Intl.DateTimeFormat("en-US", { timeZone: "America/New_York", month: "short", day: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "short", }).format(new Date(timestamp)); } function dedupeEvents(events: ImportantDateEvent[]): ImportantDateEvent[] { const seen = new Set<string>(); const deduped: ImportantDateEvent[] = []; for (const event of events) { const key = `${event.id}:${event.releaseAt}`; if (seen.has(key)) continue; seen.add(key); deduped.push(event); } return deduped; } function fallbackReason(event: ImportantDateEvent): string { if (event.referencePeriod) { return `Scheduled ${event.series} release for ${event.referencePeriod}.`; } return `Scheduled ${event.series} release.`; } function buildFallbackSummary(params: { config: ImportantDatesRoundupConfig; events: ImportantDateEvent[]; }): ImportantDatesSummary { const topEvents = params.events.slice(0, params.config.summaryMaxEvents); if (topEvents.length === 0) { return { title: params.config.title, overallSummary: `No upcoming dates were found in the next ${params.config.daysAhead} days.`, topEvents: [], }; } const label = topEvents .slice(0, 3) .map((event) => event.referencePeriod ? `${event.series} (${event.referencePeriod}) on ${formatReleaseAt(event.releaseAt)}` : `${event.series} on ${formatReleaseAt(event.releaseAt)}`, ) .join("; "); return { title: params.config.title, overallSummary: `Upcoming releases over the next ${params.config.daysAhead} days: ${label}.`, topEvents: topEvents.map((event) => ({ ...event, reason: fallbackReason(event), })), }; } async function fetchImportantDates( config: ImportantDatesRoundupConfig, ): Promise<{ events: ImportantDateEvent[]; refreshedAt: string | null }> { const gatewayBase = resolveGatewayBase(); const requestLimit = Math.min(Math.max(config.limit * 3, config.limit), 50); const search = new URLSearchParams({ daysAhead: String(config.daysAhead), limit: String(requestLimit), }); if (config.category) { search.set("category", config.category); } const response = await fetch( `${gatewayBase}/v1/macro/important-dates?${search.toString()}`, { method: "GET", }, ); if (!response.ok) { throw new Error(`important-dates request failed (${response.status})`); } const payload = (await response.json().catch(() => null)) as | ImportantDatesGatewayResponse | null; const seriesFilters = new Set( (config.series ?? []).map((series) => series.trim().toLowerCase()), ); const events = dedupeEvents( (Array.isArray(payload?.events) ? payload.events : []) .map((event) => normalizeImportantDate(event)) .filter((event): event is ImportantDateEvent => event !== null) .filter((event) => { if (seriesFilters.size === 0) return true; return seriesFilters.has(event.series.trim().toLowerCase()); }), ) .sort((left, right) => { const leftTs = Date.parse(left.releaseAt); const rightTs = Date.parse(right.releaseAt); return leftTs - rightTs; }) .slice(0, config.limit); return { events, refreshedAt: typeof payload?.refreshedAt === "string" ? payload.refreshedAt : null, }; } async function summarizeImportantDates(params: { config: ImportantDatesRoundupConfig; events: ImportantDateEvent[]; }): Promise<ImportantDatesSummary> { return buildFallbackSummary(params); } export async function runImportantDatesRoundup( config: ImportantDatesRoundupConfig, ): Promise<ImportantDatesRoundupResult> { const generatedAt = new Date().toISOString(); const { events, refreshedAt } = await fetchImportantDates(config); const summary = await summarizeImportantDates({ config, events }); return { ok: true, title: config.title, generatedAt, refreshedAt, daysAhead: config.daysAhead, limit: config.limit, category: config.category, series: config.series, summary, events, }; }