search_label built its label from sort_name, an optional sort key that
is blank for most games, so the Game and Related-game dropdowns in the
add-purchase form (and the session form and search API, which share the
property) showed a blank/"None" label. Use name, which is required.
Also route search_label and Purchase.full_name through label_with_details
so a missing year_released drops out of the parenthetical instead of
rendering a literal "None". (platform is never None at display time -
Game.save() substitutes the "Unspecified" sentinel.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds a "Name (detail, detail)" label from a name plus optional
details, dropping falsy parts and omitting the parentheses entirely
when none remain. Extracted to deduplicate the "filter present parts,
join, wrap in parens" idiom that several model display properties share.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(make): run migrate before loadplatforms in init target
make init loaded platform fixtures without first creating the database
schema, failing with 'no such table: games_platform' on a clean repo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(add-game): sync original year from year released until dirty (#35)
Mirror year_released into original_year_released live as the user types,
stopping once original is edited directly — same sync-until-dirty pattern
already used for name -> sort_name. Reorder the two year fields so
year_released renders first, otherwise the user would fill original first
and negate the sync.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The played-row "Played N times" dropdown regressed when it was migrated
from Alpine to a custom element (commit 1258c52): the hover highlight,
the row-filling click target and a consistent pointer cursor were lost
because the interactive <a>/<button> shrank to its text while the <li>
rows stopped carrying hover/click behaviour. Clicking the row's padding
hit the handler-less <li> and was silently swallowed.
Make each menu item the interactive element itself (block w-full + own
padding + hover highlight + pointer cursor), mirroring the status
selector's _SELECTOR_OPTION_CLASS, so the control fills the whole row.
Also refresh the Play Events section in place: the play-event-row now
dispatches a "play-added" event after recording a play, and
#playevents-container re-fetches itself on it (mirroring the history
section's status-changed refresh), so the table and count badge update
without a full reload.
Add e2e regression tests covering hover highlight, full-row pointer
cursor, the row-wide +1 click target, and the in-place table refresh.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add "View all" links to the Purchases / Sessions / Play Events sections
on the game-detail page, each pointing at the matching filtered list view
via filter_url(). Pure consumer of the #56 filter_url()/where() helpers;
no new filter machinery.
- _game_section() gains an optional view_all_url, rendered as a gray xs
button beside the heading (shown only when the section is non-empty).
- New arrowright icon for the link.
- Tests: each section renders the expected escaped href; a parity test
asserts each link's filter scopes to the game and excludes others.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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