pre-bemis
This commit is contained in:
@@ -19,6 +19,7 @@ type MachineRow = {
|
||||
fwVersion?: string | null;
|
||||
};
|
||||
};
|
||||
const LIVE_REFRESH_MS = 5000;
|
||||
|
||||
function secondsAgo(ts: string | undefined, locale: string, fallback: string) {
|
||||
if (!ts) return fallback;
|
||||
@@ -52,7 +53,7 @@ export default function MachinesClient({ initialMachines = [] }: { initialMachin
|
||||
const { t, locale } = useI18n();
|
||||
const router = useRouter();
|
||||
const [machines, setMachines] = useState<MachineRow[]>(() => initialMachines);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(() => initialMachines.length === 0);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
const [createName, setCreateName] = useState("");
|
||||
const [createCode, setCreateCode] = useState("");
|
||||
@@ -69,28 +70,36 @@ export default function MachinesClient({ initialMachines = [] }: { initialMachin
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
async function load() {
|
||||
async function load(initial: boolean) {
|
||||
try {
|
||||
if (!initial && typeof document !== "undefined" && document.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch("/api/machines", { cache: "no-store" });
|
||||
const json = await res.json();
|
||||
if (alive) {
|
||||
setMachines(json.machines ?? []);
|
||||
setLoading(false);
|
||||
if (initial) setLoading(false);
|
||||
}
|
||||
} catch {
|
||||
if (alive) setLoading(false);
|
||||
if (alive && initial) setLoading(false);
|
||||
} finally {
|
||||
if (!alive) return;
|
||||
timer = setTimeout(() => {
|
||||
void load(false);
|
||||
}, LIVE_REFRESH_MS);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
const t = setInterval(load, 15000);
|
||||
|
||||
void load(initialMachines.length === 0);
|
||||
return () => {
|
||||
alive = false;
|
||||
clearInterval(t);
|
||||
if (timer) clearTimeout(timer);
|
||||
};
|
||||
}, []);
|
||||
}, [initialMachines.length]);
|
||||
|
||||
async function createMachine() {
|
||||
if (!createName.trim()) {
|
||||
|
||||
@@ -128,6 +128,7 @@ const TOL = 0.10;
|
||||
const DEFAULT_MICRO_MULT = 1.5;
|
||||
const DEFAULT_MACRO_MULT = 5;
|
||||
const NORMAL_TOL_SEC = 0.1;
|
||||
const LIVE_REFRESH_MS = 5000;
|
||||
|
||||
const BUCKET = {
|
||||
normal: {
|
||||
@@ -289,6 +290,24 @@ function rowsToWorkOrders(rows: WorkOrderRow[]): WorkOrderUpload[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
function toErrorMessage(value: unknown, fallback: string): string {
|
||||
if (typeof value === "string" && value.trim().length > 0) return value;
|
||||
if (value && typeof value === "object") {
|
||||
const maybeMessage =
|
||||
("message" in value && typeof value.message === "string" && value.message) ||
|
||||
("error" in value && typeof value.error === "string" && value.error) ||
|
||||
("text" in value && typeof value.text === "string" && value.text) ||
|
||||
null;
|
||||
if (maybeMessage && maybeMessage.trim().length > 0) return maybeMessage;
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export default function MachineDetailClient() {
|
||||
const { t, locale } = useI18n();
|
||||
const { screenlessMode } = useScreenlessMode();
|
||||
@@ -319,9 +338,14 @@ export default function MachineDetailClient() {
|
||||
if (!machineId) return;
|
||||
|
||||
let alive = true;
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
async function load() {
|
||||
async function load(initial: boolean) {
|
||||
try {
|
||||
if (!initial && typeof document !== "undefined" && document.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/machines/${machineId}?windowSec=3600&events=critical`, {
|
||||
cache: "no-cache",
|
||||
credentials: "include",
|
||||
@@ -329,7 +353,7 @@ export default function MachineDetailClient() {
|
||||
|
||||
if (res.status === 304) {
|
||||
if (!alive) return;
|
||||
setLoading(false);
|
||||
if (initial) setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -338,8 +362,8 @@ export default function MachineDetailClient() {
|
||||
if (!alive) return;
|
||||
|
||||
if (!res.ok || json?.ok === false) {
|
||||
setError(json?.error ?? t("machine.detail.error.failed"));
|
||||
setLoading(false);
|
||||
setError(toErrorMessage(json?.error, t("machine.detail.error.failed")));
|
||||
if (initial) setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -350,19 +374,25 @@ export default function MachineDetailClient() {
|
||||
setThresholds(json.thresholds ?? null);
|
||||
setActiveStoppage(json.activeStoppage ?? null);
|
||||
setError(null);
|
||||
setLoading(false);
|
||||
if (initial) setLoading(false);
|
||||
} catch {
|
||||
if (!alive) return;
|
||||
setError(t("machine.detail.error.network"));
|
||||
setLoading(false);
|
||||
if (initial) {
|
||||
setError(t("machine.detail.error.network"));
|
||||
setLoading(false);
|
||||
}
|
||||
} finally {
|
||||
if (!alive) return;
|
||||
timer = setTimeout(() => {
|
||||
void load(false);
|
||||
}, LIVE_REFRESH_MS);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
const timer = setInterval(load, 15000);
|
||||
void load(true);
|
||||
return () => {
|
||||
alive = false;
|
||||
clearInterval(timer);
|
||||
if (timer) clearTimeout(timer);
|
||||
};
|
||||
}, [machineId, t]);
|
||||
|
||||
@@ -479,7 +509,7 @@ export default function MachineDetailClient() {
|
||||
} else {
|
||||
setUploadState({
|
||||
status: "error",
|
||||
message: json?.error ?? t("machine.detail.workOrders.uploadError"),
|
||||
message: toErrorMessage(json?.error, t("machine.detail.workOrders.uploadError")),
|
||||
});
|
||||
}
|
||||
event.target.value = "";
|
||||
@@ -508,7 +538,7 @@ export default function MachineDetailClient() {
|
||||
const res = await fetch(`/api/machines/${machineId}`, { method: "DELETE" });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok || !data.ok) {
|
||||
throw new Error(data.error || t("machines.delete.error.failed"));
|
||||
throw new Error(toErrorMessage(data?.error, t("machines.delete.error.failed")));
|
||||
}
|
||||
router.push("/machines");
|
||||
} catch (err: unknown) {
|
||||
@@ -886,9 +916,10 @@ export default function MachineDetailClient() {
|
||||
|
||||
const cycleDerived = useMemo(() => {
|
||||
const rows = cycles ?? [];
|
||||
const fallbackIdeal = cycleTarget && cycleTarget > 0 ? cycleTarget : null;
|
||||
|
||||
const mapped: CycleDerivedRow[] = rows.map((cycle) => {
|
||||
const ideal = cycle.ideal ?? null;
|
||||
const ideal = cycle.ideal ?? fallbackIdeal;
|
||||
const actual = cycle.actual ?? null;
|
||||
const extra = ideal != null && actual != null ? actual - ideal : null;
|
||||
|
||||
@@ -914,7 +945,7 @@ export default function MachineDetailClient() {
|
||||
const avgDeltaPct = deltas.length ? deltas.reduce((a, b) => a + b, 0) / deltas.length : null;
|
||||
|
||||
return { mapped, counts, avgDeltaPct };
|
||||
}, [cycles, thresholds]);
|
||||
}, [cycles, cycleTarget, thresholds]);
|
||||
|
||||
const deviationSeries = useMemo(() => {
|
||||
const last = cycleDerived.mapped.slice(-100);
|
||||
@@ -1313,7 +1344,7 @@ export default function MachineDetailClient() {
|
||||
className="h-[380px] rounded-3xl border border-white/10 bg-black/30 p-4 backdrop-blur"
|
||||
style={{ boxShadow: "var(--app-chart-shadow)" }}
|
||||
>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ResponsiveContainer width="100%" height="100%" minHeight={200}>
|
||||
<ComposedChart data={deviationSeries}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--app-chart-grid)" />
|
||||
<XAxis
|
||||
@@ -1407,7 +1438,7 @@ export default function MachineDetailClient() {
|
||||
className="h-[380px] rounded-3xl border border-white/10 bg-black/30 p-4 backdrop-blur"
|
||||
style={{ boxShadow: "var(--app-chart-shadow)" }}
|
||||
>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ResponsiveContainer width="100%" height="100%" minHeight={200}>
|
||||
<BarChart data={impactAgg.rows}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--app-chart-grid)" />
|
||||
<XAxis dataKey="label" tick={{ fill: "var(--app-chart-tick)" }} />
|
||||
|
||||
22
app/(app)/machines/loading.tsx
Normal file
22
app/(app)/machines/loading.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
export default function MachinesLoading() {
|
||||
return (
|
||||
<div className="p-4 sm:p-6 space-y-6 animate-pulse">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="h-6 w-36 rounded-lg bg-white/10" />
|
||||
<div className="h-4 w-60 rounded-lg bg-white/5" />
|
||||
</div>
|
||||
<div className="flex w-full gap-2 sm:w-auto">
|
||||
<div className="h-9 w-full rounded-xl border border-emerald-400/40 bg-emerald-500/10 sm:w-36" />
|
||||
<div className="h-9 w-full rounded-xl border border-white/10 bg-white/5 sm:w-32" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{Array.from({ length: 6 }).map((_, idx) => (
|
||||
<div key={idx} className="h-40 rounded-2xl border border-white/10 bg-white/5" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { requireSession } from "@/lib/auth/requireSession";
|
||||
import {
|
||||
fetchLatestHeartbeats,
|
||||
fetchMachineBase,
|
||||
mergeMachineOverviewRows,
|
||||
} from "@/lib/machines/withLatest";
|
||||
import MachinesClient from "./MachinesClient";
|
||||
|
||||
function toIso(value?: Date | null) {
|
||||
@@ -11,34 +15,32 @@ export default async function MachinesPage() {
|
||||
const session = await requireSession();
|
||||
if (!session) redirect("/login?next=/machines");
|
||||
|
||||
const machines = await prisma.machine.findMany({
|
||||
where: { orgId: session.orgId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
code: true,
|
||||
location: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
heartbeats: {
|
||||
orderBy: { tsServer: "desc" },
|
||||
take: 1,
|
||||
select: { ts: true, tsServer: true, status: true, message: true, ip: true, fwVersion: true },
|
||||
},
|
||||
},
|
||||
const machines = await fetchMachineBase(session.orgId);
|
||||
const heartbeats = await fetchLatestHeartbeats(
|
||||
session.orgId,
|
||||
machines.map((machine) => machine.id)
|
||||
);
|
||||
const rows = mergeMachineOverviewRows({
|
||||
machines,
|
||||
heartbeats,
|
||||
includeKpi: false,
|
||||
});
|
||||
|
||||
const initialMachines = machines.map((machine) => ({
|
||||
...machine,
|
||||
latestHeartbeat: machine.heartbeats[0]
|
||||
const initialMachines = rows.map((machine) => ({
|
||||
id: machine.id,
|
||||
name: machine.name,
|
||||
code: machine.code ?? null,
|
||||
location: machine.location ?? null,
|
||||
latestHeartbeat: machine.latestHeartbeat
|
||||
? {
|
||||
...machine.heartbeats[0],
|
||||
ts: toIso(machine.heartbeats[0].ts) ?? "",
|
||||
tsServer: toIso(machine.heartbeats[0].tsServer),
|
||||
ts: toIso(machine.latestHeartbeat.ts) ?? "",
|
||||
tsServer: toIso(machine.latestHeartbeat.tsServer),
|
||||
status: machine.latestHeartbeat.status,
|
||||
message: machine.latestHeartbeat.message ?? null,
|
||||
ip: machine.latestHeartbeat.ip ?? null,
|
||||
fwVersion: machine.latestHeartbeat.fwVersion ?? null,
|
||||
}
|
||||
: null,
|
||||
heartbeats: undefined,
|
||||
}));
|
||||
|
||||
return <MachinesClient initialMachines={initialMachines} />;
|
||||
|
||||
Reference in New Issue
Block a user