Skip to content

feat: add a usage recap for any week, month or year - #332

Open
DyRize wants to merge 2 commits into
chattymin:mainfrom
DyRize:feat/usage-recap
Open

DyRize wants to merge 2 commits into
chattymin:mainfrom
DyRize:feat/usage-recap

Conversation

@DyRize

@DyRize DyRize commented Sep 18, 2026

Copy link
Copy Markdown

Summary

Adds a usage recap: one calendar week, month or year at a time, stepping back through time with Previous / Next, rendered as a small Pokédex-style readout that copies or saves as a PNG. It reports what already happened: no goal, no reward, nothing to do.

What the card shows: the total, one meter per day (per month in the year view), the change against the previous period, best day, best streak, active days, and the individuals graduated in that period, next to the Pokémon being raised. The title is the period itself (Sep 14 – 20, 2026, September 2026, 2026), so an exported image says on its own what it covers.

Where the history comes from. Providers only report the current month (monthDaily), so on the 3rd a week would lose four days and a year would be out of reach. Every refresh now copies the daily totals into a small ledger in UserDefaults (UsageLedger):

  • a day never goes down (max-merge), so a rotated or pruned log cannot erase days that really happened;
  • it keeps this year and the last one, the most any view compares against (about 20 KB at most);
  • it is derived data, like the usage cache: not part of the companion save, not transferred, rebuilt by the next refresh.

"No usage" vs "not recorded yet". The ledger also stores the first day it can vouch for. The month series lists every day since the 1st, empty ones included, so the first refresh vouches for the whole month so far. Days before that point, and days still ahead, are drawn as unknown rather than as zeros (dimmer grid, "no data" for VoiceOver), and Previous stops where the history starts.

Comparisons that do not lie:

  • a period still running is compared over the same number of days: on the 18th, September 1–18 against August 1–18, not against the whole of August. A caption under the card says so;
  • a finished period is compared whole;
  • no chip at all unless the ledger covers the previous period from its first day, so a fresh install never claims a jump over days it never saw.

Weeks start on the region's first weekday; periods use a Gregorian calendar whatever the system calendar, so a year runs January to December and day keys match LocalUsageReader.localDayFormatter.

Entry point: a chart button at the end of the "This week / This month" row on Home.

One thing to check with you: the exported PNG includes the sprites shown on the card, as the trainer card in #331 does. CONTRIBUTING rules out features that "redistribute or export copyrighted assets". If that covers a user-made image of their own stats, I can make the export sprite-free and keep the sprites on screen only.

This branch is independent of #331. Both add a bullet to the same README list and strings to Localization.swift, so whichever merges second needs a trivial rebase — I'll take care of it.

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Documentation
  • Other:

UI changes

Before After
Home's week/month row shows the "This week" and "This month" totals and nothing else. A chart button (chart.bar.xaxis) at the end of that row opens the recap inside the popover, like Settings: Back (Esc works too), a Week / Month / Year picker, Previous / Next, the card, a best-day line, a note on how the chip compares while a period is running, Copy image and Save….

Screenshots (English, rendered from this branch):

pr332-recap-screen pr332-recap-week pr332-recap-month pr332-recap-year

The card is drawn with explicit colors, so the exported image looks the same in light and dark mode.

Checklist

  • swift build and swift test pass locally
  • PR title and description are written in English
  • UI changes are described above (before/after — images optional)
  • No copyrighted assets, secrets, or private tooling references are committed (see CONTRIBUTING)
  • Tests were added or updated for this change

scripts/test-gate.sh: 1097 tests, 11 skipped, 0 failures, logic-core line coverage 92.84% (UsageRecap.swift added to the logic core, 96.7% of its lines). 19 tests cover the recap: ledger merge, coverage and pruning, junk in hand-edited defaults, the first refresh filling the ledger, retention of last year, week start by region, month and year stepping (31 March back to February), meters per scope, unknown vs zero days, same-day comparison for running periods, whole comparison for finished ones, no comparison over a half-covered period, best streak cut at the period's edges, graduations in the period, where Previous stops, and rendering in all seven languages for every scope. New branches were checked with llvm-cov --show-regions rather than the coverage number; the remaining ^0 regions are API-forced unwraps on Gregorian arithmetic. Every rule above was verified by injecting the defect and watching its test fail.

Two defects found while building this are recorded in docs/reference/defect-log.md: the ledger was first written right after phase 1 of the refresh, before monthDaily exists, which left the first refresh empty; and an early version judged coverage on the newest day of the previous window instead of the oldest, which inflated the delta.

A recap screen opens from the week/month row on Home: one calendar week,
month or year at a time, with Previous and Next to step back through time.
The card shows the total, one meter per day (per month in the year view),
the change against the previous period, best day, best streak, active days
and the individuals graduated in that period. It copies to the clipboard or
saves as a PNG; as with the rest of the app, nothing leaves the Mac.

Providers only report the current month, so every refresh copies the daily
totals into a local ledger in UserDefaults. A day never goes down, which
survives rotated logs, and the ledger keeps this year and the last one so the
year view has something to compare against.

The ledger stores the first day it can vouch for, next to the rows. That is
what tells "no usage" from "not recorded yet": days before it, and days still
ahead, are drawn as unknown instead of as zeros, and a comparison is only shown
when the previous period is covered from its first day. A period still running
is compared over the same number of days, so the 18th of the month does not read
as a drop against a whole month.

The ledger is written after the enrichment phase, where the month series
actually arrives; writing it after phase 1 left the first refresh empty.
The recap was branched before Unown forms landed: the raised Pokémon and the
graduates fell back to the default A sprite, and the strip named them all
"Unown". The export preloads the same form, since ImageRenderer only draws what
is already cached.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant