diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ff33af..318a2a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,14 +9,6 @@ former name, "Daybook".) _Nothing yet._ -## [0.7.0] — 2026-09-07 - -### Added -- **Mark all as read.** Click a source's unread badge in the sidebar to mark - all of that source's messages read (clears the badge); click the **All - sources** total to mark everything read. Owner-scoped, excludes trashed; any - visible feed rows update too. Backed by `POST /feed/mark-read`. - ## [0.6.3] — 2026-09-07 ### Changed @@ -243,8 +235,7 @@ 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.7.0...HEAD -[0.7.0]: https://git.cygnus.systems/peter/daybook/compare/v0.6.3...v0.7.0 +[Unreleased]: https://git.cygnus.systems/peter/daybook/compare/v0.6.3...HEAD [0.6.3]: https://git.cygnus.systems/peter/daybook/compare/v0.6.2...v0.6.3 [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 diff --git a/artifacts/urls.py b/artifacts/urls.py index 44bab7f..158b3d1 100644 --- a/artifacts/urls.py +++ b/artifacts/urls.py @@ -13,7 +13,6 @@ urlpatterns = [ path("a//delete", views.delete_artifact, name="artifact-delete"), path("a//undelete", views.undelete_artifact, name="artifact-undelete"), path("feed/empty-trash", views.empty_trash, name="feed-empty-trash"), - path("feed/mark-read", views.mark_all_read, name="feed-mark-read"), path("a/", views.artifact_view, name="artifact"), path("s/", views.shared_artifact_view, name="artifact-shared"), path("assets/", views.asset_view, name="asset"), diff --git a/artifacts/views.py b/artifacts/views.py index 172b8c7..055c861 100644 --- a/artifacts/views.py +++ b/artifacts/views.py @@ -696,21 +696,6 @@ def empty_trash(request): return JsonResponse({"purged": count}) -@login_required -@require_POST -def mark_all_read(request): - """Mark all of the user's unread (non-trashed) messages as read, optionally - scoped to one source. Backs the sidebar 'Mark all as read' action.""" - unread = Artifact.objects.filter( - owner=request.user, deleted_at__isnull=True, read_at__isnull=True - ) - source = request.POST.get("source", "") - if source: - unread = unread.filter(source=source) - marked = unread.update(read_at=timezone.now()) - return JsonResponse({"marked": marked, "source": source}) - - @login_required def feed_older(request): """Return the next page of older day-groups as an HTML fragment for infinite scroll.""" diff --git a/daybook_project/__init__.py b/daybook_project/__init__.py index f00cfaa..b040cb3 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.7.0" +__version__ = "0.6.3" diff --git a/static/agentfeed.css b/static/agentfeed.css index 7c8110d..ca19527 100644 --- a/static/agentfeed.css +++ b/static/agentfeed.css @@ -514,18 +514,6 @@ h1 { font-size: 0.7rem; } -/* The unread badge doubles as a "mark all as read" button. */ -.source-count[data-mark-read] { - cursor: pointer; - border-radius: 8px; - padding: 0 0.35rem; -} - -.source-count[data-mark-read]:hover { - background: rgba(74, 110, 168, 0.28); - color: var(--text); -} - .pane-heading span, .artifact-time { color: var(--muted); diff --git a/templates/artifacts/feed.html b/templates/artifacts/feed.html index 4dfe047..daa2182 100644 --- a/templates/artifacts/feed.html +++ b/templates/artifacts/feed.html @@ -37,12 +37,12 @@ @@ -620,59 +620,6 @@ openRowMenu(row, event.clientX, event.clientY); }); - // Click a source's unread badge to mark all of that source's messages read. - const markSourceRead = async (badge) => { - const link = badge.closest(".source-link"); - if (!link) return; - const isAll = link.classList.contains("all-sources-link"); - const source = link.dataset.source || ""; - const body = new URLSearchParams(); - if (!isAll && source) body.set("source", source); - try { - const response = await fetch("{% url 'feed-mark-read' %}", { - method: "POST", - headers: {"X-CSRFToken": csrfToken, "Content-Type": "application/x-www-form-urlencoded"}, - body: body.toString(), - }); - if (!response.ok) return; - } catch (e) { return; } - if (isAll) { - document.querySelectorAll(".source-list .source-count").forEach((b) => b.remove()); - artifactList.querySelectorAll(".artifact-row.unread").forEach((r) => r.classList.remove("unread")); - } else { - const n = parseInt(badge.textContent, 10) || 0; - badge.remove(); - const allBadge = document.querySelector(".all-sources-link .source-count"); - if (allBadge) { - const t = (parseInt(allBadge.textContent, 10) || 0) - n; - if (t > 0) allBadge.textContent = String(t); - else allBadge.remove(); - } - artifactList - .querySelectorAll(`.artifact-row.unread[data-source="${CSS.escape(source)}"]`) - .forEach((r) => r.classList.remove("unread")); - } - }; - - const sourceListEl = document.querySelector(".source-list"); - if (sourceListEl) { - const badgeFromEvent = (event) => event.target.closest("[data-mark-read]"); - sourceListEl.addEventListener("click", (event) => { - const badge = badgeFromEvent(event); - if (!badge) return; - event.preventDefault(); - event.stopPropagation(); - markSourceRead(badge); - }); - sourceListEl.addEventListener("keydown", (event) => { - if (event.key !== "Enter" && event.key !== " ") return; - const badge = badgeFromEvent(event); - if (!badge) return; - event.preventDefault(); - markSourceRead(badge); - }); - } - const renderPreview = (row, pushState = true) => { navigationMode = "artifacts"; markRead(row); diff --git a/tests/views/test_trash.py b/tests/views/test_trash.py index 28faf70..e04601e 100644 --- a/tests/views/test_trash.py +++ b/tests/views/test_trash.py @@ -128,31 +128,3 @@ class TrashKeepViewTestCase(TestCase): self.client.post(reverse("artifact-delete", args=[fresh.id])) self.assertFalse(Artifact.objects.filter(pk=old.id).exists()) self.assertTrue(Artifact.objects.filter(pk=fresh.id, deleted_at__isnull=False).exists()) - - def test_mark_all_read_across_sources(self): - self.make(source="a") - self.make(source="b") - r = self.client.post(reverse("feed-mark-read")) - self.assertEqual(r.status_code, 200) - self.assertEqual(r.json()["marked"], 2) - self.assertFalse(Artifact.objects.filter(owner=self.user, read_at__isnull=True).exists()) - - def test_mark_all_read_scoped_to_source(self): - a = self.make(source="hermes") - b = self.make(source="janus") - self.assertEqual(self.client.post(reverse("feed-mark-read"), {"source": "hermes"}).json()["marked"], 1) - a.refresh_from_db() - b.refresh_from_db() - self.assertIsNotNone(a.read_at) - self.assertIsNone(b.read_at) - - def test_mark_all_read_owner_scoped(self): - theirs = self.make(owner=self.other) - self.client.post(reverse("feed-mark-read")) - theirs.refresh_from_db() - self.assertIsNone(theirs.read_at) - - def test_mark_all_read_excludes_trashed(self): - a = self.make() - Artifact.objects.filter(pk=a.id).update(deleted_at=timezone.now()) - self.assertEqual(self.client.post(reverse("feed-mark-read")).json()["marked"], 0)