| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180 |
- <!doctype html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>Cockpit WebSocket Scope Delta Demo</title>
- <style>
- :root { color-scheme: light; font-family: Arial, sans-serif; }
- * { box-sizing: border-box; }
- body { margin: 0; background: #f4f7fb; color: #172b4d; }
- header { background: #17324d; color: white; padding: 24px 32px; }
- h1 { margin: 0; font-size: 22px; }
- p { margin: 8px 0 0; opacity: .8; }
- main { max-width: 1080px; margin: 0 auto; padding: 24px; }
- .toolbar { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 20px; }
- button { border: 1px solid #2557a7; background: white; color: #2557a7; padding: 8px 12px; cursor: pointer; }
- button.primary { background: #2557a7; color: white; }
- button:disabled { opacity: .5; cursor: not-allowed; }
- .grid { display: grid; grid-template-columns: 1fr 360px; gap: 20px; }
- .panel { background: white; border: 1px solid #d5dce6; padding: 16px; }
- .panel h2 { margin-top: 0; font-size: 16px; }
- #devices { display: grid; gap: 10px; }
- .device { border: 1px solid #dde3ec; padding: 10px 12px; }
- .device-name { font-weight: 700; }
- .status { display: inline-block; min-width: 82px; text-align: center; padding: 2px 6px; color: white; font-size: 12px; }
- .ALARM { background: #c53030; } .WORK_ORDER { background: #b7791f; } .OFFLINE { background: #4a5568; }
- .NORMAL { background: #2f855a; } .EMPTY { background: #4a5568; }
- .meta { color: #5d6b82; font-size: 12px; margin-top: 4px; }
- #events { height: 320px; overflow: auto; font: 12px/1.5 Consolas, monospace; background: #f7f9fc; padding: 10px; }
- .event-old { color: #b83280; } .event-push { color: #2c7a7b; } .event-state { color: #805ad5; }
- @media (max-width: 860px) { .grid { grid-template-columns: 1fr; } }
- </style>
- </head>
- <body>
- <header>
- <h1>Cockpit WebSocket Scope Delta Demo</h1>
- <p>REST is the authoritative initial snapshot. WebSocket deltas are accepted only when serverUpdatedAt is newer.</p>
- </header>
- <main>
- <div class="toolbar">
- <button class="primary" id="load">1. Load REST snapshot</button>
- <button id="replay" disabled>2. Replay pushes / reconnect</button>
- <button id="live">Connect live WebSocket</button>
- </div>
- <div class="grid">
- <section class="panel">
- <h2>Devices</h2>
- <div id="devices"></div>
- </section>
- <section class="panel">
- <h2>Protocol events</h2>
- <div id="events"></div>
- </section>
- </div>
- </main>
- <script>
- const REST_URL = "http://localhost:8300/api/cockpit/v1/map-snapshot";
- const WS_URL = "ws://localhost:8300/ws/deviceStatus";
- let snapshotLoaded = false;
- const latest = new Map();
- const sampleDevices = [
- { code: "TC-PS-WS-202609020001", name: "Pressure monitor 001", status: "NORMAL", statusTime: "2026-09-02T09:58:00+08:00", serverUpdatedAt: "2026-09-02T10:00:00+08:00" },
- { code: "TC-PS-WS-202609020002", name: "Pressure monitor 002", status: "NORMAL", statusTime: "2026-09-02T09:58:00+08:00", serverUpdatedAt: "2026-09-02T10:00:00+08:00" }
- ];
- const scriptedMessages = [
- { type: "cockpitDeviceStatusDelta", deviceCode: "TC-PS-WS-202609020001", device_code: "TC-PS-WS-202609020001", monitoringPointCode: "MP-001",
- data: { status: "ALARM", statusTime: "2026-09-02T10:01:00+08:00", serverUpdatedAt: "2026-09-02T10:01:00+08:00" } },
- { type: "cockpitDeviceStatusDelta", deviceCode: "TC-PS-WS-202609020001", device_code: "TC-PS-WS-202609020001", monitoringPointCode: "MP-001",
- data: { status: "NORMAL", statusTime: "2026-09-02T10:00:30+08:00", serverUpdatedAt: "2026-09-02T10:00:30+08:00" } },
- { type: "cockpitDeviceStatusDelta", deviceCode: "TC-PS-WS-202609020099", device_code: "TC-PS-WS-202609020099", monitoringPointCode: null,
- data: { status: "EMPTY", reason: "DEVICE_NOT_FOUND", statusTime: null, serverUpdatedAt: "2026-09-02T10:02:00+08:00" } },
- { type: "__disconnect" },
- { type: "__reconnect" },
- { type: "cockpitDeviceStatusDelta", deviceCode: "TC-PS-WS-202609020001", device_code: "TC-PS-WS-202609020001", monitoringPointCode: "MP-001",
- data: { status: "NORMAL", statusTime: "2026-09-02T10:03:00+08:00", serverUpdatedAt: "2026-09-02T10:03:00+08:00" } }
- ];
- function time(value) {
- return value ? value.replace("T", " ") : "-";
- }
- function log(message, className) {
- const node = document.createElement("div");
- node.textContent = message;
- if (className) node.className = className;
- document.getElementById("events").appendChild(node);
- document.getElementById("events").scrollTop = document.getElementById("events").scrollHeight;
- }
- function render() {
- const root = document.getElementById("devices");
- root.replaceChildren();
- sampleDevices.forEach((device, index) => {
- const current = latest.get(device.code) || device;
- const item = document.createElement("article");
- item.className = "device";
- item.innerHTML = `<div class="device-name">${device.name}</div>
- <span class="status ${current.status}">${current.status}</span>
- <div class="meta">${device.code}<br>statusTime: ${time(current.statusTime)}<br>serverUpdatedAt: ${time(current.serverUpdatedAt)}</div>`;
- root.appendChild(item);
- });
- document.getElementById("replay").disabled = !snapshotLoaded;
- }
- function accept(message) {
- if (!message || message.type !== "cockpitDeviceStatusDelta") return;
- const code = message.deviceCode;
- const data = message.data || {};
- const previous = latest.get(code);
- if (previous && data.serverUpdatedAt <= previous.serverUpdatedAt) {
- log(`IGNORED OLD ${code} @ ${data.serverUpdatedAt}`, "event-old");
- return;
- }
- latest.set(code, data);
- render();
- log(`DELTA ${code} -> ${data.status}${data.reason ? ` (${data.reason})` : ""}`, "event-push");
- }
- function loadSnapshot() {
- log(`GET ${REST_URL}`);
- fetch(REST_URL, { headers: { Accept: "application/json" } })
- .then((response) => response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`)))
- .then((body) => {
- const points = body.data?.monitoringPoints || [];
- const unassociated = body.data?.unassociatedEquipment || [];
- points.forEach((point, index) => {
- const code = `${point.pointCode}-DEMO`;
- latest.set(code, { status: point.pointStatus || "EMPTY", statusTime: body.data.snapshotAt, serverUpdatedAt: body.data.snapshotAt });
- sampleDevices[index] = { code, name: point.pointName || code, status: point.pointStatus || "EMPTY", statusTime: body.data.snapshotAt, serverUpdatedAt: body.data.snapshotAt };
- });
- unassociated.forEach((device, index) => {
- const offset = points.length + index;
- const code = device.deviceCode;
- latest.set(code, { status: "EMPTY", statusTime: null, serverUpdatedAt: body.data.snapshotAt });
- sampleDevices[offset] = { code, name: device.deviceName || code, status: "EMPTY", statusTime: null, serverUpdatedAt: body.data.snapshotAt };
- });
- snapshotLoaded = true;
- log(`SNAPSHOT ${body.data.snapshotAt}`, "event-state");
- render();
- })
- .catch(() => {
- sampleDevices.forEach((device) => latest.set(device.code, device));
- snapshotLoaded = true;
- log("SNAPSHOT fallback sample (REST unavailable)", "event-state");
- render();
- });
- }
- function replay() {
- let index = 0;
- const step = () => {
- if (index >= scriptedMessages.length) return;
- const message = scriptedMessages[index++];
- if (message.type === "__disconnect") {
- log("DISCONNECTED", "event-state");
- } else if (message.type === "__reconnect") {
- log("RECONNECT {\"type\":\"cockpitSubscribe\",\"scope\":\"all\"}", "event-state");
- } else {
- accept(message);
- }
- setTimeout(step, 650);
- };
- step();
- }
- function connectLive() {
- const socket = new WebSocket(WS_URL);
- socket.onopen = () => {
- log("WEBSOCKET OPEN", "event-state");
- socket.send(JSON.stringify({ type: "cockpitSubscribe", scope: "all" }));
- };
- socket.onmessage = (event) => {
- const message = JSON.parse(event.data);
- log(`MESSAGE ${JSON.stringify(message)}`, message.type === "cockpitDeviceStatusDelta" ? "event-push" : "event-state");
- accept(message);
- };
- socket.onclose = () => log("WEBSOCKET CLOSED", "event-state");
- socket.onerror = () => log("WEBSOCKET UNAVAILABLE; use scripted replay", "event-old");
- }
- document.getElementById("load").addEventListener("click", loadSnapshot);
- document.getElementById("replay").addEventListener("click", replay);
- document.getElementById("live").addEventListener("click", connectLive);
- render();
- </script>
- </body>
- </html>
|