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) {
-
+