Merge file view into tree handler, fix blank lines
- Remove separate /blob/ route and file handler — tree handler now detects
  whether a path is a dir or file and renders the appropriate view
- All file links use /tree/<path> consistently
- Fix blank lines not rendering: render code body as a single <pre> block
  instead of per-line <pre> wrappers (empty <pre> collapses to zero height)
- Remove orphaned file.rs handler and file_entry_url template method

Assisted-by: Owl Alpha via pi
change ukovxsxrpuolzxswrotsrnwmvorxtqsm
commit 6cb66df8848ce06f1ec7824a3b39b2e8256e01ba
author Alpha Chen <alpha@kejadlen.dev>
date
parent ootnkxyu
diff --git a/quire-server/src/quire/web/handlers/file.rs b/quire-server/src/quire/web/handlers/file.rs
deleted file mode 100644
index f0df3aa..0000000
--- a/quire-server/src/quire/web/handlers/file.rs
+++ /dev/null
@@ -1,229 +0,0 @@
-//! Handler for the file (blob) view.
-
-use axum::extract::{Path as AxumPath, State};
-use axum::http::StatusCode;
-use axum::response::{IntoResponse, Response};
-
-use super::super::db;
-use super::super::templates::{Crumb, FileViewTemplate, nav_sections};
-use super::git::RepoView;
-use super::render;
-use crate::Quire;
-
-pub async fn file_view(
-    State(quire): State<Quire>,
-    AxumPath((repo, path)): AxumPath<(String, String)>,
-) -> Response {
-    let repo_display = repo.trim_end_matches(".git").to_string();
-    let repo_name = db::resolve_repo_name(&repo);
-    let git_repo = match quire.repo(&repo_name) {
-        Ok(r) if r.exists() => r,
-        _ => return StatusCode::NOT_FOUND.into_response(),
-    };
-
-    let path_clone = path.clone();
-    let result = tokio::task::spawn_blocking(move || {
-        let reader = RepoView::new(&git_repo);
-        read_file_data(&reader, &path_clone)
-    })
-    .await
-    .unwrap_or(None);
-
-    let file_data = match result {
-        Some(v) => v,
-        None => return StatusCode::NOT_FOUND.into_response(),
-    };
-
-    let crumbs = build_path_crumbs(&repo_display, &path);
-
-    let tmpl = FileViewTemplate {
-        sections: nav_sections(&repo_display, "tree", false),
-        repo: repo_display,
-        crumbs,
-        path,
-        bookmark: file_data.bookmark,
-        sha_short: file_data.sha_short.clone(),
-        sha_head: file_data.sha_short[..file_data.sha_short.len().min(4)].to_string(),
-        sha_tail: file_data.sha_short[file_data.sha_short.len().min(4)..].to_string(),
-        last_change_sha: file_data.last_change_sha,
-        last_change_head: file_data.last_change_head,
-        last_change_tail: file_data.last_change_tail,
-        last_change_msg: file_data.last_change_msg,
-        last_change_author: file_data.last_change_author,
-        last_change_age: file_data.last_change_age,
-        line_count: file_data.line_count,
-        file_size: file_data.file_size,
-        language: file_data.language,
-        mode: file_data.mode,
-        encoding: file_data.encoding,
-        line_ending: file_data.line_ending,
-        line_nums: (1..=file_data.line_count).collect(),
-        lines: file_data.lines,
-    };
-    render(&tmpl)
-}
-
-struct FileData {
-    bookmark: String,
-    sha_short: String,
-    last_change_sha: String,
-    last_change_head: String,
-    last_change_tail: String,
-    last_change_msg: String,
-    last_change_author: String,
-    last_change_age: String,
-    line_count: usize,
-    file_size: String,
-    language: String,
-    mode: String,
-    encoding: String,
-    line_ending: String,
-    lines: Vec<String>,
-}
-
-fn read_file_data(reader: &RepoView<'_>, path: &str) -> Option<FileData> {
-    let bookmark = reader
-        .run(&["symbolic-ref", "--short", "HEAD"])
-        .unwrap_or_else(|| "main".to_string());
-
-    let sha_short = reader
-        .run(&["rev-parse", "--short", "HEAD"])
-        .unwrap_or_else(|| "unknown".to_string());
-
-    // Read the blob content.
-    let blob = reader.run(&["show", &format!("HEAD:{path}")])?;
-
-    // File mode.
-    let mode = reader
-        .run(&["ls-tree", "--format=%(objectmode)", "HEAD", path])
-        .unwrap_or_else(|| "100644".to_string());
-
-    // Last commit that touched this file.
-    let log = reader.run(&["log", "-1", "--format=%H|%s|%an|%cr", "HEAD", "--", path])?;
-    let mut log_parts = log.splitn(4, '|');
-    let last_change_sha = log_parts.next()?.to_string();
-    let last_change_head = last_change_sha[..last_change_sha.len().min(4)].to_string();
-    let last_change_tail =
-        last_change_sha[last_change_sha.len().min(4)..last_change_sha.len().min(8)].to_string();
-    let last_change_msg = log_parts.next().unwrap_or("").to_string();
-    let last_change_author = log_parts.next().unwrap_or("").to_string();
-    let last_change_age = log_parts.next().unwrap_or("").to_string();
-
-    // File size.
-    let file_size = reader
-        .run(&["cat-file", "-s", &format!("HEAD:{path}")])
-        .unwrap_or_default();
-    let file_size = format_file_size(file_size.parse().unwrap_or(0));
-
-    // Detect language from extension.
-    let language = detect_language(path);
-
-    // Detect encoding and line ending from raw blob.
-    let raw = reader.run(&["cat-file", "-p", &format!("HEAD:{path}")])?;
-    let encoding = if raw.is_ascii() { "ascii" } else { "utf-8" };
-    let line_ending = if raw.contains("\r\n") { "crlf" } else { "lf" };
-
-    // Split into lines, HTML-escape each line.
-    let lines: Vec<String> = blob.lines().map(html_escape).collect();
-    let line_count = lines.len();
-
-    Some(FileData {
-        bookmark,
-        sha_short,
-        last_change_sha,
-        last_change_head,
-        last_change_tail,
-        last_change_msg,
-        last_change_author,
-        last_change_age,
-        line_count,
-        file_size,
-        language,
-        mode,
-        encoding: encoding.to_string(),
-        line_ending: line_ending.to_string(),
-        lines,
-    })
-}
-
-fn format_file_size(bytes: u64) -> String {
-    if bytes < 1024 {
-        format!("{bytes} B")
-    } else if bytes < 1024 * 1024 {
-        format!("{:.1} KB", bytes as f64 / 1024.0)
-    } else {
-        format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
-    }
-}
-
-fn detect_language(path: &str) -> String {
-    let ext = path.rsplit('.').next().unwrap_or("");
-    match ext {
-        "rs" => "rust",
-        "toml" => "toml",
-        "md" => "markdown",
-        "js" => "javascript",
-        "ts" => "typescript",
-        "tsx" => "tsx",
-        "jsx" => "jsx",
-        "py" => "python",
-        "go" => "go",
-        "c" => "c",
-        "h" => "c",
-        "cpp" => "cpp",
-        "hpp" => "cpp",
-        "java" => "java",
-        "rb" => "ruby",
-        "sh" => "shell",
-        "bash" => "shell",
-        "zsh" => "shell",
-        "fish" => "shell",
-        "html" => "html",
-        "css" => "css",
-        "scss" => "scss",
-        "json" => "json",
-        "yaml" | "yml" => "yaml",
-        "xml" => "xml",
-        "sql" => "sql",
-        "fnl" => "fennel",
-        "lua" => "lua",
-        "el" => "elisp",
-        "dockerfile" => "dockerfile",
-        "makefile" => "makefile",
-        "lock" => "toml",
-        "txt" => "text",
-        "gitignore" => "gitignore",
-        "gitattributes" => "gitignore",
-        "editorconfig" => "ini",
-        "justfile" => "just",
-        "nix" => "nix",
-        _ => "text",
-    }
-    .to_string()
-}
-
-fn html_escape(s: &str) -> String {
-    s.replace('&', "&amp;")
-        .replace('<', "&lt;")
-        .replace('>', "&gt;")
-        .replace('"', "&quot;")
-}
-
-fn build_path_crumbs(repo: &str, path: &str) -> Vec<Crumb> {
-    let mut crumbs = vec![Crumb::with_href("tree", format!("/{}/tree", repo))];
-    if path.is_empty() {
-        return crumbs;
-    }
-    let segments: Vec<&str> = path.split('/').collect();
-    for (i, seg) in segments.iter().enumerate() {
-        let href = if i == segments.len() - 1 {
-            // Last segment — link to the blob view.
-            format!("/{}/blob/{}", repo, segments[..=i].join("/"))
-        } else {
-            // Intermediate segment — link to the tree view.
-            format!("/{}/tree/{}", repo, segments[..=i].join("/"))
-        };
-        crumbs.push(Crumb::with_href(*seg, href));
-    }
-    crumbs
-}
diff --git a/quire-server/src/quire/web/handlers/mod.rs b/quire-server/src/quire/web/handlers/mod.rs
index 1db2fdc..05e7cf8 100644
--- a/quire-server/src/quire/web/handlers/mod.rs
+++ b/quire-server/src/quire/web/handlers/mod.rs
@@ -1,13 +1,11 @@
 //! Route handlers for the web view.
 
 mod ci;
-mod file;
 mod git;
 mod repo;
 mod tree;
 
 pub use ci::{run_detail, run_list};
-pub use file::file_view;
 pub use repo::repo_home;
 pub use tree::{tree_view, tree_view_path};
 
diff --git a/quire-server/src/quire/web/handlers/tree.rs b/quire-server/src/quire/web/handlers/tree.rs
index a633aa0..50fe98a 100644
--- a/quire-server/src/quire/web/handlers/tree.rs
+++ b/quire-server/src/quire/web/handlers/tree.rs
@@ -1,4 +1,4 @@
-//! Handler for the repository tree browser.
+//! Handler for the repository tree browser and file (blob) view.
 
 use axum::extract::{Path as AxumPath, State};
 use axum::http::StatusCode;
@@ -6,7 +6,9 @@ use axum::response::{IntoResponse, Response};
 
 use super::super::auth::Auth;
 use super::super::db;
-use super::super::templates::{Crumb, TreeEntry, TreeEntryKind, TreeTemplate, nav_sections};
+use super::super::templates::{
+    Crumb, FileViewTemplate, TreeEntry, TreeEntryKind, TreeTemplate, nav_sections,
+};
 use super::git::RepoView;
 use super::render;
 use crate::Quire;
@@ -16,7 +18,7 @@ pub async fn tree_view(
     auth: Auth,
     AxumPath(repo): AxumPath<String>,
 ) -> Response {
-    tree_at_path(quire, repo, String::new(), auth.0).await
+    tree_or_file_at_path(quire, repo, String::new(), auth.0).await
 }
 
 pub async fn tree_view_path(
@@ -24,10 +26,10 @@ pub async fn tree_view_path(
     auth: Auth,
     AxumPath((repo, path)): AxumPath<(String, String)>,
 ) -> Response {
-    tree_at_path(quire, repo, path, auth.0).await
+    tree_or_file_at_path(quire, repo, path, auth.0).await
 }
 
-async fn tree_at_path(quire: Quire, repo: String, path: String, authed: bool) -> Response {
+async fn tree_or_file_at_path(quire: Quire, repo: String, path: String, authed: bool) -> Response {
     let repo_display = repo.trim_end_matches(".git").to_string();
     let repo_name = db::resolve_repo_name(&repo);
     let git_repo = match quire.repo(&repo_name) {
@@ -38,51 +40,94 @@ async fn tree_at_path(quire: Quire, repo: String, path: String, authed: bool) ->
     let path_clone = path.clone();
     let result = tokio::task::spawn_blocking(move || {
         let reader = RepoView::new(&git_repo);
-        let tree_data = read_tree_data(&reader, &path_clone)?;
-        let bookmarks = reader.bookmarks();
-        let tags = reader.tags();
-        let recent_changes = reader.recent_changes_for(Some(&path_clone));
-        Some((tree_data, bookmarks, tags, recent_changes))
+
+        // Try ls-tree first — if it succeeds, this is a directory.
+        if let Some(tree_data) = read_tree_data(&reader, &path_clone) {
+            let bookmarks = reader.bookmarks();
+            let tags = reader.tags();
+            let recent_changes = reader.recent_changes_for(Some(&path_clone));
+            Some(Either::Left((tree_data, bookmarks, tags, recent_changes)))
+        } else {
+            // ls-tree failed — try reading as a file blob.
+            read_file_data(&reader, &path_clone).map(Either::Right)
+        }
     })
     .await
     .unwrap_or(None);
 
-    let (tree_data, bookmarks, tags, recent_changes) = match result {
-        Some(v) => v,
-        None => return StatusCode::NOT_FOUND.into_response(),
-    };
-
-    let crumbs = {
-        let mut c = vec![Crumb::with_href("tree", format!("/{}/tree", repo_display))];
-        if !path.is_empty() {
-            c.push(Crumb::new(
-                path.split('/').next_back().unwrap_or(&path).to_string(),
-            ));
+    match result {
+        Some(Either::Left((tree_data, bookmarks, tags, recent_changes))) => {
+            let crumbs = build_tree_crumbs(&repo_display, &path);
+            let tmpl = TreeTemplate {
+                sections: nav_sections(&repo_display, "tree", authed),
+                repo: repo_display,
+                crumbs,
+                bookmarks,
+                tags,
+                path,
+                bookmark: tree_data.bookmark,
+                sha_short: tree_data.sha_short,
+                entries: tree_data.entries,
+                recent_changes,
+            };
+            render(&tmpl)
         }
-        c
-    };
+        Some(Either::Right(file_data)) => {
+            let crumbs = build_file_crumbs(&repo_display, &path);
+            let line_nums: Vec<usize> = (1..=file_data.line_count).collect();
+            let tmpl = FileViewTemplate {
+                sections: nav_sections(&repo_display, "tree", authed),
+                repo: repo_display.clone(),
+                crumbs,
+                path,
+                bookmark: file_data.bookmark,
+                sha_short: file_data.sha_short.clone(),
+                sha_head: file_data.sha_short[..file_data.sha_short.len().min(4)].to_string(),
+                sha_tail: file_data.sha_short[file_data.sha_short.len().min(4)..].to_string(),
+                last_change_sha: file_data.last_change_sha,
+                last_change_head: file_data.last_change_head,
+                last_change_tail: file_data.last_change_tail,
+                last_change_msg: file_data.last_change_msg,
+                last_change_author: file_data.last_change_author,
+                last_change_age: file_data.last_change_age,
+                line_count: file_data.line_count,
+                file_size: file_data.file_size,
+                language: file_data.language,
+                mode: file_data.mode,
+                encoding: file_data.encoding,
+                line_ending: file_data.line_ending,
+                line_nums,
+                lines: file_data.lines,
+            };
+            render(&tmpl)
+        }
+        None => StatusCode::NOT_FOUND.into_response(),
+    }
+}
 
-    let tmpl = TreeTemplate {
-        sections: nav_sections(&repo_display, "tree", authed),
-        repo: repo_display,
-        crumbs,
-        bookmarks,
-        tags,
-        path,
-        bookmark: tree_data.bookmark,
-        sha_short: tree_data.sha_short,
-        entries: tree_data.entries,
-        recent_changes,
-    };
-    render(&tmpl)
+enum Either<L, R> {
+    Left(L),
+    Right(R),
 }
 
+// ── Tree (directory) view ──────────────────────────────────────
+
 struct TreeData {
     bookmark: String,
     sha_short: String,
     entries: Vec<TreeEntry>,
 }
 
+fn build_tree_crumbs(repo: &str, path: &str) -> Vec<Crumb> {
+    let mut c = vec![Crumb::with_href("tree", format!("/{repo}/tree"))];
+    if !path.is_empty() {
+        c.push(Crumb::new(
+            path.split('/').next_back().unwrap_or(path).to_string(),
+        ));
+    }
+    c
+}
+
 fn read_tree_data(reader: &RepoView<'_>, path: &str) -> Option<TreeData> {
     let bookmark = reader
         .run(&["symbolic-ref", "--short", "HEAD"])
@@ -95,12 +140,11 @@ fn read_tree_data(reader: &RepoView<'_>, path: &str) -> Option<TreeData> {
     let ls_target = if path.is_empty() {
         "HEAD".to_string()
     } else {
-        format!("HEAD:{}", path)
+        format!("HEAD:{path}")
     };
 
     let ls_out = reader.run(&["ls-tree", &ls_target])?;
 
-    // Parse ls-tree output: "<mode> <type> <sha>\t<name>"
     let mut raw: Vec<(TreeEntryKind, String)> = Vec::new();
     for line in ls_out.lines() {
         let Some((meta, name)) = line.split_once('\t') else {
@@ -119,7 +163,6 @@ fn read_tree_data(reader: &RepoView<'_>, path: &str) -> Option<TreeData> {
         raw.push((kind, name.to_string()));
     }
 
-    // Dirs/submodules first, then files; each group alphabetical.
     raw.sort_by(|(ak, an), (bk, bn)| {
         let ao = matches!(ak, TreeEntryKind::Dir | TreeEntryKind::Submodule) as u8;
         let bo = matches!(bk, TreeEntryKind::Dir | TreeEntryKind::Submodule) as u8;
@@ -141,7 +184,7 @@ fn read_tree_data(reader: &RepoView<'_>, path: &str) -> Option<TreeData> {
         let entry_path = if path.is_empty() {
             name.clone()
         } else {
-            format!("{}/{}", path, name)
+            format!("{path}/{name}")
         };
         let commit_info = reader.run(&["log", "-1", "--format=%s|%cr", "HEAD", "--", &entry_path]);
         let (last_msg, age) = commit_info
@@ -164,3 +207,157 @@ fn read_tree_data(reader: &RepoView<'_>, path: &str) -> Option<TreeData> {
         entries,
     })
 }
+
+// ── File (blob) view ───────────────────────────────────────────
+
+struct FileData {
+    bookmark: String,
+    sha_short: String,
+    last_change_sha: String,
+    last_change_head: String,
+    last_change_tail: String,
+    last_change_msg: String,
+    last_change_author: String,
+    last_change_age: String,
+    line_count: usize,
+    file_size: String,
+    language: String,
+    mode: String,
+    encoding: String,
+    line_ending: String,
+    lines: Vec<String>,
+}
+
+fn build_file_crumbs(repo: &str, path: &str) -> Vec<Crumb> {
+    let mut crumbs = vec![Crumb::with_href("tree", format!("/{repo}/tree"))];
+    if path.is_empty() {
+        return crumbs;
+    }
+    let segments: Vec<&str> = path.split('/').collect();
+    for (i, seg) in segments.iter().enumerate() {
+        let href = format!("/{}/tree/{}", repo, segments[..=i].join("/"));
+        crumbs.push(Crumb::with_href(*seg, href));
+    }
+    crumbs
+}
+
+fn read_file_data(reader: &RepoView<'_>, path: &str) -> Option<FileData> {
+    let bookmark = reader
+        .run(&["symbolic-ref", "--short", "HEAD"])
+        .unwrap_or_else(|| "main".to_string());
+
+    let sha_short = reader
+        .run(&["rev-parse", "--short", "HEAD"])
+        .unwrap_or_else(|| "unknown".to_string());
+
+    let blob = reader.run(&["show", &format!("HEAD:{path}")])?;
+
+    let mode = reader
+        .run(&["ls-tree", "--format=%(objectmode)", "HEAD", path])
+        .unwrap_or_else(|| "100644".to_string());
+
+    let log = reader.run(&["log", "-1", "--format=%H|%s|%an|%cr", "HEAD", "--", path])?;
+    let mut log_parts = log.splitn(4, '|');
+    let last_change_sha = log_parts.next()?.to_string();
+    let last_change_head = last_change_sha[..last_change_sha.len().min(4)].to_string();
+    let last_change_tail =
+        last_change_sha[last_change_sha.len().min(4)..last_change_sha.len().min(8)].to_string();
+    let last_change_msg = log_parts.next().unwrap_or("").to_string();
+    let last_change_author = log_parts.next().unwrap_or("").to_string();
+    let last_change_age = log_parts.next().unwrap_or("").to_string();
+
+    let file_size = reader
+        .run(&["cat-file", "-s", &format!("HEAD:{path}")])
+        .unwrap_or_default();
+    let file_size = format_file_size(file_size.parse().unwrap_or(0));
+
+    let language = detect_language(path);
+
+    let raw = reader.run(&["cat-file", "-p", &format!("HEAD:{path}")])?;
+    let encoding = if raw.is_ascii() { "ascii" } else { "utf-8" };
+    let line_ending = if raw.contains("\r\n") { "crlf" } else { "lf" };
+
+    let lines: Vec<String> = blob.lines().map(|l| html_escape(l) + "\n").collect();
+    let line_count = lines.len();
+
+    Some(FileData {
+        bookmark,
+        sha_short,
+        last_change_sha,
+        last_change_head,
+        last_change_tail,
+        last_change_msg,
+        last_change_author,
+        last_change_age,
+        line_count,
+        file_size,
+        language,
+        mode,
+        encoding: encoding.to_string(),
+        line_ending: line_ending.to_string(),
+        lines,
+    })
+}
+
+fn format_file_size(bytes: u64) -> String {
+    if bytes < 1024 {
+        format!("{bytes} B")
+    } else if bytes < 1024 * 1024 {
+        format!("{:.1} KB", bytes as f64 / 1024.0)
+    } else {
+        format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
+    }
+}
+
+fn detect_language(path: &str) -> String {
+    let ext = path.rsplit('.').next().unwrap_or("");
+    match ext {
+        "rs" => "rust",
+        "toml" => "toml",
+        "md" => "markdown",
+        "js" => "javascript",
+        "ts" => "typescript",
+        "tsx" => "tsx",
+        "jsx" => "jsx",
+        "py" => "python",
+        "go" => "go",
+        "c" => "c",
+        "h" => "c",
+        "cpp" => "cpp",
+        "hpp" => "cpp",
+        "java" => "java",
+        "rb" => "ruby",
+        "sh" => "shell",
+        "bash" => "shell",
+        "zsh" => "shell",
+        "fish" => "shell",
+        "html" => "html",
+        "css" => "css",
+        "scss" => "scss",
+        "json" => "json",
+        "yaml" | "yml" => "yaml",
+        "xml" => "xml",
+        "sql" => "sql",
+        "fnl" => "fennel",
+        "lua" => "lua",
+        "el" => "elisp",
+        "dockerfile" => "dockerfile",
+        "makefile" => "makefile",
+        "lock" => "toml",
+        "txt" => "text",
+        "gitignore" => "gitignore",
+        "gitattributes" => "gitignore",
+        "editorconfig" => "ini",
+        "justfile" => "just",
+        "nix" => "nix",
+        _ => "text",
+    }
+    .to_string()
+}
+
+fn html_escape(s: &str) -> String {
+    s.replace('&', "&amp;")
+        .replace('<', "&lt;")
+        .replace('>', "&gt;")
+        .replace('"', "&quot;")
+}
diff --git a/quire-server/src/quire/web/mod.rs b/quire-server/src/quire/web/mod.rs
index 5123102..2467138 100644
--- a/quire-server/src/quire/web/mod.rs
+++ b/quire-server/src/quire/web/mod.rs
@@ -18,7 +18,7 @@ use axum::{Router, routing::get};
 use crate::{
     Quire,
     quire::web::handlers::{
-        config, file_view, repo_home, run_detail, run_list, stylesheet, tree_view, tree_view_path,
+        config, repo_home, run_detail, run_list, stylesheet, tree_view, tree_view_path,
     },
 };
 
@@ -39,7 +39,6 @@ pub fn public_router(quire: Quire) -> Router {
         .route("/{repo}", get(repo_home))
         .route("/{repo}/tree", get(tree_view))
         .route("/{repo}/tree/{*path}", get(tree_view_path))
-        .route("/{repo}/blob/{*path}", get(file_view))
         .route("/config", get(config))
         .with_state(quire)
 }
diff --git a/quire-server/src/quire/web/templates.rs b/quire-server/src/quire/web/templates.rs
index db80e7a..eb95d86 100644
--- a/quire-server/src/quire/web/templates.rs
+++ b/quire-server/src/quire/web/templates.rs
@@ -468,14 +468,6 @@ impl TreeTemplate {
         self.entries.iter().filter(|e| e.is_file()).count()
     }
 
-    pub fn file_entry_url(&self, name: &str) -> String {
-        if self.path.is_empty() {
-            format!("/{}/blob/{}", self.repo, name)
-        } else {
-            format!("/{}/blob/{}/{}", self.repo, self.path, name)
-        }
-    }
-
     pub fn sha_head(&self) -> &str {
         &self.sha_short[..self.sha_short.len().min(4)]
     }
diff --git a/quire-server/static/style.css b/quire-server/static/style.css
index 9b20cd5..9287539 100644
--- a/quire-server/static/style.css
+++ b/quire-server/static/style.css
@@ -806,19 +806,11 @@ html.dark {
 .code-body {
   padding: 18px 56px 18px 20px;
   overflow: auto;
-}
-
-.code-line {
-  white-space: pre;
-}
-
-.code-line pre {
   margin: 0;
-  padding: 0;
+  white-space: pre;
   font-family: inherit;
   font-size: inherit;
   line-height: inherit;
-  white-space: pre;
 }
 
 /* ── Arborium syntax highlighting integration ──────────────────── */
diff --git a/quire-server/templates/file.html b/quire-server/templates/file.html
index 38fe437..f90968a 100644
--- a/quire-server/templates/file.html
+++ b/quire-server/templates/file.html
@@ -30,11 +30,7 @@
         <div class="code-line-num">{{ line_num }}</div>
         {% endfor %}
       </div>
-      <div class="code-body">
-        {% for line in lines %}
-        <div class="code-line"><pre>{{ line|safe }}</pre></div>
-        {% endfor %}
-      </div>
+      <pre class="code-body">{% for line in lines %}{{ line|safe }}{% endfor %}</pre>
     </div>
   </div>
 
diff --git a/quire-server/templates/tree.html b/quire-server/templates/tree.html
index ff0e35f..f093399 100644
--- a/quire-server/templates/tree.html
+++ b/quire-server/templates/tree.html
@@ -61,7 +61,7 @@
       <span class="tree-msg">{{ entry.last_msg }}</span>
       <span class="tree-age">{{ entry.age }}</span>
       {% else %}
-      <a class="tree-name" href="{{ self.file_entry_url(&entry.name) }}">{{ entry.name }}</a>
+      <a class="tree-name" href="{{ self.dir_entry_url(&entry.name) }}">{{ entry.name }}</a>
       <span class="tree-msg">{{ entry.last_msg }}</span>
       <span class="tree-age">{{ entry.age }}</span>
       {% endif %}