Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ is for things you can see or feel when running the app.

## [Unreleased]

### Fixed

- Book pages no longer scroll sideways on a phone when a book carries long
subject tags. Libraries with Library-of-Congress headings (for example
"France -- History -- Revolution, 1789-1799 -- Fiction") pushed the tag row
off the edge of the screen for anyone reading without an editor account,
including guests. Long tags now wrap instead. (#1170)

## [v4.1.22] - 2026-07-27

### Changed

- **The "read" mark on a book cover is now a green "Read" label at the bottom-left, instead of a small tick in the corner.** It was a 22-pixel checkmark circle in the top-right with no wording, which was easy to miss at a glance — the reporter, who reads in the light theme because of their eyesight, pointed out that the classic view's labelled green badge was far easier to spot. It now says what it means, sits where the classic one sits, and gets larger again on phones and tablets. The wording follows your language, so a German library reads "Gelesen" exactly as the classic view does. Reported by [@uschi1](https://github.com/new-usemame/Calibre-Web-NextGen/issues/351) ([#1117](https://github.com/new-usemame/Calibre-Web-NextGen/issues/1117)).
Expand All @@ -38,6 +48,8 @@ is for things you can see or feel when running the app.

- **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)).

- **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)).

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

### Added
Expand Down
17 changes: 10 additions & 7 deletions CHANGES-vs-upstream.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions cps/spa_strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ def _(message): # noqa: E743 - intentional gettext extraction marker, not the b
_("Create an app password")
_("Browse series")
_("Browse authors")
_("Browse languages")
_("Open Table view")
_("Open Admin")
_("Open account settings")
Expand Down
58 changes: 57 additions & 1 deletion frontend/e2e/book-detail.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect, Page } from '@playwright/test';
import { collectPageErrors, assertNoPageErrors, assertNoHorizontalOverflow } from './utils';
import { collectPageErrors, assertNoPageErrors, assertNoHorizontalOverflow, pageOverflow } from './utils';

/*
* Book-detail completeness pass:
Expand Down Expand Up @@ -182,3 +182,59 @@ test('book detail with a "More by" strip has no horizontal overflow on mobile',
await expect(page.locator('section[aria-label^="More by"]')).toBeVisible({ timeout: 10_000 });
await assertNoHorizontalOverflow(page);
});

// A reader without the edit role sees the read-only tag pills, and those pills
// were `white-space: nowrap` with no max-width. Real libraries carry
// Library-of-Congress subject headings ("France -- History -- Revolution,
// 1789-1799 -- Fiction") that are wider than a phone, so one tag dragged the
// whole detail page into horizontal scroll. The admin-role branch renders
// removable chips instead and wraps fine, which is why the existing mobile
// specs — all of which log in as admin — never saw this.
//
// Deterministic on any seed: the role and the tag list are both stubbed.
//
// Measures the DELTA the long tags add, not absolute overflow. The CI fixture's
// detail page carries ~35px of horizontal overflow from an unrelated cause
// (notes/e2e-mobile-overflow-ci-triage.md), and an absolute assertion here would
// fail on that instead of on the tag row, reporting nothing about the pills. The
// delta is what this fix owns: pre-fix it was ~300px, post-fix it is 0.
test('long tag names add no horizontal overflow to the read-only detail page', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/app');
const bookId = await firstBookId(page);
test.skip(bookId == null, 'seed has no books');

// Drop the edit role so the page renders the read-only Pill branch that a
// guest or viewer account gets.
await page.route('**/api/v1/auth/me', async (route) => {
const res = await route.fetch();
const me = await res.json();
if (me?.role) me.role.edit = false;
await route.fulfill({ response: res, json: me });
});

await page.goto(`/app/book/${bookId}`, { waitUntil: 'domcontentloaded' });
await expect(page.locator('main h1')).toBeVisible({ timeout: 10_000 });
const baseline = await pageOverflow(page);

// A real LoC heading plus a single unbroken token wider than the viewport.
const longTag = 'France -- History -- Revolution, 1789-1799 -- Fiction';
await page.route(`**/api/v1/books/${bookId}`, async (route) => {
const res = await route.fetch();
const book = await res.json();
book.tags = [
{ id: 990001, name: longTag },
{ id: 990002, name: 'Bildungsroman'.repeat(8) },
];
await route.fulfill({ response: res, json: book });
});

await page.reload({ waitUntil: 'domcontentloaded' });
await expect(page.getByText(longTag)).toBeVisible({ timeout: 10_000 });
const withLongTags = await pageOverflow(page);

expect(
withLongTags - baseline,
`long tags widened the page by ${withLongTags - baseline}px (baseline ${baseline}px)`,
).toBeLessThanOrEqual(1);
});
12 changes: 10 additions & 2 deletions frontend/e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,17 @@
/** No horizontal body overflow — the signature of the mobile-reflow regressions
* (#288 banner, #576 drawer, edit-cover at 375px). */
export async function assertNoHorizontalOverflow(page: Page) {
const overflow = await page.evaluate(() => {
const overflow = await pageOverflow(page);
expect(overflow, 'page scrolls horizontally (mobile reflow regression)').toBeLessThanOrEqual(1);

Check failure on line 30 in frontend/e2e/utils.ts

View workflow job for this annotation

GitHub Actions / E2E Tests (SPA)

[mobile] › e2e/hidden-books.spec.ts:252:1 › mobile detail actions and View settings stay within 390px

6) [mobile] › e2e/hidden-books.spec.ts:252:1 › mobile detail actions and View settings stay within 390px Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: page scrolls horizontally (mobile reflow regression) expect(received).toBeLessThanOrEqual(expected) Expected: <= 1 Received: 35 at utils.ts:30 28 | export async function assertNoHorizontalOverflow(page: Page) { 29 | const overflow = await pageOverflow(page); > 30 | expect(overflow, 'page scrolls horizontally (mobile reflow regression)').toBeLessThanOrEqual(1); | ^ 31 | } 32 | 33 | /** Horizontal overflow in px, for specs that need to compare one render against at assertNoHorizontalOverflow (/home/runner/work/Calibre-Web-NextGen/Calibre-Web-NextGen/frontend/e2e/utils.ts:30:76) at /home/runner/work/Calibre-Web-NextGen/Calibre-Web-NextGen/frontend/e2e/hidden-books.spec.ts:262:3

Check failure on line 30 in frontend/e2e/utils.ts

View workflow job for this annotation

GitHub Actions / E2E Tests (SPA)

[mobile] › e2e/hidden-books.spec.ts:252:1 › mobile detail actions and View settings stay within 390px

6) [mobile] › e2e/hidden-books.spec.ts:252:1 › mobile detail actions and View settings stay within 390px Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: page scrolls horizontally (mobile reflow regression) expect(received).toBeLessThanOrEqual(expected) Expected: <= 1 Received: 35 at utils.ts:30 28 | export async function assertNoHorizontalOverflow(page: Page) { 29 | const overflow = await pageOverflow(page); > 30 | expect(overflow, 'page scrolls horizontally (mobile reflow regression)').toBeLessThanOrEqual(1); | ^ 31 | } 32 | 33 | /** Horizontal overflow in px, for specs that need to compare one render against at assertNoHorizontalOverflow (/home/runner/work/Calibre-Web-NextGen/Calibre-Web-NextGen/frontend/e2e/utils.ts:30:76) at /home/runner/work/Calibre-Web-NextGen/Calibre-Web-NextGen/frontend/e2e/hidden-books.spec.ts:262:3

Check failure on line 30 in frontend/e2e/utils.ts

View workflow job for this annotation

GitHub Actions / E2E Tests (SPA)

[desktop] › e2e/hidden-books.spec.ts:252:1 › mobile detail actions and View settings stay within 390px

3) [desktop] › e2e/hidden-books.spec.ts:252:1 › mobile detail actions and View settings stay within 390px Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: page scrolls horizontally (mobile reflow regression) expect(received).toBeLessThanOrEqual(expected) Expected: <= 1 Received: 35 at utils.ts:30 28 | export async function assertNoHorizontalOverflow(page: Page) { 29 | const overflow = await pageOverflow(page); > 30 | expect(overflow, 'page scrolls horizontally (mobile reflow regression)').toBeLessThanOrEqual(1); | ^ 31 | } 32 | 33 | /** Horizontal overflow in px, for specs that need to compare one render against at assertNoHorizontalOverflow (/home/runner/work/Calibre-Web-NextGen/Calibre-Web-NextGen/frontend/e2e/utils.ts:30:76) at /home/runner/work/Calibre-Web-NextGen/Calibre-Web-NextGen/frontend/e2e/hidden-books.spec.ts:262:3

Check failure on line 30 in frontend/e2e/utils.ts

View workflow job for this annotation

GitHub Actions / E2E Tests (SPA)

[desktop] › e2e/hidden-books.spec.ts:252:1 › mobile detail actions and View settings stay within 390px

3) [desktop] › e2e/hidden-books.spec.ts:252:1 › mobile detail actions and View settings stay within 390px Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: page scrolls horizontally (mobile reflow regression) expect(received).toBeLessThanOrEqual(expected) Expected: <= 1 Received: 35 at utils.ts:30 28 | export async function assertNoHorizontalOverflow(page: Page) { 29 | const overflow = await pageOverflow(page); > 30 | expect(overflow, 'page scrolls horizontally (mobile reflow regression)').toBeLessThanOrEqual(1); | ^ 31 | } 32 | 33 | /** Horizontal overflow in px, for specs that need to compare one render against at assertNoHorizontalOverflow (/home/runner/work/Calibre-Web-NextGen/Calibre-Web-NextGen/frontend/e2e/utils.ts:30:76) at /home/runner/work/Calibre-Web-NextGen/Calibre-Web-NextGen/frontend/e2e/hidden-books.spec.ts:262:3
}

/** Horizontal overflow in px, for specs that need to compare one render against
* another rather than assert the absolute property. Asserting "no overflow at
* all" makes a spec fail for any unrelated overflow the page happens to carry,
* which tells you nothing about the element under test. */
export async function pageOverflow(page: Page): Promise<number> {
return page.evaluate(() => {
const el = document.documentElement;
return el.scrollWidth - el.clientWidth;
});
expect(overflow, 'page scrolls horizontally (mobile reflow regression)').toBeLessThanOrEqual(1);
}
8 changes: 7 additions & 1 deletion frontend/src/components/Pill.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,11 @@
border-radius: var(--r-full);
padding: var(--sp-1) var(--sp-3);
border: 1px solid var(--border);
white-space: nowrap;
/* No nowrap: real Calibre libraries carry Library-of-Congress subject
headings ("France -- History -- Revolution, 1789-1799 -- Fiction") that are
wider than a phone, and an unbreakable pill pushed the whole book detail
page into horizontal scroll (#1130). Normal wrapping still keeps every
pill that fits on one line; anywhere breaks a single oversized token. */
max-width: 100%;
overflow-wrap: anywhere;
}
43 changes: 43 additions & 0 deletions frontend/src/data/whatsNew.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,49 @@ export interface WhatsNewRelease {

/** Newest release first. The `whats-new-populate` skill prepends here. */
export const WHATS_NEW: WhatsNewRelease[] = [
{
version: 'v4.1.22',
date: '2026-07-27',
items: [
{
title: 'A book that cannot be converted still lands in your library',
body: 'If an incoming book could not be converted to your chosen format — a large PDF that ran out of time, a file Calibre chokes on, a format needing a plugin you do not have — it was dropped altogether: no entry in the library and the file gone from the ingest folder, while the log still reported success. The original file is now imported whenever a conversion fails, and a conversion that runs long ends as an ordinary failure instead of killing the import outright. Raising Ingest Timeout in CWA Settings raises the conversion limit along with it.',
category: 'Library',
},
{
title: 'The "read" mark on a cover now says Read',
body: 'A finished book was marked with a small unlabelled tick in the corner of its cover, which was easy to miss at a glance — particularly in the light theme. It is now a green "Read" label at the bottom-left, the same place and wording the classic view uses, and it stays large enough to see on a phone or tablet. The label follows the language you read the interface in.',
category: 'Library',
link: { to: '/', label: 'Open your library' },
},
{
title: 'Books recorded as "ger", "fre" or "dut" show their real language',
body: 'A language can be written two ways in book metadata, and only the form Calibre normally writes was recognised — so a book carrying the other form, common in library-catalogue records and some EPUB files, showed "Unknown" as its language, and importing one could be refused outright. Both forms are now accepted for all twenty languages where they differ. Whether a language counts as valid also no longer depends on the language you happen to read the interface in.',
category: 'Library',
link: { to: '/languages', label: 'Browse languages' },
},
{
title: 'Pages no longer fail at random when the server is busy',
body: 'The server kept one working copy of your library and shared it across everything it was doing at once, so a page being built could lose its data the moment another page finished — or the moment a background job such as a duplicate scan, thumbnail run or metadata backup completed. It surfaced as a different-looking error every time, and grew likelier the busier the server was, which hit big libraries and shared instances hardest. Every page load and every background job now works from its own copy.',
category: 'Under the hood',
},
{
title: 'Your library keeps working when its extra fields cannot be read',
body: 'Book pages, the table view and both search screens ask your library for the extra fields you can add to a book in Calibre. If that lookup failed — the library mid-write, its folder moved, the definitions not there yet — those pages returned an error rather than simply leaving the extra fields out, even though the rest of the library was perfectly readable. They now load without them and note the reason in the log.',
category: 'Under the hood',
},
{
title: 'Opening your library runs one search instead of two',
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.',
category: 'Under the hood',
},
{
title: 'The series line under a cover is easier to read',
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.',
category: 'Under the hood',
},
],
},
{
version: 'v4.1.21',
date: '2026-07-25',
Expand Down
Loading