From c0b1a6e9ce4fee4d9eb53713c15becbe35f8c4dc Mon Sep 17 00:00:00 2001 From: leukipp Date: Tue, 7 Oct 2025 21:48:49 +0200 Subject: [PATCH] feat: add mqtt error diagnostic #91 --- index.js | 29 ++++++++- js/hardware.js | 50 ++++++++-------- js/integration.js | 149 +++++++++++++++++++++++++++++----------------- js/webview.js | 31 ++++++++-- 4 files changed, 173 insertions(+), 86 deletions(-) diff --git a/index.js b/index.js index 981b606..d2be63b 100644 --- a/index.js +++ b/index.js @@ -37,7 +37,7 @@ app.whenReady().then(async () => { if ("mqtt_password" in args) { args.mqtt_password = "*".repeat((args.mqtt_password || "").length); } - console.log(`Arguments: ${JSON.stringify(args, null, 2)}`); + console.info(`Arguments: ${JSON.stringify(args, null, 2)}`); // Chained init functions const chained = [webview.init, hardware.init, integration.init]; @@ -112,7 +112,7 @@ const initArgs = async () => { if (APP.build.id) { build = ` (${APP.build.id}), built on ${APP.build.date} (${APP.build.platform}-${APP.build.arch}-${APP.build.maker})`; } - console.log(`${APP.name}-v${APP.version}${build}\n${APP.homepage}`); + console.info(`${APP.name}-v${APP.version}${build}\n${APP.homepage}`); return app.exit(0); } @@ -157,7 +157,30 @@ const initLog = async () => { console.error("Failed to delete log file:", error.message); } - // Overwrite console log settings + // Overwrite console log + APP.logs = []; + log.errorHandler.startCatching({ + showDialog: false, + onError({ error }) { + console.error(`Uncaught Error: ${error.message} (${error.stack})`); + app.exit(1); + }, + }); + log.hooks.push((message, transport, type) => { + const text = message.data.join(" "); + if (!text.startsWith("(node:") && type === "console") { + APP.logs.unshift({ + time: message.date, + level: message.level, + text: text, + }); + if (APP.logs.length > 10) { + APP.logs.splice(10); + } + EVENTS.emit("consoleLog", APP.logs[0]); + } + return message; + }); log.transports.file.resolvePathFn = () => { return APP.log; }; diff --git a/js/hardware.js b/js/hardware.js index e300118..6e24b1a 100644 --- a/js/hardware.js +++ b/js/hardware.js @@ -59,47 +59,47 @@ const init = async () => { // Show supported features process.stdout.write("\n"); - console.log(`Supported: ${JSON.stringify(HARDWARE.support, null, 2)}`); + console.info(`Supported: ${JSON.stringify(HARDWARE.support, null, 2)}`); // Show session infos process.stdout.write("\n"); - console.log("User:", HARDWARE.session.user); - console.log("Session:", HARDWARE.session.type); - console.log("Desktop:", HARDWARE.session.desktop); + console.info("User:", HARDWARE.session.user); + console.info("Session:", HARDWARE.session.type); + console.info("Desktop:", HARDWARE.session.desktop); // Show device infos process.stdout.write("\n"); - console.log("Model:", getModel()); - console.log("Vendor:", getVendor()); - console.log("Serial Number:", getSerialNumber()); - console.log("Network Addresses:", getNetworkAddresses()); - console.log("Host Name:", getHostName()); + console.info("Model:", getModel()); + console.info("Vendor:", getVendor()); + console.info("Serial Number:", getSerialNumber()); + console.info("Network Addresses:", getNetworkAddresses()); + console.info("Host Name:", getHostName()); // Show system infos process.stdout.write("\n"); - console.log("Up Time:", getUpTime()); - console.log("Memory Size:", getMemorySize()); - console.log("Memory Usage:", getMemoryUsage()); - console.log("Processor Usage:", getProcessorUsage()); - console.log("Processor Temperature:", getProcessorTemperature()); + console.info("Up Time:", getUpTime()); + console.info("Memory Size:", getMemorySize()); + console.info("Memory Usage:", getMemoryUsage()); + console.info("Processor Usage:", getProcessorUsage()); + console.info("Processor Temperature:", getProcessorTemperature()); // Show hardware infos process.stdout.write("\n"); const unsupported = "unsupported"; - console.log(`Battery Level [${HARDWARE.battery.level.path || unsupported}]:`, `${getBatteryLevel() || unsupported}`); - console.log( + console.info(`Battery Level [${HARDWARE.battery.level.path || unsupported}]:`, `${getBatteryLevel() || unsupported}`); + console.info( `Display Status [${HARDWARE.display.status.path || unsupported}]:`, `${getDisplayStatus() || unsupported} (${HARDWARE.display.status.command || unsupported})`, ); - console.log( + console.info( `Display Brightness [${HARDWARE.display.brightness.path || unsupported}]:`, `${getDisplayBrightness() || unsupported}`, ); - console.log( + console.info( `Audio Volume [${HARDWARE.support.audioVolume ? HARDWARE.audio.device : unsupported}]:`, `${getAudioVolume() || unsupported}`, ); - console.log( + console.info( `Keyboard Visibility [${HARDWARE.support.keyboardVisibility ? "squeekboard" : unsupported}]:`, `${getKeyboardVisibility() || unsupported}`, ); @@ -112,7 +112,7 @@ const init = async () => { return; } if (reply.includes("'change' on sink")) { - console.log("Update Audio Volume:", getAudioVolume()); + console.info("Update Audio Volume:", getAudioVolume()); EVENTS.emit("updateVolume"); } }); @@ -129,7 +129,7 @@ const init = async () => { return; } HARDWARE.keyboard.visibility = property.Visible === "true"; - console.log("Update Keyboard Visibility:", getKeyboardVisibility()); + console.info("Update Keyboard Visibility:", getKeyboardVisibility()); EVENTS.emit("updateKeyboard"); }); }); @@ -155,7 +155,7 @@ const update = async () => { if (HARDWARE.support.displayStatus) { const status = (await fsp.readFile(`${HARDWARE.display.status.path}/dpms`, "utf8")).trim(); if (status !== HARDWARE.display.status.value) { - console.log("Update Display Status:", getDisplayStatus()); + console.info("Update Display Status:", getDisplayStatus()); HARDWARE.display.status.value = status; EVENTS.emit("updateDisplay"); } @@ -165,7 +165,7 @@ const update = async () => { if (HARDWARE.support.displayBrightness) { const brightness = (await fsp.readFile(`${HARDWARE.display.brightness.path}/brightness`, "utf8")).trim(); if (brightness !== HARDWARE.display.brightness.value) { - console.log("Update Display Brightness:", getDisplayBrightness()); + console.info("Update Display Brightness:", getDisplayBrightness()); HARDWARE.display.brightness.value = brightness; EVENTS.emit("updateDisplay"); } @@ -891,7 +891,7 @@ const execScriptCommand = (cmd, args, callback = null) => { const output = data.toString().trim(); const lines = output.replace(/\n+/g, "\n").replace(/^\n+|\n+$|\0/g, ""); for (const line of lines.split("\n").filter(Boolean)) { - console.log(line); + console.info(line); } } }); @@ -917,7 +917,7 @@ const execScriptCommand = (cmd, args, callback = null) => { console.error(`Script exited with error code (${code}).`); if (typeof callback === "function") callback(null, code); } else { - console.log("Script exited successfully."); + console.info("Script exited successfully."); if (typeof callback === "function") callback(100, null); } }); diff --git a/js/integration.js b/js/integration.js index 58748b1..f2762d1 100644 --- a/js/integration.js +++ b/js/integration.js @@ -55,14 +55,14 @@ const init = async () => { options.rejectUnauthorized = !("ignore_certificate_errors" in ARGS); // Client connect - console.log("MQTT Connecting:", `${user}:${masked}@${url.toString()}`); + console.info("MQTT Connecting:", `${user}:${masked}@${url.toString()}`); INTEGRATION.client = mqtt.connect(url.toString(), options); INTEGRATION.client.setMaxListeners(20); // Client connected INTEGRATION.client .once("connect", () => { - console.log(`MQTT Connected: ${url.toString()}`); + console.info(`MQTT Connected: ${url.toString()}`); process.stdout.write("\n"); // Init client controls @@ -91,30 +91,35 @@ const init = async () => { initProcessorTemperature(); initBatteryLevel(); initPackageUpgrades(); - initHeartbeat(); initLastActive(); + // Init client diagnostic + initHeartbeat(); + initErrors(); + // Integration initialized INTEGRATION.initialized = true; + + // Register global events + EVENTS.on("updateStatus", updateKiosk); + EVENTS.on("updateTheme", updateTheme); + EVENTS.on("updateDisplay", updateDisplay); + EVENTS.on("updateVolume", updateVolume); + EVENTS.on("updateKeyboard", updateKeyboard); + EVENTS.on("consoleLog", updateErrors); }) .on("error", (error) => { console.error("MQTT", error.message); }); - // Update sensor states from events - EVENTS.on("updateStatus", updateKiosk); - EVENTS.on("updateTheme", updateTheme); - EVENTS.on("updateDisplay", updateDisplay); - EVENTS.on("updateVolume", updateVolume); - EVENTS.on("updateKeyboard", updateKeyboard); - // Update time sensors periodically (30s) setInterval(() => { if (APP.exiting) { return; } - updateHeartbeat(); updateLastActive(); + updateHeartbeat(); + updateErrors(); }, 30 * 1000); // Update system sensors periodically (1min) @@ -240,12 +245,12 @@ const initApp = () => { publishConfig("update", config) .on("message", (topic, message) => { if (topic === config.command_topic) { - console.log("Update App..."); + console.info("Update App..."); hardware.setDisplayStatus("ON", () => { const args = ["-c", `bash <(wget -qO- ${APP.scripts.install}) update`]; hardware.execScriptCommand("bash", args, (progress, error) => { if (progress) { - console.log(`Progress: ${progress}%`); + console.info(`Progress: ${progress}%`); } updateApp(progress); }); @@ -296,7 +301,7 @@ const initShutdown = () => { publishConfig("button", config) .on("message", (topic, message) => { if (topic === config.command_topic) { - console.log("Shutdown system..."); + console.verbose("Shutdown system..."); hardware.setDisplayStatus("ON", () => { hardware.shutdownSystem(); }); @@ -324,7 +329,7 @@ const initReboot = () => { publishConfig("button", config) .on("message", (topic, message) => { if (topic === config.command_topic) { - console.log("Rebooting system..."); + console.verbose("Rebooting system..."); hardware.setDisplayStatus("ON", () => { hardware.rebootSystem(); }); @@ -348,7 +353,7 @@ const initRefresh = () => { publishConfig("button", config) .on("message", (topic, message) => { if (topic === config.command_topic) { - console.log("Refreshing webview..."); + console.verbose("Refreshing webview..."); hardware.setDisplayStatus("ON", () => { EVENTS.emit("reloadView"); }); @@ -376,7 +381,7 @@ const initKiosk = () => { .on("message", (topic, message) => { if (topic === config.command_topic) { const status = message.toString(); - console.log("Set Kiosk Status:", status); + console.verbose("Set Kiosk Status:", status); hardware.setDisplayStatus("ON", () => { WEBVIEW.window.setStatus(status); }); @@ -390,7 +395,7 @@ const initKiosk = () => { * Updates the kiosk status via the mqtt connection. */ const updateKiosk = async () => { - const kiosk = WEBVIEW.tracker.status; + const kiosk = WEBVIEW.tracker.window.status; publishState("kiosk", kiosk); }; @@ -413,7 +418,7 @@ const initTheme = () => { .on("message", (topic, message) => { if (topic === config.command_topic) { const theme = message.toString(); - console.log("Set Application Theme:", theme); + console.verbose("Set Application Theme:", theme); WEBVIEW.theme.set(theme); } }) @@ -456,7 +461,7 @@ const initDisplay = () => { .on("message", (topic, message) => { if (topic === config.command_topic) { const status = message.toString(); - console.log("Set Display Status:", status); + console.verbose("Set Display Status:", status); hardware.setDisplayStatus(status, (reply, error) => { if (!error) { hardware.update(); @@ -466,7 +471,7 @@ const initDisplay = () => { }); } else if (topic === config.brightness_command_topic) { const brightness = parseInt(message, 10); - console.log("Set Display Brightness:", brightness); + console.verbose("Set Display Brightness:", brightness); hardware.setDisplayBrightness(brightness, (reply, error) => { if (!error) { hardware.update(); @@ -517,7 +522,7 @@ const initVolume = () => { .on("message", (topic, message) => { if (topic === config.command_topic) { const volume = parseInt(message, 10); - console.log("Set Audio Volume:", volume); + console.verbose("Set Audio Volume:", volume); hardware.setAudioVolume(volume); } }) @@ -554,7 +559,7 @@ const initKeyboard = () => { .on("message", (topic, message) => { if (topic === config.command_topic) { const status = message.toString(); - console.log("Set Keyboard Visibility:", status); + console.verbose("Set Keyboard Visibility:", status); hardware.setDisplayStatus("ON", () => { hardware.setKeyboardVisibility(status); }); @@ -598,7 +603,7 @@ const initPageNumber = () => { .on("message", (topic, message) => { if (topic === config.command_topic) { const number = parseInt(message, 10); - console.log("Set Page Number:", number); + console.verbose("Set Page Number:", number); WEBVIEW.viewActive = number || 1; EVENTS.emit("updateView"); } @@ -638,7 +643,7 @@ const initPageZoom = () => { .on("message", (topic, message) => { if (topic === config.command_topic) { const zoom = parseInt(message, 10); - console.log("Set Page Zoom:", zoom); + console.verbose("Set Page Zoom:", zoom); WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.setZoomFactor(zoom / 100.0); EVENTS.emit("updateView"); } @@ -674,7 +679,7 @@ const initPageUrl = () => { .on("message", (topic, message) => { if (topic === config.command_topic) { const url = message.toString(); - console.log("Set Page Url:", url); + console.verbose("Set Page Url:", url); WEBVIEW.views[WEBVIEW.viewActive || 1].webContents.loadURL(url); } }) @@ -990,33 +995,6 @@ const updatePackageUpgrades = async () => { publishAttributes("package_upgrades", upgrades); }; -/** - * Initializes the heartbeat sensor. - */ -const initHeartbeat = () => { - const root = `${INTEGRATION.root}/heartbeat`; - const config = { - name: "Heartbeat", - unique_id: `${INTEGRATION.node}_heartbeat`, - state_topic: `${root}/state`, - value_template: "{{ value }}", - icon: "mdi:heart-flash", - device: INTEGRATION.device, - }; - publishConfig("sensor", config); - updateHeartbeat(); -}; - -/** - * Updates the heartbeat sensor via the mqtt connection. - */ -const updateHeartbeat = async () => { - const now = new Date(); - const local = new Date(now.getTime() - now.getTimezoneOffset() * 60 * 1000); - const heartbeat = local.toISOString().replace(/\.\d{3}Z$/, ""); - publishState("heartbeat", heartbeat); -}; - /** * Initializes the last active sensor. */ @@ -1047,6 +1025,71 @@ const updateLastActive = async () => { publishAttributes("last_active", WEBVIEW.tracker.pointer); }; +/** + * Initializes the heartbeat sensor. + */ +const initHeartbeat = () => { + const root = `${INTEGRATION.root}/heartbeat`; + const config = { + name: "Heartbeat", + unique_id: `${INTEGRATION.node}_heartbeat`, + state_topic: `${root}/state`, + value_template: "{{ value }}", + entity_category: "diagnostic", + icon: "mdi:heart-flash", + device: INTEGRATION.device, + }; + publishConfig("sensor", config); + updateHeartbeat(); +}; + +/** + * Updates the heartbeat sensor via the mqtt connection. + */ +const updateHeartbeat = async () => { + const now = new Date(); + const local = new Date(now.getTime() - now.getTimezoneOffset() * 60 * 1000); + const heartbeat = local.toISOString().replace(/\.\d{3}Z$/, ""); + publishState("heartbeat", heartbeat); +}; + +/** + * Initializes the error log sensor. + */ +const initErrors = () => { + const root = `${INTEGRATION.root}/errors`; + const config = { + name: "Errors", + unique_id: `${INTEGRATION.node}_errors`, + state_topic: `${root}/state`, + json_attributes_topic: `${root}/attributes`, + value_template: "{{ value | int }}", + entity_category: "diagnostic", + icon: "mdi:alert-circle", + device: INTEGRATION.device, + }; + publishConfig("sensor", config); + updateErrors(); +}; + +/** + * Updates the error log sensor via the mqtt connection. + */ +const updateErrors = async () => { + const logs = APP.logs.slice().reverse(); + const errors = logs.filter((log) => log.level === "error"); + const history = logs.reduce((acc, log) => { + const time = log.time.toISOString().slice(0, 16); + if (!acc[time]) { + acc[time] = []; + } + acc[time].push({ [log.level.toUpperCase()]: log.text }); + return acc; + }, {}); + publishState("errors", errors.length); + publishAttributes("errors", history); +}; + module.exports = { init, update, diff --git a/js/webview.js b/js/webview.js index 53b7198..34ec42c 100644 --- a/js/webview.js +++ b/js/webview.js @@ -209,9 +209,12 @@ const updateView = () => { const url = WEBVIEW.views[WEBVIEW.viewActive].webContents.getURL(); const host = url.startsWith("data:") ? "whoopsie" : new URL(url).host; const title = `${APP.title} - ${host} (${WEBVIEW.viewActive})`; + const previous = WEBVIEW.window.getTitle(); // Update window title - console.log(`Update View: ${title}`); + if (previous !== title) { + console.info(`Update View: ${title}`); + } WEBVIEW.status.webContents.send("text-content", { id: "title", content: title }); WEBVIEW.window.setTitle(title); @@ -226,6 +229,9 @@ const updateView = () => { * Updates the window status. */ const updateStatus = () => { + const previous = WEBVIEW.tracker.status; + + // Set window status if (WEBVIEW.window.isFullScreen()) { WEBVIEW.tracker.status = "Fullscreen"; } else if (WEBVIEW.window.isMinimized()) { @@ -237,7 +243,9 @@ const updateStatus = () => { } // Update window status - console.log("Update Kiosk Status:", WEBVIEW.tracker.status); + if (previous !== WEBVIEW.tracker.status) { + console.info(`Update Kiosk Status: ${WEBVIEW.tracker.status}`); + } EVENTS.emit("updateStatus"); }; @@ -456,6 +464,9 @@ const homeView = () => { const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive]; const currentUrl = view.webContents.getURL(); + // Reset log history + APP.logs = []; + // Reload the default url or refresh the page if (currentUrl != defaultUrl) { view.webContents.loadURL(defaultUrl); @@ -479,6 +490,9 @@ const reloadView = () => { const defaultUrl = WEBVIEW.viewUrls[WEBVIEW.viewActive]; const currentUrl = view.webContents.getURL(); + // Reset log history + APP.logs = []; + // Reload the default url or refresh the page if (currentUrl.startsWith("data:")) { view.webContents.loadURL(defaultUrl); @@ -897,7 +911,14 @@ const viewEvents = async () => { if (WEBVIEW.viewActive === 0 && ready.length === WEBVIEW.views.length) { nextView(); } - view.webContents.loadURL(errorHtml(code, text, url, WEBVIEW.theme.get())); + switch (code) { + case -3: + console.warn(`Load Warning: ${url}, ERR_ABORTED (${code})`); + break; + default: + console.error(`Load Error: ${url}, ${text} (${code})`); + view.webContents.loadURL(errorHtml(code, text, url, WEBVIEW.theme.get())); + } } }); @@ -928,7 +949,7 @@ const viewEvents = async () => { WEBVIEW.tracker.pointer.time = now; WEBVIEW.tracker.pointer.position = posNew; if (delta > 30) { - console.log("Update Last Active"); + console.info("Update Last Active"); integration.update(); } } @@ -938,7 +959,7 @@ const viewEvents = async () => { case "left": // Ignore touch event if display was off if (WEBVIEW.tracker.display.off > WEBVIEW.tracker.display.on) { - console.log("Display Touch Event: Ignored"); + console.verbose("Display Touch Event: Ignored"); e.preventDefault(); } // Turn display on if it was off