diff --git a/CHANGELOG.md b/CHANGELOG.md index cf4ed70..5e1ed0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,16 @@ former name, "Daybook".) _Nothing yet._ +## [0.4.1] — 2026-09-05 + +### Changed +- Live feed updates now also keep the **source sidebar** current: as new posts + arrive, per-source and total unread counts bump, and a source posting for the + first time is added to the list (alphabetically) — all derived client-side + from the polled rows, no extra request. Sources added live stay + keyboard-navigable. Caveat: while filtered to a single source, *other* new + sources still only appear on reload (the poll is scoped to the selected view). + ## [0.4.0] — 2026-09-04 ### Added @@ -155,7 +165,8 @@ First tagged release. - Dockerised (Gunicorn + whitenoise), SQLite, `git`-based deploy with a runbook (`deploy/cloud_agent_deploy.md`) and health check. -[Unreleased]: https://git.cygnus.systems/peter/daybook/compare/v0.4.0...HEAD +[Unreleased]: https://git.cygnus.systems/peter/daybook/compare/v0.4.1...HEAD +[0.4.1]: https://git.cygnus.systems/peter/daybook/compare/v0.4.0...v0.4.1 [0.4.0]: https://git.cygnus.systems/peter/daybook/compare/v0.3.2...v0.4.0 [0.3.2]: https://git.cygnus.systems/peter/daybook/compare/v0.3.1...v0.3.2 [0.3.1]: https://git.cygnus.systems/peter/daybook/compare/v0.3.0...v0.3.1 diff --git a/daybook_project/__init__.py b/daybook_project/__init__.py index bc935aa..53f5709 100644 --- a/daybook_project/__init__.py +++ b/daybook_project/__init__.py @@ -1,4 +1,4 @@ # Single source of truth for the app version. Bump this, commit, then tag the # commit `vX.Y.Z` and deploy. Surfaced on Settings -> About and in # GET /api/capabilities ("version"). -__version__ = "0.4.0" +__version__ = "0.4.1" diff --git a/templates/artifacts/feed.html b/templates/artifacts/feed.html index a240ebc..78764fe 100644 --- a/templates/artifacts/feed.html +++ b/templates/artifacts/feed.html @@ -195,7 +195,8 @@ const artifactList = document.querySelector("[data-artifact-list]"); // Rows grow as older pages load, so always read them live rather than caching an array. const allRows = () => Array.from(document.querySelectorAll(".artifact-row")); - const sourceLinks = Array.from(document.querySelectorAll(".source-link")); + // Live-queried so sources added by live updates are keyboard-navigable too. + const sourceLinks = () => Array.from(document.querySelectorAll(".source-link")); const csrfToken = document.querySelector("[name=csrfmiddlewaretoken]")?.value || ""; const escapeHtml = (value) => value.replace(/[&<>"]/g, (character) => ({ @@ -383,6 +384,66 @@ }); }; + const findSourceLink = (name) => + sourceLinks().find((link) => link.querySelector(".source-name")?.textContent.trim() === name); + + const bumpSourceBadge = (link, delta) => { + let badge = link.querySelector(".source-count"); + if (!badge) { + badge = document.createElement("span"); + badge.className = "source-count"; + badge.textContent = "0"; + link.appendChild(badge); + } + badge.textContent = String((Number(badge.textContent) || 0) + delta); + }; + + const addSourceLink = (name, count) => { + const params = new URLSearchParams(); + params.set("source", name); + if (feed.dataset.selectedLabel) params.set("label", feed.dataset.selectedLabel); + if (feed.dataset.selectedKind) params.set("kind", feed.dataset.selectedKind); + if (feed.dataset.titleQuery) params.set("q", feed.dataset.titleQuery); + params.set("view", feed.dataset.selectedView || "rendered"); + params.set("preview", "none"); + + const link = document.createElement("a"); + link.className = "source-link"; + link.href = `?${params.toString()}`; + link.innerHTML = + `${escapeHtml(name)}` + + `${count}`; + + // Insert alphabetically among the per-source links (after "All sources"). + const list = document.querySelector(".source-list"); + const others = sourceLinks().filter( + (l) => l.querySelector(".source-name")?.textContent.trim() !== "All sources", + ); + const after = others.find( + (l) => (l.querySelector(".source-name")?.textContent.trim() || "") > name, + ); + list.insertBefore(link, after || null); + }; + + // From the newly-arrived rows: bump each source's unread count (adding a + // sidebar entry for a source seen for the first time) plus the total. + const registerNewSources = (rows) => { + const counts = {}; + rows.forEach((row) => { + const source = row.dataset.source; + if (source) counts[source] = (counts[source] || 0) + 1; + }); + let total = 0; + Object.entries(counts).forEach(([source, n]) => { + total += n; + const existing = findSourceLink(source); + if (existing) bumpSourceBadge(existing, n); + else addSourceLink(source, n); + }); + const allLink = findSourceLink("All sources"); + if (allLink && total) bumpSourceBadge(allLink, total); + }; + const renderPreview = (row, pushState = true) => { navigationMode = "artifacts"; markRead(row); @@ -438,12 +499,13 @@ }; const moveSource = (direction) => { - if (!sourceLinks.length) return; + const links = sourceLinks(); + if (!links.length) return; navigationMode = "sources"; const current = document.querySelector(".source-link.selected"); - const currentIndex = current ? sourceLinks.indexOf(current) : 0; - const nextIndex = Math.max(0, Math.min(sourceLinks.length - 1, currentIndex + direction)); - const nextSource = sourceLinks[nextIndex]; + const currentIndex = current ? links.indexOf(current) : 0; + const nextIndex = Math.max(0, Math.min(links.length - 1, currentIndex + direction)); + const nextSource = links[nextIndex]; if (nextSource && nextSource !== current) window.location.href = nextSource.href; }; @@ -623,6 +685,7 @@ node = node.nextElementSibling; } markArrived(inserted); + registerNewSources(inserted); // update sidebar: counts + any new source liveSince = data.newest; const dayCount = group.querySelector(".day-separator-count"); if (dayCount) dayCount.textContent = String((parseInt(dayCount.textContent, 10) || 0) + data.count);