pending invite link, the rest is finished
This commit is contained in:
@@ -170,7 +170,13 @@ export default function MachineDetailClient() {
|
|||||||
|
|
||||||
function isOffline(ts?: string) {
|
function isOffline(ts?: string) {
|
||||||
if (!ts) return true;
|
if (!ts) return true;
|
||||||
return Date.now() - new Date(ts).getTime() > 15000;
|
return Date.now() - new Date(ts).getTime() > 30000;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeStatus(status?: string) {
|
||||||
|
const s = (status ?? "").toUpperCase();
|
||||||
|
if (s === "ONLINE") return "RUN";
|
||||||
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusBadgeClass(status?: string, offline?: boolean) {
|
function statusBadgeClass(status?: string, offline?: boolean) {
|
||||||
@@ -192,7 +198,8 @@ export default function MachineDetailClient() {
|
|||||||
const hb = machine?.latestHeartbeat ?? null;
|
const hb = machine?.latestHeartbeat ?? null;
|
||||||
const kpi = machine?.latestKpi ?? null;
|
const kpi = machine?.latestKpi ?? null;
|
||||||
const offline = useMemo(() => isOffline(hb?.ts), [hb?.ts]);
|
const offline = useMemo(() => isOffline(hb?.ts), [hb?.ts]);
|
||||||
const statusLabel = offline ? "OFFLINE" : (hb?.status ?? "UNKNOWN");
|
const normalizedStatus = normalizeStatus(hb?.status);
|
||||||
|
const statusLabel = offline ? "OFFLINE" : (normalizedStatus || "UNKNOWN");
|
||||||
const cycleTarget = (machine as any)?.effectiveCycleTime ?? kpi?.cycleTime ?? null;
|
const cycleTarget = (machine as any)?.effectiveCycleTime ?? kpi?.cycleTime ?? null;
|
||||||
|
|
||||||
const ActiveRing = (props: any) => {
|
const ActiveRing = (props: any) => {
|
||||||
@@ -574,7 +581,7 @@ const timeline = useMemo(() => {
|
|||||||
<h1 className="truncate text-2xl font-semibold text-white">
|
<h1 className="truncate text-2xl font-semibold text-white">
|
||||||
{machine?.name ?? "Machine"}
|
{machine?.name ?? "Machine"}
|
||||||
</h1>
|
</h1>
|
||||||
<span className={`rounded-full px-3 py-1 text-xs ${statusBadgeClass(hb?.status, offline)}`}>
|
<span className={`rounded-full px-3 py-1 text-xs ${statusBadgeClass(normalizedStatus, offline)}`}>
|
||||||
{statusLabel}
|
{statusLabel}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,13 @@ function secondsAgo(ts?: string) {
|
|||||||
|
|
||||||
function isOffline(ts?: string) {
|
function isOffline(ts?: string) {
|
||||||
if (!ts) return true;
|
if (!ts) return true;
|
||||||
return Date.now() - new Date(ts).getTime() > 15000; // 15s threshold
|
return Date.now() - new Date(ts).getTime() > 30000; // 30s threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeStatus(status?: string) {
|
||||||
|
const s = (status ?? "").toUpperCase();
|
||||||
|
if (s === "ONLINE") return "RUN";
|
||||||
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
function badgeClass(status?: string, offline?: boolean) {
|
function badgeClass(status?: string, offline?: boolean) {
|
||||||
@@ -261,7 +267,8 @@ export default function MachinesPage() {
|
|||||||
{(!loading ? machines : []).map((m) => {
|
{(!loading ? machines : []).map((m) => {
|
||||||
const hb = m.latestHeartbeat;
|
const hb = m.latestHeartbeat;
|
||||||
const offline = isOffline(hb?.ts);
|
const offline = isOffline(hb?.ts);
|
||||||
const statusLabel = offline ? "OFFLINE" : hb?.status ?? "UNKNOWN";
|
const normalizedStatus = normalizeStatus(hb?.status);
|
||||||
|
const statusLabel = offline ? "OFFLINE" : normalizedStatus || "UNKNOWN";
|
||||||
const lastSeen = secondsAgo(hb?.ts);
|
const lastSeen = secondsAgo(hb?.ts);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -280,7 +287,7 @@ export default function MachinesPage() {
|
|||||||
|
|
||||||
<span
|
<span
|
||||||
className={`shrink-0 rounded-full px-3 py-1 text-xs ${badgeClass(
|
className={`shrink-0 rounded-full px-3 py-1 text-xs ${badgeClass(
|
||||||
hb?.status,
|
normalizedStatus,
|
||||||
offline
|
offline
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ type CycleRow = {
|
|||||||
ideal: number | null;
|
ideal: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const OFFLINE_MS = 15000;
|
const OFFLINE_MS = 30000;
|
||||||
const EVENT_WINDOW_SEC = 1800;
|
const EVENT_WINDOW_SEC = 1800;
|
||||||
const MAX_EVENT_MACHINES = 6;
|
const MAX_EVENT_MACHINES = 6;
|
||||||
const TOL = 0.10;
|
const TOL = 0.10;
|
||||||
@@ -74,6 +74,12 @@ function isOffline(ts?: string) {
|
|||||||
return Date.now() - new Date(ts).getTime() > OFFLINE_MS;
|
return Date.now() - new Date(ts).getTime() > OFFLINE_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeStatus(status?: string) {
|
||||||
|
const s = (status ?? "").toUpperCase();
|
||||||
|
if (s === "ONLINE") return "RUN";
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
function fmtPct(v?: number | null) {
|
function fmtPct(v?: number | null) {
|
||||||
if (v === null || v === undefined || Number.isNaN(v)) return "--";
|
if (v === null || v === undefined || Number.isNaN(v)) return "--";
|
||||||
return `${v.toFixed(1)}%`;
|
return `${v.toFixed(1)}%`;
|
||||||
@@ -273,7 +279,7 @@ export default function OverviewPage() {
|
|||||||
const offline = isOffline(hb?.ts);
|
const offline = isOffline(hb?.ts);
|
||||||
if (!offline) online += 1;
|
if (!offline) online += 1;
|
||||||
|
|
||||||
const status = (hb?.status ?? "").toUpperCase();
|
const status = normalizeStatus(hb?.status);
|
||||||
if (!offline) {
|
if (!offline) {
|
||||||
if (status === "RUN") running += 1;
|
if (status === "RUN") running += 1;
|
||||||
else if (status === "IDLE") idle += 1;
|
else if (status === "IDLE") idle += 1;
|
||||||
|
|||||||
@@ -139,6 +139,22 @@ function CycleTooltip({ active, payload }: any) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DowntimeTooltip({ active, payload }: any) {
|
||||||
|
if (!active || !payload?.length) return null;
|
||||||
|
const row = payload[0]?.payload ?? {};
|
||||||
|
const label = row.name ?? payload[0]?.name ?? "";
|
||||||
|
const value = row.value ?? payload[0]?.value ?? 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-white/10 bg-zinc-950/95 px-4 py-3 shadow-lg">
|
||||||
|
<div className="text-sm font-semibold text-white">{label}</div>
|
||||||
|
<div className="mt-2 text-xs text-zinc-300">
|
||||||
|
Downtime: <span className="text-white">{Number(value)} min</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function buildCsv(report: ReportPayload) {
|
function buildCsv(report: ReportPayload) {
|
||||||
const rows = new Map<string, Record<string, string | number>>();
|
const rows = new Map<string, Record<string, string | number>>();
|
||||||
const addSeries = (series: ReportTrendPoint[], key: string) => {
|
const addSeries = (series: ReportTrendPoint[], key: string) => {
|
||||||
@@ -674,10 +690,7 @@ export default function ReportsPage() {
|
|||||||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.08)" />
|
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.08)" />
|
||||||
<XAxis dataKey="name" tick={{ fill: "#a1a1aa" }} />
|
<XAxis dataKey="name" tick={{ fill: "#a1a1aa" }} />
|
||||||
<YAxis tick={{ fill: "#a1a1aa" }} />
|
<YAxis tick={{ fill: "#a1a1aa" }} />
|
||||||
<Tooltip
|
<Tooltip content={<DowntimeTooltip />} />
|
||||||
contentStyle={{ background: "rgba(0,0,0,0.85)", border: "1px solid rgba(255,255,255,0.1)" }}
|
|
||||||
formatter={(val: any) => [`${Number(val)} min`, "Downtime"]}
|
|
||||||
/>
|
|
||||||
<Bar dataKey="value" radius={[10, 10, 0, 0]} isAnimationActive={false}>
|
<Bar dataKey="value" radius={[10, 10, 0, 0]} isAnimationActive={false}>
|
||||||
{downtimeSeries.map((row, idx) => (
|
{downtimeSeries.map((row, idx) => (
|
||||||
<Cell key={`${row.name}-${idx}`} fill={downtimeColors[row.name] ?? "#94a3b8"} />
|
<Cell key={`${row.name}-${idx}`} fill={downtimeColors[row.name] ?? "#94a3b8"} />
|
||||||
|
|||||||
Reference in New Issue
Block a user