import type { Redis } from "ioredis"; import { CACHE_KEY } from "./ops-summary.js"; export interface EocPushResult { ok: boolean; status: number | null; error: string | null; } const EOC_PUSH_TIMEOUT_MS = 15_000; function ingestUrl(base: string): string { const trimmed = base.trim().replace(/\/+$/, ""); return `${trimmed}/api/v1/apps/cibello/ingest`; } interface EocFeedbackPost { typ?: string; rubrik?: string; status?: string; created_at?: string; skapad?: string; [key: string]: unknown; } interface EocFeedback { oppna?: number; senaste?: EocFeedbackPost[]; [key: string]: unknown; } /** * Build a payload that matches EOC's authoritative contract: * feedback.senaste[].created_at must be sent as feedback.senaste[].skapad. * All other keys are forwarded unchanged. */ function mapPayloadForEoc(parsed: Record): Record { const mapped: Record = { ...parsed }; const feedback = mapped.feedback as EocFeedback | undefined; if (feedback && Array.isArray(feedback.senaste)) { mapped.feedback = { ...feedback, senaste: feedback.senaste.map((post) => { const { created_at, ...rest } = post; return { ...rest, ...(created_at !== undefined ? { skapad: created_at } : {}), }; }), }; } return mapped; } /** * Push the cached ops summary to EOC. Never throws; failures are returned * as `ok: false` and must be logged by the caller. */ export async function pushOpsSummaryToEoc(redis: Redis): Promise { const eocUrl = process.env.EOC_URL?.trim(); const token = process.env.EOC_PUSH_TOKEN?.trim(); if (!eocUrl || !token) { return { ok: false, status: null, error: "EOC_URL or EOC_PUSH_TOKEN not configured" }; } const json = await redis.get(CACHE_KEY); if (!json) { return { ok: false, status: null, error: "ops summary cache miss" }; } // Guard against accidentally sending non-object payloads. let parsed: unknown; try { parsed = JSON.parse(json); } catch { return { ok: false, status: null, error: "ops summary cache contains invalid JSON" }; } if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { return { ok: false, status: null, error: "ops summary cache is not a JSON object" }; } const body = JSON.stringify(mapPayloadForEoc(parsed as Record)); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), EOC_PUSH_TIMEOUT_MS); try { const res = await fetch(ingestUrl(eocUrl), { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body, signal: controller.signal, }); clearTimeout(timeout); if (res.status === 204) { return { ok: true, status: 204, error: null }; } const text = await res.text().catch(() => ""); return { ok: false, status: res.status, error: text.slice(0, 500) }; } catch (err) { clearTimeout(timeout); const message = err instanceof Error ? err.message : String(err); return { ok: false, status: null, error: message }; } }