feat(worker): EOC push-agent for ops summary

- Add EOC_URL + EOC_PUSH_TOKEN config
- Add pushOpsSummaryToEoc processor
- Schedule PUSH_OPS_SUMMARY_TO_EOC every 2 minutes
- Add unit tests
This commit is contained in:
Sven (AAMOS AI)
2026-08-11 17:03:53 +07:00
parent 4eddcfbefe
commit af05f084b8
4 changed files with 140 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
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`;
}
/**
* 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 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: json,
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 };
}
}