Release v1.49.0
Some checks are pending
Test and SonarQube / quality (push) Waiting to run

This commit is contained in:
mARTin-B78 2026-09-22 23:14:27 +02:00
parent 6ab2d3cdd1
commit ab10516383
6 changed files with 80 additions and 10 deletions

View File

@ -9,6 +9,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
--- ---
## [1.49.0] — 2026-09-22
### Added
- **Magazine: a page-coverage gap finder.** Approve only ever advances to the next detected article, so a page never grouped into any article at all — an ad, a missed piece, anything detection skipped entirely — was never surfaced by that workflow. Confirmed live: 73 of 204 pages (36%) in one real issue belonged to no article whatsoever. The article list now shows a banner listing every such gap as page ranges, each a one-click jump straight into Review at that page (the review UI's own page navigation already isn't restricted to one article's pages — the missing piece was purely that nothing pointed there).
---
## [1.48.2] — 2026-09-22 ## [1.48.2] — 2026-09-22
### Changed ### Changed
@ -4379,7 +4386,8 @@ Initial feature-complete release.
--- ---
[Unreleased]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.48.2...HEAD [Unreleased]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.49.0...HEAD
[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 [1.48.2]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.48.1...v1.48.2
[1.48.1]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.48.0...v1.48.1 [1.48.1]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.48.0...v1.48.1
[1.48.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.47.1...v1.48.0 [1.48.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.47.1...v1.48.0

View File

@ -1 +1 @@
1.48.2 1.49.0

View File

@ -10,7 +10,7 @@
<meta name="format-detection" content="telephone=no"> <meta name="format-detection" content="telephone=no">
<meta name="color-scheme" content="light dark"> <meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#2563EB"> <meta name="theme-color" content="#2563EB">
<meta name="app-version" content="1.48.2"> <meta name="app-version" content="1.49.0">
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
<link rel="icon" href="/static/icon.svg" type="image/svg+xml"> <link rel="icon" href="/static/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/static/icon.svg"> <link rel="apple-touch-icon" href="/static/icon.svg">
@ -27,12 +27,12 @@
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── --> <!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css"> <link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/style.css?v=1.48.2"> <link rel="stylesheet" href="/static/style.css?v=1.49.0">
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.48.2"> <link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.49.0">
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.48.2"> <link rel="stylesheet" href="/static/css/background-jobs.css?v=1.49.0">
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.48.2"> <link rel="stylesheet" href="/static/css/voice-design.css?v=1.49.0">
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.48.2"> <link rel="stylesheet" href="/static/css/rehearser.css?v=1.49.0">
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.48.2"> <link rel="stylesheet" href="/static/css/audiobook.css?v=1.49.0">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── --> <!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -403,7 +403,7 @@ window.toggleNavTree = function(treeId, chevronId) {
</script> </script>
<!-- loader.js: fetches sections → loads JS modules → removes skeleton --> <!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
<script src="/static/loader.js?v=1.48.2"></script> <script src="/static/loader.js?v=1.49.0"></script>
</body> </body>
</html> </html>

View File

@ -619,6 +619,59 @@ function confidenceLabel(c) {
return { cls: 'bad', text: Math.round(c * 100) + '% · little body copy — likely not an article', badge: Math.round(c * 100) + '%' }; return { cls: 'bad', text: Math.round(c * 100) + '% · little body copy — likely not an article', badge: Math.round(c * 100) + '%' };
} }
// ── Page coverage ────────────────────────────────────────────────────────
// "Approve" only ever advances to the next ARTICLE, so a page that never
// got grouped into any article -- an ad, a missed piece, a page the
// detection pass just skipped -- is never surfaced by that workflow at all.
// Confirmed live: 73 of 204 pages (36%) in one real issue belonged to no
// article whatsoever. This finds those gaps and gives a one-click way into
// Review at an arbitrary page, which the review UI already supports
// (magReviewGoMagazinePage is not bounded to the anchor article's own
// pages) -- the missing piece was purely that nothing pointed there.
function magUncoveredPageRanges() {
const total = state.doc?.pageCount || 0;
if (!total) return [];
const covered = new Set();
state.articles.forEach((a) => (a.pages || []).forEach((p) => covered.add(p)));
const gaps = [];
let start = null;
for (let p = 1; p <= total; p++) {
if (!covered.has(p)) { if (start === null) start = p; }
else if (start !== null) { gaps.push([start, p - 1]); start = null; }
}
if (start !== null) gaps.push([start, total]);
return gaps;
}
function renderCoverageBanner() {
const el = $('mag-coverage-banner');
if (!el) return;
const gaps = magUncoveredPageRanges();
if (!gaps.length) { el.hidden = true; el.innerHTML = ''; return; }
const pageCount = gaps.reduce((n, [a, b]) => n + (b - a + 1), 0);
el.hidden = false;
el.innerHTML =
'<span class="mdi mdi-alert-outline" aria-hidden="true"></span> ' +
'<span>' + pageCount + ' page' + (pageCount === 1 ? '' : 's') + ' (' + gaps.length + ' gap' + (gaps.length === 1 ? '' : 's') +
') ' + (pageCount === 1 ? "isn't" : "aren't") + ' part of any detected article, and will never come up just by approving articles one by one:</span>' +
'<span class="mag-coverage-ranges">' +
gaps.map(([a, b]) => '<button type="button" class="btn-sm" onclick="magReviewPage(' + a + ')">' +
(a === b ? 'p. ' + a : 'pp. ' + a + '–' + b) + '</button>').join('') +
'</span>';
}
// Opens Review at a specific page even when no article covers it, by
// anchoring on whichever approved-or-not article is nearest that page (the
// 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);
magReviewGoMagazinePage(pageNumber);
}
function renderContents() { function renderContents() {
const host = $('mag-contents'); const host = $('mag-contents');
if (!host) return; if (!host) return;
@ -631,6 +684,7 @@ function renderContents() {
host.innerHTML = '<p class="muted">No articles yet. Run ingestion to detect them.</p>'; host.innerHTML = '<p class="muted">No articles yet. Run ingestion to detect them.</p>';
return; return;
} }
renderCoverageBanner();
const bySection = new Map(); const bySection = new Map();
state.articles.forEach((a) => { state.articles.forEach((a) => {
const key = a.section || 'Articles'; const key = a.section || 'Articles';
@ -3452,6 +3506,7 @@ root.magContentsToggleSelect = magContentsToggleSelect;
root.magContentsCommitTitle = magContentsCommitTitle; root.magContentsCommitTitle = magContentsCommitTitle;
root.magGenerateAllAudio = magGenerateAllAudio; root.magGenerateAllAudio = magGenerateAllAudio;
root.magTogglePlaylist = magTogglePlaylist; root.magTogglePlaylist = magTogglePlaylist;
root.magReviewPage = magReviewPage;
root.magPlaylistPlay = magPlaylistPlay; root.magPlaylistPlay = magPlaylistPlay;
root.magContentsToggleArticle = magContentsToggleArticle; root.magContentsToggleArticle = magContentsToggleArticle;
root.magContentsSelectAllVisible = magContentsSelectAllVisible; root.magContentsSelectAllVisible = magContentsSelectAllVisible;

View File

@ -79,6 +79,7 @@
</button> </button>
</div> </div>
</div> </div>
<div id="mag-coverage-banner" class="mag-coverage-banner" hidden></div>
<div id="mag-contents-bulkbar" class="mag-bulkbar" hidden></div> <div id="mag-contents-bulkbar" class="mag-bulkbar" hidden></div>
<div class="mag-audio-export"> <div class="mag-audio-export">
<label for="mag-audio-voice">Narrator for exported MP3s</label> <label for="mag-audio-voice">Narrator for exported MP3s</label>

View File

@ -4930,6 +4930,12 @@ input[list="lang-codes"] {
.mag-bulkbar-progress { flex: 1; min-width: 0; } .mag-bulkbar-progress { flex: 1; min-width: 0; }
.mag-bulkbar-actions { display: flex; gap: 6px; margin-left: auto; } .mag-bulkbar-actions { display: flex; gap: 6px; margin-left: auto; }
.mag-coverage-banner { display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap;
padding: 10px 14px; margin-bottom: 12px; border: 1px solid var(--yellow, #d97706); border-radius: var(--radius);
background: color-mix(in srgb, var(--yellow, #d97706) 10%, var(--surface)); }
.mag-coverage-banner .mdi { color: var(--yellow, #d97706); flex-shrink: 0; margin-top: 2px; }
.mag-coverage-ranges { display: flex; flex-wrap: wrap; gap: 6px; }
.mag-audio-export { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; .mag-audio-export { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
padding: 10px 14px; margin-bottom: 12px; border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 14px; margin-bottom: 12px; border: 1px solid var(--border); border-radius: var(--radius);
background: var(--panel); } background: var(--panel); }