feat(dashboard): collapsible This Week widget, library-scoped + switch-surviving; title into the subbar

Three dashboard complaints, one build:

- The widget collapses to a single summary row (This Week · minutes ·
  pages · sessions · books · streak · Full stats) on every screen size
  and expands to the full grid via a details/summary toggle — the
  carousel sections start immediately below it again.
- The 'Your Library' heading moved into the library switcher subbar
  (LibrarySwitcher gains an optional title param; collections/series
  keep the plain 'Library' label). The standalone heading block is
  gone.
- Root-caused the JS-switch vanishing: renderDashboardCollections
  replaces ALL of #collections-container's innerHTML, so anything
  server-rendered inside it (title, widget) died on the first library
  switch. The widget now re-fetches ITSELF: a /dashboard/week-widget
  fragment (same resolveLibrary family-lens rules as the page) is
  pulled by htmx whenever the switcher dispatches week-refresh —
  carrying the current #library-select value — so the widget both
  survives switches and RE-SCOPES to the switched library. Initial SSR
  renders the same scope the sections use.

ThisWeek (and the streak calendar) gained an optional library filter.
Tests: TestReadingStatsWeekLibraryScope — per-library totals, scoped
streaks, invisible-library isolation, malformed id 400. Full suite
PASS.
This commit is contained in:
John O'Keefe committed 2026-10-14 13:46:18 -04:00
1 parent 5a2c1585ae
commit 0914c935d9
16 files changed
+576 -271

No files matched your search

+6 -2
View File
@@ -431,8 +431,10 @@ func (s *StatisticsService) UserSummary(ctx context.Context, userID pgtype.UUID,
// ThisWeek builds the dashboard widget: the current rolling 7-day
// window (today inclusive) versus the previous seven days, plus the
// streak context. One scan over 14 days, split in Go.
func (s *StatisticsService) ThisWeek(ctx context.Context, userID pgtype.UUID, tz string) (WeekSummaryStats, error) {
// streak context. One scan over 14 days, split in Go. libraryID scopes
// everything to one library (invalid = all visible libraries) so the
// widget tracks the dashboard's library switcher.
func (s *StatisticsService) ThisWeek(ctx context.Context, userID pgtype.UUID, tz string, libraryID pgtype.UUID) (WeekSummaryStats, error) {
statsRange, err := ResolveStatsRange(tz, "7")
if err != nil {
return WeekSummaryStats{}, err
@@ -450,6 +452,7 @@ func (s *StatisticsService) ThisWeek(ctx context.Context, userID pgtype.UUID, tz
UserID: userID,
StartTime: pgtype.Timestamptz{Time: scanStart, Valid: true},
EndTime: pgtype.Timestamptz{Time: statsRange.End, Valid: true},
LibraryID: libraryID,
VisibleLibraryIds: visibleOrNil(visibleIDs),
})
if err != nil {
@@ -511,6 +514,7 @@ func (s *StatisticsService) ThisWeek(ctx context.Context, userID pgtype.UUID, tz
Tz: loc.String(),
UserID: userID,
StartTime: pgtype.Timestamptz{Time: statsRange.Start.AddDate(0, 0, -(streakLookbackDays - statsRange.Days)), Valid: true},
LibraryID: libraryID,
VisibleLibraryIds: visibleOrNil(visibleIDs),
})
if err == nil {