diff --git a/internal/storage/metrics.go b/internal/storage/metrics.go index 4d6f4fb..ffa9026 100644 --- a/internal/storage/metrics.go +++ b/internal/storage/metrics.go @@ -69,22 +69,20 @@ func StoryViewTotals(ids []int64) map[int64]int { } out[id] = n } + if err := rows.Err(); err != nil { + slog.Error("metrics: story view totals iteration failed", "err", err) + } return out } // intInClause builds a "?, ?, …" placeholder string and matching args slice for // a SQL IN (…) over int64 ids. func intInClause(ids []int64) (string, []any) { - ph := make([]byte, 0, len(ids)*2) args := make([]any, len(ids)) for i, id := range ids { - if i > 0 { - ph = append(ph, ',') - } - ph = append(ph, '?') args[i] = id } - return string(ph), args + return placeholders(len(ids)), args } // PathStat is one row of the per-page usage breakdown. diff --git a/internal/storage/queries.go b/internal/storage/queries.go index f1f6523..46f2e87 100644 --- a/internal/storage/queries.go +++ b/internal/storage/queries.go @@ -438,6 +438,9 @@ func StoryContentLengths(ids []int64) map[int64]int { } out[id] = int(n) } + if err := rows.Err(); err != nil { + slog.Error("story content lengths iteration failed", "err", err) + } return out } diff --git a/internal/web/static/js/reader.js b/internal/web/static/js/reader.js index fa3c115..06063be 100644 --- a/internal/web/static/js/reader.js +++ b/internal/web/static/js/reader.js @@ -44,6 +44,7 @@ var open = false; var contentCache = {}; // id -> {content, lede} var inflight = null; + var bodyReady = false; // true once the real article body is rendered (not the loading placeholder) // ---- read-state store ----------------------------------------------------- function loadRead() { @@ -264,6 +265,7 @@ } function loadingBody(it) { + bodyReady = false; // read-aloud waits for the real body, not this placeholder renderBody(it, '

Loading the full story…

'); } @@ -274,6 +276,7 @@ html = '

No article text was captured for this story. Open the original to read it.

'; } renderBody(it, html); + bodyReady = true; } function fetchContent(it) { @@ -483,6 +486,8 @@ // ---- read aloud (signed-in only) ------------------------------------------ var speaking = false; + var speakGen = 0; // bumped on every stop/start; stale utterance callbacks that + // fire after speechSynthesis.cancel() check it and no-op var speakParas = []; //

elements being read, for highlight var speakQueue = []; // remaining {el, text} to utter function speakingText() { @@ -502,6 +507,7 @@ } function stopSpeak() { if (!TTS_OK) return; + speakGen++; try { window.speechSynthesis.cancel(); } catch (e) {} speaking = false; speakQueue = []; @@ -518,15 +524,18 @@ speakParas.push(item.el); try { item.el.scrollIntoView({ block: "center", behavior: "smooth" }); } catch (e) {} } + var gen = speakGen; var u = new SpeechSynthesisUtterance(item.text); - u.onend = function () { if (speaking) speakNext(); }; - u.onerror = function () { if (speaking) speakNext(); }; + u.onend = function () { if (speaking && gen === speakGen) speakNext(); }; + u.onerror = function () { if (speaking && gen === speakGen) speakNext(); }; try { window.speechSynthesis.speak(u); } catch (e) { stopSpeak(); } } function startSpeak() { if (!TTS_OK) return; + if (!bodyReady) { toast("Still loading…"); return; } var parts = speakingText(); if (!parts.length) { toast("Nothing to read yet"); return; } + speakGen++; try { window.speechSynthesis.cancel(); } catch (e) {} speaking = true; speakQueue = parts.slice();