diff --git a/crates/llmvm-cli/src/dashboard.rs b/crates/llmvm-cli/src/dashboard.rs
index 7ee7a3c..e27933b 100644
--- a/crates/llmvm-cli/src/dashboard.rs
+++ b/crates/llmvm-cli/src/dashboard.rs
@@ -323,27 +323,86 @@ fn html_escape(s: &str) -> String {
const PAGE_STYLE: &str = r#"
"#;
+/// Top navigation bar shared by both dashboard pages. `active` is the
+/// nav key of the current page ("runs" for the list; detail pages are
+/// also under "runs") so the matching link gets an `aria-current` cue.
+fn render_header(active: &str) -> String {
+ let cur = |k: &str| {
+ if k == active {
+ r#" aria-current="page""#
+ } else {
+ ""
+ }
+ };
+ format!(
+ r#""#,
+ r = cur("runs"),
+ j = cur("api"),
+ )
+}
+
fn render_banner(bind_warning: Option<&str>) -> String {
match bind_warning {
Some(addr) => format!(
@@ -358,7 +417,11 @@ fn render_banner(bind_warning: Option<&str>) -> String {
fn render_index(runs: &[RunRow], bind_warning: Option<&str>) -> String {
let banner = render_banner(bind_warning);
let mut body = String::new();
- body.push_str("
Boruna runs ");
+ body.push_str("Workflow runs ");
+ body.push_str(
+ r#"Every workflow execution recorded in this Boruna instance. Each row is one
+run; click its Run ID to see per-step progress, outputs, and errors. Times are UTC.
"#,
+ );
body.push_str(&format!(
r#"{} run{} total
"#,
runs.len(),
@@ -366,17 +429,24 @@ fn render_index(runs: &[RunRow], bind_warning: Option<&str>) -> String {
));
if runs.is_empty() {
- body.push_str(r#"No runs yet. Start one with boruna workflow run ....
"#);
+ body.push_str(r#"No runs yet. Start one with boruna workflow run ..., then reload this page.
"#);
} else {
- body.push_str("");
+ body.push_str(r#""#);
body.push_str(
- "Run ID Workflow Status Started Updated ",
+ r#"Status: running
+ paused
+ completed
+ failed "#,
+ );
+ body.push_str("");
+ body.push_str(
+ r#"Run ID Workflow Status Started Updated "#,
);
body.push_str(" ");
for run in runs {
let status_class = format!("status-{}", run.status.as_str());
body.push_str(&format!(
- r#"{} {} {} {} {} "#,
+ r#"{}{} {} {} {} "#,
html_escape(&run.run_id),
html_escape(&run.run_id),
html_escape(&run.workflow_name),
@@ -386,12 +456,12 @@ fn render_index(runs: &[RunRow], bind_warning: Option<&str>) -> String {
format_ms(run.updated_at_ms),
));
}
- body.push_str("
");
+ body.push_str("
");
}
- body.push_str(r#"JSON: GET /api/runs
"#);
+ body.push_str(r#"Machine-readable list: GET /api/runs
"#);
- wrap_page("Boruna runs", &banner, &body)
+ wrap_page("Boruna runs", &render_header("runs"), &banner, &body)
}
/// Maximum chars of inline output rendered in the HTML detail
@@ -418,47 +488,59 @@ fn render_detail(
let mut body = String::new();
body.push_str(r#"← All runs
"#);
body.push_str(&format!("Run {} ", html_escape(&run.run_id)));
+ body.push_str(
+ r#"One workflow run. The summary below identifies which workflow ran and its
+current state; the steps table shows each step's progress, retry count, output, and any error.
"#,
+ );
- body.push_str("");
+ body.push_str(r#""#);
body.push_str(&format!(
- "Workflow {} ",
+ r#"Workflow {} "#,
html_escape(&run.workflow_name)
));
body.push_str(&format!(
- "Workflow hash {} ",
- html_escape(&run.workflow_hash)
+ r#"Workflow hash {h} "#,
+ h = html_escape(&run.workflow_hash)
));
if let Some(op) = operational {
let status_class = format!("status-{}", op.transient_status.as_str());
body.push_str(&format!(
- r#"Status {} "#,
+ r#"Status {} "#,
status_class,
op.transient_status.as_str()
));
body.push_str(&format!(
- "Started {} ",
+ r#"Started {} "#,
format_ms(op.started_at_ms)
));
body.push_str(&format!(
- "Updated {} ",
+ r#"Updated {} "#,
format_ms(op.updated_at_ms)
));
}
if let Some(t) = &run.terminal_status {
body.push_str(&format!(
- "Terminal {} ",
- t.as_str()
+ r#"Terminal {c} "#,
+ c = t.as_str()
));
}
- body.push_str("
");
+ body.push_str("
");
body.push_str(&format!("Steps ({}) ", steps.len()));
if steps.is_empty() {
- body.push_str(r#"No step checkpoints recorded.
"#);
+ body.push_str(
+ r#"No step checkpoints recorded — the run may not have started
+ any steps yet, or it ran in a mode that does not persist per-step checkpoints.
"#,
+ );
} else {
- body.push_str("");
+ body.push_str(r#""#);
body.push_str(
- "Step Status Attempts Started Ended Output Error ",
+ r#"Output shows the recorded JSON result (long values are truncated with … and
+ large ones link out as a blob). Attempts counts retries. Times are UTC. "#,
+ );
+ body.push_str("");
+ body.push_str(
+ r#"Step Status Attempts Started Ended Output Error "#,
);
body.push_str(" ");
for (step, output) in steps.iter().zip(outputs.iter()) {
@@ -472,7 +554,7 @@ fn render_detail(
.unwrap_or_default();
let output_cell = render_output_cell(&run.run_id, output);
body.push_str(&format!(
- r#"{}{} {} {} {} {} {} "#,
+ r#"{}{} {} {} {} {} {} "#,
html_escape(&step.step_id),
status_class,
step.status.as_str(),
@@ -483,15 +565,20 @@ fn render_detail(
error,
));
}
- body.push_str("
");
+ body.push_str("
");
}
body.push_str(&format!(
- r#"JSON: GET /api/runs/{}
"#,
+ r#"Machine-readable detail: GET /api/runs/{}
"#,
html_escape(&run.run_id)
));
- wrap_page(&format!("Run {}", run.run_id), &banner, &body)
+ wrap_page(
+ &format!("Run {}", run.run_id),
+ &render_header("runs"),
+ &banner,
+ &body,
+ )
}
/// Render a single step's Output cell. Sprint 0.5-S7b.
@@ -553,15 +640,20 @@ fn truncate_chars(s: &str, max_chars: usize) -> &str {
&s[..end]
}
-fn wrap_page(title: &str, banner: &str, body: &str) -> String {
+fn wrap_page(title: &str, header: &str, banner: &str, body: &str) -> String {
format!(
r#"
-{} {}
-{}{}
+{} {}
+{}{}{}
+Read-only view over runs.db . This dashboard cannot start, stop, or
+change runs — manage workflows with the boruna CLI. It ships no authentication; keep it on
+127.0.0.1 unless fronted by an auth-enforcing proxy.
+
"#,
html_escape(title),
PAGE_STYLE,
+ header,
banner,
body
)
@@ -835,7 +927,7 @@ mod tests {
.unwrap()
.0;
// Output column header present.
- assert!(html.contains("Output "));
+ assert!(html.contains(r#"Output "#));
// Inline value rendered (with HTML-escaped quotes).
assert!(
html.contains(""hello world""),
diff --git a/crates/llmvm-cli/src/evidence_serve.rs b/crates/llmvm-cli/src/evidence_serve.rs
index 9f58f4d..216f45a 100644
--- a/crates/llmvm-cli/src/evidence_serve.rs
+++ b/crates/llmvm-cli/src/evidence_serve.rs
@@ -96,7 +96,25 @@ pub(crate) fn load_bundle(dir: &Path) -> Result String {
+fn page(title: &str, active: &str, body: &str) -> String {
+ // "you are here" cue: mark the matching nav link with aria-current.
+ let cur = |key: &str| {
+ if key == active {
+ r#" aria-current="page""#
+ } else {
+ ""
+ }
+ };
+ let nav = format!(
+ r#"Overview
+ Audit Log
+ Outputs
+ JSON API "#,
+ b = cur("bundle"),
+ a = cur("audit"),
+ o = cur("outputs"),
+ j = cur("api"),
+ );
format!(
r#"
@@ -105,45 +123,74 @@ fn page(title: &str, body: &str) -> String {
{title} — Boruna Evidence
{body}
+
+ Read-only inspector for one evidence bundle. Runs on 127.0.0.1 with no
+ authentication — do not expose it to a network. Verification and export are also available via
+ boruna evidence verify / inspect .
+