A newly linked PSN title booked its whole history on the sync day.
Now only new play counts, on its last played day. A day is capped
at 24 h. V49 deletes the wrong old day rows.
Feed notes get profileName and appid; follows get lastActivityAt.
New GET /api/library/games/{appid}/preview: catalog data, any game.
New GET /api/public/profiles/library/games/{appid}: one profile game.
Local WireMock library grows from 8 to 30 games.
Six games get schema, achievement and percentage stubs.
LocalMocksParseTest checks each stub appid is an owned game.
A hand-only digest used to move the cursor past all status rows.
It now stops at the last by_user change, so the sync digest still
gets its status detail lines.
Digest notes now keep the lines behind their counts in
inbox_note_detail (V48). GET /api/inbox/{id}/details shows them
while the reader still follows the subject and it is public.
Add GET /api/library/dashboard-games for the top and playing tiles
in one call. It has no count query.
GET /api/library/games/{appid}?refresh=false answers from stored
data and sets achievementsStale. It does not wait for Steam.
entryPlatforms reads the view once in a materialized CTE:
planning ~33 ms -> 15 ms.
Test CORS Access-Control-Max-Age 3600 on preflight.
Refs #47
speedrun.com returns 429 or 420 when we ask too fast. These were
counted as errors: 3 in a row dropped both queues. Now the thread
pauses and retries the same game. Rate: 1 request per 2 s. Refs #48
Loose name matches gave wrong links. Now a link needs one sure hit:
same name and within 1 year of IGDB, or one hit when no year is known.
V47 marks old matches (not corrected ones) for one re-check. Refs #8
A sync that unlocks the last achievement of a game not COMPLETED
adds an ALL_UNLOCKED change. The overview hides it once the game is
COMPLETED, hidden or removed. Refs #21
Steam sync added a second row for a game the user added by hand (#9).
The manual row moves to the Steam appid and keeps its data, such as
status, notes, sets and PSN links. Manual rows on other platforms stay
and join the Steam game's set.
Refs #44. Check the last achievement unlock, not the first status change.
Old 100% games no longer pass. New reason COMPLETION_TOO_OLD.
Add earlySince to the games and wallet GET responses.
A paid FINISHED_MANUAL on a set edition blocked COMPLETED for the
Steam game forever. It now opens COMPLETED, but only when the Steam
game's first finish is on or after app.rewards.early-since. Refs #35
Played, never started, under one hour and oldest unplayed used the
counted edition's own minutes, so a set played only on PS5 showed as
never started. They and their drill-down lists now use the set total
(user_game_entry_playtime). Refs #38. Nightly snapshot (#43) unchanged.
RewardControllerTest leaves user_game_playtime_day rows for user 1,
affecting paceWeeks calculation. Delete them in seed() to ensure
clean test isolation.
A linked PlayStation title no longer blocks. Its playtime never
proves a finish. An unproven Steam finish pays 10 early points,
the rest comes with proof. Back pay only from app.rewards.early-since.
Sort, total playtime, most played, genres, moods and the public
profile use the set total of an entry, as the library card does.
New view user_game_entry_playtime (V45) is the one SQL rule.
Store PSN play per title. A PS title on a Steam row keeps its minutes.
Steam rtime_last_played now fills last played.
Dropped? questions read the newest PSN last played too.
Gitea #34 slice 2 of 3. Lists and stats count a set once.
V43 views map an entry to its editions and platforms.
Remove the hideDuplicates param; GameDto gets platforms[].
One entry per game across platforms (Gitea #34, slice 1 of 3).
Same IGDB game is linked; a user can split one key off its set.
V42 keeps status, rating and notes per set; rewards pay once per set.
Add open questions per game: Start it?, Finished?, Dropped?, Rate it?.
A "No" is saved in user_game_question (V41) and snoozes the question.
Account cleanup clears the new table too. Refs Gitea #32.
Sets map by exact title id, else by name and platform.
Trophy gains count as achievements; platinum or all earned is perfect.
Max 30 PSN calls per run; trophy lists load only when changed.
Hidden trophies stay hidden (name and detail) until earned.
Exact IGDB matches are added; unsure titles wait in a review import.
Playtime and last played come from PSN: summed per game, not editable.
PSN games give no Pile Points. Users with only a PSN link sync too
(V39 drops steam_id NOT NULL). Linking starts a sync, or schedules one
for the end of the cooldown. Inbox notes 2 days before the PSN link
ends and when it has ended.
The NPSSO is used once to get a refresh token; only that token is
stored, sealed with AES-GCM (SecretBox, key APP_PSN_TOKEN_KEY; no key
= feature off). One-time import of played PS4/PS5 titles through the
import pipeline as source PSN. Unlink keeps the games. One PSN call
per user at a time. IGDB search now sends UTF-8, so non-ASCII names
like "Yōtei" match again.
Backlog games a followed user finished or played >= 5 h. One game per
friend; strongest signal first (finished, then playtime). Requires an
active follow of a public profile that shows SUMMARY or STATUS. Skips
DROPPED games, blocked and deleted users, hidden and removed games.
New V37 tables game_credit and game_credit_check store IGDB
collections and developer/publisher companies per game.
A scheduled job backfills existing games (250 per 30 s);
sync, import, manual add, merge and detail page refresh them.
SERIES row: next part of a collection the user played.
STUDIO row: backlog games from developers of top played games.
Picker returns categories in fixed order: NEXT_UP, SIMILAR, LONGEST,
RANDOM, each with up to 3 games. Empty rows are left out.
Param `count` is removed; new optional `category` loads one row (reroll).
Budget and exclude apply to all rows; mood only to RANDOM.
SIMILAR takes the 3 most played games, one similar game each.
Remove PickMix.
Picker slots rotate three sources: backlog games that share moods with
one of the top 5 played games, oldest first seen, and random. Each game
carries a reason (source, anchor game, first seen). Empty slots fall
back to random. Issue #28.
V32 stores the last role change per user, sent by Auth. A token
issued before that change, or of a blocked user, gets 401 within
about 30 s instead of working until it expires.
A visit starts a background sync when the last successful sync is
older than 20 h; one try per hour, one sync at a time. Migration V31
adds last_success_at and login_sync_tried_at. The nightly sync skips
only users with a successful sync today after 23:00. games-count
returns syncStarted.
Allows dashboard to show what the last sync changed. Migration V30
creates sync_change and sync_result tables. Sync records additions,
removals, playtime, achievements, and status changes per game.
The number and list share the same SQL query, so they cannot drift.
StatKey enum maps each statistic to its query, validation rules, and visibility.
StatGamesService reads games per key with 40-game pages; repositories share the
same set logic as the stat counts. Three endpoints open games: own library stats,
public profile stats (with ticket checks), and public landing-page stats (cached
10 min). Rewards track and bonus rows support drill-down too.
Five progress tracks: games finished, completed, weekly finish
streaks, pile-down months and play-more months. Settle books UNLOCK
rows once, also retroactively, when a track reaches an item's target.
12 unlock-only items (incl. the Steam skin) and 3 skins to buy. The
wallet returns progress; buying an unlock item answers NOT_FOR_SALE.
V29 adds game_merge_undo and game_merge_undo_user: every new merge saves
a copy of what it changed. POST /api/library/merges/{mergeId}/undo
(auditors, newest merge first) puts it back; users keep later status
and rating, nothing is paid twice. A nightly job drops copies after 30
days. Old keys follow game_alias to the kept game. Manual games can now
be hidden and shown like Steam games.
GET /api/library/catalog finds any game a user has, by name, for
auditors; GET /api/library/catalog/{appid} loads it for the correction
dialog. New correction field TIME_TO_BEAT (normal + completionist,
1..60000 min, completionist >= normal) that survives the IGDB refresh.
Every correction and reset writes GAME_CORRECT / GAME_CORRECTION_RESET
to admin_audit.
GET /api/library/audit-log for auditors and admins, paged, filter by
action. The account state push stores displayName in account_name so
the log shows names. Contract test updated.
All game counts read the counted_user_game view, which keeps only the
most advanced edition of a linked set (older edition wins a tie). Add
the hideDuplicates filter to the library query.