recent changes

This commit is contained in:
Marcelo
2026-04-29 05:05:00 +00:00
parent 7e0fe5c2e1
commit 62169b163c
25 changed files with 6698 additions and 1013 deletions

View File

@@ -0,0 +1,308 @@
#!/usr/bin/env node
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const NON_AUTHORITATIVE_REASON_CODES = new Set(["PENDIENTE", "UNCLASSIFIED"]);
function parseArgs(argv) {
const out = {
dryRun: false,
since: "30d",
orgId: null,
machineId: null,
};
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i];
if (token === "--dry-run") {
out.dryRun = true;
continue;
}
if (token === "--since") {
out.since = argv[i + 1] || out.since;
i += 1;
continue;
}
if (token === "--org-id") {
out.orgId = argv[i + 1] || null;
i += 1;
continue;
}
if (token === "--machine-id") {
out.machineId = argv[i + 1] || null;
i += 1;
continue;
}
throw new Error(`Unknown argument: ${token}`);
}
return out;
}
function parseSince(value) {
const now = Date.now();
const text = String(value || "30d").trim().toLowerCase();
const relative = text.match(/^(\d+)\s*([dhm])$/);
if (relative) {
const amount = Number(relative[1]);
const unit = relative[2];
const factor = unit === "d" ? 24 * 60 * 60 * 1000 : unit === "h" ? 60 * 60 * 1000 : 60 * 1000;
return new Date(now - amount * factor);
}
const dt = new Date(value);
if (Number.isNaN(dt.getTime())) {
throw new Error(`Invalid --since value: ${value}. Use ISO date, or relative like 30d / 12h / 90m.`);
}
return dt;
}
function asRecord(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
}
function clampText(value, maxLen) {
if (value === null || value === undefined) return null;
const text = String(value).trim().replace(/[\u0000-\u001f\u007f]/g, "");
if (!text) return null;
return text.length > maxLen ? text.slice(0, maxLen) : text;
}
function canonicalId(input) {
const text = String(input ?? "")
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
return text || null;
}
function toReasonCode(categoryId, detailId) {
const cat = canonicalId(categoryId);
const det = canonicalId(detailId);
if (!cat || !det) return null;
return `${cat}__${det}`.toUpperCase();
}
function isNonAuthoritativeReasonCode(code) {
const normalized = clampText(code, 64)?.toUpperCase();
return !!normalized && NON_AUTHORITATIVE_REASON_CODES.has(normalized);
}
function extractReasonPayload(data) {
const rec = asRecord(data);
if (!rec) return null;
const direct = asRecord(rec.reason);
if (direct) return direct;
const downtime = asRecord(rec.downtime);
const nested = asRecord(downtime?.reason);
return nested || null;
}
function extractIncidentKey(data, reason) {
const rec = asRecord(data);
const downtime = asRecord(rec?.downtime);
return (
clampText(rec?.incidentKey, 128) ??
clampText(downtime?.incidentKey, 128) ??
clampText(reason?.incidentKey, 128) ??
null
);
}
function normalizeAckReason(reasonRaw) {
const categoryId = clampText(reasonRaw?.categoryId, 64);
const detailId = clampText(reasonRaw?.detailId, 64);
const categoryLabel = clampText(reasonRaw?.categoryLabel, 120);
const detailLabel = clampText(reasonRaw?.detailLabel, 120);
const reasonCode =
clampText(reasonRaw?.reasonCode, 64)?.toUpperCase() ??
toReasonCode(categoryId ?? categoryLabel, detailId ?? detailLabel) ??
null;
if (!reasonCode) return null;
const reasonLabel =
clampText(reasonRaw?.reasonText, 240) ??
(categoryLabel && detailLabel ? `${categoryLabel} > ${detailLabel}` : null) ??
detailLabel ??
categoryLabel ??
reasonCode;
return {
type: "downtime",
categoryId,
categoryLabel,
detailId,
detailLabel,
reasonCode,
reasonLabel,
reasonText: reasonLabel,
};
}
async function main() {
const args = parseArgs(process.argv.slice(2));
const since = parseSince(args.since);
const where = {
eventType: "downtime-acknowledged",
ts: { gte: since },
...(args.orgId ? { orgId: args.orgId } : {}),
...(args.machineId ? { machineId: args.machineId } : {}),
};
const ackEvents = await prisma.machineEvent.findMany({
where,
orderBy: { ts: "desc" },
select: {
id: true,
orgId: true,
machineId: true,
ts: true,
data: true,
},
});
const latestByIncident = new Map();
for (const event of ackEvents) {
const reasonRaw = extractReasonPayload(event.data);
if (!reasonRaw) continue;
const normalized = normalizeAckReason(reasonRaw);
if (!normalized) continue;
if (isNonAuthoritativeReasonCode(normalized.reasonCode)) continue;
const incidentKey = extractIncidentKey(event.data, reasonRaw);
if (!incidentKey) continue;
const mapKey = `${event.orgId}::${incidentKey}`;
if (latestByIncident.has(mapKey)) continue;
latestByIncident.set(mapKey, {
orgId: event.orgId,
machineId: event.machineId,
incidentKey,
eventId: event.id,
eventTs: event.ts,
reason: normalized,
});
}
let scanned = 0;
let candidates = 0;
let updated = 0;
let missingReasonEntry = 0;
let alreadyManual = 0;
let skippedNonPendingIncoming = 0;
const samples = [];
for (const item of latestByIncident.values()) {
scanned += 1;
const existing = await prisma.reasonEntry.findFirst({
where: {
orgId: item.orgId,
kind: "downtime",
episodeId: item.incidentKey,
},
select: {
id: true,
reasonCode: true,
reasonLabel: true,
reasonText: true,
capturedAt: true,
schemaVersion: true,
},
});
if (!existing) {
missingReasonEntry += 1;
continue;
}
if (!isNonAuthoritativeReasonCode(existing.reasonCode)) {
alreadyManual += 1;
continue;
}
if (isNonAuthoritativeReasonCode(item.reason.reasonCode)) {
skippedNonPendingIncoming += 1;
continue;
}
candidates += 1;
const next = {
reasonCode: item.reason.reasonCode,
reasonLabel: item.reason.reasonLabel ?? item.reason.reasonCode,
reasonText: item.reason.reasonText ?? item.reason.reasonLabel ?? item.reason.reasonCode,
schemaVersion: Math.max(1, Number(existing.schemaVersion || 1)),
meta: {
source: "backfill:downtime-acknowledged",
eventId: item.eventId,
eventTs: item.eventTs.toISOString(),
incidentKey: item.incidentKey,
reason: {
type: "downtime",
categoryId: item.reason.categoryId,
categoryLabel: item.reason.categoryLabel,
detailId: item.reason.detailId,
detailLabel: item.reason.detailLabel,
reasonText: item.reason.reasonText,
},
},
};
samples.push({
reasonEntryId: existing.id,
orgId: item.orgId,
machineId: item.machineId,
incidentKey: item.incidentKey,
from: {
reasonCode: existing.reasonCode,
reasonLabel: existing.reasonLabel,
reasonText: existing.reasonText,
},
to: {
reasonCode: next.reasonCode,
reasonLabel: next.reasonLabel,
reasonText: next.reasonText,
},
});
if (!args.dryRun) {
await prisma.reasonEntry.update({
where: { id: existing.id },
data: next,
});
updated += 1;
}
}
const summary = {
ok: true,
mode: args.dryRun ? "dry-run" : "apply",
since: since.toISOString(),
filters: {
orgId: args.orgId,
machineId: args.machineId,
},
eventsRead: ackEvents.length,
incidentsDeduped: latestByIncident.size,
scanned,
candidates,
updated,
missingReasonEntry,
alreadyManual,
skippedNonPendingIncoming,
sampleUpdates: samples.slice(0, 25),
};
console.log(JSON.stringify(summary, null, 2));
}
main()
.catch((err) => {
console.error("[backfill-downtime-reasons] failed:", err);
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});