This commit is contained in:
Marcelo
2026-04-24 20:47:25 +00:00
parent 30513ff73d
commit 66c89f9bf4
5 changed files with 83 additions and 2 deletions

View File

@@ -39,4 +39,4 @@ Truncate ReasonEntry (kills 99% of the insanity).
Fix "Ayer" to be calendar-based.
Fix KPI row label to reflect selected range.
If no OrgShift rows exist, show a toast or disable "Turno actual" button instead of silently falling back.
Improve dual-banner priority (offline > mold-change).
Improve dual-banner priority (offline > mold-change)

54
fix3.md Normal file
View File

@@ -0,0 +1,54 @@
The fix created the production segment, but the mold-change active event never got a matching resolved event in CT. Two checks will tell us which.
On CT Postgres
SELECT ts, data->>'status' AS status,
data->>'incidentKey' AS ikey,
data->>'is_update' AS is_update
FROM "MachineEvent"
WHERE "machineId" = '<uuid-M4-5>'
AND "eventType" = 'mold-change'
ORDER BY ts DESC LIMIT 10;
Expected when working: active + resolved rows with same incidentKey.
If only active exists → resolved event never reached CT. Likely causes:
Flow wasn't redeployed after the edit (Node-RED still running old version — check if node.warn in auto-close is firing in debug sidebar).
state.moldChange persisted stale from before (cleared active manually somewhere).
User hit COMENZAR before deploying the updated flow → no close event ever emitted for that episode.
If both exist but incidentKey differs → my close event and start event used different startMs. Send me both rows and I'll trace.
Manual cleanup for the stuck episode
Until a new resolved event arrives, the banner won't clear. Force it:
-- Insert a synthetic resolved event matching the stuck active one
INSERT INTO "MachineEvent" (id, "orgId", "machineId", ts, topic, "eventType", severity, "requiresAck", title, description, data, "createdAt")
SELECT gen_random_uuid(), "orgId", "machineId", NOW(), 'mold-change', 'mold-change', 'info', false,
'Cambio de molde cerrado manualmente', 'cierre manual',
jsonb_build_object(
'status','resolved',
'incidentKey', data->>'incidentKey',
'start_ms', (data->>'start_ms')::bigint,
'end_ms', extract(epoch from NOW())*1000
),
NOW()
FROM "MachineEvent"
WHERE "machineId" = '<uuid-M4-5>' AND "eventType" = 'mold-change' AND data->>'status' = 'active'
ORDER BY ts DESC LIMIT 1;
Banner disappears on next recap refresh (cache 60s).
Permanent safeguard (CT)
In lib/recap/getRecapData.ts ~line 817, add a freshness cap: an "active" mold-change older than 12h is almost always stuck data. Treat as resolved:
const STALE_ACTIVE_MS = 12 * 60 * 60 * 1000;
for (const event of machineMoldEvents) {
const key = eventIncidentKey(event.data, "mold-change", event.ts);
const status = eventStatus(event.data);
if (status === "resolved") { moldActiveByIncident.delete(key); continue; }
if (status === "active" || !status) {
// ignore if too old to be real
if (params.end.getTime() - event.ts.getTime() > STALE_ACTIVE_MS) continue;
moldActiveByIncident.set(key, moldStartMs(event.data, event.ts));
}
}
Same for the timeline extension logic in lib/recap/timeline.ts line 662 — cap isFreshActive at the same threshold.

1
flows (63).json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -27,6 +27,7 @@ const STOP_TYPES = new Set(["microstop", "macrostop"]);
const STOP_STATUS = new Set(["STOP", "DOWN", "OFFLINE"]);
const CACHE_TTL_SEC = 60;
const MOLD_LOOKBACK_MS = 14 * 24 * 60 * 60 * 1000;
const MOLD_ACTIVE_STALE_MS = 12 * 60 * 60 * 1000;
function safeNum(value: unknown) {
if (typeof value === "number" && Number.isFinite(value)) return value;
@@ -822,6 +823,7 @@ async function computeRecap(params: Required<Pick<RecapQuery, "orgId">> & {
continue;
}
if (status === "active" || !status) {
if (params.end.getTime() - event.ts.getTime() > MOLD_ACTIVE_STALE_MS) continue;
moldActiveByIncident.set(key, moldStartMs(event.data, event.ts));
}
}

View File

@@ -1,6 +1,7 @@
import type { RecapTimelineSegment } from "@/lib/recap/types";
const ACTIVE_STALE_MS = 2 * 60 * 1000;
const MOLD_ACTIVE_STALE_MS = 12 * 60 * 60 * 1000;
const MERGE_GAP_MS = 30 * 1000;
const MICRO_CLUSTER_GAP_MS = 60 * 1000;
const ABSORB_SHORT_SEGMENT_MS = 30 * 1000;
@@ -584,6 +585,21 @@ export function buildTimelineSegments(input: {
}
if (currentProduction) rawSegments.push(currentProduction);
// If production evidence appears after a mold-change "active" event, we cap that
// mold-change segment at the first production timestamp to avoid stale overwrite.
const productionWindows = rawSegments
.filter((segment): segment is Extract<RawSegment, { type: "production" }> => segment.type === "production")
.map((segment) => ({ startMs: segment.startMs, endMs: segment.endMs }))
.sort((a, b) => a.startMs - b.startMs || a.endMs - b.endMs);
const firstProductionMsAfter = (startMs: number) => {
for (const window of productionWindows) {
if (window.endMs <= startMs) continue;
return Math.max(startMs, window.startMs);
}
return null;
};
const eventEpisodes = new Map<
string,
{
@@ -661,8 +677,16 @@ export function buildTimelineSegments(input: {
let endMs = Math.trunc(episode.endMs ?? episode.lastTsMs);
if (episode.statusActive && !episode.statusResolved) {
const isFreshActive = rangeEndMs - episode.lastTsMs <= ACTIVE_STALE_MS;
const activeStaleMs = episode.type === "mold-change" ? MOLD_ACTIVE_STALE_MS : ACTIVE_STALE_MS;
const isFreshActive = rangeEndMs - episode.lastTsMs <= activeStaleMs;
endMs = isFreshActive ? rangeEndMs : episode.lastTsMs;
if (episode.type === "mold-change") {
const productionResumeMs = firstProductionMsAfter(startMs);
if (productionResumeMs != null) {
endMs = Math.min(endMs, productionResumeMs);
}
}
} else if (endMs <= startMs && episode.durationSec != null && episode.durationSec > 0) {
endMs = startMs + episode.durationSec * 1000;
}