Commit Graph

879 Commits

Author SHA1 Message Date
lukas 6459bb983e feat(sorting): SortSpec/SortTerm types + parse_sort_terms (#68) 2026-06-21 13:43:11 +02:00
lukas e19f0ee63b docs(sorting): implementation plan for list-view sort param (#68)
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>
2026-06-21 13:40:03 +02:00
lukas 427179b886 docs(claude): name primitive roles with PEP 695 type aliases
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>
2026-06-21 13:35:00 +02:00
lukas 74805a7019 docs(sorting): design spec for sort query param on list views (#68)
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>
2026-06-21 13:35:00 +02:00
lukas 32beec115c Merge pull request #69 from KucharczykL/feat/issue-67-playevent-datecriterion
feat(filters): date-range filtering on PlayEventFilter.started/ended (#67)
2026-06-21 12:47:48 +02:00
lukas 799d500bfc Merge remote-tracking branch 'origin/main' into feat/issue-67-playevent-datecriterion
# Conflicts:
#	common/components/filters.py
#	tests/test_filter_bars.py
2026-06-21 12:35:23 +02:00
lukas c5f957be0d Merge pull request #72 from KucharczykL/feat/playevent-date-filters-ui
feat(filters): surface started/ended date filters on the PlayEvent filter bar
2026-06-21 12:29:02 +02:00
lukas b711f9b845 Merge pull request #71 from KucharczykL/fix/playevent-filter-slider-label
fix(filters): label the Days to Finish slider on the PlayEvent filter bar
2026-06-21 12:27:43 +02:00
lukas 6e7f000e1c feat(filters): surface started/ended date filters on the PlayEvent filter bar
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>
2026-06-21 12:08:27 +02:00
lukas 8ec8ba6c8c fix(filters): label the Days to Finish slider on the PlayEvent filter bar
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>
2026-06-21 12:05:39 +02:00
lukas ab4dae55eb feat(filters): date-range filtering on PlayEventFilter.started/ended (#67)
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>
2026-06-21 11:47:39 +02:00
lukas 31ff5ae83e Merge pull request #62 from KucharczykL/feat/issue-56-filter-links
feat(filters): programmatic filter links + navbar playtime links (#56)
2026-06-21 10:52:32 +02:00
lukas 2d0b1d67b5 Merge pull request #63 from KucharczykL/fix/staging-comment-on-pr-open
fix(ci): comment staging URL when the PR is opened, not just on deploy
2026-06-21 10:51:25 +02:00
lukas b0b2194806 fix(ci): comment staging URL when the PR is opened, not just on deploy
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
2026-06-21 09:26:31 +02:00
lukas 3fd02bbcf9 feat(filters): programmatic filter links + navbar playtime links (#56)
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
2026-06-21 08:53:06 +02:00
lukas b9545a780b docs(spec): add OperatorFilter.where() ergonomic constructor
Adds a Django-.filter()-style where(**lookups) classmethod (Component 1b)
to issue #56 spec. Additive: keeps the typed explicit constructor intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RF5L4HtbcykTfY9YUYGds3
2026-06-21 08:38:36 +02:00
lukas 260169b521 docs(spec): programmatic filter links for issue #56
Design for a reverse()-style filter_url() helper plus navbar 'today' /
'last 7 days' links. Stats-table and view_game-table links deferred to a
follow-up issue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RF5L4HtbcykTfY9YUYGds3
2026-06-21 07:50:25 +02:00
lukas b7d667a07f Merge pull request #60 from KucharczykL/fix/issue-39-dropdown-clipped
Django CI/CD / test (push) Failing after 6m30s
Django CI/CD / build-and-push (push) Has been skipped
fix(dropdown): flip-up menu collapses off-screen (follow-up to #59)
2026-06-20 23:50:32 +02:00
lukas 184749bf6d fix(dropdown): flip-up menu must override top-[105%] class
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>
2026-06-20 23:47:46 +02:00
lukas 4c1d7a9a93 Merge pull request #59 from KucharczykL/fix/issue-39-dropdown-clipped
fix(dropdown): stop table wrapper clipping the device dropdown (#39)
2026-06-20 23:16:29 +02:00
lukas 3bd14e8c89 fix(dropdown): position menu fixed so it isn't clipped by table wrapper
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>
2026-06-20 23:14:06 +02:00
lukas 5f3271b3da Merge pull request #58 from KucharczykL/chore/untrack-base-css
chore: untrack generated base.css, build it in test/CI
2026-06-20 23:06:18 +02:00
lukas ccc3db81c7 chore: untrack generated base.css, build it in test/CI paths
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>
2026-06-20 23:03:50 +02:00
lukas fdbd0cdea8 Merge pull request #57 from KucharczykL/feat/issue-53-session-row-fragment
fix(session): rebuild session row fragment via shared builder (#53)
2026-06-20 22:56:51 +02:00
lukas b6f6da309f fix(popover): remove hidden popover from layout to kill phantom scrollbar
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>
2026-06-20 22:39:49 +02:00
lukas 8637c547e4 refactor(session): address review minors for issue #53
- SessionRowData.cell_data: list[Node | str] (date cells are str)
- strengthen test_session_row_fragment_via_htmx to assert OOB navbar

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:37:44 +02:00
lukas ec6423cba5 style(session): apply ruff format to issue #53 changes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:33:25 +02:00
lukas 93252350bb test(e2e): in-place session-row finish swap
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:31:23 +02:00
lukas 4a3e40ef29 feat(session): in-place row swap for finish/reset with OOB navbar
Delete stale _session_row_fragment; end_session and reset_session_start
return the canonical row plus an OOB navbar-playtime fragment. Clone keeps
HX-Refresh since it changes row count. Fixes #53.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 21:27:03 +02:00
lukas 7d10884db7 feat(layout): extract NavbarPlaytime as OOB-swappable component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 21:21:14 +02:00
lukas ba1849e80e refactor(session): extract canonical session_row_data builder
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:17:21 +02:00
lukas 796753e3c9 docs: implementation plan for issue #53
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:14:39 +02:00
lukas 7a3b275d2f docs: return Node not SafeText in issue #53 spec
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:14:39 +02:00
lukas 644b9944da docs: design spec for issue #53 rebuild session row fragment
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:14:39 +02:00
lukas bda0657a6b Merge pull request #54 from KucharczykL/feat/issue-33-reset-session-start
feat(session): reset running session start time to now (#33)
2026-06-20 20:48:19 +02:00
lukas 2a1585831f feat(session): reset running session start time to now from list
Adds a confirm-gated button on running sessions in the session list that
sets timestamp_start to now (issue #33). The htmx path returns HX-Refresh;
ButtonGroup gains optional hx_confirm/hx_swap keys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:38:48 +02:00
lukas bf60a2a06b docs: design spec for issue #33 reset session start to now
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:37:52 +02:00
lukas 7d17986e68 Merge pull request #52 from KucharczykL/feat/issue-31-submit-create-session
feat(game): add Submit & Create Session button to add-game form
2026-06-20 20:10:02 +02:00
lukas 3fbb0996e5 test(e2e): add Submit & Create Session redirect test for add-game form 2026-06-20 20:04:54 +02:00
lukas 8005e97f01 fix(session): use get_object_or_404 for game lookup in add_session 2026-06-20 19:59:17 +02:00
lukas e7a0eb7dca feat(game): add Submit & Create Session button to add-game form
Closes #31
2026-06-20 19:49:00 +02:00
lukas fe3e070669 Merge pull request #51 from KucharczykL/feat/custom-elements-issue-18
fix(search-select): move field id to inner search input (issue #30)
2026-06-20 19:15:43 +02:00
lukas c25ebec484 test(e2e): remove what-comment from searchselect border test
The comment described what the code does (find wrapper by name attr),
not why. The locator is self-explanatory.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 19:03:56 +02:00
lukas b229f34b8d test(search-select): assert id lands on inner input not wrapper 2026-06-20 19:01:30 +02:00
lukas b816c68cb8 fix(search-select): move field id to inner search input (issue #30)
The id (e.g. id_related_game) sat on the <search-select> wrapper, a
non-labelable custom element. Consequences:
- <label for="id_X"> focused nothing (a11y gap)
- .disabled / .focus() on #id_X silently no-oped
- add_purchase.ts needed a [data-search-select-search] descendant
  workaround to gate related_game on the type field

id is now on the [data-search-select-search] <input>, making it a real
labelable, disableable control. add_purchase.ts drops the workaround
and gates via #id_related_game directly. E2e tests updated; new test
asserts label-click focuses the search box.

Closes #30
2026-06-20 18:34:07 +02:00
lukas 12b426e64a Merge pull request #50 from KucharczykL/feat/custom-elements-issue-18
Custom-element widgets + remove inline-handler → window.* contract (#18, #28)
2026-06-20 16:08:05 +02:00
lukas b3fa7fac96 Remove inline-handler → window.* contract (issue #28)
Finish the behavioural refactor from #28: no first-party JS lives on the
global object solely to be reachable from a server-rendered inline on*
attribute, and no inline Alpine blobs remain in the filter bar / year picker.

- Filter-bar collapse: drop the inline onclick for a delegated click listener
  on the persistent <filter-bar> custom element (data-filter-bar-toggle). The
  inner #filter-bar body is htmx-swapped while connectedCallback does not re-run,
  so delegation on the host preserves the swap-survival the inline handler had.
- YearPicker: convert the Alpine x-data/x-on/x-ref/_pickerInstance f-string into
  a <year-picker> custom element with typed props (YearPickerProps). Behavior
  moves to ts/elements/year-picker.ts; ts/year_picker.ts and _YEAR_PICKER_MEDIA
  are removed. The builder lives in primitives.py (next to YearPicker) to avoid a
  circular import; registration stays in custom_elements.py for codegen.
- Add bindPopupDismiss (ts/utils.ts): shared Escape + outside-click dismiss with
  a cleanup return and an extraInside hook for popups mounted on document.body.
  Adopted by date-range-picker.ts (1:1) and year-picker.ts (Datepicker popup is
  body-mounted, passed as an extra inside root).

Follow-up #49 tracks unifying popup/dismiss/positioning across the remaining
dropdown/search-select/Flowbite cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 15:54:00 +02:00
lukas 0ec564f920 Merge pull request #38 from KucharczykL/feat/custom-elements-issue-18
Convert onSwap widgets to custom elements (issue #18)
2026-06-20 15:19:23 +02:00
lukas 6603a07b85 Merge pull request #41 from KucharczykL/claude/sweet-ritchie-tnt1g6
Fix staging deployment to use single machine and prevent session loss
2026-06-20 15:01:25 +02:00
Claude 5259b1e553 Run staging on a single Fly machine
Staging stores sessions in machine-local SQLite with no shared volume.
Fly's default deploy provisions two machines (HA), so requests after
login could land on the machine that never wrote the session row,
bouncing logged-in users straight back to the login page.

Deploy with --ha=false and scale count 1 so each per-branch staging app
runs on exactly one machine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWAvjVEAibhwbeVgbYmg94
2026-06-20 12:54:12 +00:00