almost final

This commit is contained in:
Marcelo
2026-05-01 06:02:12 +00:00
parent b2214ec46f
commit 864be8d932
11 changed files with 1550 additions and 25 deletions

View File

@@ -175,24 +175,90 @@ function addDays(input: { year: number; month: number; day: number }, days: numb
};
}
function statusFromMachine(machine: RecapMachine, endMs: number) {
// Active stoppage = freshest macrostop episode whose latest event is "active"
// and whose latest event timestamp is within ACTIVE_STALE_MS of rangeEnd.
// Mirrors the same rules used by lib/recap/timeline.ts so the card status
// agrees with the timeline rendering.
const STOPPAGE_ACTIVE_STALE_MS = 2 * 60 * 1000;
function detectActiveMacrostop(events: TimelineEventRow[] | undefined, endMs: number) {
if (!events || events.length === 0) return null;
type Episode = { firstTsMs: number; lastTsMs: number; lastStatus: string };
const episodes = new Map<string, Episode>();
for (const event of events) {
if (String(event.eventType || "").toLowerCase() !== "macrostop") continue;
// Defensive: parse data the same way timeline.ts does.
let parsed: unknown = event.data;
if (typeof parsed === "string") {
try { parsed = JSON.parse(parsed); } catch { parsed = null; }
}
const data: Record<string, unknown> =
parsed && typeof parsed === "object" && !Array.isArray(parsed)
? (parsed as Record<string, unknown>)
: {};
// Drop only the auto-ack pings (same rule as timeline.ts Fix B).
const isAutoAck =
data.is_auto_ack === true ||
data.isAutoAck === true ||
data.is_auto_ack === "true" ||
data.isAutoAck === "true";
if (isAutoAck) continue;
const status = String(data.status ?? "").trim().toLowerCase();
const incidentKey = String(data.incidentKey ?? data.incident_key ?? "").trim()
|| `macrostop:${event.ts.getTime()}`;
const tsMs = event.ts.getTime();
const existing = episodes.get(incidentKey);
if (!existing) {
episodes.set(incidentKey, { firstTsMs: tsMs, lastTsMs: tsMs, lastStatus: status });
continue;
}
existing.firstTsMs = Math.min(existing.firstTsMs, tsMs);
if (tsMs >= existing.lastTsMs) {
existing.lastTsMs = tsMs;
existing.lastStatus = status;
}
}
let activeOngoingMin = 0;
for (const ep of episodes.values()) {
if (ep.lastStatus !== "active") continue;
if (endMs - ep.lastTsMs > STOPPAGE_ACTIVE_STALE_MS) continue;
const ongoingMin = Math.max(0, Math.floor((endMs - ep.firstTsMs) / 60000));
if (ongoingMin > activeOngoingMin) activeOngoingMin = ongoingMin;
}
return activeOngoingMin > 0 ? activeOngoingMin : null;
}
function statusFromMachine(machine: RecapMachine, endMs: number, events?: TimelineEventRow[]) {
const lastSeenMs = machine.heartbeat.lastSeenAt ? new Date(machine.heartbeat.lastSeenAt).getTime() : null;
const offlineForMs = lastSeenMs == null ? Number.POSITIVE_INFINITY : Math.max(0, endMs - lastSeenMs);
const offline = !Number.isFinite(lastSeenMs ?? Number.NaN) || offlineForMs > OFFLINE_THRESHOLD_MS;
const ongoingStopMin = machine.downtime.ongoingStopMin ?? 0;
// ongoingStopMin from the legacy heartbeat-based path (typically null) OR
// from the macrostop event detection (preferred — accurate)
const macrostopOngoingMin = detectActiveMacrostop(events, endMs);
const legacyOngoingStopMin = machine.downtime.ongoingStopMin ?? 0;
const ongoingStopMin = macrostopOngoingMin ?? (legacyOngoingStopMin > 0 ? legacyOngoingStopMin : null);
const moldActive = machine.workOrders.moldChangeInProgress;
let status: RecapMachineStatus = "running";
if (offline) status = "offline";
else if (moldActive) status = "mold-change";
else if (ongoingStopMin > 0) status = "stopped";
else if (ongoingStopMin != null && ongoingStopMin > 0) status = "stopped";
return {
status,
lastSeenMs,
offlineForMin: offline ? Math.max(0, Math.floor(offlineForMs / 60000)) : null,
ongoingStopMin: machine.downtime.ongoingStopMin,
ongoingStopMin,
};
}
@@ -281,9 +347,10 @@ function toSummaryMachine(params: {
machine: RecapMachine;
miniTimeline: ReturnType<typeof compressTimelineSegments>;
rangeEndMs: number;
events?: TimelineEventRow[];
}): RecapSummaryMachine {
const { machine, miniTimeline, rangeEndMs } = params;
const status = statusFromMachine(machine, rangeEndMs);
const { machine, miniTimeline, rangeEndMs, events } = params;
const status = statusFromMachine(machine, rangeEndMs, events);
return {
machineId: machine.machineId,
@@ -349,6 +416,7 @@ async function computeRecapSummary(params: { orgId: string; hours: number }) {
machine,
miniTimeline,
rangeEndMs: end.getTime(),
events: timelineRows.eventsByMachine.get(machine.machineId),
});
});
@@ -608,7 +676,11 @@ async function computeRecapMachineDetail(params: {
rangeEnd: range.end,
});
const status = statusFromMachine(machine, range.end.getTime());
const status = statusFromMachine(
machine,
range.end.getTime(),
timelineRows.eventsByMachine.get(params.machineId)
);
const downtimeTotalMin = Math.max(0, machine.downtime.totalMin);
const downtimeTop = machine.downtime.topReasons.slice(0, 3).map((row) => ({