Skip to content

Commit a46e9ca

Browse files
authored
Fold the long-tag fix into the v4.1.22 cut so the tag ships documented (#1176)
Supersedes #1168, which went CONFLICTING because 41905a9 (the #1171 tag-pill fix) had already carried the v4.1.22 CHANGELOG header and whatsNew block onto main. Moves the #1170 entry out of [Unreleased] into the v4.1.22 Fixed section, adds the matching What's New item so the shipped SPA documents the build it ships in, and backfills the release cell on the #1171 CHANGES row. [Unreleased] is now empty, which is the correct pre-tag state. Docs and release-data only, no production code path. tsc --noEmit clean, SPA build clean, test_whats_new_spa.py + test_533_version_release_link.py 16/16 green, Test Suite Summary green. The advisory E2E red is the pre-existing ~35px CI-baseline detail-page overflow tracked in notes/e2e-mobile-overflow-ci-triage.md, re-measured independently this tick at 0px locally on both the guest and admin paths.
1 parent 75473e5 commit a46e9ca

3 files changed

Lines changed: 8 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,6 @@ is for things you can see or feel when running the app.
1616

1717
## [Unreleased]
1818

19-
### Fixed
20-
21-
- Book pages no longer scroll sideways on a phone when a book carries long
22-
subject tags. Libraries with Library-of-Congress headings (for example
23-
"France -- History -- Revolution, 1789-1799 -- Fiction") pushed the tag row
24-
off the edge of the screen for anyone reading without an editor account,
25-
including guests. Long tags now wrap instead. (#1170)
26-
2719
## [v4.1.22] - 2026-07-27
2820

2921
### Changed
@@ -48,6 +40,8 @@ is for things you can see or feel when running the app.
4840

4941
- **Opening your library no longer runs the same search twice.** Every visit to the library asked the server for the first page of books, then immediately asked for it again at a different size and threw the first answer away. Nothing looked wrong — the wasted answer never reached the screen — but on a large library that first page is the slowest query on it, and it was being run twice on every load, for every reader. The grid now waits until it knows how many columns it has before asking, so the page is fetched once ([#1144](https://github.com/new-usemame/Calibre-Web-NextGen/issues/1144)).
5042

43+
- **Book pages no longer scroll sideways on a phone when a book carries long subject tags.** Libraries catalogued with Library-of-Congress subject headings — "France -- History -- Revolution, 1789-1799 -- Fiction" and the like — carry tags wider than a phone screen, and the tag row would not break one onto a second line, so the whole book page could be dragged sideways. It hit anyone reading without an editor account, guests included, because the editing view already wrapped its tags. Long tags now wrap ([#1170](https://github.com/new-usemame/Calibre-Web-NextGen/issues/1170)).
44+
5145
- **The series line on a book cover is dark enough to read again.** The small series text under a title on each cover was styled with the muted grey the rest of the interface uses, and then faded a second time on top of that. The two together took it to a contrast of about 4:1 against the card, below the 4.5:1 that small text needs to stay legible, which made it hard to read for anyone whose eyesight or screen is less than ideal. The second fade is gone, so the line is the muted grey it was meant to be ([#1135](https://github.com/new-usemame/Calibre-Web-NextGen/issues/1135)).
5246

5347
## [v4.1.21] - 2026-07-25

CHANGES-vs-upstream.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Format: each row is one fork-PR, mapped to its upstream PR or issue (if any), wi
6262

6363
| Fork PR | Upstream issue | Description | SHA | Release |
6464
|---|---|---|---|---|
65-
| [#1171](https://github.com/new-usemame/Calibre-Web-NextGen/pull/1171) | (fork issue [#1170](https://github.com/new-usemame/Calibre-Web-NextGen/issues/1170)) | **The book detail page scrolled horizontally on a phone when a book carried a long subject tag.** The read-only tag pill (`frontend/src/components/Pill.module.css`) was `white-space: nowrap` with no `max-width`, so a Library-of-Congress heading such as `France -- History -- Revolution, 1789-1799 -- Fiction` rendered wider than the wrapping flex row containing it and pushed the document past the viewport. Measured on a stock library as a guest: 12px of overflow at 390px, 82px at 320px, 323px with a single unbroken token. Only the no-edit-role render is affected — the editor branch uses removable chips that already wrap, which is why every admin-authenticated mobile spec missed it. Fix drops `nowrap` and adds `max-width: 100%` + `overflow-wrap: anywhere`; pills that fit still occupy one line, only over-long ones wrap. Verified `max-width` alone with `nowrap` retained does **not** fix it (82px unchanged), confirming `nowrap` as the root cause. Regression test measures the overflow the long tags ADD (baseline vs pathological render), not absolute page overflow, so it holds on any fixture — CI’s detail page carries ~35px of unrelated baseline overflow (see `notes/e2e-mobile-overflow-ci-triage.md`). Red at 323px against a redeployed pre-fix build, green after. | 41905a99e | TBD |
65+
| [#1171](https://github.com/new-usemame/Calibre-Web-NextGen/pull/1171) | (fork issue [#1170](https://github.com/new-usemame/Calibre-Web-NextGen/issues/1170)) | **The book detail page scrolled horizontally on a phone when a book carried a long subject tag.** The read-only tag pill (`frontend/src/components/Pill.module.css`) was `white-space: nowrap` with no `max-width`, so a Library-of-Congress heading such as `France -- History -- Revolution, 1789-1799 -- Fiction` rendered wider than the wrapping flex row containing it and pushed the document past the viewport. Measured on a stock library as a guest: 12px of overflow at 390px, 82px at 320px, 323px with a single unbroken token. Only the no-edit-role render is affected — the editor branch uses removable chips that already wrap, which is why every admin-authenticated mobile spec missed it. Fix drops `nowrap` and adds `max-width: 100%` + `overflow-wrap: anywhere`; pills that fit still occupy one line, only over-long ones wrap. Verified `max-width` alone with `nowrap` retained does **not** fix it (82px unchanged), confirming `nowrap` as the root cause. Regression test measures the overflow the long tags ADD (baseline vs pathological render), not absolute page overflow, so it holds on any fixture — CI’s detail page carries ~35px of unrelated baseline overflow (see `notes/e2e-mobile-overflow-ci-triage.md`). Red at 323px against a redeployed pre-fix build, green after. | 41905a99e | v4.1.22 |
6666
| [#1165](https://github.com/new-usemame/Calibre-Web-NextGen/pull/1165) | (fork issue [#1109](https://github.com/new-usemame/Calibre-Web-NextGen/issues/1109)) | **Books carrying an ISO 639-2/B language code (`ger`, `fre`, `dut`) displayed "Unknown" and were rejected on upload.** `cps/iso_language_names.py`'s `LANGUAGE_NAMES` is keyed on ISO 639-2/**T** only (`deu`, `fra`, `nld`); the 20 bibliographic variants are absent from all 28 locale tables. `get_language_name()` did a bare `names.get(lang_code, "Unknown")`, so a /B code missed the table on every read path — book detail (`cps/web.py:1886`, `:3515`), the language browse list (`cps/db.py:1975`/`:1995`), the edit form (`cps/editbooks.py:572`, `:1684`), magic shelves and OPDS — and wrote `ERROR {cps.isoLanguages:79} Missing translation for language name: ger` once per lookup per book, polluting logs users attach to unrelated reports. **Second, worse surface**: `get_valid_language_codes_from_code()` matches input against table keys, so a /B code fell through to *remainder*, and `edit_book_languages(upload_mode=True)` turns a remainder entry into `ValueError("'ger' is not a valid language")` — an EPUB whose OPF declares `ger` (passed through untouched by `get_lang3`, which returns any 3-letter code as-is) was refused. **Fix**: `ISO6392B_TO_T`, a fixed 20-entry /B→/T table in `cps/isoLanguages.py`, consulted on miss by a new `_resolve_lang_code()` helper (returns `(name, resolved)` so callers distinguish a hit from the "Unknown" sentinel) plus a `canonical_lang_code()` normalizer; `get_valid_language_codes_from_code()` now accepts /B input and stores the /T form, de-duplicating when a book declares both. The table is written out rather than derived from pycountry at import (a packaging change could silently empty it) — 639-2/B is a closed list — and `test_iso6392b_table_matches_pycountry` pins it against pycountry as a drift alarm. The unmappable-code log drops ERROR→WARNING: an unrecognised code is a metadata property, not an application fault. **21 new tests** in `tests/unit/test_1109_iso6392b_language_codes.py` (all 20 codes × display + validation, cross-locale non-divergence, log level, dedupe, drift pin) plus the **3 pre-written smoke tests un-quarantined** from `tests/quarantine.py`. Mutation-verified: removing the display alias fails 46, removing the validation alias fails 23, reverting the log level fails 1. **Live-verified on cwn-local** — book 2 tagged `ger`: pre-fix `<span class="meta-pill">Unknown</span>` + 2 ERROR lines per page load; post-fix `German` on the detail page, `German` in the `/language` browse list, 0 ERROR lines. Suite 2F/4327P = baseline. **Two further defects, fixed in follow-up commits on the same PR.** (1) `get_valid_language_codes_from_code()` validated against the *per-locale* name table. Those tables are translation data and are legitimately incomplete — 27 of the 28 shipped locales are missing between 1 and 48 of the 424 codes, and `ell` alone is absent from 11 — so a valid code missing from the user's UI locale fell into *remainder* and became the same upload `ValueError`: **a Greek book tagged `ell` was refused for a pt_BR user and imported fine for an English one**, and a book declaring both `ger` and `deu` was rejected outright because `deu` resolved while `ger` did not. Validity is a property of the code, not of the locale's translation coverage, so validation now resolves against `_reference_language_codes()` — the one complete table — with `test_reference_table_is_the_superset_of_every_locale` pinning the property that makes it a valid oracle. (2) From the cross-family review: canonicalization ran *after* the locale-table lookup, so a table that ever gained a /B key would capture `ger` and store it verbatim while defeating the `ger`+`deu` de-duplication; the two passes are now one that normalizes /B→/T before any lookup. That collapse surfaced that `names.items()` on an unrecognised locale had always raised `AttributeError` even though `get_language_names()` is documented to return `None` for one and `get_language_name()` already guards it — the validator guards it too now and falls through to the reference table. **The real caller is under test**: `edit_book_languages(upload_mode=True)` is the only production consumer and the place *remainder* becomes the reporter's `ValueError`, and nothing exercised it, so four tests now drive it directly. **Terra review**: no critical/high/medium; six low/nit, five fixed (ordering, the `canonical_lang_code` monkeypatch test that only proved routing, the backend-coupled `AttributeError` assertion, the reference-table docstring overclaim, the missing caller test) and one declined with reason. **107 tests** total, each red-proofed against the commit it pins. **Live A/B through the production module in cwn-local** (Python 3.13, pycountry 24.6.1): `ell`@pt_BR, `ell`@gl, `ger`@en and `ger,deu`@en all go rejected→accepted, `zzz` stays rejected, display unchanged (`en/ger`→German, `de/ger`→Deutsch). Suite 2F/4340P = baseline. | `30dd3fae5` | v4.1.22 |
6767
| [#1157](https://github.com/new-usemame/Calibre-Web-NextGen/pull/1157) | (fork issue [#1094](https://github.com/new-usemame/Calibre-Web-NextGen/issues/1094) @auspex) | **A failed conversion dropped the book from the library entirely.** `main()` in `scripts/ingest_processor.py` imported the incoming book only inside `if convert_successful:` and had no `else`, so any conversion failure — the reporter's 63MB PDF killed by the service safety timeout, but equally a corrupt source, unsupported internals, or a missing input plugin — returned without ever calling `add_book_to_library()`, while the `finally` block's `delete_current_file()` removed the source from the ingest folder. The service then logged `Successfully processed`. The two sibling branches that decline to convert (`auto_convert` off; format in `auto_convert_ignored_formats`) already import the original, so the fallback makes behaviour consistent rather than adding a rule. **Fix**: an `elif conversion_attempted:` branch imports the original. The guard flag matters — the ignore-list branch reports `convert_successful=False` *having already imported*, so a bare `else` would have filed every deliberately-unconverted book twice; that near-miss is pinned by `test_ignored_format_is_imported_exactly_once`. Also addressed from the same report: `backup()` now logs the absolute destination and the safety-timeout branch names `/config/processed_books/failed/<file>` (the reporter's "I've got no idea where 'failed backup' is"), and the service no longer claims the processor "should have timed out internally at N seconds" — `ingest_timeout_minutes` bounds only `is_file_in_use()`, so no internal conversion timeout exists; it now points at the setting that does govern it. **Second defect, found by the cross-family review and confirmed by reading `run:245`:** the fallback could not help the reporter's own case at all. The service runs the processor under `timeout "$safety_timeout"`, and nothing inside the processor bounded conversion — so an overrun was a SIGTERM to the processor, not a Python-visible failure, and `main()` never regained control to reach the new branch. (Their log shows `CON_ERROR ... EXIT/ERROR CODE: -15` a second before the shell's `SAFETY TIMEOUT`, which is `timeout` signalling the process group: whether Python survives long enough to print anything is a race, and whether it survives long enough to *import* is a race it usually loses.) **Fix**: `convert_book()`/`convert_to_kepub()` now run the converter with `subprocess.run(timeout=...)` from `CWA_CONVERSION_DEADLINE_SECONDS`, which the s6 script derives from the same `safety_timeout` it enforces (one source of truth; ~10% reserve, capped at a fifth of the budget; unset when `timeout 0` means no hard limit). An overrun is now an ordinary `(False, "")` and the book is imported. **Fourth defect, found by the second review leg:** that deadline was read fresh at each `subprocess.run()`, so a kepub ingest of a non-EPUB — which converts twice back to back inside one hard `timeout` — granted each stage the full deadline, letting the two outlast the watchdog and re-opening this very bug at the DEFAULT setting (at `S=2700/D=2430`, stage one can burn 2429s and stage two starts with a fresh 2430s against 271s of real time). The budget is now ONE allowance anchored to process start (the span `timeout` actually measures); each stage draws on what is left, and an exhausted budget yields a small positive value so it expires as a handled `TimeoutExpired` rather than a `ValueError` escaping the handlers. Two smaller repairs from the same leg: the shell reserve had a cliff (a flat 30s floor inverted the limits — a 31s timeout left 1s to convert while 30s left 23s), now capped so the deadline is monotonic in the timeout, with the shipped 900s/2700s endpoints unchanged and pinned; and `float("inf")` parses but fails at `int()` with `OverflowError`, which is neither `TypeError` nor `ValueError` and escaped while evaluating the `timeout=` argument, now caught. The whole-run anchor means a slow copy-in eats conversion time — the correct direction, since the watchdog starts at process launch and anchoring later lets the inner deadline outrun it; the timeout message now says so. **Third defect, same review**: `convert_to_kepub()`'s generic `except Exception` printed and fell through, returning `None` — `main()` unpacks that call, so it raised `TypeError`, skipped the fallback and dropped the book. Every non-success path now returns a pair. Also, that path backed up only the intermediate epub, so for a non-epub input the failed folder never held the file the user actually dropped in; it now keeps both. **Declined, with reason**: the review's `automerge=overwrite` finding (a rescued file could replace a good existing format) is real but pre-existing and not introduced here — the same unconditional `add_book_to_library(filepath)` runs on the auto-convert-off and ignore-list branches today, and the default is `new_record` (`cwa_schema.sql:46`). Tracked separately rather than folded into this fix. **Verified live in cwn-local mirroring the reporter's config** (`auto_convert=1`, target epub): pre-fix an unconvertible PDF logged `CON_ERROR` then `Successfully processed` with the book count unchanged at 42; post-fix the same file imports (42→43), `/book/205` and `/api/v1/books/205` both 200 with the PDF format attached. Deadline path verified live too: a 12MB txt with a 1s deadline overran, logged the actionable message, backed up to the named folder and imported as **TXT** — the original, not a converted file. Happy path re-checked (txt→epub converts, one EPUB row, fallback silent), ignore-list path re-checked (exactly one row), and an `.acsm` ticket re-checked (not filed, guidance printed, its promise about processed_books/failed now true). 27 regression tests, mutation-verified falsifiable; full unit+smoke suite 3F/4206P against a 3F baseline on `main` — the same three pre-existing failures. 49 regression tests in the module, every fix mutation-verified falsifiable; full unit+smoke 2F/4246P against the same 2F baseline on `main` (`0e423fb04`). Live re-verified in cwn-local (container Python 3.13.14): a 309MB txt with a 1s budget fires the deadline, backs up and imports as **TXT** (42→43), while a generous budget and an unset budget both still convert to **EPUB** — the anti-vacuity control that the fallback fires only on failure. `/security-review` clean (no finding above the confidence bar). Split out rather than stretched: **#1161** (converter descendants can outlive the deadline; `start_new_session` would take them out of the outer `timeout`'s group, so it needs its own signal design) and **#1163** (a failure *after* a successful conversion still unwinds without importing either file). No dependency, license, route or external URL change. Fork-original. | `a7f13064e` | v4.1.22 |
6868
| [#1156](https://github.com/new-usemame/Calibre-Web-NextGen/pull/1156) | (fork issue [#1117](https://github.com/new-usemame/Calibre-Web-NextGen/issues/1117)) | **The SPA's "read" marker was a 22px unlabelled checkmark circle in the top-right of a cover, far less visible than the classic view's labelled green badge.** The reporter reads in the light theme because of their eyesight and could not pick the marker out at a glance; the classic theme has always drawn a green pill reading "Read" at the bottom-left of the cover, so the two views disagreed on the same state. **Fix**: `BookCard` now renders a labelled green pill at the bottom-left, matching the classic placement and wording, and scales back up on phone and tablet breakpoints instead of shrinking. The label goes through `t()` so a German library reads "Gelesen" exactly as the classic view does, with the msgid anchored in `cps/spa_strings.py` (babel does not scan `.tsx`). No dependency, license, route or external URL change. Fork-original. | `0e423fb04` | v4.1.22 |

frontend/src/data/whatsNew.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ export const WHATS_NEW: WhatsNewRelease[] = [
9191
body: 'Every visit asked the server for the first page of books, then immediately asked again at a different size and threw the first answer away. Nothing looked wrong, but on a large library that first page is the slowest query there is, and it was running twice on every load for every reader. The grid now waits until it knows how many columns it has before asking.',
9292
category: 'Under the hood',
9393
},
94+
{
95+
title: 'Book pages stop scrolling sideways on a phone',
96+
body: 'Libraries catalogued with Library-of-Congress subject headings — "France -- History -- Revolution, 1789-1799 -- Fiction" and the like — carry tags wider than a phone screen, and the tag row would not break one onto a second line, so the whole book page could be dragged sideways. It hit anyone reading without an editor account, guests included, because the editing view already wrapped its tags. Long tags now wrap.',
97+
category: 'Library',
98+
},
9499
{
95100
title: 'The series line under a cover is easier to read',
96101
body: 'The small series text on a book cover was given the muted grey the rest of the interface uses and then faded a second time on top of that, which took it below the contrast small text needs to stay legible. The extra fade is gone, so the line is the muted grey it was meant to be.',

0 commit comments

Comments
 (0)