From d712058d684ee4b5b22859a0e84d96dcbf2f295a Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Mon, 28 Sep 2026 16:33:24 +0200 Subject: [PATCH] fix(magazine): an approval wins over the quality flag in the page grid Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 1 + static/js/magazine.js | 12 +++++++----- tests/magazine.spec.js | 25 +++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c95d03b..3ac371b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ยท versioned wi - **Unboxed ink in or beside a picture, chart or advert is no longer reported as missed text** (labels of a chart, text set into a photo, an advert's own wording). ### Fixed +- **An approved page is no longer purple.** A page the quality check flagged stayed purple even after you approved its article; now your approval wins and the page shows its review colour (the warning badge on the card still lists what the check found). Purple means flagged *and* nobody has approved anything on the page. - **Reading order is now applied automatically after import, *OCR again* and every per-page fix**, not only by the manual *Fix reading order* button. Until now the order after import was whatever the vision/LLM step returned, so a column's short last box could be read after the whole other column; on the measured issue 101 of 204 pages had a saved order that differs from the column-aware order. Pages whose order you fixed by hand stay untouched, except after *OCR again*, when every region is new and a hand order can no longer apply. - **The review list shows each region's whole text**, not the first 220 characters, so every misspelling gets its red wavy underline (like a word processor). The engine's `[Non-Text]` marker and hairline boxes (under 3 pt tall) are now treated as OCR noise by *Clean up boxes* and after a page re-OCR, so they no longer show up as near-invisible regions. Fitted boxes keep a little air on grown sides. - **The quality check no longer flags correct text as "suspect OCR".** The word check guessed the language per fragment (`iPhones statt Token` read as English) and flagged valid German; it now uses the page's language, the same lenient German rules as the spellchecker, and treats words that recur on the page (names, brands) as vocabulary. Page 61 of the measured issue went from 11 suspect regions to 3 (one a genuinely truncated word), and the issue from 758 to 610 before the language fix's full effect. diff --git a/static/js/magazine.js b/static/js/magazine.js index 33b9ae0..7aa8158 100644 --- a/static/js/magazine.js +++ b/static/js/magazine.js @@ -809,7 +809,7 @@ const MAG_PAGES_COLS_KEY = 'ttsvc_mag_pages_cols'; // The last whole-issue quality run splits the two unreviewed states further: // checked -- quality run found nothing left to fix (blue) // flagged -- quality run found uncovered text / overlaps / clipping / suspect words (purple) -// flagged wins over every review state except a fully rejected page; clean only replaces 'unreviewed'. +// flagged shows only while nothing on the page is approved (an approval wins); clean only replaces 'unreviewed'. // Mirrors page_is_advertisement() in core/magazine.py: flagged, or detection found advertisement // regions and nothing narratable on the page. const MAG_NARRATABLE_TYPES = ['title', 'heading', 'deck', 'byline', 'body', 'quote']; @@ -822,13 +822,15 @@ function magIsAdPage(page) { } function magPageState(arts, q, page) { - // A page the quality run flagged stays purple whatever its review state (a reviewed or - // audio page can still hide clipped or uncovered text) -- except one whose every article was - // rejected, which is a settled "nothing to narrate". + // A page the quality run flagged is purple until somebody has looked at it: once one of its + // articles is approved (or has audio) the reviewer's decision wins and the page shows its review + // colour; the warning badge on the card still lists what the check found. A page whose every + // article was rejected is a settled "nothing to narrate". const flagged = !!(q && q.needs_review); if (page != null && magIsAdPage(page)) return 'ad'; // approved as advertisement: settled, green const rejectedOnly = arts.length > 0 && arts.every((a) => a.status === 'rejected'); - if (flagged && !rejectedOnly) return 'flagged'; + const reviewed = arts.some((a) => a.status === 'approved' || a.has_audio); + if (flagged && !rejectedOnly && !reviewed) return 'flagged'; const unreviewed = q ? 'checked' : null; if (!arts.length) return unreviewed || 'blank'; if (arts.some((a) => a.has_audio)) return 'audio'; diff --git a/tests/magazine.spec.js b/tests/magazine.spec.js index 0d464d1..99ec1cd 100644 --- a/tests/magazine.spec.js +++ b/tests/magazine.spec.js @@ -464,6 +464,31 @@ test.describe('Magazine pronunciation and spelling checks', () => { }); }); +test.describe('Magazine: page colours', () => { + async function openGrid(page, articleStatus) { + await mockBackend(page, { articleStatus }); + await page.route(`**/api/magazine/${DOC_ID}/quality/report`, route => route.fulfill({ json: { + summary: {}, pages: [{ page: 3, uncovered: [], uncovered_ratio: 0, suspect_blocks: [], overlaps: [], clipped: [{ id: 'b3', side: 'right' }], + measured: true, spelling: [], needs_review: true, words_to_check: false }] } })); + await page.goto('/', { waitUntil: 'domcontentloaded' }); + await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 }); + await navTo(page, 's-magazine'); + await page.locator('#mag-library-list .mag-card').first().click(); + await page.locator('#mag-contents-view-toggle').click(); + return page.locator('.mag-card-wrap').filter({ hasText: 'Page 3' }).locator('.mag-card'); + } + + test('a flagged page nobody has approved is purple', async ({ page }) => { + await expect(await openGrid(page, 'detected')).toHaveClass(/is-page-flagged/); + }); + + test('once the page\'s article is approved the approval wins over the flag', async ({ page }) => { + const card = await openGrid(page, 'approved'); + await expect(card).not.toHaveClass(/is-page-flagged/); + await expect(card).toHaveClass(/is-page-approved/); + }); +}); + test.describe('Magazine: unreviewed articles', () => { async function openContents(page, opts) { await mockBackend(page, opts);