darcsweb - reviews/post-claude-webapp.md

[root] / reviews / post-claude-webapp.md
# Post-change web application review — darcsweb

Scope: the uncommitted darcs diff (`/tmp/darcs-diff.txt`) that touches
`app/Main.hs`, `src/DarcsWeb/Config.hs`, `src/DarcsWeb/Darcs.hs`,
`src/DarcsWeb/Html.hs`, `test/Properties/Clone.hs`, and
`test/Properties/Html.hs`. Review focuses on mobile-first design,
consistent behavior, and frontend performance; `static/style.css` was
consulted only where a Haskell-side change has CSS implications.

High-level verification
- Semantic landmarks: the swap from `<div class="page-header/page-body/page-footer">`
  to `<header>/<main>/<footer>` at
  `src/DarcsWeb/Html.hs:41,47,50` and the `<div class="log-entry">` →
  `<article class="log-entry">` swap at `src/DarcsWeb/Html.hs:208,226`
  are class-preserving and do not break the existing CSS (all selectors
  target the classes, not the tag).
- Percent-encoded hrefs: every `esc repoName` / `esc (psHash ps)` used
  inside an `href=` has been replaced with
  `encodePathSegment (...)` (see calls at `src/DarcsWeb/Html.hs:83,101,
  171-172,205-206,232,285-286,321,340,364,368,391,440,455`).
  Scotty's regex routes read from WAI's `pathInfo`, which is already
  percent-decoded, so a repo name or directory with `%`, `#`, `?`,
  space or UTF-8 that was previously a broken link now routes back
  cleanly. `pathSegments` at `src/DarcsWeb/Html.hs:357` guards
  `init []` crashes in the blob breadcrumb for a single-segment path.
- Favicon: `<link rel="icon" type="image/png" href="/static/darcs-logo.png">`
  at `src/DarcsWeb/Html.hs:38` is correct; the file exists in
  `static/darcs-logo.png` and the MIME table in
  `app/Main.hs` (`.png` → `image/png`) matches the `type=`.
- Top-level `<h1>` on the index: `src/DarcsWeb/Html.hs:62` renders the
  configured title once per index page, giving every route a single
  heading landmark. Escaping uses `esc`, which is correct.
- `aria-current="page"`: `navLink` at `src/DarcsWeb/Html.hs:453-458`
  emits `aria-current="page"` only in the `path == active` branch, and
  every caller of `repoNavBar` passes a section name that matches one
  of the five emitted links (`src/DarcsWeb/Html.hs:108,152,196,267,
  308,423`). Patch detail deliberately omits the nav (pre-existing).

---

## Consistency findings

### C1. Empty breadcrumb `<nav aria-label="Breadcrumb">` rendered on index and 404
- Severity: low
- File and line: `src/DarcsWeb/Html.hs:45`, called with `breadcrumbs = ""`
  from `src/DarcsWeb/Html.hs:60` (index) and
  `src/DarcsWeb/Html.hs:434` (404).
- Issue: `renderPage` always emits
  `<nav class="breadcrumbs" aria-label="Breadcrumb">…</nav>` even when
  `breadcrumbs` is empty. On the index page and on 404 the nav is
  empty, but the landmark is present in the accessibility tree because
  `aria-label` forces it to be exposed regardless of content.
- Why it matters for users: screen-reader users landing on the index
  or a 404 will hear a "Breadcrumb navigation" landmark announced with
  zero items. The same pages on other routes do have crumb entries, so
  the landmark set is inconsistent across pages.
- Practical fix: in `renderPage`, only emit the breadcrumb `<nav>`
  when `breadcrumbs` is non-empty, e.g.
  `if T.null breadcrumbs then "" else "<nav class=…>" <> breadcrumbs <> "</nav>\n"`.
  No CSS change needed.

### C2. Breadcrumb contents start with a leading `" / "`
- Severity: low
- File and line: `src/DarcsWeb/Html.hs:102, 149, 193, 233, 264, 305,
  420` (every non-index page) and `src/DarcsWeb/Html.hs:440`
  (`repoBreadcrumb`).
- Issue: every crumb string begins with `" / <a …>"`. Previously this
  sat inside a `<nav class="breadcrumbs">` with no `aria-label`; now
  `aria-label="Breadcrumb"` promotes it to a first-class landmark, so
  the leading bare slash is the first thing a screen reader reads —
  "Breadcrumb navigation, slash, repo-name, link".
- Why it matters for users: the inconsistency between the visible
  separator (`" / "`) and the semantic meaning of the label worsens
  now that the landmark is explicit. Also diverges from how
  `renderTreeBreadcrumb` builds crumbs (no leading slash, anchored by
  a `[root]` link).
- Practical fix: have `repoBreadcrumb` return an unprefixed crumb
  (`<a href=…>repo</a>`) and let callers prepend their own separator
  only when chaining further segments; or render breadcrumbs as an
  ordered list (`<ol>`) and drop the inline separators entirely.

### C3. Tree breadcrumb ends with a trailing `" / "` when at the subtree root entry
- Severity: low
- File and line: `src/DarcsWeb/Html.hs:305` and
  `src/DarcsWeb/Html.hs:318-332`.
- Issue: `renderTree` composes a header-bar breadcrumb by concatenating
  `repoBreadcrumb repoName <> " / tree" <> pathSuffix` where
  `pathSuffix` is `" / " <> esc subPath`. The in-body tree breadcrumb
  (the `.tree-path` rendered by `renderTreeBreadcrumb`) is built
  independently from `pathSegments` and emits a distinct trail. Both
  now live in the same page, each with different escaping rules
  (header bar uses raw `esc subPath`, body uses per-segment
  percent-encoded hrefs), which is visually and semantically
  redundant.
- Why it matters for users: a mobile user with a narrow viewport sees
  the same path fragment rendered twice in two different styles, the
  top one wrapping inside the sticky header, the bottom one scrolling
  horizontally. Tapping them produces the same destinations, but the
  URLs built by each branch are built by different code paths and
  can drift.
- Practical fix: drive both from `pathSegments` + `encodePathList` so
  that the two trails cannot disagree, or drop the in-body
  `.tree-path` when the header-bar breadcrumb already carries the
  subpath.

### C4. Index `<h1>` duplicates the value in `<title>` but tag nav is hidden on index
- Severity: low (inferred)
- File and line: `src/DarcsWeb/Html.hs:62,67`.
- Issue: the index page now renders `<h1>siteTitle</h1>` immediately
  followed by the repo table. There is no `repoNavBar` on the index
  (correct: no repo), but because the header `<nav>` becomes empty
  too (see C1) the index has exactly one nav landmark (the
  still-emitted empty one), while per-repo pages have three (header
  breadcrumbs, `repo-nav`, footer-less) — a consistency gap worth
  collapsing once C1 is fixed.
- Why it matters for users: screen-reader "list landmarks" / "list
  navigations" output changes shape when moving between the index and
  any repo page.
- Practical fix: combined with C1, skip the empty breadcrumb nav on
  the index and 404 to keep the landmark set stable.

---

## Mobile-first / design findings

### D1. `<article class="log-entry">` inherits the desktop `box-shadow` hover rule on touch devices
- Severity: low
- File and line: `src/DarcsWeb/Html.hs:208` (tag change);
  rule in `static/style.css` lines 474-486.
- Issue: the swap from `div` to `<article>` does not break the
  selector (both are class-matched), but the `.log-entry:hover` rule
  is now attached to a block that represents a full landmark in the
  page. On mobile Safari / Chrome the `:hover` state sticks after the
  first tap until the next tap elsewhere, so "hovered" cards linger
  with `box-shadow: var(--shadow-md)` after navigation.
- Why it matters for users: phantom hover shadows accumulate on
  scrolling lists of patches; this was the same on desktop, but
  `<article>` is more likely to be interactive-looking, so users will
  tap directly on the card rather than the inner link.
- Practical fix: gate the hover shadow to pointer devices, e.g.
  wrap the `:hover` rule in `@media (hover: hover)`. CSS-only change,
  not part of this review's scope, but flagged because the Haskell
  change makes the card feel more clickable.

### D2. `<header class="page-header">` stays sticky while the breadcrumb `<nav>` can wrap to a second line
- Severity: low (inferred)
- File and line: `src/DarcsWeb/Html.hs:41-46` combined with
  `static/style.css` lines 184-196 (`position: sticky; top: 0;`,
  `flex-wrap: wrap`).
- Issue: on very long breadcrumb trails (deep tree subpath) the
  breadcrumb `<nav>` wraps, making the sticky `<header>` grow to two
  or three lines and eat a large share of a phone viewport. The
  previous code had the same behavior, but the `<header>` landmark
  and `aria-label="Breadcrumb"` promote the region to a more
  prominent role, so the vertical cost is more visible.
- Why it matters for users: on a 360×640 viewport a three-line
  sticky header can consume ~30% of vertical space above the fold.
- Practical fix: cap the breadcrumb height with
  `max-height` + `overflow-x: auto` on the nav so it scrolls
  horizontally instead of wrapping vertically. CSS-only; flagging
  only because the diff touches this layout.

---

## Performance findings

### P1. `renderTreeBreadcrumb` / `renderBlobBreadcrumb` rebuild prefix lists in O(n²)
- Severity: low
- File and line: `src/DarcsWeb/Html.hs:322-323, 344`.
- Issue: `prefixes = drop 1 (scanl appendSeg [] parts)` where
  `appendSeg acc p = acc ++ [p]` rebuilds the prefix list with
  list-append at every step, so the total work is O(depth²) in the
  number of segments, and each prefix is re-percent-encoded from
  scratch by `encodePathList`.
- Why it matters for users: negligible for typical repo depths (≤10
  segments), but rendering a tree page for a deep path walks this
  cost twice (once for the header-bar crumb via `pathSuffix`, once in
  the `.tree-path` body). It is wasted server CPU on every tree hit.
- Practical fix: build hrefs incrementally by threading the
  percent-encoded accumulator through a single `foldl`:
  `scanl (\acc seg -> acc <> "/" <> encodePathSegment seg) "" parts`.
  O(depth) time, one pass, and removes the intermediate `[[Text]]`
  allocation.

### P2. `encodePathSegment` is re-applied to the same repo name on every row of every table
- Severity: low
- File and line: `src/DarcsWeb/Html.hs:83,86,92,93` (repo row),
  `src/DarcsWeb/Html.hs:171,178,185` (shortlog row),
  `src/DarcsWeb/Html.hs:205,211` (full-log row),
  `src/DarcsWeb/Html.hs:285,290,296` (tag row),
  `src/DarcsWeb/Html.hs:368,377,391,395,396` (tree row).
- Issue: each `render*Row` re-runs `encodePathSegment repoName`
  (byte-by-byte UTF-8 encode + hex escape) for every table row. A
  tree with 500 entries pays this cost 500 times, even though
  `repoName` never changes across a single render call.
- Why it matters for users: for small repos negligible, but a large
  shortlog (thousands of patches) spends measurable time
  re-encoding the same segment. It also materialises N copies of
  the same `Text` via `T.concat` / `T.pack`.
- Practical fix: precompute `nameSeg` once in the caller
  (`renderRepoTable`, `renderShortLogTable`, `renderFullLog`,
  `renderTagList`, `renderTreeTable`) and pass it into the per-row
  renderer. Same applies to the `"/repo/" <> nameSeg <> "/…"`
  prefixes, which can be baked once per table.

### P3. `renderFullLog` still calls `renderFullLogEntry` with per-entry `encodePathSegment repoName`
- Severity: low
- File and line: `src/DarcsWeb/Html.hs:199,205`.
- Issue: same pattern as P2, but in the full-log path where the
  inner `<pre>` already dominates CPU. Not a hot path, but the
  redundant allocation is trivially avoidable.
- Why it matters for users: mostly GC pressure. Mobile browsers are
  not affected; server-side throughput is.
- Practical fix: same as P2 — hoist `nameSeg` to `renderFullLog` and
  pass it in.

---

## Areas worth testing on real mobile devices

- A sticky-header summary page on a 360×640 viewport with a very
  long repository name — verify the breadcrumb wraps without
  pushing the `<main>` below the fold.
- Tap-hold / long-press on `<article class="log-entry">` on iOS
  Safari; confirm the hover shadow does not linger after scroll
  (D1).
- Tree view with a directory whose name contains `#`, `?`, `%`,
  space, or non-ASCII (e.g. `café`). Previously the browser
  auto-encoded these; now they are pre-encoded server-side.
  Navigating, pressing "back", and copying the URL should all
  round-trip identically.
- `aria-current="page"` announcement on VoiceOver / TalkBack for
  the active tab in `.repo-nav`, both when landing on the section
  directly and after client-side back/forward.

## Performance paths worth measuring

- Rendering time of `/repo/:name/tree/<deep>` for a path with
  ~20 segments — verify the O(depth²) crumb build (P1) is not
  visible in a flamegraph.
- Shortlog / full-log render time on a repo with many patches
  (thousands) — verify that hoisting `nameSeg` per table (P2/P3)
  actually reduces allocation.
- Favicon fetch: the linked `/static/darcs-logo.png` is a 10 KiB
  PNG served with `Cache-Control: public, max-age=86400, immutable`
  (`app/Main.hs:387`). On a cold cache a mobile client downloads
  10 KiB for what browsers typically display at 16×16 — worth
  measuring LCP impact and considering an optimized 16×16 /
  32×32 `.ico` or inline SVG. The diff only adds the `<link>`;
  the asset itself is unchanged.

## Consistency risks that could not be fully verified from code alone

- Whether Scotty 0.22's regex routes apply `pathInfo` (decoded)
  consistently in all versions installed via `stack.yaml` — the
  percent-encoding round-trip assumes that. Claim supported by
  code inspection but not by a runtime test.
- Whether all previously-emitted raw hrefs (escaped via `esc`) are
  now routed through `encodePathSegment`. I checked every
  `href="/repo/` / `href="/clone/` site in `Html.hs`; a grep for
  `esc repoName` inside `href=` in the module returns no results
  (only inside visible anchor text). Verified.
- Whether existing bookmarks that used unencoded URLs (i.e., with
  a literal space or `#`) will still work. They will not: the
  server never decoded them correctly before either, so those
  bookmarks were already broken — but this is worth mentioning in
  release notes.