Skip to content

Commit d634b60

Browse files
fix(summary): order projects and topics by live activity (#17)
The live "active … ago" times moved, but the project list, the project sections and each project's topics kept the order of the summary run. The page now renders them newest first by the live times, and the minute refresh reorders the sidebar, the sections and the topics in place, keeping whatever is at the top of the window where it was. Claude-Session: https://claude.ai/code/session_01NtvhFvhWywmqE7ZEmKxLJy Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e12e1d1 commit d634b60

4 files changed

Lines changed: 50 additions & 1 deletion

File tree

‎CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ All notable changes are documented here. The format follows
66

77
## [Unreleased]
88

9+
## [0.16.5] - 2026-11-07
10+
11+
### Fixed
12+
13+
- The Summary page's project list, its project sections and the topics in each project are ordered
14+
by the live "active … ago" times, newest first, and reorder as those times move. The part of the
15+
page you are reading stays in place.
16+
917
## [0.16.4] - 2026-11-07
1018

1119
### Fixed

‎page.html‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2907,6 +2907,32 @@
29072907
// asks for the latest scan time and session activity instead. Every tab keeps its
29082908
// relative times current.
29092909
const synced = $("#synced");
2910+
// Newest work first, as the server orders it, by the times the tick just moved.
2911+
// Whatever is at the top of the window stays where it is on screen.
2912+
function summaryReorder() {
2913+
const newest = (time) => time ? new Date(time.dateTime).getTime() || 0 : 0;
2914+
const order = (items, time) => {
2915+
const sorted = [...items].sort((a, b) => newest(time(b)) - newest(time(a)));
2916+
return sorted.some((item, i) => item !== items[i]) ? sorted : null;
2917+
};
2918+
const anchor = $$(".summary-project, .summary-topic").find(el => el.getBoundingClientRect().bottom > 0);
2919+
const top = anchor?.getBoundingClientRect().top;
2920+
let moved = false;
2921+
$$(".summary-toc-list").forEach(list => {
2922+
const sorted = order([...list.children], li => $("time[data-summary-project]", li));
2923+
if (sorted) { list.append(...sorted); moved = true; }
2924+
});
2925+
const links = $$(".summary-toc-list a[href^='#summary-project-']").slice(0, $$(".summary-project[id]").length);
2926+
const sections = links.map(link => document.getElementById(link.getAttribute("href").slice(1))).filter(Boolean);
2927+
const content = sections[0]?.parentElement;
2928+
if (content && sections.some((section, i) => section !== $$(".summary-project[id]")[i])) { content.prepend(...sections); moved = true; }
2929+
$$(".summary-project[id]").forEach(section => {
2930+
const topics = $$(":scope > .summary-topic:has(time[data-summary-topic])", section);
2931+
const sorted = order(topics, topic => $("time[data-summary-topic]", topic));
2932+
if (sorted) { (topics[0].previousElementSibling ? topics[0].previousElementSibling.after(...sorted) : section.prepend(...sorted)); moved = true; }
2933+
});
2934+
if (moved && anchor && anchor.isConnected) window.scrollBy(0, anchor.getBoundingClientRect().top - top);
2935+
}
29102936
async function tick() {
29112937
if (tab === "summary" && synced) {
29122938
try {
@@ -2921,6 +2947,7 @@
29212947
if (at && at > new Date(time.dateTime)) { time.dateTime = at.toISOString(); time.title = at.toLocaleString(); }
29222948
});
29232949
}
2950+
summaryReorder();
29242951
}
29252952
} catch {}
29262953
}

‎summary_activity.go‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"log"
5+
"slices"
56
"time"
67
)
78

@@ -107,7 +108,8 @@ func (snap *summarySnapshot) hasProject(project string) bool {
107108
}
108109

109110
// apply returns copies of snap and the snoozed topics showing the newer times in
110-
// act. The saved summary is shared, so nothing it points to is changed.
111+
// act, with projects and topics reordered by them. The saved summary is shared,
112+
// so nothing it points to is changed.
111113
func (act summaryActivity) apply(snap *summarySnapshot, snoozed []summarySnoozedTopic) (*summarySnapshot, []summarySnoozedTopic) {
112114
topic := func(t *summarySnapshotTopic) *summarySnapshotTopic {
113115
at, ok := act.Topics[t.ID]
@@ -130,8 +132,11 @@ func (act summaryActivity) apply(snap *summarySnapshot, snoozed []summarySnoozed
130132
for _, t := range project.Topics {
131133
copied.Topics = append(copied.Topics, topic(t))
132134
}
135+
// The newest work comes first, as in the summary, but by the live times.
136+
slices.SortStableFunc(copied.Topics, func(a, b *summarySnapshotTopic) int { return b.LastActive.Compare(a.LastActive) })
133137
view.Projects = append(view.Projects, &copied)
134138
}
139+
slices.SortStableFunc(view.Projects, func(a, b *summarySnapshotProject) int { return b.LastActive.Compare(a.LastActive) })
135140
snap = &view
136141
}
137142
out := make([]summarySnoozedTopic, len(snoozed))

‎summary_activity_test.go‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ func TestSummaryActivityFollowsLiveSessions(t *testing.T) {
5656
if !view.Projects[0].LastActive.Equal(at.Add(30*time.Minute)) || !view.Projects[0].Topics[0].LastActive.Equal(at.Add(30*time.Minute)) || !views[0].Topic.LastActive.Equal(at.Add(10*time.Minute)) {
5757
t.Fatal("apply did not show the newer times")
5858
}
59+
// The newest work comes first, by the live times: acme/infra (20 min) is
60+
// still after acme/api (30 min), and the carried-over topic passes "idle".
61+
if view.Projects[0].Project != "acme/api" || view.Projects[1].Project != "acme/infra" || view.Projects[0].Topics[1].ID != "carried" || view.Projects[0].Topics[2].ID != "idle" {
62+
t.Fatalf("order: %s, %s / %s, %s", view.Projects[0].Project, view.Projects[1].Project, view.Projects[0].Topics[1].ID, view.Projects[0].Topics[2].ID)
63+
}
64+
later := summaryActivity{Projects: map[string]time.Time{"acme/infra": at.Add(2 * time.Hour)}}
65+
if view, _ := later.apply(snap, nil); view.Projects[0].Project != "acme/infra" || snap.Projects[0].Project != "acme/api" {
66+
t.Fatal("a project active more recently did not move first, or the saved order changed")
67+
}
5968
if !snap.Projects[0].LastActive.Equal(before) || !snap.Projects[0].Topics[0].LastActive.Equal(before) || !snoozed[0].Topic.LastActive.Equal(before.Add(-48*time.Hour)) {
6069
t.Fatal("apply changed the saved summary")
6170
}

0 commit comments

Comments
 (0)