From ba9f643cf5672d1448b0bb5786f37f7f468ce8ba Mon Sep 17 00:00:00 2001 From: Eszter Schuffert Date: Thu, 5 Nov 2026 22:08:50 +0100 Subject: [PATCH 1/6] Keep synced pages on reading-time calendar days without recorded time Once a reader sends daily time, the Stats calendar switched to the time view, and every day known only from sync history (before stats existed, or read on a position-only device) showed a flat "read". Such a day now keeps its synced pages: shown in the tip and shaded on the pages scale. Days with recorded time are unchanged. Fixes #63 --- app/src/Stats.jsx | 18 ++++++++++++++---- app/src/Stats.test.jsx | 20 +++++++++++++++++--- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/app/src/Stats.jsx b/app/src/Stats.jsx index e57ea87d..e02ee1d9 100644 --- a/app/src/Stats.jsx +++ b/app/src/Stats.jsx @@ -348,13 +348,17 @@ const DAYS_VIEW = { } // byDay: every read day -> amount (undefined when unknown); view describes the available data. -function ReadingCalendar({ byDay, view, days = [], books = [] }) { +// pages: synced print pages per day, shown for a day whose amount is unknown (#63). +function ReadingCalendar({ byDay, view, days = [], books = [], pages }) { const [scale, setScale] = useState('year') const [back, setBack] = useState(0) const [sharing, setSharing] = useState(false) const scroller = useRef(null) const { level: amountLevel } = shadeLevels(byDay, { fallback: view.fallback }) - const level = (day) => (!byDay.has(day) ? 0 : amountLevel(byDay.get(day))) + const { level: pageLevel } = shadeLevels(pages ?? new Map(), { fallback: FALLBACK_PAGES }) + const syncedPages = (day) => (byDay.get(day) === undefined ? pages?.get(day) ?? 0 : 0) + const level = (day) => (!byDay.has(day) ? 0 : syncedPages(day) > 0 ? pageLevel(syncedPages(day)) : amountLevel(byDay.get(day))) + const tipFor = (day) => (syncedPages(day) > 0 ? PAGES_VIEW.tip(syncedPages(day)) : view.tip(byDay.get(day))) const today = new Date() today.setHours(12, 0, 0, 0) const { weeks, first, last, label } = calendarRange(scale, back, today) @@ -374,7 +378,7 @@ function ReadingCalendar({ byDay, view, days = [], books = [] }) { const cell = (d, round) => { if (!shown(d)) return
const key = localDay(d) - const tip = `${fmt(d, { weekday: 'short', month: 'short', day: 'numeric' })}: ${byDay.has(key) ? view.tip(byDay.get(key)) : 'no reading'}` + const tip = `${fmt(d, { weekday: 'short', month: 'short', day: 'numeric' })}: ${byDay.has(key) ? tipFor(key) : 'no reading'}` return
} // Share the visible period, reading days, available page/time totals and longest streak. @@ -925,7 +929,13 @@ export default function Stats({ session, tab = '', summary, activity, books, act ['Books finished', books.filter((b) => b.status === 'finished').length], ]} /> - +
diff --git a/app/src/Stats.test.jsx b/app/src/Stats.test.jsx index 7f362f65..e4ff6a7f 100644 --- a/app/src/Stats.test.jsx +++ b/app/src/Stats.test.jsx @@ -195,7 +195,7 @@ describe('reading grids', () => { expect(queryByText('Today')).toBeNull() expect(queryByText('7-day average')).toBeNull() expect(queryByText('Less')).toBeNull() - expect(getByLabelText('Wed, Sep 30: read')).toBeTruthy() + expect(getByLabelText('Wed, Sep 30: 5 pages')).toBeTruthy() expect(getByLabelText('Tue, Sep 29: read')).toBeTruthy() fireEvent.click(getByRole('button', { name: 'Share your reading days' })) fireEvent.click(getByRole('button', { name: 'Render calendar share' })) @@ -222,7 +222,7 @@ describe('reading grids', () => { const daily = [{ date: '2025-08-01', seconds: 600 }] const { getByText, getByLabelText } = render() expect(getByText('Reading time recorded by your reader')).toBeTruthy() - expect(getByLabelText('Wed, Sep 30: read')).toBeTruthy() + expect(getByLabelText('Wed, Sep 30: 5 pages')).toBeTruthy() expect(getByText('Today').nextElementSibling.textContent).toBe('0m') }) @@ -234,7 +234,8 @@ describe('reading grids', () => { rerender() expect(queryByRole('group', { name: 'Reading calendar measure' })).toBeNull() expect(queryByRole('button', { name: 'Pages read' })).toBeNull() - expect(getByLabelText(tip)).toBeTruthy() + // Recorded time wins; without it the day keeps its synced pages (#63). + expect(getByLabelText(hasDaily ? tip : 'Wed, Sep 30: 5 pages')).toBeTruthy() // The reader says Sep 29 was read; sync progress has pages only on Sep 28 and Sep 30. expect(getByLabelText('Tue, Sep 29: read')).toBeTruthy() expect(getByLabelText('Mon, Sep 28: no reading')).toBeTruthy() @@ -250,6 +251,19 @@ describe('reading grids', () => { expect(getByLabelText('Wed, Sep 30: 5 pages')).toBeTruthy() }) + // #63: once a reader sends time, days known only from sync history (before it did, or read on a + // position-only device) kept their pages instead of turning into a flat "read". + it('shows synced pages for days without recorded time', () => { + const history = { + books: [], pages_total: 32, reading_days: ['2026-09-20', '2026-09-24', '2026-09-30'], + days: [{ day: '2026-09-20', syncs: 2, pages: 12 }, { day: '2026-09-24', syncs: 1, pages: 0 }, { day: '2026-09-30', syncs: 1, pages: 20 }], + } + const { getByLabelText } = render() + expect(getByLabelText('Wed, Sep 30: 1h 10m reading time')).toBeTruthy() + expect(getByLabelText('Sun, Sep 20: 12 pages')).toBeTruthy() + expect(getByLabelText('Thu, Sep 24: read')).toBeTruthy() + }) + it('updates both streaks from the same dates that fill the grid', () => { const summary = { devices: [{ device: 'CrossInk' }], current_streak: 8, streak: 12, From da813d919c48b7d4b54d955b969316f758730e1d Mon Sep 17 00:00:00 2001 From: Eszter Schuffert Date: Thu, 5 Nov 2026 22:17:43 +0100 Subject: [PATCH 2/6] Say when the time calendar shows synced pages --- app/src/Stats.jsx | 7 ++++++- app/src/Stats.test.jsx | 5 +++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/Stats.jsx b/app/src/Stats.jsx index e02ee1d9..0f724f14 100644 --- a/app/src/Stats.jsx +++ b/app/src/Stats.jsx @@ -358,6 +358,7 @@ function ReadingCalendar({ byDay, view, days = [], books = [], pages }) { const { level: pageLevel } = shadeLevels(pages ?? new Map(), { fallback: FALLBACK_PAGES }) const syncedPages = (day) => (byDay.get(day) === undefined ? pages?.get(day) ?? 0 : 0) const level = (day) => (!byDay.has(day) ? 0 : syncedPages(day) > 0 ? pageLevel(syncedPages(day)) : amountLevel(byDay.get(day))) + const usesPages = [...byDay.keys()].some((day) => syncedPages(day) > 0) const tipFor = (day) => (syncedPages(day) > 0 ? PAGES_VIEW.tip(syncedPages(day)) : view.tip(byDay.get(day))) const today = new Date() today.setHours(12, 0, 0, 0) @@ -470,7 +471,11 @@ function ReadingCalendar({ byDay, view, days = [], books = [], pages }) { ))}
- {view.source &&

{view.source}

} + {view.source && ( +

+ {usesPages ? `${view.source}; synced pages where no time was recorded` : view.source} +

+ )}