From ffd6bc81795658109e3e08576ffe622da66934e7 Mon Sep 17 00:00:00 2001 From: Nikola Katsarov Date: Fri, 17 Jul 2026 14:50:10 +0300 Subject: [PATCH] feat(cli-serve): make Evidence Inspector & Workflow Dashboard legible to non-experts Add explanatory microcopy (what an evidence bundle is, what VALID proves, what each hash/status means), color-coded status/verification badges, active-nav 'you are here' cues, semantic tables (th scope, caption), horizontal-scroll wrappers, hash truncation with full-value-on-hover, richer empty states, a footer clarifying read-only/no-auth posture, and prefers-color-scheme dark mode. All CSS stays inline (no CDN/network). Read-only surface, routes, JSON API, and XSS escaping unchanged. --- crates/llmvm-cli/src/dashboard.rs | 174 +++++++++++++++++++------ crates/llmvm-cli/src/evidence_serve.rs | 162 ++++++++++++++++++----- 2 files changed, 262 insertions(+), 74 deletions(-) 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#"
+ Boruna Workflow Dashboard + +
"#, + 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( - "", + r#""#, + ); + body.push_str(""); + body.push_str( + r#""#, ); 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("
Run IDWorkflowStatusStartedUpdated
Status: running + paused + completed + failed
Run IDWorkflowStatusStartedUpdated
{}{}{}{}{}
{}{}{}{}{}
"); + 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!( - "", + r#""#, html_escape(&run.workflow_name) )); body.push_str(&format!( - "", - html_escape(&run.workflow_hash) + r#""#, + 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#""#, + r#""#, status_class, op.transient_status.as_str() )); body.push_str(&format!( - "", + r#""#, format_ms(op.started_at_ms) )); body.push_str(&format!( - "", + r#""#, format_ms(op.updated_at_ms) )); } if let Some(t) = &run.terminal_status { body.push_str(&format!( - "", - t.as_str() + r#""#, + c = t.as_str() )); } - body.push_str("
Workflow{}
Workflow{}
Workflow hash{}
Workflow hash{h}
Status{}
Status{}
Started{}
Started{}
Updated{}
Updated{}
Terminal{}
Terminal{c}
"); + 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( - "", + r#""#, + ); + body.push_str(""); + body.push_str( + r#""#, ); 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("
StepStatusAttemptsStartedEndedOutputError
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.
StepStatusAttemptsStartedEndedOutputError
{}{}{}{}{}{}{}
{}{}{}{}{}{}{}
"); + 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#" -{}{} -{}{} +{}{} +{}{}
{}
+ + "#, 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

Boruna Evidence Inspector

-
{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. +
"# ) @@ -194,35 +241,61 @@ pub(crate) fn render_bundle_page(data: &BundleData) -> String { .map(|b| b.format_version.as_str()) .unwrap_or("(missing bundle.json)")); + // Explain what VALID / INVALID actually proves — the whole point of the page. + let verify_hint = if data.verify_valid { + "Every file in this bundle matches the SHA-256 checksum recorded in the manifest, and the \ + audit log's hash chain is intact. Nothing has been altered since the run was sealed." + } else { + "One or more checks failed — a file, hash, or audit-chain link does not match what was \ + sealed at run time. Treat this bundle as untrustworthy until the errors below are resolved." + }; + let body = format!( r#"

Bundle Overview

+

An evidence bundle is a tamper-evident, self-contained record of one +workflow run: its inputs' hashes, a hash-chained audit log of every step, the recorded outputs, and a +manifest of SHA-256 checksums that ties it all together. This page verifies that record and shows what +it contains.

{errors_html}
verification
{verify_badge}
-
run_id
{run_id}
+
+

{verify_hint}

+
+
run_id
{run_id}
workflow
{workflow}
started_at
{started}
completed_at
{completed}
format_version
{format_version}
{enc_row}
+

Look up this same run_id in the Boruna Workflow + Dashboard to see the live run and its per-step status.

Hashes

+

Fingerprints that make the bundle tamper-evident. bundle_hash +covers the whole bundle; workflow_hash / policy_hash +identify exactly which workflow and policy ran; audit_log_hash seals the audit +chain. Re-running the same inputs reproduces the same hashes.

-
bundle_hash
{bundle_hash}
-
workflow_hash
{workflow_hash}
-
policy_hash
{policy_hash}
-
audit_log_hash
{audit_hash}
+
bundle_hash
{bundle_hash}
+
workflow_hash
{workflow_hash}
+
policy_hash
{policy_hash}
+
audit_log_hash
{audit_hash}

File Checksums ({file_count} files)

+
- - {file_rows} + + + {file_rows}
FileSHA-256
Each row is a file in the bundle and the SHA-256 it must match. Verification recomputes + these; any mismatch flips the bundle to INVALID. Hover a value to read it in full.
FileSHA-256
+
"#, run_id = esc(&data.manifest.run_id), workflow = esc(&data.manifest.workflow_name), @@ -239,14 +312,14 @@ pub(crate) fn render_bundle_page(data: &BundleData) -> String { .iter() .map(|(f, h)| { format!( - r#"{}{}"#, + r#"{}{h}"#, esc(f), - esc(h) + h = esc(h) ) }) .collect::(), ); - page("Bundle", &body) + page("Bundle", "bundle", &body) } pub(crate) fn render_audit_page(data: &BundleData) -> String { @@ -269,17 +342,34 @@ pub(crate) fn render_audit_page(data: &BundleData) -> String { }) .collect(); - let body = format!( - r#"

Audit Log ({count} entries)

-
+ let inner = if data.audit_entries.is_empty() { + String::from( + r#"

This bundle recorded no audit entries. That usually +means a partial or demo-mode bundle — a fully recorded run logs one entry per lifecycle event +(workflow started, each step started/completed, policy checks, and workflow completed).

"#, + ) + } else { + format!( + r#"
+
- - {rows} + + + {rows}
#StepEventDetail
Chronological, append-only record of the run. Each entry is + hash-linked to the one before it (entry #N embeds the hash of #N-1), so removing or editing any + line breaks the chain and fails verification.
#StepEventDetail
-
"#, +
+
"# + ) + }; + + let body = format!( + r#"

Audit Log ({count} entries)

+{inner}"#, count = data.audit_entries.len() ); - page("Audit Log", &body) + page("Audit Log", "audit", &body) } fn describe_event(event: &AuditEvent) -> (String, &'static str, String) { @@ -367,7 +457,11 @@ fn describe_event(event: &AuditEvent) -> (String, &'static str, String) { pub(crate) fn render_outputs_page(data: &BundleData) -> String { let items: String = if data.outputs.is_empty() { - String::from("

No outputs found.

") + String::from( + r#"

No outputs found in this bundle. Demo-mode runs and workflows that +don't record step results show nothing here — the run may still be valid. Click a step below (when +present) to expand its recorded JSON result.

"#, + ) } else { data.outputs .iter() @@ -386,10 +480,12 @@ pub(crate) fn render_outputs_page(data: &BundleData) -> String { let body = format!( r#"

Step Outputs ({count} steps)

+

The exact JSON result each step produced, as it was sealed into the bundle. These are +the recorded outputs verified by the checksums on the Overview page.

{items}
"#, count = data.outputs.len() ); - page("Outputs", &body) + page("Outputs", "outputs", &body) } // ---------------------------------------------------------------------------