Files
Cibello-app/apps/worker/src/processors/eoc-push.ts
T

112 lines
3.2 KiB
TypeScript

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<string, unknown>): Record<string, unknown> {
const mapped: Record<string, unknown> = { ...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<EocPushResult> {
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<string, unknown>));
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(() => "<unreadable body>");
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 };
}
}