Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion server/app/db/version.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version": "2026-0902-123259-beta" }
{"version": "2026-0910-102045-beta" }
8 changes: 8 additions & 0 deletions server/app/handlers/projectController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions server/app/logSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
};
}
Expand Down Expand Up @@ -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;
Expand Down