fix(magazine): an approval wins over the quality flag in the page grid

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-09-28 16:33:24 +02:00
parent 3d5da78a98
commit d712058d68
3 changed files with 33 additions and 5 deletions

View File

@ -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.

View File

@ -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';

View File

@ -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);