From c453295ab00f0ede37bf06a303b996bfdf6bcb42 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Tue, 22 Sep 2026 23:23:46 +0200 Subject: [PATCH] Release v1.51.0 --- CHANGELOG.md | 11 ++++++++- VERSION | 2 +- routes/magazine.py | 30 +++++++++++++++++++++++ static/index.html | 16 ++++++------ static/js/magazine.js | 57 +++++++++++++++++++++++++++++++++++++------ 5 files changed, 98 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4c81fc..2dcdf46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.51.0] — 2026-09-22 + +### Fixed +- **Magazine: reviewing a page-coverage gap borrowed an unrelated article as context.** Approve or "Not an article" on that page would have silently changed the status of whatever nearby article was borrowed, not said anything about the page actually being looked at. A gap page now gets its own dedicated article record (`POST /api/magazine/{doc_id}/articles`) instead. +- **Magazine: "Approve" (now "Approve & Next") jumps to the next unresolved page, not the next article.** Advancing by article order alone was exactly what left page-coverage gaps invisible to this workflow in the first place; it now walks forward page by page and stops at whichever needs a decision next — an unreviewed article, or a page nothing covers yet — reusing the same one-click flow as the coverage-gap banner either way. + +--- + ## [1.50.0] — 2026-09-22 ### Added @@ -4393,7 +4401,8 @@ Initial feature-complete release. --- -[Unreleased]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.50.0...HEAD +[Unreleased]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.51.0...HEAD +[1.51.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.50.0...v1.51.0 [1.50.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.49.0...v1.50.0 [1.49.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.48.2...v1.49.0 [1.48.2]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.48.1...v1.48.2 diff --git a/VERSION b/VERSION index 5a5c721..ba0a719 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.50.0 +1.51.0 diff --git a/routes/magazine.py b/routes/magazine.py index 7d3961e..81d7629 100644 --- a/routes/magazine.py +++ b/routes/magazine.py @@ -456,6 +456,36 @@ async def magazine_layout(doc_id: str, page: int | None = None): return layout +def _next_article_id(articles: list[dict]) -> str: + nums = [int(m.group(1)) for a in articles + if (m := re.fullmatch(r"article-(\d+)", a.get("id") or ""))] + return f"article-{(max(nums) + 1) if nums else 1:03d}" + + +@router.post("/api/magazine/{doc_id}/articles") +async def magazine_create_article(doc_id: str, request: Request): + """A new, otherwise-empty article -- for a page no detected article + covers. Reviewing that page previously had to borrow a nearby unrelated + article as context, so Approve or "Not an article" would have silently + changed THAT article's status instead of saying anything about the page + actually being looked at. This gives the page its own record instead. + """ + d, _ = _require_magazine(doc_id) + body = await request.json() if await request.body() else {} + page = body.get("page") + if page is not None and not isinstance(page, int): + raise HTTPException(400, "page must be an integer") + articles = mag.load_articles(d) + article = mag.Article( + id=_next_article_id(articles), + title=str(body.get("title") or "").strip(), + pages=[page] if page else [], + ).to_json() + articles.append(article) + mag.save_articles(d, articles) + return {"article": article} + + @router.get("/api/magazine/{doc_id}/articles") async def magazine_articles(doc_id: str): d, meta = _require_magazine(doc_id) diff --git a/static/index.html b/static/index.html index 7131eb2..809234c 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,12 +27,12 @@ - - - - - - + + + + + + @@ -403,7 +403,7 @@ window.toggleNavTree = function(treeId, chevronId) { - + diff --git a/static/js/magazine.js b/static/js/magazine.js index ecfc661..a9ebb3f 100644 --- a/static/js/magazine.js +++ b/static/js/magazine.js @@ -643,6 +643,24 @@ function magUncoveredPageRanges() { return gaps; } +// The next page after `afterPage` that still needs a human decision -- no +// article covers it at all, or one does but is still sitting at its +// detected/unreviewed status. A page touched by several articles only +// counts as resolved once none of them are still pending. +function magNextUnresolvedPage(afterPage) { + const pageCount = state.doc?.pageCount || 0; + const byPage = new Map(); + state.articles.forEach((a) => (a.pages || []).forEach((p) => { + if (!byPage.has(p)) byPage.set(p, []); + byPage.get(p).push(a); + })); + for (let p = afterPage + 1; p <= pageCount; p++) { + const arts = byPage.get(p) || []; + if (!arts.length || arts.some((a) => a.status === 'detected')) return p; + } + return null; +} + function renderCoverageBanner() { const el = $('mag-coverage-banner'); if (!el) return; @@ -665,10 +683,24 @@ function renderCoverageBanner() { // review UI needs *an* article as context, but its own page navigation is // not restricted to that article's pages) and then jumping. async function magReviewPage(pageNumber) { - if (!state.articles.length) return; - const anchor = state.articles.slice().sort((a, b) => - Math.abs((a.pages?.[0] ?? 0) - pageNumber) - Math.abs((b.pages?.[0] ?? 0) - pageNumber))[0]; - await magReview(anchor.id); + // Reuse whatever already covers this page if something does (idempotent + // if called twice for the same page); otherwise give the page its own + // article record rather than borrowing an unrelated one as review + // context -- confirmed live: Approve and "Not an article" both act on + // whatever article is open, so borrowing one meant either action would + // have silently changed a completely different, already-fine article. + let article = state.articles.find((a) => (a.pages || []).includes(pageNumber)); + if (!article) { + try { + const r = await api('/api/magazine/' + state.doc.id + '/articles', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ page: pageNumber, title: 'Page ' + pageNumber }), + }); + article = r.article; + state.articles.push(article); + } catch (e) { return say('Could not open page ' + pageNumber + ': ' + e.message, 'err'); } + } + await magReview(article.id); magReviewGoMagazinePage(pageNumber); } @@ -1433,7 +1465,9 @@ function renderReview() { html += '
' + '' + '' + - '' + + '' + '
'; host.innerHTML = html; if (state.orderMode) { @@ -2709,10 +2743,17 @@ async function magApprove() { // the list meant re-finding and re-opening whichever article came next // after every single approval. Go straight to it instead, and only fall // back to the list once nothing is left to review. - const idx = state.articles.findIndex((x) => x.id === a.id); - const nextId = idx >= 0 ? state.articles[idx + 1]?.id : null; + // Next PAGE needing attention, not next article -- an article can span + // many pages or share one with others, and jumping by article order + // alone is exactly what left pages uncovered by any article invisible + // to this whole workflow (see magUncoveredPageRanges). Walking forward + // page by page and reusing magReviewPage's "create a stub if nothing + // covers it" logic means a gap gets exactly the same one-click + // attention as an already-detected-but-unreviewed article. + const lastPage = Math.max(0, ...(a.pages || [])); await magRefreshArticles(); - if (nextId && state.articles.some((x) => x.id === nextId)) magReview(nextId); + const nextPage = magNextUnresolvedPage(lastPage); + if (nextPage) magReviewPage(nextPage); else magShow('contents'); } catch (e) { say('Could not approve: ' + e.message, 'err');