From c1be2face7174883b356d69e0a5e8285cca12bd1 Mon Sep 17 00:00:00 2001 From: peter Date: Mon, 7 Sep 2026 21:18:49 +0100 Subject: [PATCH] v0.6.2: order sources by most recent message (recency) - Sidebar sources ordered by Max(created_at) desc instead of alphabetically; "All sources" stays pinned at top (rendered separately). - Live updates move a source to the top when it gets a new message, so the recency order holds without a reload. 152 tests pass; JS re-validated with node --check. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 10 +++++++++- artifacts/views.py | 8 +++++--- daybook_project/__init__.py | 2 +- templates/artifacts/feed.html | 26 ++++++++++++++++---------- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d6c6c6..1366216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ former name, "Daybook".) _Nothing yet._ +## [0.6.2] — 2026-09-07 + +### Changed +- Source sidebar is now ordered by **most recent message** (Telegram-style) + instead of alphabetically; "All sources" stays pinned at the top. Live + updates move a source to the top when it receives a new message. + ## [0.6.1] — 2026-09-07 ### Changed @@ -214,7 +221,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.6.1...HEAD +[Unreleased]: https://git.cygnus.systems/peter/daybook/compare/v0.6.2...HEAD +[0.6.2]: https://git.cygnus.systems/peter/daybook/compare/v0.6.1...v0.6.2 [0.6.1]: https://git.cygnus.systems/peter/daybook/compare/v0.6.0...v0.6.1 [0.6.0]: https://git.cygnus.systems/peter/daybook/compare/v0.5.1...v0.6.0 [0.5.1]: https://git.cygnus.systems/peter/daybook/compare/v0.5.0...v0.5.1 diff --git a/artifacts/views.py b/artifacts/views.py index 1d2aff0..055c861 100644 --- a/artifacts/views.py +++ b/artifacts/views.py @@ -19,7 +19,7 @@ from django.contrib.auth import views as auth_views from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from django.db import OperationalError, transaction -from django.db.models import Count, Q, Sum +from django.db.models import Count, Max, Q, Sum from django.http import FileResponse, Http404, HttpResponse, HttpResponseForbidden, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.template.defaultfilters import date as date_filter, slugify @@ -586,10 +586,12 @@ def feed(request): source_scoped = apply_feed_filters( live, "", selected_label, selected_kind, title_query ) + # Sources ordered by most recent message first (Telegram-style); "All + # sources" is rendered separately and stays pinned at the top. source_counts = ( source_scoped.values("source") - .annotate(count=Count("id", filter=Q(read_at__isnull=True))) - .order_by("source") + .annotate(count=Count("id", filter=Q(read_at__isnull=True)), latest=Max("created_at")) + .order_by("-latest") ) keep_count = live.filter(is_kept=True).count() trash_count = Artifact.objects.filter(owner=request.user, deleted_at__isnull=False).count() diff --git a/daybook_project/__init__.py b/daybook_project/__init__.py index 6227cb2..28b8174 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.6.1" +__version__ = "0.6.2" diff --git a/templates/artifacts/feed.html b/templates/artifacts/feed.html index 54b3d20..a8e9cca 100644 --- a/templates/artifacts/feed.html +++ b/templates/artifacts/feed.html @@ -443,15 +443,17 @@ `${escapeHtml(name)}` + `${count}`; - // Insert alphabetically among the per-source links (after "All sources"). + // Newest activity goes to the top of the per-source list (just under + // the pinned "All sources"), matching the recency ordering. 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); + const firstSource = list.querySelector(".source-link:not(.all-sources-link)"); + list.insertBefore(link, firstSource); + }; + + const moveSourceToTop = (link) => { + const list = document.querySelector(".source-list"); + const firstSource = list.querySelector(".source-link:not(.all-sources-link)"); + if (firstSource !== link) list.insertBefore(link, firstSource); }; // From the newly-arrived rows: bump each source's unread count (adding a @@ -466,8 +468,12 @@ Object.entries(counts).forEach(([source, n]) => { total += n; const existing = findSourceLink(source); - if (existing) bumpSourceBadge(existing, n); - else addSourceLink(source, n); + if (existing) { + bumpSourceBadge(existing, n); + moveSourceToTop(existing); // most-recent activity -> top (recency order) + } else { + addSourceLink(source, n); + } }); const allLink = findSourceLink("All sources"); if (allLink && total) bumpSourceBadge(allLink, total);