v0.4.1: live updates also refresh the source sidebar

Extends v0.4.0 without a second request: the sidebar is updated client-side
from the rows the existing /feed/updates poll already returns.

- Each new row's data-source drives the sidebar: per-source + "All sources"
  unread counts bump; a first-time source is inserted into the list
  (alphabetically). Reuses the existing count-badge pattern.
- sourceLinks is now live-queried (was cached at load) so sources added live
  stay keyboard-navigable.
- No endpoint/model change. 142 tests pass; feed renders.

Caveat: while filtered to a single source, other new sources appear only on
reload (the poll is scoped to the selected view).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
peter 2026-09-05 14:37:45 +01:00
parent cae414c307
commit 23f9acea81
3 changed files with 81 additions and 7 deletions

View file

@ -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

View file

@ -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"

View file

@ -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 =
`<span class="source-name">${escapeHtml(name)}</span>` +
`<span class="source-count">${count}</span>`;
// 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);