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 <noreply@anthropic.com>
This commit is contained in:
peter 2026-09-07 21:18:49 +01:00
parent ab3e92268a
commit c1be2face7
4 changed files with 31 additions and 15 deletions

View file

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

View file

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

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.6.1"
__version__ = "0.6.2"

View file

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