hidden status-bar & machine.status

This commit is contained in:
2026-06-01 19:41:16 +02:00
parent c170807172
commit 7cf063431d
+345 -272
View File
@@ -1,330 +1,403 @@
<!DOCTYPE html> <!doctype html>
<html lang="pl"> <html lang="pl">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ALS Alert Screen</title> <title>ALS Alert Screen</title>
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } * {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body { body {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
background: #000; background: #000;
color: #fff; color: #fff;
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
} }
/* ── NORMAL VIEW ──────────────────────────────── */ /* ── NORMAL VIEW ──────────────────────────────── */
#normal-view { #normal-view {
width: 100%; width: 100%;
height: 100vh; height: 100vh;
border: none; border: none;
display: block; display: block;
} }
/* ── ALARM VIEW ──────────────────────────────── */ /* ── ALARM VIEW ──────────────────────────────── */
#alarm-view { #alarm-view {
display: none; display: none;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
height: 100vh; height: 100vh;
background: #0d0000; background: #0d0000;
overflow: hidden; overflow: hidden;
} }
#alarm-view.active { display: flex; } #alarm-view.active {
display: flex;
}
.alarm-header { .alarm-header {
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 24px; gap: 24px;
padding: 18px 24px; padding: 18px 24px;
background: #1e0000; background: #1e0000;
border-bottom: 3px solid #ff0000; border-bottom: 3px solid #ff0000;
animation: headerBlink 1s step-start infinite; animation: headerBlink 1s step-start infinite;
}
@keyframes headerBlink {
0%,
100% {
background: #1e0000;
} }
@keyframes headerBlink { 50% {
0%, 100% { background: #1e0000; } background: #3a0000;
50% { background: #3a0000; }
} }
}
.alarm-icon { font-size: clamp(28px, 4vw, 56px); } .alarm-icon {
font-size: clamp(28px, 4vw, 56px);
}
.alarm-title { .alarm-title {
font-size: clamp(22px, 3.5vw, 52px); font-size: clamp(22px, 3.5vw, 52px);
font-weight: 900; font-weight: 900;
color: #ff2222; color: #ff2222;
letter-spacing: .15em; letter-spacing: 0.15em;
text-transform: uppercase; text-transform: uppercase;
} }
.alarm-count { .alarm-count {
font-size: clamp(18px, 2.5vw, 38px); font-size: clamp(18px, 2.5vw, 38px);
font-weight: 700; font-weight: 700;
color: #ff8888; color: #ff8888;
background: #3a0000; background: #3a0000;
border: 2px solid #ff3333; border: 2px solid #ff3333;
border-radius: 8px; border-radius: 8px;
padding: 4px 18px; padding: 4px 18px;
} }
/* cards area */ /* cards area */
#machines-grid { #machines-grid {
flex: 1; flex: 1;
display: grid; display: grid;
/* rows, columns & --cols/--rows set dynamically in JS */ /* rows, columns & --cols/--rows set dynamically in JS */
gap: 3px; gap: 3px;
background: #000; background: #000;
overflow: hidden; overflow: hidden;
} }
.machine-card { .machine-card {
background: #1a0000; background: #1a0000;
border: none; border: none;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 0.5em; gap: 0.5em;
overflow: hidden; overflow: hidden;
container-type: inline-size; container-type: inline-size;
animation: cardGlow 2s ease-in-out infinite; animation: cardGlow 2s ease-in-out infinite;
}
@keyframes cardGlow {
0%,
100% {
box-shadow: 0 0 12px rgba(255, 0, 0, 0.25);
} }
@keyframes cardGlow { 50% {
0%, 100% { box-shadow: 0 0 12px rgba(255,0,0,.25); } box-shadow: 0 0 30px rgba(255, 0, 0, 0.7);
50% { box-shadow: 0 0 30px rgba(255,0,0,.70); }
} }
}
/* ── big machine number - skala przez --cols / --rows + cqw ── */ /* ── big machine number - skala przez --cols / --rows + cqw ── */
.card-machine-id { .card-machine-id {
font-size: min(calc(55vw / var(--cols, 1)), calc(55vh / var(--rows, 1)), 35cqw); font-size: min(
font-weight: 900; calc(55vw / var(--cols, 1)),
color: #ff2222; calc(55vh / var(--rows, 1)),
line-height: 1; 35cqw
letter-spacing: .04em; );
text-align: center; font-weight: 900;
text-shadow: 0 0 24px rgba(255,0,0,.6); color: #ff2222;
} line-height: 1;
letter-spacing: 0.04em;
text-align: center;
text-shadow: 0 0 24px rgba(255, 0, 0, 0.6);
}
.card-status { .card-status {
font-size: min(calc(7vw / var(--cols, 1)), calc(7vh / var(--rows, 1)), 5cqw); font-size: min(
font-weight: 700; calc(7vw / var(--cols, 1)),
color: #ff9999; calc(7vh / var(--rows, 1)),
text-transform: uppercase; 5cqw
letter-spacing: .08em; );
text-align: center; font-weight: 700;
} color: #ff9999;
text-transform: uppercase;
letter-spacing: 0.08em;
text-align: center;
}
/* ── detail grid ── */ /* ── detail grid ── */
.card-info { .card-info {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 6px 14px; gap: 6px 14px;
border-top: 1px solid #3a0000; border-top: 1px solid #3a0000;
padding-top: 8px; padding-top: 8px;
} }
.info-item { display: flex; flex-direction: column; } .info-item {
.info-label { display: flex;
font-size: clamp(9px, .8vw, 12px); flex-direction: column;
color: #664444; }
text-transform: uppercase; .info-label {
letter-spacing: .05em; font-size: clamp(9px, 0.8vw, 12px);
} color: #664444;
.info-value { text-transform: uppercase;
font-size: clamp(11px, 1.1vw, 16px); letter-spacing: 0.05em;
color: #ddcccc; }
font-weight: 700; .info-value {
} font-size: clamp(11px, 1.1vw, 16px);
color: #ddcccc;
font-weight: 700;
}
.progress-row { .progress-row {
grid-column: 1 / -1; grid-column: 1 / -1;
margin-top: 2px; margin-top: 2px;
} }
.progress-bar { .progress-bar {
height: 12px; height: 12px;
background: #3a0000; background: #3a0000;
border-radius: 6px; border-radius: 6px;
overflow: hidden; overflow: hidden;
} }
.progress-fill { .progress-fill {
height: 100%; height: 100%;
background: linear-gradient(90deg, #990000, #ff4444); background: linear-gradient(90deg, #990000, #ff4444);
border-radius: 6px; border-radius: 6px;
transition: width .8s ease; transition: width 0.8s ease;
} }
.progress-label { .progress-label {
text-align: right; text-align: right;
font-size: clamp(9px, .8vw, 11px); font-size: clamp(9px, 0.8vw, 11px);
color: #775555; color: #775555;
margin-top: 3px; margin-top: 3px;
} }
/* ── STATUS BAR ──────────────────────────────── */ /* ── STATUS BAR ──────────────────────────────── */
#status-bar { #status-bar {
position: fixed; position: fixed;
bottom: 6px; bottom: 6px;
right: 10px; right: 10px;
font-size: 11px; font-size: 11px;
color: #444; color: #444;
background: rgba(0,0,0,.6); background: rgba(0, 0, 0, 0.6);
padding: 3px 10px; padding: 3px 10px;
border-radius: 4px; border-radius: 4px;
pointer-events: none; pointer-events: none;
z-index: 9999; z-index: 9999;
} }
</style> </style>
</head> </head>
<body> <body>
<iframe
id="normal-view"
src="http://192.168.40.102/terminal/ift/5/PL/index"
allowfullscreen
></iframe>
<iframe id="normal-view" src="http://192.168.40.102/terminal/ift/5/PL/index" allowfullscreen></iframe> <div id="alarm-view">
<div class="alarm-header">
<div id="alarm-view">
<div class="alarm-header">
<span class="alarm-icon"></span> <span class="alarm-icon"></span>
<span class="alarm-title">Alarm</span> <span class="alarm-title">Alarm</span>
<span class="alarm-count" id="alarm-count">0</span> <span class="alarm-count" id="alarm-count">0</span>
<span class="alarm-icon"></span> <span class="alarm-icon"></span>
</div>
<div id="machines-grid"></div>
</div> </div>
<div id="machines-grid"></div>
</div>
<div id="status-bar">WS: 0 / 0</div> <!-- <div id="status-bar">WS: 0 / 0</div> -->
<script> <script>
const MACHINES = [ const MACHINES = [
"1.64","1.76","2.60","2.63","2.67","2.72","2.74", "1.64",
"3.01","3.04","3.08","3.22","3.26","3.51","3.55","3.56", "1.76",
"3.59","3.61","3.66","3.69","3.70","3.71","3.73","3.75", "2.60",
"4.05","4.09","4.14","4.52","4.53","4.57","4.58","4.62", "2.63",
"4.65","4.68","5.29","5.54" "2.67",
]; "2.72",
"2.74",
"3.01",
"3.04",
"3.08",
"3.22",
"3.26",
"3.51",
"3.55",
"3.56",
"3.59",
"3.61",
"3.66",
"3.69",
"3.70",
"3.71",
"3.73",
"3.75",
"4.05",
"4.09",
"4.14",
"4.52",
"4.53",
"4.57",
"4.58",
"4.62",
"4.65",
"4.68",
"5.29",
"5.54",
];
const WS_BASE = "ws://192.168.40.127/ws/als-data/"; const WS_BASE = "ws://192.168.40.127/ws/als-data/";
const RECONNECT_MS = 5000; const RECONNECT_MS = 5000;
// ── Debug mode ──────────────────────────────────────────────────────────── // ── Debug mode ────────────────────────────────────────────────────────────
// Otwórz stronę z ?debug aby wymusić widok alarmowy z przykładowymi danymi. // Otwórz stronę z ?debug aby wymusić widok alarmowy z przykładowymi danymi.
// Możesz też wpisać w konsoli: DEBUG_ALARM = true; updateDisplay(); // Możesz też wpisać w konsoli: DEBUG_ALARM = true; updateDisplay();
let DEBUG_ALARM = new URLSearchParams(location.search).has("debug"); let DEBUG_ALARM = new URLSearchParams(location.search).has("debug");
const DEBUG_MACHINES = ["2.60", "3.22", "4.09"]; const DEBUG_MACHINES = ["2.60", "3.22", "4.09"];
const DEBUG_DATA = { const DEBUG_DATA = {
"2.60": { machine:"2.60", status:"Alarm - brak materiału" }, "2.60": { machine: "2.60", status: "Alarm - brak materiału" },
"3.22": { machine:"3.22", status:"Alarm - awaria" }, "3.22": { machine: "3.22", status: "Alarm - awaria" },
"4.09": { machine:"4.09", status:"Alarm - przekroczony cykl" }, "4.09": { machine: "4.09", status: "Alarm - przekroczony cykl" },
}; };
const machineData = {}; // id → last data payload const machineData = {}; // id → last data payload
const wsStatus = {}; // id → 'connected' | 'error' | 'disconnected' const wsStatus = {}; // id → 'connected' | 'error' | 'disconnected'
const wsMap = {}; // id → WebSocket instance const wsMap = {}; // id → WebSocket instance
// ── WebSocket management ────────────────────────────────────────────────── // ── WebSocket management ──────────────────────────────────────────────────
function connect(id) { function connect(id) {
if (wsMap[id]) { if (wsMap[id]) {
try { wsMap[id].close(); } catch (_) {} try {
} wsMap[id].close();
} catch (_) {}
}
const ws = new WebSocket(WS_BASE + id + "/"); const ws = new WebSocket(WS_BASE + id + "/");
wsMap[id] = ws; wsMap[id] = ws;
wsStatus[id] = "connecting"; wsStatus[id] = "connecting";
ws.onopen = () => { ws.onopen = () => {
wsStatus[id] = "connected"; wsStatus[id] = "connected";
refreshStatus(); refreshStatus();
}; };
ws.onmessage = (ev) => { ws.onmessage = (ev) => {
try { try {
const msg = JSON.parse(ev.data); const msg = JSON.parse(ev.data);
if (msg.type === "machine_update" && msg.data) { if (msg.type === "machine_update" && msg.data) {
machineData[id] = { ...msg.data, _ts: msg.timestamp }; machineData[id] = { ...msg.data, _ts: msg.timestamp };
updateDisplay(); updateDisplay();
} }
} catch (e) { /* ignore malformed frames */ } } catch (e) {
}; /* ignore malformed frames */
}
};
ws.onerror = () => { ws.onerror = () => {
wsStatus[id] = "error"; wsStatus[id] = "error";
refreshStatus(); refreshStatus();
}; };
ws.onclose = () => { ws.onclose = () => {
wsStatus[id] = "disconnected"; wsStatus[id] = "disconnected";
refreshStatus(); refreshStatus();
setTimeout(() => connect(id), RECONNECT_MS); setTimeout(() => connect(id), RECONNECT_MS);
}; };
} }
// ── Display logic ───────────────────────────────────────────────────────── // ── Display logic ─────────────────────────────────────────────────────────
function isAlarm(data) { function isAlarm(data) {
if (DEBUG_ALARM) return true; // w trybie debug wszystkie wstrzyknięte maszyny to alarmy if (DEBUG_ALARM) return true; // w trybie debug wszystkie wstrzyknięte maszyny to alarmy
return data && typeof data.status === "string" && return (
data.status.toLowerCase().includes("alarm"); data &&
} typeof data.status === "string" &&
data.status.toLowerCase().includes("alarm")
);
}
function updateDisplay() { function updateDisplay() {
const alarms = Object.entries(machineData) const alarms = Object.entries(machineData)
.filter(([, d]) => isAlarm(d)) .filter(([, d]) => isAlarm(d))
.sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true })); .sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }));
const normalView = document.getElementById("normal-view"); const normalView = document.getElementById("normal-view");
const alarmView = document.getElementById("alarm-view"); const alarmView = document.getElementById("alarm-view");
const countEl = document.getElementById("alarm-count"); const countEl = document.getElementById("alarm-count");
if (alarms.length > 0) { if (alarms.length > 0) {
normalView.style.display = "none"; normalView.style.display = "none";
alarmView.classList.add("active"); alarmView.classList.add("active");
countEl.textContent = alarms.length === 1 countEl.textContent =
? "1 maszyna" alarms.length === 1 ? "1 maszyna" : alarms.length + " maszyny";
: alarms.length + " maszyny"; renderCards(alarms);
renderCards(alarms); } else {
} else { normalView.style.display = "block";
normalView.style.display = "block"; alarmView.classList.remove("active");
alarmView.classList.remove("active"); }
} }
}
function renderCards(alarms) { function renderCards(alarms) {
const grid = document.getElementById("machines-grid"); const grid = document.getElementById("machines-grid");
grid.innerHTML = ""; grid.innerHTML = "";
const n = alarms.length; const n = alarms.length;
const cols = Math.ceil(Math.sqrt(n)); const cols = Math.ceil(Math.sqrt(n));
const rows = Math.ceil(n / cols); const rows = Math.ceil(n / cols);
grid.style.gridTemplateColumns = `repeat(${cols}, 1fr)`; grid.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
grid.style.gridTemplateRows = `repeat(${rows}, 1fr)`; grid.style.gridTemplateRows = `repeat(${rows}, 1fr)`;
grid.style.setProperty("--cols", cols); grid.style.setProperty("--cols", cols);
grid.style.setProperty("--rows", rows); grid.style.setProperty("--rows", rows);
alarms.forEach(([id, d]) => { alarms.forEach(([id, d]) => {
const card = document.createElement("div"); const card = document.createElement("div");
card.className = "machine-card"; card.className = "machine-card";
card.innerHTML = ` card.innerHTML = `
<div class="card-machine-id">${d.machine ?? id}</div> <div class="card-machine-id">${d.machine ?? id}</div>
<!--- <div class="card-status">${d.status ?? "ALARM"}</div> --> <!--- <div class="card-status">${d.status ?? "ALARM"}</div> -->
`; `;
grid.appendChild(card); grid.appendChild(card);
}); });
} }
function refreshStatus() { function refreshStatus() {
const connected = Object.values(wsStatus).filter(s => s === "connected").length; const connected = Object.values(wsStatus).filter(
document.getElementById("status-bar").textContent = (s) => s === "connected",
`WS: ${connected} / ${MACHINES.length}`; ).length;
} document.getElementById("status-bar").textContent =
`WS: ${connected} / ${MACHINES.length}`;
}
// ── Bootstrap ───────────────────────────────────────────────────────────── // ── Bootstrap ─────────────────────────────────────────────────────────────
if (DEBUG_ALARM) { if (DEBUG_ALARM) {
DEBUG_MACHINES.forEach(id => { machineData[id] = DEBUG_DATA[id]; }); DEBUG_MACHINES.forEach((id) => {
document.getElementById("status-bar").textContent = "[DEBUG] widok alarmowy"; machineData[id] = DEBUG_DATA[id];
} else { });
MACHINES.forEach(connect); document.getElementById("status-bar").textContent =
} "[DEBUG] widok alarmowy";
updateDisplay(); } else {
</script> MACHINES.forEach(connect);
</body> }
updateDisplay();
</script>
</body>
</html> </html>