updates
This commit is contained in:
@@ -218,6 +218,8 @@ function eventIncidentKey(data: unknown, eventType: string, ts: Date) {
|
||||
const inner = extractEventData(data);
|
||||
const direct = String(inner.incidentKey ?? inner.incident_key ?? "").trim();
|
||||
if (direct) return direct;
|
||||
const alertId = String(inner.alert_id ?? inner.alertId ?? "").trim();
|
||||
if (alertId) return `${eventType}:${alertId}`;
|
||||
const startMs = safeNum(inner.start_ms) ?? safeNum(inner.startMs);
|
||||
if (startMs != null) return `${eventType}:${Math.trunc(startMs)}`;
|
||||
return `${eventType}:${ts.getTime()}`;
|
||||
@@ -291,7 +293,7 @@ async function computeRecap(params: Required<Pick<RecapQuery, "orgId">> & {
|
||||
const machines = await prisma.machine.findMany({
|
||||
where: { orgId: params.orgId, ...machineFilter },
|
||||
orderBy: { name: "asc" },
|
||||
select: { id: true, name: true, location: true },
|
||||
select: { id: true, name: true, location: true, tsServer: true },
|
||||
});
|
||||
|
||||
if (!machines.length) {
|
||||
@@ -421,9 +423,9 @@ async function computeRecap(params: Required<Pick<RecapQuery, "orgId">> & {
|
||||
where: {
|
||||
orgId: params.orgId,
|
||||
machineId: { in: machineIds },
|
||||
ts: { lte: params.end },
|
||||
tsServer: { lte: params.end },
|
||||
},
|
||||
orderBy: [{ machineId: "asc" }, { ts: "desc" }],
|
||||
orderBy: [{ machineId: "asc" }, { tsServer: "desc" }],
|
||||
distinct: ["machineId"],
|
||||
select: {
|
||||
machineId: true,
|
||||
@@ -814,8 +816,36 @@ async function computeRecap(params: Required<Pick<RecapQuery, "orgId">> & {
|
||||
);
|
||||
}
|
||||
|
||||
const firstProductionMsAfterMoldStart = (startMs: number) => {
|
||||
let best: number | null = null;
|
||||
for (const cycle of dedupedCycles) {
|
||||
const t = cycle.ts.getTime();
|
||||
if (t <= startMs) continue;
|
||||
const g = safeNum(cycle.goodDelta) ?? 0;
|
||||
const s = safeNum(cycle.scrapDelta) ?? 0;
|
||||
if (g > 0 || s > 0) {
|
||||
if (best == null || t < best) best = t;
|
||||
}
|
||||
}
|
||||
for (const kpi of dedupedKpis) {
|
||||
const t = kpi.ts.getTime();
|
||||
if (t <= startMs) continue;
|
||||
const g = safeNum(kpi.good) ?? safeNum(kpi.goodParts) ?? 0;
|
||||
const s = safeNum(kpi.scrap) ?? safeNum(kpi.scrapParts) ?? 0;
|
||||
if (g > 0 || s > 0) {
|
||||
if (best == null || t < best) best = t;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
};
|
||||
|
||||
const moldActiveByIncident = new Map<string, number>();
|
||||
for (const event of machineMoldEvents) {
|
||||
const inner = extractEventData(event.data);
|
||||
const isUpdate = safeBool(inner.is_update ?? inner.isUpdate);
|
||||
const isAutoAck = safeBool(inner.is_auto_ack ?? inner.isAutoAck);
|
||||
if (isUpdate || isAutoAck) continue;
|
||||
|
||||
const key = eventIncidentKey(event.data, "mold-change", event.ts);
|
||||
const status = eventStatus(event.data);
|
||||
if (status === "resolved") {
|
||||
@@ -827,6 +857,12 @@ async function computeRecap(params: Required<Pick<RecapQuery, "orgId">> & {
|
||||
moldActiveByIncident.set(key, moldStartMs(event.data, event.ts));
|
||||
}
|
||||
}
|
||||
for (const [k, startMs] of [...moldActiveByIncident.entries()]) {
|
||||
const resumeMs = firstProductionMsAfterMoldStart(startMs);
|
||||
if (resumeMs != null && resumeMs <= params.end.getTime()) {
|
||||
moldActiveByIncident.delete(k);
|
||||
}
|
||||
}
|
||||
let moldChangeStartMs: number | null = null;
|
||||
for (const startMs of moldActiveByIncident.values()) {
|
||||
if (moldChangeStartMs == null || startMs > moldChangeStartMs) moldChangeStartMs = startMs;
|
||||
@@ -879,7 +915,14 @@ async function computeRecap(params: Required<Pick<RecapQuery, "orgId">> & {
|
||||
moldChangeStartMs,
|
||||
},
|
||||
heartbeat: {
|
||||
lastSeenAt: toIso(latestTs),
|
||||
lastSeenAt: toIso(
|
||||
(() => {
|
||||
const hbMs = latestHb ? (latestHb.tsServer ?? latestHb.ts).getTime() : null;
|
||||
const machineMs = machine.tsServer.getTime();
|
||||
if (hbMs != null) return new Date(Math.max(hbMs, machineMs));
|
||||
return machine.tsServer;
|
||||
})()
|
||||
),
|
||||
uptimePct,
|
||||
},
|
||||
};
|
||||
|
||||
27
lib/recap/progressDisplay.ts
Normal file
27
lib/recap/progressDisplay.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Recap & work-order progress: large targets (e.g. 301k) make raw % < 1.
|
||||
* Rounding to integer shows 0%; bar width 0.17% is invisible. Use decimals + a visual floor for the bar.
|
||||
*/
|
||||
|
||||
/** "0.17%" with enough precision when needed; "—" for null. */
|
||||
export function formatRecapProgressPercent(
|
||||
pct: number | null | undefined,
|
||||
locale: string
|
||||
): string {
|
||||
if (pct == null || Number.isNaN(pct)) return "—";
|
||||
if (pct <= 0) return "0%";
|
||||
if (pct < 10) {
|
||||
return `${pct.toLocaleString(locale, { maximumFractionDigits: 2, minimumFractionDigits: 0 })}%`;
|
||||
}
|
||||
return `${Math.round(pct).toLocaleString(locale)}%`;
|
||||
}
|
||||
|
||||
/**
|
||||
* For CSS width %: keep proportional when ≥2%; below that, any positive progress
|
||||
* needs a minimum or the bar looks like a single pixel.
|
||||
*/
|
||||
export function progressBarWidthPercent(pct: number | null | undefined): number {
|
||||
if (pct == null || Number.isNaN(pct) || pct <= 0) return 0;
|
||||
if (pct < 2) return Math.max(2, Math.min(100, pct));
|
||||
return Math.min(100, pct);
|
||||
}
|
||||
4
lib/recap/recapUiConstants.ts
Normal file
4
lib/recap/recapUiConstants.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Client-safe recap thresholds. Kept in sync with OFFLINE logic in lib/recap/redesign.ts.
|
||||
*/
|
||||
export const RECAP_HEARTBEAT_STALE_MS = 10 * 60 * 1000;
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type TimelineCycleRow,
|
||||
type TimelineEventRow,
|
||||
} from "@/lib/recap/timeline";
|
||||
import { RECAP_HEARTBEAT_STALE_MS } from "@/lib/recap/recapUiConstants";
|
||||
import type {
|
||||
RecapDetailResponse,
|
||||
RecapMachine,
|
||||
@@ -25,7 +26,7 @@ type DetailRangeInput = {
|
||||
end?: string | null;
|
||||
};
|
||||
|
||||
const OFFLINE_THRESHOLD_MS = 10 * 60 * 1000;
|
||||
const OFFLINE_THRESHOLD_MS = RECAP_HEARTBEAT_STALE_MS;
|
||||
const TIMELINE_EVENT_LOOKBACK_MS = 24 * 60 * 60 * 1000;
|
||||
const RECAP_CACHE_TTL_SEC = 60;
|
||||
const WEEKDAY_KEYS: ShiftOverrideDay[] = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
||||
|
||||
Reference in New Issue
Block a user