diff --git a/rollup.config.js b/rollup.config.js index 2e43462..c211d03 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -42,18 +42,20 @@ export default { file: "public/build/bundle.js", }, plugins: [ - terser({ - ecma: 2020, - mangle: { toplevel: true }, - compress: { - module: true, - toplevel: true, - unsafe_arrows: true, - drop_console: true, - drop_debugger: true, - }, - output: { quote_style: 1 }, - }), + // Minify and drop console only in production (npm run build); in dev (npm run dev) keep console + production && + terser({ + ecma: 2020, + mangle: { toplevel: true }, + compress: { + module: true, + toplevel: true, + unsafe_arrows: true, + drop_console: true, + drop_debugger: true, + }, + output: { quote_style: 1 }, + }), svelte({ compilerOptions: { // enable run-time checks when not in production @@ -87,9 +89,6 @@ export default { // browser on changes when not in production !production && livereload("public"), - // If we're building for production (npm run build - // instead of npm run dev), minify - production && terser(), ], watch: { clearScreen: false, diff --git a/scripts/README-mock.md b/scripts/README-mock.md index 2cb9552..fd7c984 100644 --- a/scripts/README-mock.md +++ b/scripts/README-mock.md @@ -4,8 +4,14 @@ Emulates the IoTManager ESP backend: WebSocket on port 81, HTTP for `/iotm/ver.j ## Run +**Option A — from `scripts/` (venv already in scripts):** +```bash +cd IoTManagerWeb/scripts +.venv/bin/python mock_backend.py +``` + +**Option B — from project root:** ```bash -# From IoTManagerWeb root python3 -m venv .venv-mock .venv-mock/bin/pip install -r scripts/requirements-mock.txt .venv-mock/bin/python scripts/mock_backend.py --host 0.0.0.0 --ws-port 81 --http-port 8081 diff --git a/scripts/mock_backend.py b/scripts/mock_backend.py index 2387ccf..530affe 100644 --- a/scripts/mock_backend.py +++ b/scripts/mock_backend.py @@ -78,34 +78,34 @@ def make_binary_frame(header: str, payload: str) -> bytes: def get_layout(device_slot: int) -> list: - """Layout: 4 pages by meaning (Sensors, Charts, Relays/Light, Params). All 5 widget types.""" + """Widgets distributed across 3 devices so dashboard shows each widget exactly once (4+4+3).""" prefix = f"/mock/d{device_slot}" + if device_slot == 0: + return [ + {"ws": device_slot, "topic": f"{prefix}/anydata1", "page": "Сенсоры", "widget": "anydata", "descr": "Temperature", "status": "22.5", "after": "°C", "sent": False}, + {"ws": device_slot, "topic": f"{prefix}/chart1", "page": "Сенсоры", "widget": "chart", "descr": "Chart", "status": [], "maxCount": 100}, + {"ws": device_slot, "topic": f"{prefix}/toggle1", "page": "Сенсоры", "widget": "toggle", "descr": "Toggle", "status": "0", "sent": False}, + {"ws": device_slot, "topic": f"{prefix}/range1", "page": "Сенсоры", "widget": "range", "descr": "Range", "status": "50", "min": 0, "max": 100, "after": "%", "sent": False}, + {"ws": device_slot, "topic": f"{prefix}/num0", "page": "Управление", "widget": "input-number", "descr": "Number (int)", "status": "50", "mode": "int", "min": 0, "max": 100, "step": 1, "sent": False}, + {"ws": device_slot, "topic": f"{prefix}/date0", "page": "Управление", "widget": "input-date", "descr": "Date", "status": "03.01.2025", "mode": "date", "sent": False}, + {"ws": device_slot, "topic": f"{prefix}/time0", "page": "Управление", "widget": "input-date", "descr": "Time", "status": "08:30", "mode": "time", "timePrecision": "hm", "sent": False}, + ] + if device_slot == 1: + return [ + {"ws": device_slot, "topic": f"{prefix}/input1", "page": "Управление", "widget": "input-number", "descr": "Number (int)", "status": "50", "mode": "int", "min": 0, "max": 100, "step": 1, "sent": False}, + {"ws": device_slot, "topic": f"{prefix}/num2", "page": "Управление", "widget": "input-number", "descr": "Number (float)", "status": "3.14", "mode": "float", "min": 0, "max": 10, "step": 0.1, "decimals": 2, "sent": False}, + {"ws": device_slot, "topic": f"{prefix}/input2", "page": "Управление", "widget": "input", "descr": "Text", "status": "Room", "type": "text", "sent": False}, + {"ws": device_slot, "topic": f"{prefix}/input3", "page": "Управление", "widget": "input-date", "descr": "Date", "status": "03.01.2025", "mode": "date", "sent": False}, + {"ws": device_slot, "topic": f"{prefix}/input4", "page": "Управление", "widget": "input-date", "descr": "Time", "status": "08:30", "mode": "time", "timePrecision": "hm", "sent": False}, + {"ws": device_slot, "topic": f"{prefix}/date2", "page": "Управление", "widget": "input-date", "descr": "Date & time", "status": "03.01.2025 12:00", "mode": "datetime", "timePrecision": "hm", "sent": False}, + {"ws": device_slot, "topic": f"{prefix}/btn1", "page": "Управление", "widget": "btn", "descr": "Button", "status": "OK", "sent": False}, + {"ws": device_slot, "topic": f"{prefix}/progressline1", "page": "Управление", "widget": "progress-line", "descr": "Progress line", "status": "65", "min": 0, "max": 100, "before": "", "after": "%"}, + {"ws": device_slot, "topic": f"{prefix}/progressround1", "page": "Управление", "widget": "progress-round", "descr": "Progress round", "status": "75", "min": 0, "max": 100, "after": "%", "semicircle": "1", "stroke": 10, "color": "#6366f1"}, + ] return [ - # --- 1. Сенсоры: read-only anydata --- - {"ws": device_slot, "topic": f"{prefix}/temp", "page": "Сенсоры", "widget": "anydata", "descr": "Temperature", "status": "22.5", "after": "°C"}, - {"ws": device_slot, "topic": f"{prefix}/hum", "page": "Сенсоры", "widget": "anydata", "descr": "Humidity", "status": "45", "after": "%"}, - {"ws": device_slot, "topic": f"{prefix}/pressure", "page": "Сенсоры", "widget": "anydata", "descr": "Pressure", "status": "1013", "after": " hPa"}, - {"ws": device_slot, "topic": f"{prefix}/voltage", "page": "Сенсоры", "widget": "anydata", "descr": "Voltage", "status": "3.28", "after": " V"}, - {"ws": device_slot, "topic": f"{prefix}/power", "page": "Сенсоры", "widget": "anydata", "descr": "Power", "status": "120", "after": " W", "color": [{"level": 0, "value": ""}, {"level": 200, "value": "#009933"}, {"level": 2000, "value": "#FF9900"}]}, - {"ws": device_slot, "topic": f"{prefix}/rssi", "page": "Сенсоры", "widget": "anydata", "descr": "WiFi RSSI", "status": "-65", "after": " dBm"}, - # --- 2. Графики 1 и 2 (two cards) --- - {"ws": device_slot, "topic": f"{prefix}/log", "page": "Графики 1", "widget": "chart", "descr": "Temperature log", "status": [], "maxCount": 100}, - {"ws": device_slot, "topic": f"{prefix}/log2", "page": "Графики 1", "widget": "chart", "descr": "Humidity log", "status": [], "maxCount": 100}, - {"ws": device_slot, "topic": f"{prefix}/log3", "page": "Графики 2", "widget": "chart", "descr": "Power log", "status": [], "maxCount": 100}, - {"ws": device_slot, "topic": f"{prefix}/logBar", "page": "Графики 2", "widget": "chart", "type": "bar", "descr": "Bar chart", "status": [], "maxCount": 100}, - # --- 3. Реле и свет: toggles + dimmer --- - {"ws": device_slot, "topic": f"{prefix}/relay1", "page": "Реле и свет", "widget": "toggle", "descr": "Relay 1", "status": "0", "sent": False}, - {"ws": device_slot, "topic": f"{prefix}/relay2", "page": "Реле и свет", "widget": "toggle", "descr": "Relay 2", "status": "1", "sent": False}, - {"ws": device_slot, "topic": f"{prefix}/relay3", "page": "Реле и свет", "widget": "toggle", "descr": "Light", "status": "0", "sent": False}, - {"ws": device_slot, "topic": f"{prefix}/fan", "page": "Реле и свет", "widget": "toggle", "descr": "Fan", "status": "0", "sent": False}, - {"ws": device_slot, "topic": f"{prefix}/dimmer", "page": "Реле и свет", "widget": "range", "descr": "Dimmer", "status": "50", "min": 0, "max": 100, "after": "%", "sent": False}, - # --- 4. Параметры: range + inputs --- - {"ws": device_slot, "topic": f"{prefix}/volume", "page": "Параметры", "widget": "range", "descr": "Volume", "status": "70", "min": 0, "max": 100, "after": "%", "sent": False}, - {"ws": device_slot, "topic": f"{prefix}/setpoint", "page": "Параметры", "widget": "range", "descr": "Setpoint", "status": "21", "min": 15, "max": 30, "after": "°C", "sent": False}, - {"ws": device_slot, "topic": f"{prefix}/settemp", "page": "Параметры", "widget": "input", "descr": "Set temp", "status": "20.5", "type": "number", "sent": False}, - {"ws": device_slot, "topic": f"{prefix}/label", "page": "Параметры", "widget": "input", "descr": "Label", "status": "Room 1", "type": "text", "sent": False}, - {"ws": device_slot, "topic": f"{prefix}/alarmtime", "page": "Параметры", "widget": "input", "descr": "Alarm time", "status": "08:30", "type": "time", "sent": False}, - {"ws": device_slot, "topic": f"{prefix}/eventdate", "page": "Параметры", "widget": "input", "descr": "Event date", "status": "2025-02-06", "type": "date", "sent": False}, + {"ws": device_slot, "topic": f"{prefix}/select1", "page": "Индикаторы", "widget": "select", "descr": "Select", "status": "1", "options": ["Option A", "Option B", "Option C"], "sent": False}, + {"ws": device_slot, "topic": f"{prefix}/doughnut1", "page": "Индикаторы", "widget": "doughnut", "descr": "Doughnut", "status": "60", "max": 100, "before": "", "after": "%", "stroke": 12, "color": "#10b981"}, + {"ws": device_slot, "topic": f"{prefix}/fillgauge1", "page": "Индикаторы", "widget": "fillgauge", "descr": "Fill gauge", "status": "40", "min": 0, "max": 100, "before": "", "after": "%", "color": "#3b82f6"}, ] @@ -129,11 +129,22 @@ def _ensure_params_for_layout(slot: int, layout: list) -> None: continue widget_type = (w.get("widget") or "").lower() if "chart" in widget_type: - state[wid] = "" + # Chart is updated only via chartb; do not add to state so params never overwrite its status + continue elif "toggle" in widget_type: state[wid] = "0" elif "range" in widget_type: state[wid] = str(w.get("min", 0) if isinstance(w.get("min"), (int, float)) else 50) + elif widget_type == "input-number": + state[wid] = str(w.get("status", "0") if w.get("status") not in (None, "") else "0") + elif widget_type == "input-date": + mode = (w.get("mode") or "date").lower() + if mode == "time": + state[wid] = w.get("status") or "08:30" + elif mode == "datetime": + state[wid] = w.get("status") or "01.01.2025 12:00" + else: + state[wid] = w.get("status") or "01.01.2025" elif "input" in widget_type: itype = w.get("type") or "number" if itype == "number": @@ -146,19 +157,42 @@ def _ensure_params_for_layout(slot: int, layout: list) -> None: state[wid] = "2025-01-01" else: state[wid] = "0" + elif "btn" in widget_type: + state[wid] = "OK" + elif "progress-line" in widget_type: + state[wid] = str(w.get("status", 50) if w.get("status") not in (None, "") else 50) + elif "progress-round" in widget_type: + state[wid] = str(w.get("status", 50) if w.get("status") not in (None, "") else 50) + elif "select" in widget_type: + state[wid] = "0" + elif "doughnut" in widget_type: + state[wid] = str(w.get("status", 50) if w.get("status") not in (None, "") else 50) + elif "fillgauge" in widget_type: + state[wid] = str(w.get("status", 50) if w.get("status") not in (None, "") else 50) + elif "anydata" in widget_type: + state[wid] = str(w.get("status", "")) else: state[wid] = "0" def _get_params_state(slot: int) -> dict: if slot not in _params_state: - prefix = f"/mock/d{slot}" _params_state[slot] = { - "temp": "22.5", "hum": "45", "pressure": "1013", "voltage": "3.28", "power": "120", "rssi": "-65", - "relay1": "0", "relay2": "1", "relay3": "0", "fan": "0", - "dimmer": "50", "volume": "70", "setpoint": "21", - "settemp": "20.5", "label": "Room 1", "alarmtime": "08:30", "eventdate": "2025-02-06", - "log": "", "log2": "", "log3": "", "logBar": "", + "anydata1": "22.5", + "toggle1": "0", + "range1": "50", + "input1": "50", + "num2": "3.14", + "input2": "Room", + "input3": "03.01.2025", + "input4": "08:30", + "date2": "03.01.2025 12:00", + "btn1": "OK", + "progressline1": "65", + "progressround1": "75", + "select1": "1", + "doughnut1": "60", + "fillgauge1": "40", } return _params_state[slot] @@ -272,10 +306,11 @@ def _normalize_devlist(arr: list, default_ip: str = "127.0.0.1") -> list: def get_devlist(host: str) -> list: - """Default device list from 'heap' (udps!=0). First entry = this device (IP we're connected to); frontend sets deviceList[0].status=true. Rest = other devices; frontend connects to each.""" + """Three IoT Manager devices (same firmware); frontend connects to each, each returns same layout protocol.""" return _normalize_devlist([ - {"name": "Mock Device 1", "ip": host, "id": "mock-dev-1", "status": False, "fv": "1.0.0", "wg": "group1"}, - {"name": "Mock Device 2", "ip": host, "id": "mock-dev-2", "status": False, "fv": "1.0.0", "wg": "group1"}, + {"name": "IoTManager 1", "ip": host, "id": "iotm-1", "status": False, "fv": "1.0.0", "wg": "group1"}, + {"name": "IoTManager 2", "ip": host, "id": "iotm-2", "status": False, "fv": "1.0.0", "wg": "group1"}, + {"name": "IoTManager 3", "ip": host, "id": "iotm-3", "status": False, "fv": "1.0.0", "wg": "group1"}, ], host) @@ -320,7 +355,7 @@ def get_items_json() -> list: def get_widgets_json() -> list: - """All frontend widget types: anydata, chart (line + bar), toggle, range, input (number, text, time, date).""" + """All frontend widget types. input-number and input-date are separate (same as app collection).""" return [ {"name": "anydataDef", "label": "Text", "widget": "anydata", "icon": ""}, {"name": "anydataTmp", "label": "Temperature", "widget": "anydata", "after": "°C", "icon": "speedometer"}, @@ -328,10 +363,12 @@ def get_widgets_json() -> list: {"name": "chartBar", "label": "Chart bar", "widget": "chart", "type": "bar", "icon": ""}, {"name": "toggleDef", "label": "Toggle", "widget": "toggle", "icon": ""}, {"name": "rangeDef", "label": "Range", "widget": "range", "min": 0, "max": 100, "icon": ""}, - {"name": "inputNumber", "label": "Number", "widget": "input", "type": "number", "icon": ""}, {"name": "inputText", "label": "Text", "widget": "input", "type": "text", "icon": ""}, - {"name": "inputTime", "label": "Time", "widget": "input", "type": "time", "icon": ""}, - {"name": "inputDate", "label": "Date", "widget": "input", "type": "date", "icon": ""}, + {"name": "inputNumberInt", "label": "Input number (int)", "widget": "input-number", "mode": "int", "min": 0, "max": 100, "step": 1, "icon": ""}, + {"name": "inputNumberFloat", "label": "Input number (float)", "widget": "input-number", "mode": "float", "min": 0, "max": 10, "step": 0.1, "decimals": 2, "icon": ""}, + {"name": "inputDateDate", "label": "Input date", "widget": "input-date", "mode": "date", "icon": ""}, + {"name": "inputDateTime", "label": "Input time", "widget": "input-date", "mode": "time", "timePrecision": "hm", "icon": ""}, + {"name": "inputDateDatetime", "label": "Input date & time", "widget": "input-date", "mode": "datetime", "timePrecision": "hm", "icon": ""}, ] @@ -362,9 +399,10 @@ _connections: list = [] # list of (websocket, slot) def assign_slot() -> int: + """Assign slot 0, 1, 2 for 3 devices so each connection gets a unique layout (no duplicate widgets).""" global _next_slot with _slot_lock: - s = _next_slot % 2 + s = _next_slot % 3 _next_slot += 1 return s @@ -551,27 +589,30 @@ async def _send_bin(ws, header: str, payload: str) -> None: def _tick_sensor_values() -> None: - """Update read-only sensor values in _params_state (like backend sensors in loop).""" + """Update read-only widget values in _params_state (anydata, progress, doughnut, fillgauge).""" t = time.time() for slot in list(_params_state.keys()): s = _params_state[slot] - # Small variation around base values - s["temp"] = f"{22 + 3 * math.sin(t * 0.5):.1f}" - s["hum"] = str(int(45 + 10 * math.sin(t * 0.3))) - s["pressure"] = str(1012 + int(5 * math.sin(t * 0.2))) - s["voltage"] = f"{3.2 + 0.1 * math.sin(t):.2f}" - s["power"] = str(int(120 + 30 * math.sin(t * 0.4))) - s["rssi"] = str(int(-65 + 5 * math.sin(t * 0.5))) + if "anydata1" in s: + s["anydata1"] = f"{22 + 3 * math.sin(t * 0.5):.1f}" + if "progressline1" in s: + s["progressline1"] = str(int(50 + 20 * math.sin(t * 0.3))) + if "progressround1" in s: + s["progressround1"] = str(int(50 + 25 * math.sin(t * 0.25))) + if "doughnut1" in s: + s["doughnut1"] = str(int(50 + 30 * math.sin(t * 0.2))) + if "fillgauge1" in s: + s["fillgauge1"] = str(int(40 + 25 * math.sin(t * 0.35))) # Interval (seconds): backend sends status on sensor change (publishStatusWs); we emulate same. PARAMS_INTERVAL = 3 -# Param keys that are widget values (not chart). Front updateWidget(topic, status) matches layout by full topic. +# Param keys for widgets that receive status blob (chart1 is updated only via chartb). _PARAM_IDS = [ - "temp", "hum", "pressure", "voltage", "power", "rssi", - "relay1", "relay2", "relay3", "fan", "dimmer", "volume", "setpoint", - "settemp", "label", "alarmtime", "eventdate", + "anydata1", "toggle1", "range1", "num0", "date0", "time0", + "input1", "num2", "input2", "input3", "input4", "date2", "btn1", + "progressline1", "progressround1", "select1", "doughnut1", "fillgauge1", ] @@ -600,16 +641,17 @@ async def _periodic_broadcast() -> None: await _send_bin(ws, "status", payload) except Exception: pass - # Chart points: backend Loging::publishValue sends chartb with one point; front apdateWidgetByArray merges into layout + # Chart points: only slot 0 has chart widget in layout; skip chartb for other slots to avoid "topic not found" now = int(time.time()) for (ws, slot) in list(_connections): + if slot != 0: + continue try: prefix = f"/mock/d{slot}" - for topic_suf, base, amp in [("log", 20.0, 5.0), ("log2", 45.0, 15.0), ("log3", 100.0, 50.0), ("logBar", 30.0, 10.0)]: - topic = f"{prefix}/{topic_suf}" - val = base + amp * math.sin(now * 0.1) - point = {"topic": topic, "maxCount": 100, "status": [{"x": now, "y1": round(val, 2)}]} - await _send_bin(ws, "chartb", json.dumps(point)) + topic = f"{prefix}/chart1" + val = 20.0 + 5.0 * math.sin(now * 0.1) + point = {"topic": topic, "maxCount": 100, "status": [{"x": now, "y1": round(val, 2)}]} + await _send_bin(ws, "chartb", json.dumps(point)) except Exception: pass diff --git a/src/App.svelte b/src/App.svelte index c48a944..0e942d3 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -103,6 +103,7 @@ onMount(async () => { console.log("[i]", "mounted"); + console.log("[layout] Layout debug ON. Open dashboard (/) to see layout logs."); const JWT = Cookies.get("token_iotm2"); const res = await portal.getUser(JWT); if (res.ok) { @@ -267,14 +268,14 @@ } /*=============================================card and items inside===============================================*/ .crd-itm-psn { - @apply flex mb-2 h-6 items-center; + @apply flex min-h-[2.25rem] mb-4 items-center; } .wgt-dscr-stl { @apply pr-4 text-gray-500 font-bold; } /*====================================================others=====================================================*/ .btn-i { - @apply py-2 px-4 bg-indigo-500 text-white font-semibold rounded-lg shadow-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:ring-opacity-75; + @apply py-2 px-4 bg-blue-100 hover:bg-blue-200 text-gray-700 font-semibold rounded-lg border border-gray-300 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:ring-opacity-75; } .wgt-adt-stl { @apply text-center text-gray-500 font-bold; diff --git a/src/api/deviceSocket.js b/src/api/deviceSocket.js index 5c6a04b..e079bcd 100644 --- a/src/api/deviceSocket.js +++ b/src/api/deviceSocket.js @@ -14,7 +14,7 @@ const sockets = new Map(); export function createConnection(wsIndex, ip, callbacks) { const url = "ws://" + ip + ":81"; const socket = new WebSocket(url); - socket.binaryType = "blob"; // fix: set on instance, not on array + socket.binaryType = "blob"; sockets.set(wsIndex, socket); socket.addEventListener("open", () => { @@ -25,11 +25,15 @@ export function createConnection(wsIndex, ip, callbacks) { if (callbacks.onMessage) callbacks.onMessage(wsIndex, event.data); }); - socket.addEventListener("close", () => { + socket.addEventListener("close", (ev) => { + if (socket.readyState !== 1) { + console.warn("[WS] closed before open", "url:", url, "wsIndex:", wsIndex, "code:", ev.code, "reason:", ev.reason || ""); + } if (callbacks.onClose) callbacks.onClose(wsIndex); }); socket.addEventListener("error", () => { + console.warn("[WS] error connecting to", url, "wsIndex:", wsIndex, "- is mock_backend.py running on port 81?"); if (callbacks.onError) callbacks.onError(wsIndex); }); } diff --git a/src/lib/WebSocketManager.js b/src/lib/WebSocketManager.js index a398226..bc2cc8e 100644 --- a/src/lib/WebSocketManager.js +++ b/src/lib/WebSocketManager.js @@ -20,6 +20,7 @@ export default class WebSocketManager { constructor(initialDeviceList, options = {}) { this.options = options; this.debug = options.debug !== false && debug; + console.log("[layout] debug: layout console logs enabled (binary blobs, combineLayoutsInOne)"); // State (mirror of App.svelte) this.deviceList = Array.isArray(initialDeviceList) ? [...initialDeviceList] : []; @@ -208,6 +209,7 @@ export default class WebSocketManager { return; } if (data instanceof Blob) { + console.log("[layout] binary blob received", "ws:", ws, "page:", this.currentPageName, "layoutJson.length:", this.layoutJson.length); if (ws === this.selectedWs) this.parseBlob(data, ws); if (this.currentPageName === "/|") this.parseAllBlob(data, ws); } @@ -350,11 +352,17 @@ export default class WebSocketManager { } async combineLayoutsInOne(ws, devLayout) { + const beforeLen = this.layoutJson.length; + const forThisWs = this.layoutJson.filter((item) => item.ws === ws).length; + // Replace layout for this device (avoid duplicates when layout is sent more than once) + this.layoutJson = this.layoutJson.filter((item) => item.ws !== ws); + const afterFilterLen = this.layoutJson.length; for (let i = 0; i < devLayout.length; i++) { devLayout[i].ws = ws; } this.layoutJson = this.layoutJson.concat(devLayout); - console.log("[2]", ws, "devLayout pushed to layout"); + const topics = (devLayout || []).map((w) => (w.topic || "").split("/").pop()).join(", "); + console.log("[layout] combineLayoutsInOne", "ws:", ws, "devLayout.length:", (devLayout || []).length, "| layoutJson: before:", beforeLen, "forThisWs:", forThisWs, "afterFilter:", afterFilterLen, "afterConcat:", this.layoutJson.length, "| topics:", topics); this.sortingLayout(ws); } diff --git a/src/lib/blobProtocol.js b/src/lib/blobProtocol.js index 8860908..0a3c5ba 100644 --- a/src/lib/blobProtocol.js +++ b/src/lib/blobProtocol.js @@ -148,13 +148,18 @@ export async function parseBlob(blob, ws, handlers) { */ export async function parseAllBlob(blob, ws, handlers) { const { header, size } = await readHeader(blob); + console.log("[layout] parseAllBlob header:", header, "ws:", ws); const out = {}; if (header === "status") { if (await getPayloadAsJson(blob, size, out)) handlers.updateWidget(out.json); } if (header === "layout") { - if (await getPayloadAsJson(blob, size, out)) handlers.combineLayoutsInOne(ws, out.json); + if (await getPayloadAsJson(blob, size, out)) { + const arr = Array.isArray(out.json) ? out.json : []; + console.log("[layout] blob header=layout", "ws:", ws, "payload length:", arr.length); + handlers.combineLayoutsInOne(ws, out.json); + } } if (header === "params") { if (await getPayloadAsJson(blob, size, out)) { diff --git a/src/main.js b/src/main.js index d6cacbb..e693054 100644 --- a/src/main.js +++ b/src/main.js @@ -1,10 +1,10 @@ import App from './App.svelte'; +console.log("[layout] IoTManagerWeb loaded — layout debug logs enabled"); + const app = new App({ target: document.body, - props: { - name: 'world' - } + props: {} }); export default app; \ No newline at end of file diff --git a/src/pages/Dashboard.svelte b/src/pages/Dashboard.svelte index ed72779..960b084 100644 --- a/src/pages/Dashboard.svelte +++ b/src/pages/Dashboard.svelte @@ -1,10 +1,18 @@ + +
+
+

{!widget.descr ? "" : widget.descr}

+
+
+ +
+
diff --git a/src/widgets/Chart.svelte b/src/widgets/Chart.svelte index 32a582c..07a03ac 100644 --- a/src/widgets/Chart.svelte +++ b/src/widgets/Chart.svelte @@ -2,15 +2,18 @@ import Chart from "svelte-frappe-charts"; export let widget; - let datachart = { - labels: ["0", "0"], - datasets: [ - { - name: (widget && widget.descr) ? widget.descr : "", - values: [0, 0], - }, - ], - }; + function safeDatachart(labels, values, descr) { + const lab = Array.isArray(labels) && labels.length > 0 ? labels : ["0", "0"]; + let vals = Array.isArray(values) && values.length > 0 ? values : [[0], [0]]; + // Ensure all numbers are finite (frappe-charts throws on NaN) + vals = vals.map((row) => (Array.isArray(row) ? row : [row]).map((v) => (Number.isFinite(Number(v)) ? Number(v) : 0))); + if (vals.length === 0) vals = [[0], [0]]; + return { + labels: lab, + datasets: [{ name: descr || "Chart", values: vals }], + }; + } + let datachart = safeDatachart([], [], widget && widget.descr); let prevStatus = {}; @@ -21,7 +24,7 @@ let axisOptions = { xAxisMode: "tick", xIsSeries: true, xIsSeries: true }; let lineOptions; - if (widget.pointRadius == "0") { + if (widget && widget.pointRadius == "0") { lineOptions = { regionFill: 1, hideDots: 1, spline: 1 }; } else { lineOptions = { regionFill: 1, dotSize: 3, spline: 1 }; @@ -65,15 +68,7 @@ labels = safeLabels; values = safeValues; - datachart = { - labels: labels, - datasets: [ - { - name: widget.descr || "", - values: values, - }, - ], - }; + datachart = safeDatachart(labels, values, widget.descr); } } firstTime = false; @@ -96,12 +91,7 @@ if (widget) widget.status = []; labels = []; values = []; - datachart = { - labels: ["0", "0"], - datasets: [ - { name: (widget && widget.descr) ? widget.descr : "", values: [0, 0] }, - ], - }; + datachart = safeDatachart([], [], widget && widget.descr); } @@ -109,4 +99,6 @@

{!widget.descr ? "" : widget.descr}

- +{#if widget && datachart && datachart.datasets && datachart.datasets[0] && Array.isArray(datachart.datasets[0].values) && datachart.datasets[0].values.length > 0} + +{/if} diff --git a/src/widgets/Doughnut.svelte b/src/widgets/Doughnut.svelte new file mode 100644 index 0000000..270f0fb --- /dev/null +++ b/src/widgets/Doughnut.svelte @@ -0,0 +1,37 @@ + + +
+

{!widget.descr ? "" : widget.descr}

+
+ + + + + {widget.before || ""}{numStatus}{widget.after || ""} +
+
diff --git a/src/widgets/Fillgauge.svelte b/src/widgets/Fillgauge.svelte new file mode 100644 index 0000000..37f91ad --- /dev/null +++ b/src/widgets/Fillgauge.svelte @@ -0,0 +1,24 @@ + + +
+

{!widget.descr ? "" : widget.descr}

+
+
+
+

{widget.before || ""}{numStatus}{widget.after || ""}

+
diff --git a/src/widgets/InputDate.svelte b/src/widgets/InputDate.svelte new file mode 100644 index 0000000..8cd0ad0 --- /dev/null +++ b/src/widgets/InputDate.svelte @@ -0,0 +1,117 @@ + + +
+
+

{widget.descr || "Date"}

+
+
+ {#if mode === "date"} + + {:else if mode === "time"} + + {:else} + + + {/if} +
+
diff --git a/src/widgets/InputNumber.svelte b/src/widgets/InputNumber.svelte new file mode 100644 index 0000000..e914d85 --- /dev/null +++ b/src/widgets/InputNumber.svelte @@ -0,0 +1,46 @@ + + +
+
+

{widget.descr || "Number"}

+
+
+ +
+
diff --git a/src/widgets/ProgressLine.svelte b/src/widgets/ProgressLine.svelte new file mode 100644 index 0000000..3d010ed --- /dev/null +++ b/src/widgets/ProgressLine.svelte @@ -0,0 +1,29 @@ + + +
+

{!widget.descr ? "" : widget.descr}

+
+
+
+

{widget.before || ""}{numStatus} / {safeMax}{widget.after || ""}

+
diff --git a/src/widgets/ProgressRound.svelte b/src/widgets/ProgressRound.svelte new file mode 100644 index 0000000..4fcd5ae --- /dev/null +++ b/src/widgets/ProgressRound.svelte @@ -0,0 +1,47 @@ + + +
+

{!widget.descr ? "" : widget.descr}

+
+ + + + + {widget.before || ""}{numStatus}{widget.after || ""} +
+
diff --git a/src/widgets/Select.svelte b/src/widgets/Select.svelte new file mode 100644 index 0000000..742bb79 --- /dev/null +++ b/src/widgets/Select.svelte @@ -0,0 +1,36 @@ + + +
+
+

{!widget.descr ? "" : widget.descr}

+
+
+ +
+