fix: Run Now agent-run timing + non-404 webview placeholder

Two independent fixes that both blocked the "install → Run Now → see
the Site tab render" loop.

1. CronViewModel.runNow stopped blocking on `cron tick`. Previously
   the UI waited up to 60 s on the tick before deciding whether the
   job succeeded, so any agent run that did real work (an LLM call +
   a few HTTP GETs + a file write = easily 90 s+) surfaced a false
   "Run failed" toast while the job kept running in the background.
   Dashboard updates landed minutes later, confusing the user.

   New shape: show "Agent started — dashboard will update when it
   finishes" the instant `cron run` queues the job, then call `cron
   tick` with a 300 s timeout to force execution. Tick failures are
   logged but don't overwrite the started-toast — HermesFileWatcher
   picks up the dashboard.json rewrite automatically when the agent
   finishes.

2. site-status-checker's webview widget pointed at
   `github.com/awizemann/scarf/tree/main/templates/awizemann/...`.
   The templates/ path only exists on project-sharing, not main, so
   GitHub returned 404 in the Site tab until the first cron run
   replaced the URL with the user's configured site. Switched the
   placeholder to `awizemann.github.io/scarf/` which always renders.

Bundle + catalog rebuilt against the updated dashboard.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-04-23 16:35:57 +02:00
parent 3fe8f4efa0
commit 96b5da3cd5
5 changed files with 68 additions and 18 deletions
@@ -73,23 +73,46 @@ final class CronViewModel {
// with `hermes cron tick` which runs all due jobs once and // with `hermes cron tick` which runs all due jobs once and
// exits. Redundant-but-harmless when the gateway is running; // exits. Redundant-but-harmless when the gateway is running;
// the actual trigger when it isn't. // the actual trigger when it isn't.
//
// Feedback model: show a "Agent started" toast as soon as
// `cron run` succeeds, WITHOUT waiting for `cron tick` to
// return. Agent jobs routinely run past a minute (network IO +
// an LLM call + a file rewrite), and earlier versions with a
// 60s tick timeout surfaced a misleading "Run failed" toast
// every time while the job kept running in the background.
// The app's HermesFileWatcher picks up the dashboard.json
// rewrite that the agent lands at the end that's what the
// user actually watches for, not this toast.
let svc = fileService let svc = fileService
let jobID = job.id let jobID = job.id
Task.detached { [weak self] in Task.detached { [weak self] in
let runResult = svc.runHermesCLI(args: ["cron", "run", jobID], timeout: 30) let runResult = svc.runHermesCLI(args: ["cron", "run", jobID], timeout: 30)
// Give `cron run` a moment to register the queue entry
// before forcing the tick. A few hundred ms is enough;
// longer only delays the user-visible feedback.
try? await Task.sleep(for: .milliseconds(250))
let tickResult = svc.runHermesCLI(args: ["cron", "tick"], timeout: 60)
await MainActor.run { [weak self] in await MainActor.run { [weak self] in
guard let self else { return } guard let self else { return }
if runResult.exitCode == 0 && tickResult.exitCode == 0 { if runResult.exitCode != 0 {
self.message = "Job executed (see Output panel for details)" self.message = "Run failed to queue: \(runResult.output.prefix(200))"
} else { self.logger.warning("cron run failed: \(runResult.output)")
let errOutput = runResult.exitCode != 0 ? runResult.output : tickResult.output self.load()
self.message = "Run failed: \(errOutput.prefix(200))" DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
self.logger.warning("cron runNow failed: run=\(runResult.exitCode), tick=\(tickResult.exitCode) output=\(errOutput)") self?.message = nil
}
return
}
self.message = "Agent started — dashboard will update when it finishes"
self.load()
}
// `cron run` is queued; now force the tick. The 300s
// timeout catches truly stuck processes without killing
// the long-but-valid agent case that blew up the 60s
// version. A timeout here is survivable the Hermes
// scheduler re-runs due jobs on its own cadence so we
// log but don't surface it as a failure toast.
try? await Task.sleep(for: .milliseconds(250))
let tickResult = svc.runHermesCLI(args: ["cron", "tick"], timeout: 300)
await MainActor.run { [weak self] in
guard let self else { return }
if tickResult.exitCode != 0 {
self.logger.warning("cron tick exited non-zero (job may still complete via scheduler): \(tickResult.output)")
} }
self.load() self.load()
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
+31 -4
View File
@@ -889,6 +889,10 @@
}, },
"••••••••••" : { "••••••••••" : {
},
"+ %lld more…" : {
"comment" : "A button that shows the number of files that were left behind by the template uninstaller.",
"isCommentAutoGenerated" : true
}, },
"<%@>" : { "<%@>" : {
@@ -6664,6 +6668,10 @@
} }
} }
}, },
"Delete %@ from Finder if you don't need these files anymore." : {
"comment" : "A note that lets the user delete",
"isCommentAutoGenerated" : true
},
"Delete %@?" : { "Delete %@?" : {
"localizations" : { "localizations" : {
"de" : { "de" : {
@@ -15447,6 +15455,9 @@
}, },
"Project directory will also be removed (nothing user-owned left inside)." : { "Project directory will also be removed (nothing user-owned left inside)." : {
},
"Project folder kept" : {
}, },
"Project Name" : { "Project Name" : {
"localizations" : { "localizations" : {
@@ -16511,6 +16522,10 @@
"comment" : "A label that instructs the user to remove a project from Scarf's list of installed projects.", "comment" : "A label that instructs the user to remove a project from Scarf's list of installed projects.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Remove %@ from Scarf's project list (files are kept on disk)" : {
"comment" : "A confirmation dialog that",
"isCommentAutoGenerated" : true
},
"Remove %@?" : { "Remove %@?" : {
"localizations" : { "localizations" : {
"de" : { "de" : {
@@ -16591,8 +16606,16 @@
} }
} }
}, },
"Remove from Scarf" : { "Remove from List" : {
"comment" : "A context menu option to remove a project from Scarf.", "comment" : "A confirmation dialog that asks whether a user is sure they want to remove a project from Scarf's list.",
"isCommentAutoGenerated" : true
},
"Remove from List (keep files)…" : {
"comment" : "A button that removes a project from Scarf's list, but not from disk.",
"isCommentAutoGenerated" : true
},
"Remove from Scarf's project list?" : {
"comment" : "Title of a dialog that asks the user to confirm removing a project from Scarf's project list.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Remove the entire namespace dir recursively" : { "Remove the entire namespace dir recursively" : {
@@ -21572,6 +21595,10 @@
} }
} }
}, },
"These files weren't installed by the template (the agent or you created them after install), so Scarf left them in place along with the folder itself." : {
"comment" : "A description of the files Scarf left in place when uninstalling a template.",
"isCommentAutoGenerated" : true
},
"These list fields must be edited directly in config.yaml." : { "These list fields must be edited directly in config.yaml." : {
"localizations" : { "localizations" : {
"de" : { "de" : {
@@ -22602,8 +22629,8 @@
"comment" : "A button that uninstalls a template.", "comment" : "A button that uninstalls a template.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Uninstall Template…" : { "Uninstall Template (remove installed files)…" : {
"comment" : "A contextual menu item that uninstalls a template.", "comment" : "A button that removes a project's files from the system.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Unknown: %@" : { "Unknown: %@" : {
@@ -54,7 +54,7 @@
{ {
"type": "webview", "type": "webview",
"title": "First Watched Site", "title": "First Watched Site",
"url": "https://github.com/awizemann/scarf/tree/main/templates/awizemann/site-status-checker", "url": "https://awizemann.github.io/scarf/",
"height": 420 "height": 420
} }
] ]
+2 -2
View File
@@ -7,8 +7,8 @@
"name": "Alan Wizemann", "name": "Alan Wizemann",
"url": "https://github.com/awizemann/scarf" "url": "https://github.com/awizemann/scarf"
}, },
"bundleSha256": "2a4e0aba5bd4d86be3153d87c6ce219b9068223daebfad6f9db2b82c3752fac5", "bundleSha256": "0a20802a8830a7cfdd1afa2888e42e113c9a17a37439384a3037d32ad1f24c1f",
"bundleSize": 7583, "bundleSize": 7569,
"category": "monitoring", "category": "monitoring",
"config": { "config": {
"modelRecommendation": { "modelRecommendation": {