feat(ops): /ops/v1/summary med ai/aktivering/engagemang/betalning/jobb/sakerhet + canary

- Nya toppnivåblock i /ops/v1/summary: ai_scan, aktivering, engagemang,
  betalning, jobb, sakerhet. Alla värden läses från DB/cache; null där data
  saknas, inga påhittade värden.
- AI-kostnad i USD mikrocent (native); intäkter fortsatt SEK-öre.
- product_analytics_events är källa för volym, latens, lyckandegrad,
  felfrekvens, aktivering, retention och engagemang.
- Nya händelser: scan_started (API) och scan_failed/scan_completed med
  latencyMs + felkod (worker). latencyMs flödar nu in i scan_completed.
- Safety canary-jobb varje timme: re-härleder allergener för alla recept,
  räknar överifierade publika recept och food-safety-lint; skriver EN rad till
  ops_safety_canary. Endpointen läser endast sista raden.
- Cache-refresh-jobb var 60 s skriver hela summariet till Redis; endpointen
  serverar cachen med 503 vid cache-miss.
- Bearer-token-skydd med OPS_TOKEN; HTTPS-tvång i produktion; ingen PII.
- Tester för endpoint, auth, cache-miss och safety canary.
This commit is contained in:
Sven (AAMOS AI)
2026-08-10 05:47:43 +07:00
parent f4a603e977
commit 8f20b2ef1b
27 changed files with 1169 additions and 41 deletions
+38 -2
View File
@@ -20,6 +20,8 @@ import {
processTrainingExport,
processTrustDecay,
} from "./processors/maintenance.js";
import { processSafetyCanary } from "./processors/safety-canary.js";
import { refreshOpsSummaryCache } from "./processors/ops-summary.js";
import {
DEAD_LETTER_QUEUE_NAME,
@@ -131,6 +133,18 @@ const worker = new Worker(
return;
}
case "RUN_SAFETY_CANARY": {
const result = await processSafetyCanary(ctx);
log(`Safety canary: ${JSON.stringify(result)}`);
return;
}
case "REFRESH_OPS_SUMMARY_CACHE": {
await refreshOpsSummaryCache(ctx);
log("Ops summary cache uppdaterad.");
return;
}
// Deterministiska/planerade jobb som inte kräver egen processor ännu
case "NORMALIZE_PRODUCTS":
case "DEDUPLICATE_INVENTORY":
@@ -230,12 +244,20 @@ async function registerRepeatableJobs() {
await queue.upsertJobScheduler(
"scheduler-cooking-timeout",
{ every: 60 * 60 * 1000 }, // varje timme räcker för 24 h-timeout
{ name: "COOKING_SESSION_TIMEOUT", data: { jobType: "COOKING_SESSION_TIMEOUT" }, opts: baseOpts },
{
name: "COOKING_SESSION_TIMEOUT",
data: { jobType: "COOKING_SESSION_TIMEOUT" },
opts: baseOpts,
},
);
await queue.upsertJobScheduler(
"scheduler-expiry",
{ pattern: "0 7 * * *", tz: "Europe/Stockholm" },
{ name: "SEND_EXPIRY_NOTIFICATION", data: { jobType: "SEND_EXPIRY_NOTIFICATION" }, opts: baseOpts },
{
name: "SEND_EXPIRY_NOTIFICATION",
data: { jobType: "SEND_EXPIRY_NOTIFICATION" },
opts: baseOpts,
},
);
await queue.upsertJobScheduler(
"scheduler-memory",
@@ -257,6 +279,20 @@ async function registerRepeatableJobs() {
{ pattern: "45 2 * * 0", tz: "Europe/Stockholm" },
{ name: "BUILD_TRAINING_SAMPLE", data: { jobType: "BUILD_TRAINING_SAMPLE" }, opts: baseOpts },
);
await queue.upsertJobScheduler(
"scheduler-safety-canary",
{ every: 60 * 60 * 1000 }, // varje timme
{ name: "RUN_SAFETY_CANARY", data: { jobType: "RUN_SAFETY_CANARY" }, opts: baseOpts },
);
await queue.upsertJobScheduler(
"scheduler-ops-summary",
{ every: 60 * 1000 }, // var 60:e sekund
{
name: "REFRESH_OPS_SUMMARY_CACHE",
data: { jobType: "REFRESH_OPS_SUMMARY_CACHE" },
opts: baseOpts,
},
);
}
// --- Minimal healthcheck-server så att flera instanser kan övervakas ---