Use crate's typed Error in web::db
Mapping rusqlite::Error to String dropped its source chain, leaving
both tracing and the error template with just the top-level message.
Switch the loaders to crate::Result and let `?` propagate via the
existing Error::Sql variant; render details through display_chain so
the full chain reaches logs and the error page.

Assisted-by: Claude Opus 4.7 via Claude Code
change oqkrxysyqukrmpqlmqnknlmxqpvwyrpk
commit a0ae0cd910b81bd2dfdd7171731b393633206077
author Alpha Chen <alpha@kejadlen.dev>
date
parent lztoqmux
diff --git a/src/quire/web/db.rs b/src/quire/web/db.rs
index 6a4c052..47a24bb 100644
--- a/src/quire/web/db.rs
+++ b/src/quire/web/db.rs
@@ -2,7 +2,7 @@
 
 use rusqlite::Connection;
 
-use crate::Quire;
+use crate::{Quire, Result};
 
 /// Raw run row from the database.
 pub struct RunRow {
@@ -33,16 +33,14 @@ pub struct ShEvent {
     pub cmd: String,
 }
 
-pub fn load_runs(quire: &Quire, repo: &str) -> Result<Vec<RunRow>, String> {
-    let db = Connection::open(quire.db_path()).map_err(|e| e.to_string())?;
-    let mut stmt = db
-        .prepare(
-            "SELECT id, state, sha, ref_name, queued_at_ms, started_at_ms, finished_at_ms
-             FROM runs WHERE repo = ?1
-             ORDER BY queued_at_ms DESC
-             LIMIT 50",
-        )
-        .map_err(|e| e.to_string())?;
+pub fn load_runs(quire: &Quire, repo: &str) -> Result<Vec<RunRow>> {
+    let db = Connection::open(quire.db_path())?;
+    let mut stmt = db.prepare(
+        "SELECT id, state, sha, ref_name, queued_at_ms, started_at_ms, finished_at_ms
+         FROM runs WHERE repo = ?1
+         ORDER BY queued_at_ms DESC
+         LIMIT 50",
+    )?;
 
     let rows = stmt
         .query_map(rusqlite::params![repo], |row| {
@@ -55,10 +53,8 @@ pub fn load_runs(quire: &Quire, repo: &str) -> Result<Vec<RunRow>, String> {
                 started_at_ms: row.get(5)?,
                 finished_at_ms: row.get(6)?,
             })
-        })
-        .map_err(|e| e.to_string())?
-        .collect::<Result<Vec<_>, _>>()
-        .map_err(|e| e.to_string())?;
+        })?
+        .collect::<rusqlite::Result<Vec<_>>>()?;
 
     Ok(rows)
 }
@@ -67,35 +63,31 @@ pub fn load_run_detail(
     quire: &Quire,
     repo: &str,
     run_id: &str,
-) -> Result<(RunRow, Vec<JobRow>, Vec<ShEvent>), String> {
-    let db = Connection::open(quire.db_path()).map_err(|e| e.to_string())?;
-
-    let run = db
-        .query_row(
-            "SELECT id, state, sha, ref_name, queued_at_ms, started_at_ms, finished_at_ms
-             FROM runs WHERE id = ?1 AND repo = ?2",
-            rusqlite::params![run_id, repo],
-            |row| {
-                Ok(RunRow {
-                    id: row.get(0)?,
-                    state: row.get(1)?,
-                    sha: row.get(2)?,
-                    ref_name: row.get(3)?,
-                    queued_at_ms: row.get(4)?,
-                    started_at_ms: row.get(5)?,
-                    finished_at_ms: row.get(6)?,
-                })
-            },
-        )
-        .map_err(|e| e.to_string())?;
-
-    let mut job_stmt = db
-        .prepare(
-            "SELECT job_id, state, exit_code, started_at_ms, finished_at_ms
-             FROM jobs WHERE run_id = ?1
-             ORDER BY rowid",
-        )
-        .map_err(|e| e.to_string())?;
+) -> Result<(RunRow, Vec<JobRow>, Vec<ShEvent>)> {
+    let db = Connection::open(quire.db_path())?;
+
+    let run = db.query_row(
+        "SELECT id, state, sha, ref_name, queued_at_ms, started_at_ms, finished_at_ms
+         FROM runs WHERE id = ?1 AND repo = ?2",
+        rusqlite::params![run_id, repo],
+        |row| {
+            Ok(RunRow {
+                id: row.get(0)?,
+                state: row.get(1)?,
+                sha: row.get(2)?,
+                ref_name: row.get(3)?,
+                queued_at_ms: row.get(4)?,
+                started_at_ms: row.get(5)?,
+                finished_at_ms: row.get(6)?,
+            })
+        },
+    )?;
+
+    let mut job_stmt = db.prepare(
+        "SELECT job_id, state, exit_code, started_at_ms, finished_at_ms
+         FROM jobs WHERE run_id = ?1
+         ORDER BY rowid",
+    )?;
 
     let jobs = job_stmt
         .query_map(rusqlite::params![run_id], |row| {
@@ -106,18 +98,14 @@ pub fn load_run_detail(
                 started_at_ms: row.get(3)?,
                 finished_at_ms: row.get(4)?,
             })
-        })
-        .map_err(|e| e.to_string())?
-        .collect::<Result<Vec<_>, _>>()
-        .map_err(|e| e.to_string())?;
-
-    let mut sh_stmt = db
-        .prepare(
-            "SELECT job_id, started_at_ms, finished_at_ms, exit_code, cmd
-             FROM sh_events WHERE run_id = ?1
-             ORDER BY job_id, started_at_ms",
-        )
-        .map_err(|e| e.to_string())?;
+        })?
+        .collect::<rusqlite::Result<Vec<_>>>()?;
+
+    let mut sh_stmt = db.prepare(
+        "SELECT job_id, started_at_ms, finished_at_ms, exit_code, cmd
+         FROM sh_events WHERE run_id = ?1
+         ORDER BY job_id, started_at_ms",
+    )?;
 
     let sh_events = sh_stmt
         .query_map(rusqlite::params![run_id], |row| {
@@ -128,10 +116,8 @@ pub fn load_run_detail(
                 exit_code: row.get(3)?,
                 cmd: row.get(4)?,
             })
-        })
-        .map_err(|e| e.to_string())?
-        .collect::<Result<Vec<_>, _>>()
-        .map_err(|e| e.to_string())?;
+        })?
+        .collect::<rusqlite::Result<Vec<_>>>()?;
 
     Ok((run, jobs, sh_events))
 }
diff --git a/src/quire/web/handlers.rs b/src/quire/web/handlers.rs
index cbd2c15..55d1c00 100644
--- a/src/quire/web/handlers.rs
+++ b/src/quire/web/handlers.rs
@@ -8,6 +8,7 @@ use axum::response::{Html, IntoResponse, Response};
 use super::db;
 use super::templates::*;
 use crate::Quire;
+use crate::error::display_chain;
 
 /// Render a template into an HTML response, returning 500 on render failure.
 fn render<T: Template>(tmpl: &T) -> Response {
@@ -48,12 +49,12 @@ pub async fn run_list(State(quire): State<Quire>, AxumPath(repo): AxumPath<Strin
     let runs = match db::load_runs(&quire, &repo_name) {
         Ok(r) => r,
         Err(e) => {
-            tracing::error!(repo = %repo, error = %e, "failed to load runs");
+            tracing::error!(repo = %repo, error = %display_chain(&e), "failed to load runs");
             return render_error(
                 repo_display,
                 StatusCode::INTERNAL_SERVER_ERROR,
                 "Failed to load runs",
-                e,
+                display_chain(&e).to_string(),
             );
         }
     };
@@ -93,12 +94,12 @@ pub async fn run_detail(
     let (run, jobs, sh_events) = match result {
         Ok(d) => d,
         Err(e) => {
-            tracing::error!(repo = %repo, run_id = %run_id, error = %e, "failed to load run detail");
+            tracing::error!(repo = %repo, run_id = %run_id, error = %display_chain(&e), "failed to load run detail");
             return render_error(
                 repo_display,
                 StatusCode::INTERNAL_SERVER_ERROR,
                 "Failed to load run",
-                e,
+                display_chain(&e).to_string(),
             );
         }
     };