112 lines
3.2 KiB
TypeScript
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 };
|
|
}
|
|
}
|