From 47120c1f2cf42d65eeb1bb9f8572e64e2075d6d3 Mon Sep 17 00:00:00 2001 From: Naoyuki Sogo Date: Thu, 10 Sep 2026 10:12:09 +0900 Subject: [PATCH 1/2] fix: surface project run/resume errors to the client A run that aborts before or during dispatch left the UI with no toast, only a log line. Two independent gaps: Bug 1: the socketIO log appender was unified onto a single "WHEEL_LOG" event (f24aac5, "revamp log screen"), so the per-level "logERR" event the client's onLogErr toast handlers (Home/Viewer/Workflow/remotehostManager) listen for is no longer emitted. Re-emit FATAL only on "logERR" as plain text - the log screen still receives everything via "WHEEL_LOG", and only run-aborting errors pop a toast. Bug 2: onRunProject/onContinueProject fire runDispatcher without awaiting it and projectOperator resolves the socket ack with ack(true) first, so the ack(err) in runDispatcher's preparing-phase and parsing-workflow catch blocks is a dropped second ack. Notify the client with a "showMessage" toast from those catch blocks. Server-side only; no client change. Refs #131 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01C3jKNM1qubomM8UTRdkEWu --- server/app/handlers/projectController.js | 8 ++++++++ server/app/logSettings.js | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/server/app/handlers/projectController.js b/server/app/handlers/projectController.js index f6171a6e..faee0690 100644 --- a/server/app/handlers/projectController.js +++ b/server/app/handlers/projectController.js @@ -323,11 +323,16 @@ async function runDispatcher(clientID, projectRootDir, ack) { getLogger(projectRootDir).debug(err.message); } else if (err.reason === "invalidRemoteStorage") { getLogger(projectRootDir).error(`you do not have write permission to ${err.storagePath} on ${err.host}`); + emitAll(projectRootDir, "showMessage", `you do not have write permission to ${err.storagePath} on ${err.host}`); } else { getLogger(projectRootDir).error("fatal error occurred while preparing phase:", err); + emitAll(projectRootDir, "showMessage", `failed to start project: ${err.message || err}`); } removeSsh(projectRootDir); removeAllJWTServerPassphrase(projectRootDir); + //runDispatcher is fire-and-forget from onRunProject/onContinueProject, so projectOperator + //has already resolved the socket ack with ack(true); this ack(err) is a dropped second + //ack on the wire. the client is notified via "showMessage" (toast) above instead. ack(err); return false; } @@ -392,6 +397,9 @@ async function runDispatcher(clientID, projectRootDir, ack) { await _internal.unlockIfFinished(projectRootDir); } catch (err) { getLogger(projectRootDir).error("fatal error occurred while parsing workflow:", err); + //the socket ack was already resolved by projectOperator's ack(true) (runDispatcher is + //not awaited), so ack(err) below does nothing on the wire - surface it as a toast here. + emitAll(projectRootDir, "showMessage", `project run aborted: ${err.message || err}`); await updateProjectState(projectRootDir, "failed"); ack(err); } finally { diff --git a/server/app/logSettings.js b/server/app/logSettings.js index e51a4db4..d357eba2 100644 --- a/server/app/logSettings.js +++ b/server/app/logSettings.js @@ -32,6 +32,14 @@ function socketIOAppender(layout, timezoneOffset, argEventName) { if (eventName) { const message = layout(loggingEvent, timezoneOffset); _internal.emitAll(projectRootDir, "WHEEL_LOG", message); + //the structured "WHEEL_LOG" stream above feeds the log screen. the client also + //raises a toast for "logERR" events (onLogErr in Home.vue/Viewer.vue/Workflow.vue), + //but that per-level channel stopped being emitted once the socketIO appender was + //unified onto "WHEEL_LOG". re-emit FATAL only (as plain text) so a run-aborting + //error still pops a toast, without flooding it with every ERROR-level line. + if (loggingEvent.level.levelStr === "FATAL") { + _internal.emitAll(projectRootDir, "logERR", formatToastMessage(loggingEvent)); + } } }; } @@ -73,6 +81,18 @@ function formatLogArg(value) { return value; } +//build a short one-line string from a logging event's data parts for a client-side +//toast. unlike formatLogArg(), an Error is reduced to its message (no stack trace) +//since the toast has no room for it - the full entry is still in the log screen/file. +function formatToastMessage(loggingEvent) { + const data = Array.isArray(loggingEvent.data) ? loggingEvent.data : [loggingEvent.data]; + return data + .map((value)=>{ + return value instanceof Error ? value.message : formatLogArg(value); + }) + .join(" "); +} + const socketIO = { configure: (config, layouts)=>{ let layout = layouts.basicLayout; From 78a7935dbdf3dbc69a8e1da5837fc5a4e747595d Mon Sep 17 00:00:00 2001 From: "version-number-updater[bot]" Date: Thu, 10 Sep 2026 10:20:46 +0900 Subject: [PATCH 2/2] [skip ci] update version number --- server/app/db/version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/app/db/version.json b/server/app/db/version.json index 6604efce..476e383f 100644 --- a/server/app/db/version.json +++ b/server/app/db/version.json @@ -1 +1 @@ -{"version": "2026-0902-123259-beta" } \ No newline at end of file +{"version": "2026-0910-102045-beta" } \ No newline at end of file