feat(books): fold More-in-Series + Files into Details bottom tabs, polish series scroller (bookshelf-7ahb0) #1088

Merged
zombor merged 6 commits from bd-bookshelf-7ahb0 into main 2026-07-11 00:14:49 +00:00
Owner

What changed

Restructures the book detail page to match Grimmory: the standalone More in Series and Files top-level tabs are folded into the Details page bottom sub-tab bar.

Top-level tab bar (before → after)

  • Before: Details / Edit Metadata / Fetch Metadata / Files / More in Series
  • After: Details / Edit Metadata / Fetch Metadata (3 tabs only)

Details sub-tab bar (before → after)

  • Before: Similar Books / Notes / Reading History
  • After: More in Series / Similar Books / Files / Notes / Reading History (Grimmory order)

Polish

  • Series number badge: each series card now shows a corner badge (#2, #3) via an absolutely-positioned .cover-card-series-num span inside the .cover-card-image div (no inline style — CSP safe)
  • Series scroller: reuses home-book-strip with scrollbar-width: thin — identical to the Similar Books scroller
  • View full series page link: styled with .series-panel__series-link CSS (accent colour, hover underline) instead of a bare unstyled link; no inline style
  • Old links graceful: ?tab=files and ?tab=series fall through to ?tab=details (removed from validBookTab) — no 404

Files changed

  • internal/books/handler.govalidBookTab trimmed, buildShowSubTabs helper added, SubTabs wired
  • internal/books/handler_test.go — updated tab-fallback tests + new SubTabs coverage
  • templates/pages/books_show.html — top-level nav trimmed, two new sub-tab panels added, old panels removed
  • static/css/main.css.cover-card-series-num corner badge + .series-panel__* styles
  • e2e/browser/journey_book_detail_subtabs_test.go — new Ordered browser journey

Test plan

  • make test — all unit tests pass including new SubTabs handler tests
  • make coverage — 100% coverage gate passes
  • make e2e-policy-check — new browser test is an Ordered journey container
  • make test-policy-check — no white-box tests
  • go build -tags e2e ./... — e2e browser tests compile
  • CI green (waiting on CI run)
  • Browser e2e: seeds a book with series + sibling, navigates to detail page, clicks More in Series and Files sub-tabs, asserts panels become visible with expected content; captures screenshots for PR

Closes bead bookshelf-7ahb0 on merge.

## What changed Restructures the book detail page to match Grimmory: the standalone **More in Series** and **Files** top-level tabs are folded into the **Details** page bottom sub-tab bar. ### Top-level tab bar (before → after) - Before: Details / Edit Metadata / Fetch Metadata / Files / More in Series - After: **Details / Edit Metadata / Fetch Metadata** (3 tabs only) ### Details sub-tab bar (before → after) - Before: Similar Books / Notes / Reading History - After: **More in Series / Similar Books / Files / Notes / Reading History** (Grimmory order) ### Polish - **Series number badge**: each series card now shows a corner badge (#2, #3) via an absolutely-positioned `.cover-card-series-num` span inside the `.cover-card-image` div (no inline style — CSP safe) - **Series scroller**: reuses `home-book-strip` with `scrollbar-width: thin` — identical to the Similar Books scroller - **View full series page link**: styled with `.series-panel__series-link` CSS (accent colour, hover underline) instead of a bare unstyled link; no inline style - **Old links graceful**: `?tab=files` and `?tab=series` fall through to `?tab=details` (removed from `validBookTab`) — no 404 ### Files changed - `internal/books/handler.go` — `validBookTab` trimmed, `buildShowSubTabs` helper added, `SubTabs` wired - `internal/books/handler_test.go` — updated tab-fallback tests + new SubTabs coverage - `templates/pages/books_show.html` — top-level nav trimmed, two new sub-tab panels added, old panels removed - `static/css/main.css` — `.cover-card-series-num` corner badge + `.series-panel__*` styles - `e2e/browser/journey_book_detail_subtabs_test.go` — new Ordered browser journey ## Test plan - [x] `make test` — all unit tests pass including new SubTabs handler tests - [x] `make coverage` — 100% coverage gate passes - [x] `make e2e-policy-check` — new browser test is an Ordered journey container - [x] `make test-policy-check` — no white-box tests - [x] `go build -tags e2e ./...` — e2e browser tests compile - [ ] CI green (waiting on CI run) - Browser e2e: seeds a book with series + sibling, navigates to detail page, clicks More in Series and Files sub-tabs, asserts panels become visible with expected content; captures screenshots for PR Closes bead bookshelf-7ahb0 on merge.
feat(books): fold More-in-Series + Files into Details bottom sub-tabs (bookshelf-7ahb0)
Some checks failed
/ JS Unit Tests (pull_request) Successful in 41s
/ E2E Browser (pull_request) Failing after 2m5s
/ E2E API (pull_request) Successful in 2m23s
/ Lint (pull_request) Successful in 2m59s
/ Integration (pull_request) Successful in 3m7s
/ Test (pull_request) Successful in 6m11s
d2335c0a4b
- Remove standalone Files and More in Series top-level tabs from book detail nav.
  Top-level tabs are now: Details, Edit Metadata, Fetch Metadata.
- Add buildShowSubTabs helper that prepends More in Series (when the book has a
  series) and appends Files before the existing Notes / Reading History sub-tabs.
  New sub-tab order: More in Series, Similar Books, Files, Notes, Reading History.
- Move files table into a new sub-tab panel (subtab-panel-files) inside the
  Details page sub-tab bar; reuses all existing file-action controllers.
- Move series grid into a new sub-tab panel (subtab-panel-series) first in the
  bar; reuses home-book-strip scroller (thin scrollbar already via scrollbar-width: thin).
- Style the View full series page link with canonical series-panel__series-link CSS
  (accent colour, hover underline) and the link class — no inline style (CSP safe).
- Add cover-card-series-num: absolutely-positioned corner badge inside
  cover-card-image (position:relative) showing the series number when > 0.
- Remove files and series from validBookTab; old ?tab=files and ?tab=series links
  fall through to the details tab (no 404, graceful degradation).
- Update handler tests: ?tab=files now falls back to details; ?tab=series falls back.
  Add ShowHandler SubTabs tests verifying the 4/5-tab list via rendered HTML.
- Add browser e2e journey (Ordered) that seeds a series book + sibling, navigates
  to the book detail page, clicks the More in Series and Files sub-tabs, and
  asserts each panel becomes visible with expected content; captures PR screenshots.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(books): fix page.Has() 3-value return in browser e2e test
Some checks failed
/ JS Unit Tests (pull_request) Successful in 32s
/ E2E API (pull_request) Successful in 2m32s
/ Lint (pull_request) Successful in 3m15s
/ Integration (pull_request) Successful in 3m21s
/ E2E Browser (pull_request) Failing after 4m30s
/ Test (pull_request) Successful in 6m30s
d14c7d78fc
rod's page.Has() returns (bool, *Element, error) — not (bool, error).
Fix the two Has() calls in journey_book_detail_subtabs_test.go.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(e2e-browser): use series_name column + update file-replace test for sub-tab nav
All checks were successful
/ JS Unit Tests (pull_request) Successful in 36s
/ E2E API (pull_request) Successful in 2m27s
/ Lint (pull_request) Successful in 3m8s
/ Integration (pull_request) Successful in 3m24s
/ E2E Browser (pull_request) Successful in 4m14s
/ Test (pull_request) Successful in 6m23s
7c70b6571c
Two e2e browser test fixes:
1. journey_book_detail_subtabs: use book_metadata.series_name (not series)
   so the UPDATE succeeds and Book.Series is populated by the handler.
2. journey_book_file_replace: files is now a Details sub-tab, not a top-level
   tab — navigate to the book page and click the Files sub-tab before
   asserting on the book-file-replace controller.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

Security Review — PR #1088 (bookshelf-7ahb0)

Scope: Book detail page restructure — More in Series + Files moved from top-level tabs into the Details sub-tab bar; series-number badge added; series link styled.


Multi-user scoping

No scoping violations introduced. The SeriesBooks and Book.Files slices are populated by the existing Show handler before rendering; the PR does not touch the data-fetch paths. listSeriesBooks is called with showUserLibraryIDs (derived from the authenticated session's library memberships, fail-closed to []int64{}), which is unchanged. The file download links remain at /books/{{$.Book.ID}}/file/{{.ID}} — the existing authz-guarded route — in both the new sub-tab location and the removed top-level panel. No cross-user leak, no ownership-check removal.

XSS / template escaping

All new template output goes through html/template auto-escaping:

  • {{.Title}} in alt= and data-cover-img-title-value= — attribute context, auto-escaped.
  • {{printf "%.4g" .SeriesNumber}}printf returns a plain string; in HTML text node context html/template HTML-escapes it. Numeric float values cannot contain HTML special characters regardless.
  • {{.FileName}}, {{.BookType}}, {{.ArchiveType}} — auto-escaped.
  • {{.SeriesLink}} in href=buildSeriesLink always constructs a root-relative URL (/series/<url.PathEscape(name)>); html/template allows /-prefixed relative paths and additionally attribute-escapes the value. No javascript: injection risk.
  • {{$.Book.ID}} and {{.ID}} in URL paths — int64 values; numeric, no escaping concern.

Inline style= / CSP

No style= attributes added. The series-number badge (.cover-card-series-num) and series panel header (.series-panel__header, .series-panel__series-link) are all CSS class–based in static/css/main.css. CSP style-src 'self' unaffected.

No new unauthenticated surface

validBookTab now excludes "files" and "series" (both fall through to "details"), which is correct — no authz gap. No new route or handler added.


Findings

[MINOR] internal/books/handler_test.go:336 — HTTP call placed in BeforeEach instead of JustBeforeEach
The new Describe("Show handler SubTabs", ...) suite performs the actual handler invocation (HTTP GET + io.ReadAll) inside BeforeEach, violating the project convention that BeforeEach is pure setup and JustBeforeEach is the single point of invocation. This makes it impossible to override setup in nested Context blocks without re-running the call, and breaks the standard Ginkgo expectation-ordering guarantee. The fix is to move the makeServer + HTTP call into a JustBeforeEach, reading into closure-scoped resp/body/respErr vars that It blocks then assert on.


REVIEW VERDICT: 0 blocker, 0 major, 1 minor

## Security Review — PR #1088 (bookshelf-7ahb0) **Scope:** Book detail page restructure — More in Series + Files moved from top-level tabs into the Details sub-tab bar; series-number badge added; series link styled. --- ### Multi-user scoping No scoping violations introduced. The `SeriesBooks` and `Book.Files` slices are populated by the existing `Show` handler before rendering; the PR does not touch the data-fetch paths. `listSeriesBooks` is called with `showUserLibraryIDs` (derived from the authenticated session's library memberships, fail-closed to `[]int64{}`), which is unchanged. The file download links remain at `/books/{{$.Book.ID}}/file/{{.ID}}` — the existing authz-guarded route — in both the new sub-tab location and the removed top-level panel. No cross-user leak, no ownership-check removal. ### XSS / template escaping All new template output goes through `html/template` auto-escaping: - `{{.Title}}` in `alt=` and `data-cover-img-title-value=` — attribute context, auto-escaped. - `{{printf "%.4g" .SeriesNumber}}` — `printf` returns a plain `string`; in HTML text node context `html/template` HTML-escapes it. Numeric float values cannot contain HTML special characters regardless. - `{{.FileName}}`, `{{.BookType}}`, `{{.ArchiveType}}` — auto-escaped. - `{{.SeriesLink}}` in `href=` — `buildSeriesLink` always constructs a root-relative URL (`/series/<url.PathEscape(name)>`); `html/template` allows `/`-prefixed relative paths and additionally attribute-escapes the value. No `javascript:` injection risk. - `{{$.Book.ID}}` and `{{.ID}}` in URL paths — int64 values; numeric, no escaping concern. ### Inline `style=` / CSP No `style=` attributes added. The series-number badge (`.cover-card-series-num`) and series panel header (`.series-panel__header`, `.series-panel__series-link`) are all CSS class–based in `static/css/main.css`. CSP `style-src 'self'` unaffected. ### No new unauthenticated surface `validBookTab` now excludes `"files"` and `"series"` (both fall through to `"details"`), which is correct — no authz gap. No new route or handler added. --- ### Findings [MINOR] internal/books/handler_test.go:336 — HTTP call placed in `BeforeEach` instead of `JustBeforeEach` The new `Describe("Show handler SubTabs", ...)` suite performs the actual handler invocation (HTTP GET + `io.ReadAll`) inside `BeforeEach`, violating the project convention that `BeforeEach` is pure setup and `JustBeforeEach` is the single point of invocation. This makes it impossible to override setup in nested `Context` blocks without re-running the call, and breaks the standard Ginkgo expectation-ordering guarantee. The fix is to move the `makeServer` + HTTP call into a `JustBeforeEach`, reading into closure-scoped `resp`/`body`/`respErr` vars that `It` blocks then assert on. --- REVIEW VERDICT: 0 blocker, 0 major, 1 minor
Author
Owner

CODE REVIEW: APPROVED

Phase 0: DEMO

No DEMO block in bead comments — the bead is a spec-and-work list, not a runnable command. CI is green per requester context. Proceeding on green CI.

Phase 1: Spec Compliance

All six required work items are present: More in Series prepended as first sub-tab, Files folded into sub-tab bar, two top-level tabs removed from nav and from validBookTab, "View full series page" link styled, .home-book-strip reused for the series scroller, and series number badge added in cover-corner position. All panels remain server-rendered in the single page response.

Phase 2: Code Quality

[MINOR] templates/pages/books_show.html:494 — dead link CSS class in series link markup
<a class="series-panel__series-link link"> references a .link utility class that does not exist anywhere in main.css. All styling comes from series-panel__series-link. The dead class is harmless but misleading — remove link from the class list or define the utility.

[MINOR] internal/books/handler.go:1147 vs templates/pages/books_show.html:491 — sub-tab guard condition mismatch
buildShowSubTabs adds the "series" nav button only when book.Series != nil && book.Series.Name != "". The template panel uses {{if .Book.Series}} (nil check only). If a Book.Series pointer is non-nil but Name is empty, the panel renders but its nav button is absent — an orphaned, inaccessible panel. Unlikely in production but logically inconsistent. Fix: {{if and .Book.Series .Book.Series.Name}}.

[MINOR] internal/books/handler_test.go:6217+ — new SubTabs tests violate BeforeEach/JustBeforeEach convention
The two Context blocks under Describe("Show handler SubTabs") both make the HTTP call to the SUT inside BeforeEach (not JustBeforeEach). Per project-conventions.md: BeforeEach is pure setup; JustBeforeEach calls the system under test. Additionally, io.ReadAll errors are silently discarded with _. Tests pass (CI green) but do not follow the canonical pattern — move makeServer(getBook) and the HTTP GET into a JustBeforeEach, and handle the io.ReadAll error.

[MINOR] internal/books/delete_file_handler.go:86, replace_content_handler.go:113, upload_handler.go:121, static/js/controllers/book_file_delete_controller.js:172, book_file_detach_controller.js:184,188 — post-file-operation deep-links are now dead
These redirect/navigate to ?tab=files, which silently falls through to the details tab (first sub-tab active, not Files). After deleting, replacing, uploading, or detaching a file, users land on the Details panel rather than the Files sub-tab. The bead spec explicitly permitted "at minimum default to details without 404", so this is within spec — but these are known dead deep-links worth a follow-up bead.

No issues found on:

  • CSP: no style= attribute in template changes; badge uses .cover-card-series-num CSS class
  • cover-card-image already has position: relative; overflow: hidden — badge absolute positioning works
  • Canonical reuse: .home-book-strip, .status-badge, .btn, .table, --space-* tokens all reused; no bespoke parallel class system
  • Duplicate role="tab" on the sub-tab nav button was correctly removed from the old template
  • Multi-user/scope: data fetching for series and files unchanged; move is purely markup restructuring
  • New browser e2e: Ordered, BeforeAll seeds DB once, BeforeEach calls refreshPageTimeout (go-rod deadline reset), clicks sub-tabs, asserts panel visibility with Eventually — all per spec
  • Test package: package books_test (black-box)
  • Old ?tab=series and ?tab=files handler tests correctly updated to assert fall-through to details

REVIEW VERDICT: 0 blocker, 0 major, 4 minor

CODE REVIEW: APPROVED ## Phase 0: DEMO No DEMO block in bead comments — the bead is a spec-and-work list, not a runnable command. CI is green per requester context. Proceeding on green CI. ## Phase 1: Spec Compliance All six required work items are present: More in Series prepended as first sub-tab, Files folded into sub-tab bar, two top-level tabs removed from nav and from `validBookTab`, "View full series page" link styled, `.home-book-strip` reused for the series scroller, and series number badge added in cover-corner position. All panels remain server-rendered in the single page response. ## Phase 2: Code Quality **[MINOR] `templates/pages/books_show.html:494` — dead `link` CSS class in series link markup** `<a class="series-panel__series-link link">` references a `.link` utility class that does not exist anywhere in `main.css`. All styling comes from `series-panel__series-link`. The dead class is harmless but misleading — remove `link` from the class list or define the utility. **[MINOR] `internal/books/handler.go:1147` vs `templates/pages/books_show.html:491` — sub-tab guard condition mismatch** `buildShowSubTabs` adds the "series" nav button only when `book.Series != nil && book.Series.Name != ""`. The template panel uses `{{if .Book.Series}}` (nil check only). If a `Book.Series` pointer is non-nil but `Name` is empty, the panel renders but its nav button is absent — an orphaned, inaccessible panel. Unlikely in production but logically inconsistent. Fix: `{{if and .Book.Series .Book.Series.Name}}`. **[MINOR] `internal/books/handler_test.go:6217+` — new SubTabs tests violate BeforeEach/JustBeforeEach convention** The two `Context` blocks under `Describe("Show handler SubTabs")` both make the HTTP call to the SUT inside `BeforeEach` (not `JustBeforeEach`). Per `project-conventions.md`: `BeforeEach` is pure setup; `JustBeforeEach` calls the system under test. Additionally, `io.ReadAll` errors are silently discarded with `_`. Tests pass (CI green) but do not follow the canonical pattern — move `makeServer(getBook)` and the HTTP GET into a `JustBeforeEach`, and handle the `io.ReadAll` error. **[MINOR] `internal/books/delete_file_handler.go:86`, `replace_content_handler.go:113`, `upload_handler.go:121`, `static/js/controllers/book_file_delete_controller.js:172`, `book_file_detach_controller.js:184,188` — post-file-operation deep-links are now dead** These redirect/navigate to `?tab=files`, which silently falls through to the `details` tab (first sub-tab active, not Files). After deleting, replacing, uploading, or detaching a file, users land on the Details panel rather than the Files sub-tab. The bead spec explicitly permitted "at minimum default to details without 404", so this is within spec — but these are known dead deep-links worth a follow-up bead. **No issues found on:** - CSP: no `style=` attribute in template changes; badge uses `.cover-card-series-num` CSS class - `cover-card-image` already has `position: relative; overflow: hidden` — badge absolute positioning works - Canonical reuse: `.home-book-strip`, `.status-badge`, `.btn`, `.table`, `--space-*` tokens all reused; no bespoke parallel class system - Duplicate `role="tab"` on the sub-tab nav button was correctly removed from the old template - Multi-user/scope: data fetching for series and files unchanged; move is purely markup restructuring - New browser e2e: `Ordered`, `BeforeAll` seeds DB once, `BeforeEach` calls `refreshPageTimeout` (go-rod deadline reset), clicks sub-tabs, asserts panel visibility with `Eventually` — all per spec - Test package: `package books_test` (black-box) - Old `?tab=series` and `?tab=files` handler tests correctly updated to assert fall-through to `details` REVIEW VERDICT: 0 blocker, 0 major, 4 minor
Author
Owner

Book detail: More in Series sub-tab active

Book detail: More in Series sub-tab active

**Book detail: More in Series sub-tab active** ![Book detail: More in Series sub-tab active](/attachments/53995dcb-6c30-44f0-aa80-20051febdb22)
Author
Owner

Book detail: Files sub-tab active

Book detail: Files sub-tab active

**Book detail: Files sub-tab active** ![Book detail: Files sub-tab active](/attachments/e6f22deb-488c-4d91-8a2d-e180d0e2012f)
Author
Owner

UI Review — PR #1088 (bookshelf-7ahb0) · Details page sub-tab restructure

Screenshots read and reviewed (More in Series panel + Files panel). Source cross-checked against static/css/main.css and templates/pages/books_show.html on origin/bd-bookshelf-7ahb0.


What looks correct

  • Sub-tab bar ("More in Series / Similar Books / Files / Notes / Reading History"): uses the canonical .subtab-nav / .subtab-btn / .subtab-btn--active pattern with the accent-color underline indicator. Spacing and alignment match the existing Similar Books / Notes / Reading History tabs that were already on the page.
  • Horizontal scroller in More in Series: correctly reuses the canonical .home-book-strip class — not a bespoke parallel scroller. Card sizing and spacing are visually consistent with the dashboard scrollers.
  • Cover cards inside the scroller: use canonical .cover-card / .cover-card-image / .cover-card-meta / .cover-card-title throughout. No bespoke card class.
  • Series-number badge (#2): positioned bottom-right of the cover image, readable, not overlapping the title. Sits inside cover-card-image which is position:relative — layout is sound.
  • "View full series page →" link: rendered in accent blue, horizontally left-aligned with reasonable vertical spacing before the scroller. Styled — not unstyled.
  • Files sub-tab table: uses canonical .table, .btn, .btn-sm, .btn--secondary, .btn-danger-ghost throughout. Delete button rendered in danger styling as expected. No bespoke button classes.
  • No inline style= attributes anywhere in the changed files.
  • No bespoke parallel component system — this PR does not re-invent modals, form rows, or tab patterns from scratch.

Findings

[MINOR] static/css/main.css:1302.cover-card-series-num uses hardcoded color: #fff and background: rgba(0, 0, 0, 0.65) instead of CSS variable tokens

color: #fff should be color: var(--bg) (or a new --overlay-text token), and rgba(0,0,0,0.65) should follow the project token convention. The codebase has many existing rgba() literals for overlays, so this is consistent with the current codebase norm, but the #fff hard-code is a clear token miss. Fix: color: var(--bg); and optionally extract --overlay-bg: rgba(0,0,0,0.65) at :root to match the one existing --danger-alpha pattern.

[MINOR] static/css/main.css:1316.series-panel__series-link is partially redundant alongside the base a { color: var(--accent) } rule

The bespoke class adds only font-size: 0.875rem and text-decoration: none beyond what the base a selector already provides. This is a lightweight touch (not a parallel class system), but it introduces a named class for two properties that could be handled by a shared utility (e.g., font-size: 0.875rem via a .text-sm util) or dropped entirely since a already suppresses underline by default in this stylesheet. Low-friction to simplify on a follow-up.


REVIEW VERDICT: 0 blocker, 0 major, 2 minor

## UI Review — PR #1088 (bookshelf-7ahb0) · Details page sub-tab restructure Screenshots read and reviewed (More in Series panel + Files panel). Source cross-checked against `static/css/main.css` and `templates/pages/books_show.html` on `origin/bd-bookshelf-7ahb0`. --- ### What looks correct - **Sub-tab bar** ("More in Series / Similar Books / Files / Notes / Reading History"): uses the canonical `.subtab-nav` / `.subtab-btn` / `.subtab-btn--active` pattern with the accent-color underline indicator. Spacing and alignment match the existing Similar Books / Notes / Reading History tabs that were already on the page. - **Horizontal scroller** in More in Series: correctly reuses the canonical `.home-book-strip` class — not a bespoke parallel scroller. Card sizing and spacing are visually consistent with the dashboard scrollers. - **Cover cards** inside the scroller: use canonical `.cover-card` / `.cover-card-image` / `.cover-card-meta` / `.cover-card-title` throughout. No bespoke card class. - **Series-number badge** (`#2`): positioned bottom-right of the cover image, readable, not overlapping the title. Sits inside `cover-card-image` which is `position:relative` — layout is sound. - **"View full series page →" link**: rendered in accent blue, horizontally left-aligned with reasonable vertical spacing before the scroller. Styled — not unstyled. - **Files sub-tab table**: uses canonical `.table`, `.btn`, `.btn-sm`, `.btn--secondary`, `.btn-danger-ghost` throughout. Delete button rendered in danger styling as expected. No bespoke button classes. - **No inline `style=`** attributes anywhere in the changed files. - **No bespoke parallel component system** — this PR does not re-invent modals, form rows, or tab patterns from scratch. --- ### Findings **[MINOR] `static/css/main.css:1302` — `.cover-card-series-num` uses hardcoded `color: #fff` and `background: rgba(0, 0, 0, 0.65)` instead of CSS variable tokens** `color: #fff` should be `color: var(--bg)` (or a new `--overlay-text` token), and `rgba(0,0,0,0.65)` should follow the project token convention. The codebase has many existing `rgba()` literals for overlays, so this is consistent with the current codebase norm, but the `#fff` hard-code is a clear token miss. Fix: `color: var(--bg);` and optionally extract `--overlay-bg: rgba(0,0,0,0.65)` at `:root` to match the one existing `--danger-alpha` pattern. **[MINOR] `static/css/main.css:1316` — `.series-panel__series-link` is partially redundant alongside the base `a { color: var(--accent) }` rule** The bespoke class adds only `font-size: 0.875rem` and `text-decoration: none` beyond what the base `a` selector already provides. This is a lightweight touch (not a parallel class system), but it introduces a named class for two properties that could be handled by a shared utility (e.g., `font-size: 0.875rem` via a `.text-sm` util) or dropped entirely since `a` already suppresses underline by default in this stylesheet. Low-friction to simplify on a follow-up. --- REVIEW VERDICT: 0 blocker, 0 major, 2 minor
- Remove undefined .link CSS class from series-panel__series-link anchor
- Match series panel template guard to handler condition (Series.Name != "")
- Replace hardcoded #fff badge color with var(--fg) design token

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move HTTP GET + io.ReadAll out of BeforeEach into JustBeforeEach per convention
- Handle io.ReadAll error instead of discarding with _
- Add context for series with empty Name to verify the guard fix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(books): restore files sub-tab deep-link via ?tab=details&subtab=files
All checks were successful
/ E2E API (pull_request) Successful in 3m9s
/ JS Unit Tests (pull_request) Successful in 1m20s
/ Lint (pull_request) Successful in 4m19s
/ Integration (pull_request) Successful in 4m21s
/ E2E Browser (pull_request) Successful in 5m3s
/ Test (pull_request) Successful in 8m24s
5db9e718f6
- Update 3 server-side redirects (delete, replace, upload) to use
  ?tab=details&subtab=files so the browser lands on the Files sub-tab
- Update sub_tab_controller.js connect() to read ?subtab= URL param and
  activate the matching panel (fallback to first panel)
- Update JS navigation in book_file_delete_controller.js and
  book_file_detach_controller.js (2 call sites)
- Update Go and JS tests to assert the new URL format
- Extend browser e2e journey to assert Files sub-tab is active after
  direct deep-link navigation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

CODE REVIEW (re-review of fix commits): APPROVED

Focused re-review of the three fix commits (73e62fb5, efbc0135, 5db9e718) on top of the previously approved base.

What was checked

73e62fb5 fix(ui) — template + CSS fixes

  • .link class removed from <a class="series-panel__series-link"> as intended.
  • Series panel guard changed to {{if and .Book.Series .Book.Series.Name}} — matches the server-side buildShowSubTabs condition (series tab only appended when book.Series != nil && book.Series.Name != ""). Consistent.
  • Badge color: color: var(--fg) resolves to #e2e4ef (off-white) on background: rgba(0,0,0,0.65). Light on dark — correct. The bug was var(--bg) (#0f1117, near-black on near-black), which would have been invisible.

efbc0135 fix(test) — handler_test.go SubTabs corrections

  • SUT call correctly moved from BeforeEach to JustBeforeEach in all three Context blocks. ✓
  • io.ReadAll error captured into respErr; else branch assigns the Get error when resp == nil. Correct per http.Client contract. ✓
  • Empty-series-name Context added; Expect(body, respErr).NotTo(...) folds the nil-error check. ✓
  • Package declared books_test — black-box. ✓

5db9e718 fix(books): restore files sub-tab deep-link

Safety of ?subtab= param:
sub_tab_controller.js:33–36params.get("subtab") result is matched via el.dataset.panelId === requested (string equality against actual DOM panel elements). If no panel matches, falls back to panelTargets[0].dataset.panelId. The URL value is never used as a CSS selector or passed to innerHTML. No injection surface. ✓

Interaction with outer book_tabs_controller:
book_tabs_controller._activate uses url.searchParams.set("tab", panelId) — preserves ?subtab= in the URL unchanged. sub_tab_controller.connect() runs after (inner controller, DOM order), reads ?subtab=files correctly. No race or overwrite. ✓

Default preserved: when no ?subtab= is present, panelTargets[0] is used. Covered by Vitest "shows the first panel on connect". ✓

Nonexistent param: find() returns undefined → falsy → fallback to first panel. Covered by Vitest "keeps the first panel active when ?subtab= param does not match any panel". ✓

CSP-safe: no style= attribute written, no eval. el.hidden and classList.toggle only. ✓

Redirect consistency: all five callsites updated to ?tab=details&subtab=files: delete_file_handler.go:86, replace_content_handler.go:113, upload_handler.go:120, book_file_delete_controller.js:172, book_file_detach_controller.js:184+188. ✓

Server-side ?tab=details renders details panel without hidden (validBookTab["details"] == true, template: {{if ne .ActiveTab "details"}} hidden{{end}}), so sub_tab_controller is connected immediately, not inside a hidden panel. ✓

Browser e2e: Ordered journey, page = refreshPageTimeout(page) per-It timeout reset, Eventually(5s/100ms) polling #subtab-panel-files .hidden property. Correct go-rod pattern. ✓


[MINOR] internal/books/upload_handler_test.go:232, internal/books/replace_content_handler_test.go:221 — These two tests assert StatusSeeOther but not the Location header after the redirect URL changed to ?tab=details&subtab=files. delete_file_handler_test.go was updated to assert the new Location. The gap is pre-existing (these tests never checked Location on main either), but the fix commit left the test coverage inconsistent across the three handlers. Not a blocker — the browser e2e covers the full round-trip — but worth closing for symmetry.


REVIEW VERDICT: 0 blocker, 0 major, 1 minor

## CODE REVIEW (re-review of fix commits): APPROVED Focused re-review of the three fix commits (73e62fb5, efbc0135, 5db9e718) on top of the previously approved base. ### What was checked **73e62fb5 fix(ui) — template + CSS fixes** - `.link` class removed from `<a class="series-panel__series-link">` as intended. - Series panel guard changed to `{{if and .Book.Series .Book.Series.Name}}` — matches the server-side `buildShowSubTabs` condition (series tab only appended when `book.Series != nil && book.Series.Name != ""`). Consistent. - Badge color: `color: var(--fg)` resolves to `#e2e4ef` (off-white) on `background: rgba(0,0,0,0.65)`. Light on dark — correct. The bug was `var(--bg)` (`#0f1117`, near-black on near-black), which would have been invisible. **efbc0135 fix(test) — handler_test.go SubTabs corrections** - SUT call correctly moved from `BeforeEach` to `JustBeforeEach` in all three `Context` blocks. ✓ - `io.ReadAll` error captured into `respErr`; `else` branch assigns the `Get` error when `resp == nil`. Correct per `http.Client` contract. ✓ - Empty-series-name `Context` added; `Expect(body, respErr).NotTo(...)` folds the nil-error check. ✓ - Package declared `books_test` — black-box. ✓ **5db9e718 fix(books): restore files sub-tab deep-link** *Safety of `?subtab=` param:* `sub_tab_controller.js:33–36` — `params.get("subtab")` result is matched via `el.dataset.panelId === requested` (string equality against actual DOM panel elements). If no panel matches, falls back to `panelTargets[0].dataset.panelId`. The URL value is never used as a CSS selector or passed to innerHTML. No injection surface. ✓ *Interaction with outer `book_tabs_controller`:* `book_tabs_controller._activate` uses `url.searchParams.set("tab", panelId)` — preserves `?subtab=` in the URL unchanged. `sub_tab_controller.connect()` runs after (inner controller, DOM order), reads `?subtab=files` correctly. No race or overwrite. ✓ *Default preserved:* when no `?subtab=` is present, `panelTargets[0]` is used. Covered by Vitest "shows the first panel on connect". ✓ *Nonexistent param:* `find()` returns undefined → falsy → fallback to first panel. Covered by Vitest "keeps the first panel active when ?subtab= param does not match any panel". ✓ *CSP-safe:* no `style=` attribute written, no `eval`. `el.hidden` and `classList.toggle` only. ✓ *Redirect consistency:* all five callsites updated to `?tab=details&subtab=files`: `delete_file_handler.go:86`, `replace_content_handler.go:113`, `upload_handler.go:120`, `book_file_delete_controller.js:172`, `book_file_detach_controller.js:184+188`. ✓ *Server-side `?tab=details` renders details panel without `hidden`* (`validBookTab["details"] == true`, template: `{{if ne .ActiveTab "details"}} hidden{{end}}`), so `sub_tab_controller` is connected immediately, not inside a hidden panel. ✓ *Browser e2e:* `Ordered` journey, `page = refreshPageTimeout(page)` per-It timeout reset, `Eventually(5s/100ms)` polling `#subtab-panel-files` `.hidden` property. Correct go-rod pattern. ✓ --- [MINOR] `internal/books/upload_handler_test.go:232`, `internal/books/replace_content_handler_test.go:221` — These two tests assert `StatusSeeOther` but not the `Location` header after the redirect URL changed to `?tab=details&subtab=files`. `delete_file_handler_test.go` was updated to assert the new Location. The gap is pre-existing (these tests never checked Location on main either), but the fix commit left the test coverage inconsistent across the three handlers. Not a blocker — the browser e2e covers the full round-trip — but worth closing for symmetry. --- REVIEW VERDICT: 0 blocker, 0 major, 1 minor
zombor force-pushed bd-bookshelf-7ahb0 from 5db9e718f6
All checks were successful
/ E2E API (pull_request) Successful in 3m9s
/ JS Unit Tests (pull_request) Successful in 1m20s
/ Lint (pull_request) Successful in 4m19s
/ Integration (pull_request) Successful in 4m21s
/ E2E Browser (pull_request) Successful in 5m3s
/ Test (pull_request) Successful in 8m24s
to 8c6015263c
All checks were successful
/ E2E API (pull_request) Successful in 2m55s
/ JS Unit Tests (pull_request) Successful in 1m10s
/ Integration (pull_request) Successful in 3m49s
/ Lint (pull_request) Successful in 3m58s
/ E2E Browser (pull_request) Successful in 3m32s
/ Test (pull_request) Successful in 7m31s
2026-07-11 00:06:54 +00:00
Compare
zombor merged commit f085dd7991 into main 2026-07-11 00:14:49 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
zombor/pergamum!1088
No description provided.