Commit Graph

803 Commits

Author SHA1 Message Date
lukas 97fff21b28 Ignore more SQLite database files 2026-06-19 12:37:25 +02:00
lukas c6aa3d25cc Update uv.lock 2026-06-19 12:36:40 +02:00
lukas 733da3419b Model refundable orders as separate purchases; add split action
Django CI/CD / test (push) Successful in 3m41s
Staging deployment / deploy (push) Successful in 1m7s
Staging deployment / comment (push) Has been skipped
Staging deployment / teardown (push) Has been skipped
Django CI/CD / build-and-push (push) Failing after 12m17s
A multi-game Purchase is now treated as an *unsplittable* bundle (one
price, whole-purchase refund). Independently-refundable multi-item orders
(e.g. a Steam cart) are instead recorded as N separate single-game
purchases, so per-game pricing and per-game refunds work with the
existing single-purchase machinery — no through-model needed.

Add-purchase form (single form, single endpoint):
- 1 game: unchanged.
- 2+ games: a "Separate price per game" toggle appears (default off =
  one bundle price). On, the bundle Price hides and one price input per
  game appears; the view creates one single-game Purchase each from
  price_for_game_<id>. `price` is now optional so combined mode still
  validates.

Split action:
- A Split button on multi-game purchase rows opens a confirmation modal
  that replaces the bundle with one single-game purchase per game (price
  split evenly, needs_price_update set), then HX-Redirects to the list.

New general-purpose `selection-fields` custom element renders one synced
form field per selected item of a source SearchSelect (consuming the
existing search-select:change contract); it knows nothing about prices,
so it is reusable. Behavior in ts/elements/selection-fields.ts.

Adds the bundle-vs-separate-purchases convention to CLAUDE.md, a split
icon, and unit + Playwright e2e coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 11:36:47 +02:00
lukas f693f8280f Fix pre-existing lint and format issues in domain.py and layout.py
Remove two unused `*Props` imports flagged by ruff (F401) and apply
`ruff format` line-wrapping. Pure cleanup, no behavior change — unblocks
`make check` independently of the purchase changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 11:36:47 +02:00
lukas dfccfbff51 Anchor DLC purchases to a base game instead of a parent purchase
Add-on purchases (DLC, Season Pass, Battle Pass) previously linked to a
parent *purchase* via the `related_purchase` self-FK. When the base game
was bought inside a multi-game purchase (e.g. a bundle), there was no
per-game purchase to point at — only the whole bundle.

Replace it with a `related_game` FK (Game -> Game): an add-on belongs to
a *game*, which is unambiguous regardless of how the base game was bought.

- models: drop `related_purchase`; add `related_game`
  (SET_NULL, related_name="addon_purchases"); require it for non-GAME
  types in `save()`.
- forms: replace the parent-purchase picker with a flat `related_game`
  game search (reusing SearchSelectWidget/_game_options); drop the now
  unused related_purchase_queryset/RelatedPurchaseChoiceField.
- views/urls: remove the obsolete related_purchase_by_game endpoint.
- add_purchase.js: drop the parent-dropdown refetch; keep platform
  auto-fill; retarget the type toggle to #id_related_game.
- migration 0020: add -> backfill (related_game = parent's first game by
  sort_name) -> remove related_purchase.
- tests: model validation unit tests + an e2e test for the flat picker.

related_game is deliberately game->game so it can later be synced from
IGDB's parent_game without schema changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 11:36:47 +02:00
lukas 62f0c6c261 Add tests for multiple APP_URLS
Django CI/CD / test (push) Successful in 3m20s
Django CI/CD / build-and-push (push) Successful in 3m48s
2026-06-19 11:28:16 +02:00
lukas d0d6b3f999 Make APP_URLS accept list 2026-06-19 11:28:16 +02:00
lukas 6f58eb3fde Surface the staging URL reliably
Echo the staging URL into the deploy log (not just the step summary),
and comment it when a PR is opened for an already-deployed branch
instead of waiting for the next push.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-19 11:28:16 +02:00
lukas d45ae357c4 Seed staging databases from a prod snapshot on first deploy
When a branch's staging volume doesn't exist yet, take a WAL-safe
online snapshot of the prod SQLite database (sqlite3.backup() in a
throwaway container, prod is only read) into the new volume. Later
pushes keep the staging data; deleting the branch (or the volume)
causes a fresh seed next time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-19 11:28:16 +02:00
lukas ae7fa5bae7 Add CSS-less dev mode
Django CI/CD / test (push) Successful in 5m54s
Django CI/CD / build-and-push (push) Successful in 3m27s
2026-06-19 11:26:01 +02:00
lukas be95c32e7b Change the default APP_URL in docker-compose.yml 2026-06-19 11:26:01 +02:00
lukas 32588226de Fix docker-compose not forwarding SECRET_KEY into container
SECRET_KEY, APP_URL, and DEBUG were hardcoded/missing in the compose
environment block, so passing SECRET_KEY from the host env had no effect
and the container always raised ImproperlyConfigured in production mode.

All three are now forwarded via ${VAR} substitution, consistent with
the other configurable values.
2026-06-19 11:26:01 +02:00
Claude 2ae01bfecf Add unified config system (issue #24)
Introduce timetracker/config.py with a single config() helper that resolves
settings from a fixed priority chain: NAME__FILE (opt-in secret) -> env var
-> .env -> settings.ini -> in-code default. Supports type casting
(bool/list/int/Path), file-based secrets with .strip(), and required_in_prod
validation.

Migrate settings.py off the previous ad-hoc idioms:
- DEBUG via config() (PROD kept as deprecated alias)
- SECRET_KEY required in prod, supports SECRET_KEY__FILE
- APP_URL derives ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS (kept separate,
  each independently overridable); ALLOWED_HOSTS is now configurable
- TZ and DATA_DIR via config()

Fix DATA_DIR inconsistency: entrypoint.sh now reads DATA_DIR (was hardcoded)
so the bash bootstrap and Django agree on the database directory.

Document the container/entrypoint-only flags (PUID/PGID/
CREATE_DEFAULT_SUPERUSER/STAGING/LOAD_SAMPLE_DATA) as bash concerns.

Update deployment configs to set APP_URL (and DEBUG), add docs/configuration.md,
settings.ini.example, regrouped .env.example, CLAUDE.md, and tests.

https://claude.ai/code/session_01FFn8BiGrQpEJarC8xGse8s
2026-06-19 11:26:01 +02:00
Claude 6a3f66b1a9 Provision pnpm via Corepack in CI to honor packageManager pin
CI installed pnpm with 'npm install -g pnpm', which pulls the latest
release and ignores the pnpm@10.33.0 pin in package.json's
packageManager field. This let CI drift to a different pnpm major than
the Docker image and local dev, the exact drift Corepack prevents.

Switch both the GitHub and Gitea build workflows to 'corepack enable',
matching the Dockerfile assets stage and the documented model where
local, CI, and Docker all follow the packageManager field.

https://claude.ai/code/session_01VWXYQxUPWdhoV4otwr6Cyk
2026-06-14 16:03:11 +00:00
lukas 1b0cccacf8 Merge pull request #23 from KucharczykL/claude/optimistic-volta-dx6xhd
Django CI/CD / test (push) Successful in 4m35s
Django CI/CD / build-and-push (push) Successful in 2m39s
Harden staging and bring GitHub/Gitea CI to parity
2026-06-14 16:46:54 +02:00
lukas 2b450c6d47 Fix sample.yaml fixture for current schema
Three issues from when the fixture was created before schema evolved:
- Game and Platform lacked created_at (auto_now_add bypassed by loaddata)
- Purchase lacked created_at/updated_at
- Purchase used 'game' FK that no longer exists; field is now the M2M
  'games', serialized as a list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 16:36:39 +02:00
lukas 9d02121c5b Grant pull-requests: write to staging deploy job
The github-script PR comment step needs this permission; without it
the GITHUB_TOKEN gets Resource not accessible by integration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 16:27:58 +02:00
lukas d2bf6efdb4 Fix Docker assets stage missing pnpm-workspace.yaml
The tar override lives in pnpm-workspace.yaml, which pnpm-lock.yaml
records. Copying only package.json + pnpm-lock.yaml left pnpm without
the overrides config, causing ERR_PNPM_LOCKFILE_CONFIG_MISMATCH on
frozen install.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 16:25:44 +02:00
lukas 227b1f674d Fix scrub_staging test isolation: use TransactionTestCase
TestCase wraps each test in a savepoint — when scrub_staging deletes
all django_session rows inside that savepoint, the rollback restores
any sessions committed by earlier tests (e.g. force_login in
test_paths_return_200). Those restored rows then leaked into the e2e
live-server tests, causing intermittent Session.MultipleObjectsReturned
errors.

TransactionTestCase flushes the DB before each test instead of using
savepoints, giving scrub_staging a clean slate and removing the leakage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 16:21:46 +02:00
Claude 017e3a61a8 Harden staging and bring GitHub/Gitea CI to parity
Address issue #20 and the CI divergence between Gitea and GitHub.

Issue #20 (staging seeded from a prod snapshot):
- Read SECRET_KEY from the environment with the insecure dev key as
  fallback, so each deployment can have its own key.
- Add a `scrub_staging` management command that clears django_session and
  the django-q schedule/queue/results, removing copied prod sessions and
  the inherited convert_prices() schedule.
- Run the scrub from entrypoint.sh when STAGING=true, and wire STAGING plus
  a per-branch SECRET_KEY into the Gitea staging deploy.

CI parity (both systems kept, independent):
- Add the Node/pnpm/TypeScript build steps to the Gitea build workflow to
  match the GitHub test job.
- Add a GitHub staging workflow that deploys per-branch ephemeral instances
  to Fly.io (*.fly.dev) with a fresh database seeded from sample fixtures
  and its own SECRET_KEY, never production data. Tears the app down on
  branch delete and comments the URL on the open PR via github-script.
- Add fly.staging.toml and a LOAD_SAMPLE_DATA entrypoint hook for the
  fresh-database public staging.

https://claude.ai/code/session_01KYjUcNjLfZ8Hq1GAC8J4oZ
2026-06-14 13:15:19 +00:00
lukas 2c699eb976 Merge pull request #22 from KucharczykL/claude/pnpm-npm-v12-compat-9xyk8e
Provision pnpm via Corepack and pin the version
2026-06-14 14:52:19 +02:00
Claude f19d24ee98 Document Corepack-based pnpm provisioning in CLAUDE.md 2026-06-14 12:49:28 +00:00
Claude 263299ca52 Bootstrap pnpm via Corepack and pin packageManager version
Replace the npm-based pnpm bootstrap in the Docker assets stage with
Corepack (ships with Node, no npm needed) and pin the pnpm version via
package.json's packageManager field for reproducible builds.
2026-06-14 12:48:30 +00:00
lukas 0b7ddc260f Merge pull request #21 from KucharczykL/claude/upgrade-tar-dependency-9fvn4d 2026-06-14 14:25:34 +02:00
Claude d9a8835696 Move tar override to pnpm-workspace.yaml
pnpm v11 (installed in CI via `npm install -g pnpm`) no longer reads the
`pnpm.overrides` field from package.json, which caused
ERR_PNPM_LOCKFILE_CONFIG_MISMATCH during the frozen install. Move the
override to pnpm-workspace.yaml, the new home for the setting, so CI's
pnpm reads it and matches the lockfile.

https://claude.ai/code/session_01NPQ9AiNNnapeoTQFAR1ShY
2026-06-14 12:22:22 +00:00
Claude 029c65da79 Update tar to 7.5.11+ to fix Dependabot alert
tar@6.2.1 was pulled in transitively via npm-check-updates' toolchain
(cacache, node-gyp, pacote). Add a pnpm override forcing tar >=7.5.11
to resolve the security advisory. Now resolves to tar@7.5.16.

https://claude.ai/code/session_01NPQ9AiNNnapeoTQFAR1ShY
2026-06-14 12:19:52 +00:00
lukas 008d92d433 Merge pull request #16 from KucharczykL/claude/custom-elements 2026-06-14 13:26:27 +02:00
lukas 9e17b94516 Add migration for FilterPreset.mode devices/platforms choices
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 13:09:15 +02:00
lukas 507353bb48 Commit pnpm lockfile for reproducible CI builds
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 13:03:49 +02:00
lukas a9e148701d Add TS build step to CI so e2e custom element tests have compiled JS
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 13:03:49 +02:00
lukas c3de90e805 Merge pull request #19 from KucharczykL/claude/custom-elements-experimental-unity
Try unifying 3 different element interfaces
2026-06-14 11:50:46 +02:00
lukas 1d2dfd23af Delete pnpm-workspace.yaml 2026-06-14 11:49:04 +02:00
lukas 395f6e8dea Fix logging out 2026-06-14 11:48:43 +02:00
lukas abfdd03c6e Fix Dockerfile build 2026-06-14 11:48:39 +02:00
lukas e15b197623 Remove base.css 2026-06-14 11:48:33 +02:00
lukas e12c667572 Add CREATE_DEFAULT_SUPERUSER env 2026-06-14 11:48:02 +02:00
lukas 874d3e236e Fix Button tests 2026-06-14 10:48:34 +02:00
lukas f036a246a8 Rename Button to StyledButton, simplify A 2026-06-14 10:47:23 +02:00
lukas 7751c29529 add custom element api proposal doc 2026-06-14 01:40:03 +02:00
lukas 5f411b8ae9 Try unifying 3 different element interfaces 2026-06-14 01:34:44 +02:00
lukas 3fb9aa9f84 Fix session-count script rendered as visible text
_GET_SESSION_COUNT_SCRIPT was a mark_safe string used as a child of the
view_game content tree. Under the "only Safe nodes render unescaped" rule, a
mark_safe *string* child is escaped — so the <script> showed as literal text
on the page. Make it a Safe node (and drop the now-unused mark_safe import).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:17:10 +02:00
lukas 138136e285 Fix dropdown item spacing + played-row button alignment
- Selector menu options were bare <button>s with no padding, so the open
  dropdown items were cramped. Add a shared option class (block w-full
  text-left px-4 py-2 + hover), matching the original <a> list items.
- The played-row's relative menu wrapper was a block div, so in the inline-flex
  button group the chevron toggle sat lower than the count button. Make the
  wrapper inline-flex and the group items-stretch so the two buttons align into
  one rounded group again.
- Rebuild base.css for the newly-used utilities.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:14:47 +02:00
lukas 2364d868fa Fix ported-component styling regressions
Two visual regressions from the custom-element port:

1. The played-row nested its dropdown menu (which contains <button> options)
   inside the toggle <button>. A <button> may not contain another <button>;
   the HTML parser force-closes the toggle on the nested button, and the
   source's explicit </div> tags then close ancestors early — ejecting the
   Purchases/Sessions/etc. sections out of the centered max-w container
   (they rendered full-width). Make the menu a sibling of the toggle, wrapped
   in a relative div so it still anchors under the toggle.

2. Both selector toggles dropped the original
   `flex flex-row gap-4 justify-between items-center` wrapper around their
   content, so the chevron stacked under the label (the GameStatus label is a
   display:flex block). Restore the wrapper — chevron sits on the right with
   proper spacing again.

Verified by screenshot: sections back inside the centered container; both
dropdowns render correctly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:10:26 +02:00
lukas ce976e8f2e Build TS in Docker (Node assets stage); document custom-element pattern 2026-06-13 21:28:20 +02:00
lukas c7af814364 Clear pre-existing ruff lint + format debt (make check now green) 2026-06-13 21:27:46 +02:00
lukas 1258c529d2 played-row: custom element; delete @@TOKEN@@ template + Alpine 2026-06-13 21:15:49 +02:00
lukas 48644037f6 SessionDeviceSelector: custom element; delete Alpine dropdown helper 2026-06-13 21:12:46 +02:00
lukas 04552aa8f6 GameStatusSelector: custom element + typed contract (retire Alpine)
The Game status dropdown is now a <game-status-selector> light-DOM custom
element: the Python builder emits the tag + kebab attrs htpy-style, behavior
lives in ts/elements/{dropdown,game-status-selector}.ts wired by the native
connectedCallback, and GameStatusSelectorProps is the codegen'd contract. The
~70-line inline-Alpine f-string is gone.

Also fix SimpleTable to collect and re-attach the media of its row/header
nodes: it stringifies cells into the table markup, which silently dropped each
cell component's declared Media — so a <game-status-selector> in a cell never
got its <script> emitted. Now Page() emits it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 21:09:52 +02:00
lukas 0f0dfc48fb Custom-element registry, builder, and TS codegen 2026-06-13 21:05:49 +02:00
lukas 763c00c50e htpy-style sugar on Element: kwargs attributes + [] children 2026-06-13 21:03:57 +02:00