Arrow Left/Right called target.select(), which painted the segment with
the browser's default text-selection color instead of the brand focus
background used everywhere else — so a part looked pink when reached by
arrow keys but blue when clicked or tabbed into. The select() was
redundant (the digit handler already restarts a full part on the next
keypress), so removing it makes the focus highlight consistent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the stats page to filter_url()/stats_links:
- Per-row session links on game superlatives, games-by-playtime, platform
and month rows (game rows keep their detail GameLink, add a session icon).
- Count links: sessions, games, total/refunded/dropped/unfinished/backlog
purchases.
- Cap preview lists to 5 with a 'View all (N)' link passing ?sort= for order
parity; remove the redundant 'All Purchases' list.
- stats_data: carry platform_id for platform links; drop the all-time
games-by-playtime [:10] slice so the view-all count is honest (rendering
caps the preview).
Also make the filter bar's _extract_labeled tolerate bare choice/multi values
so a programmatically-built filter URL renders instead of crashing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RF5L4HtbcykTfY9YUYGds3
stats_links.py: pure functions returning a filter per stats category
(sessions for game/platform/month, games played, total/refunded/dropped/
unfinished/finished/finished-released/bought-and-finished/backlog purchases).
Each is verified to produce a queryset whose count equals the stat it links
from, on single-game data (the modeling norm).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RF5L4HtbcykTfY9YUYGds3
Backlog decrease is expressible with the existing prereq #67 (no new
machinery), so it moves into Tier 2. List-view sort support is filed as
non-prereq #68; view-all links carry a TODO at each site until it lands.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RF5L4HtbcykTfY9YUYGds3
Typed digits in the segmented date field were unvalidated — you could
enter 60 for a day or 30 for a month. Now each digit is clamped to its
part's range and auto-advances:
- A digit that cannot validly extend the current part commits as a
zero-padded value and moves to the next part (month 9 → 09▶, day 6 →
06▶).
- An ambiguous digit that could still take a second stays pending
(month 1 → 01; then 2 → 12▶, or 9 → 09▶ dropping the overflowed 1).
- Day/month show a pending single digit zero-padded; the year part keeps
its existing right-fill placeholder display and 4-digit advance.
Logic lives in a pure applyDigit() helper; completion is normalized to a
full-width buffer so syncHiddenFromSegments commits it. Adds 10 e2e tests
covering clamping, auto-advance, overflow-drop, zero-pad display, the
single-digit commit invariant, and restart-on-full.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes#64. The segmented date-range field now responds to arrow keys:
- Left/Right move focus between DD/MM/YYYY parts, crossing the min→max
separator; focus clamps at the first/last part (no wrap).
- Up/Down increment/decrement the focused part, clamped to its valid
range (day 1-31, month 1-12, year 1-9999). An empty part seeds to 01
for day/month and the current year for year on the first press.
Arrows with modifiers (Ctrl/Alt/Meta) still fall through to native
behavior. Adds e2e coverage for focus walking, boundary clamping, value
stepping, hidden-ISO commit, and modifier passthrough.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Final-review follow-up: LinkedPurchase reads purchase.platform per row;
eager-load it alongside the games prefetch to remove the residual N+1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Test was passing for wrong reason: both default order (-date) and -duration sort
put Beta first because Beta had longer duration AND later date. Make order diverge
by swapping durations, so -duration must override date-based ordering to pass. Also
fix test to extract tbody to avoid matching "Beta" in header's last-session button.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Six TDD tasks: sorting.py core types + parse_sort_terms; per-model maps +
apply_sort + parse_find_filter; wire each of the three list views (sort +
N+1 eager-load + unknown-key warning toast); regression smoke. Links new
follow-up #77 (presets persist/restore sort) in spec + plan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend the "name compound types" convention to cover bare str/int that
stand for a domain concept (SortKey, AnnotationName, ...). Surfaced while
designing games/sorting.py (#68), where several distinct string roles meet
in one signature.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Honor a signed comma-list ?sort= param on list_games/list_sessions/
list_purchases via a new games/sorting.py (SortSpec + per-model maps +
parse_sort_terms + apply_sort). Backend applies the full multi-term list.
Unknown sort keys are ignored with a user-facing warning toast (never a
400), surfacing #65 link drift without breaking hand-editable URLs.
Named string/compound roles throughout (SortKey, SortString, AnnotationName,
OrderField, SortTerm, SortResult) so signatures say which value goes where.
Includes an N+1 select_related/prefetch_related prereq on the list
querysets. Adjacent follow-ups filed: #73 (header UI), #74 (FindFilter
unify; also owns the unused FindFilter.direction/page/per_page fields),
#75 (purchase search), #76 (shared list_view helper).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add Started and Finished DateRangePicker widgets to the PlayEvent filter bar
and wire filter-started / filter-ended into the filter-bar date-range
serializer, so the started/ended DateCriterion fields (added for #67) are
reachable from the UI — enabling "finished in year Y" range filtering.
Builds on #67 (PlayEventFilter.started/ended are DateCriterion); the bare
field names round-trip through _parse_range like the Purchase date fields.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RangeSlider does not render its own label — the field label is emitted by
the _filter_field wrapper. The PlayEvent filter bar added the Days to Finish
slider bare, so it showed no label. Wrap it in _filter_field like every
other slider (GameFilterBar/PurchaseFilterBar).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Change PlayEventFilter.started/ended from StringCriterion to DateCriterion
so they support GREATER_THAN / LESS_THAN / BETWEEN, enabling
"finished in year Y" to be expressed through the filter system.
PlayEvent.started/ended are DateField columns, so the criteria apply with
bare field names (no __date lookup, unlike SessionFilter.timestamp_start
which is a datetime). This mirrors the existing PurchaseFilter DateField
precedent. Deserialization auto-switches via the field annotation and the
serialized JSON shape is unchanged, so the type change is backward-compatible.
Prerequisite for #65 Tier-2 stats-page filtered links. Part of #61.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The staging deploy runs on push to a non-main branch and tries to comment
the staging URL on the branch's PR. When the branch is pushed before the PR
exists (the common case), the comment is skipped and never reappears once the
PR is opened.
Add a pull_request [opened, reopened] trigger and move the comment into its
own job that runs both after a successful push-deploy and on PR open/reopen.
The branch is taken from github.head_ref on PR events and github.ref_name on
push; the existing dedupe-by-body keeps the two paths from double-posting.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RF5L4HtbcykTfY9YUYGds3
Add filter_url(), a reverse()-style helper that builds a URL to a filtered
list view from a filter object (target inferred from the filter type).
Add OperatorFilter.where(**lookups), a Django-.filter()-style ergonomic
constructor that resolves each field's criterion class from its annotation
(shared with from_json via _criterion_class_for, removing duplication).
Make SessionFilter.timestamp_start/timestamp_end DateCriterion applied via
the __date lookup, so date ranges over the timestamp columns are expressible.
Wire the navbar 'today' / 'last 7 days' totals as links to the matching
filtered session lists, and align the 'last 7 days' total to the same
calendar-day window so the number matches the list it links to.
Stats-table and game-detail links remain a follow-up (see spec).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RF5L4HtbcykTfY9YUYGds3
The flip-up branch cleared inline `top` to "", which let the menu's
`top-[105%]` utility class reassert top:105% on the now-fixed element —
collapsing the menu to a 2px sliver below the viewport, so toggles near the
viewport bottom appeared not to open. Set the unused anchor to "auto" so the
inline value wins over the class. Add an e2e regression for the flip-up path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The device/status dropdown menu is absolutely positioned inside the session
list's overflow-x-auto wrapper. Because overflow-x:auto forces overflow-y:auto,
a menu taller than a short table was clipped (issue #39). Open the menu with
position:fixed anchored to its toggle so it escapes the clipping ancestor,
bound it to the viewport with an internal scroll, flip it up when there is more
room above, and reposition on scroll/resize while open.
Fixes#39.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
base.css is a Tailwind build artifact already listed in .gitignore, but it
had been tracked since it was first committed (gitignore can't untrack an
already-tracked file), so it kept getting re-committed against intent. Untrack
it to match js/dist (the TS artifact, also gitignored + untracked).
Because nothing in the test path rebuilt it, e2e/static serving relied on the
committed copy. Add 'css' (and 'ts') as prerequisites of the test/test-e2e
make targets and a Build CSS step in CI so the stylesheet is generated before
tests run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Flowbite re-initialises popovers on every htmx swap. A popover hidden via
Tailwind `invisible` (visibility:hidden) still occupies layout, so once
Popper parks it with a transform offset it expands the table's
overflow-x-auto wrapper and a spurious scrollbar appears (horizontal here,
vertical in #40). Add `[&.invisible]:hidden` so the popover is removed from
layout while hidden; Flowbite drops `invisible` on show, restoring display.
Relates to #40. e2e regression covers no-overflow-after-swap plus
popover-still-shows-on-hover.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>