Public Access
feat(library): undo auditor merges for 30 days, redirect old keys
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.
This commit is contained in:
1 parent
44af19d32e
commit
bb8341dfef
22 files changed
+1163
-50
No files matched your search
@@ -43,7 +43,7 @@ To Be Played is split into three repositories:
|
||||
weaker hint compares the playtime with IGDB's normal time to beat (at least 90 %), only for games IGDB does not
|
||||
mark as endless (sandbox, survival, 4X, simulator, MMO). "Not yet" hides a hint per game; a dismissed playtime hint
|
||||
still lets the ending hint through.
|
||||
- **Hide games**: a user can hide a Steam game (e.g. a playtest or a tool). It leaves every list and number: library,
|
||||
- **Hide games**: a user can hide a game, Steam or added by hand (e.g. a playtest or a tool). It leaves every list and number: library,
|
||||
picker, Pile of Shame, activity, achievement summary, public profile and landing page stats. The sync still
|
||||
updates its playtime, but no achievements, time to beat, moods or speedrun data are fetched for it. The library
|
||||
lists hidden games with `hidden=true`.
|
||||
@@ -140,14 +140,15 @@ ArchUnit role rules (`RoleAnnotationRules`) and `src/test/resources/contract/aut
|
||||
| `GET` | `/api/library/catalog?q=&page=&size=` (every game some user still has, by name part, `PageDto` of `{appid, name, imageUrl, platforms}`; `q` at most 100 characters; size 20, 40 or 80) | JWT, AUDITOR |
|
||||
| `GET` | `/api/library/catalog/{appid}` (what the correction dialog reads: name, image, speedrun, moods, endless, genres, timeToBeat, corrections; `404 GAME_NOT_FOUND`) | JWT, AUDITOR |
|
||||
| `POST` | `/api/library/games/{appid}/merge` (`{otherAppid}`; `200 {appid}` = the key that stays, `400 SAME_GAME` / `STEAM_GAMES`, `404 GAME_NOT_FOUND`) | JWT, AUDITOR |
|
||||
| `POST` | `/api/library/merges/{mergeId}/undo` (`mergeId` = the `id` of a `GAME_MERGE` audit row, within 30 days; `204`, `404 MERGE_NOT_FOUND`, `409 UNDO_NOT_AVAILABLE` / `NEWER_MERGE`, `410 UNDO_EXPIRED`) | JWT, AUDITOR |
|
||||
| `POST`, `DELETE` | `/api/library/games/{appid}/link` (POST `{otherAppid}`, `204`, `400 SAME_GAME`, `404 GAME_NOT_FOUND`, `409 ALREADY_LINKED`; DELETE takes the game out of its set, `204`, `404 NOT_LINKED`) | JWT, AUDITOR |
|
||||
| `GET` | `/api/library/audit-log?action=&page=&size=` (the newest `admin_audit` rows, `PageDto` of `{id, actorUserId, actorName, action, appid, gameName, targetAppid, targetGameName, detail, createdAt}`; `action` is `GAME_MERGE`, `GAME_LINK`, `GAME_UNLINK`, `GAME_LINK_DISMISS`, `GAME_CORRECT` or `GAME_CORRECTION_RESET`; size 20, 40 or 80) | JWT, AUDITOR |
|
||||
| `GET` | `/api/library/audit-log?action=&page=&size=` (the newest `admin_audit` rows, `PageDto` of `{id, actorUserId, actorName, action, appid, gameName, targetAppid, targetGameName, detail, createdAt, undoable}`; `action` is `GAME_MERGE`, `GAME_MERGE_UNDO`, `GAME_LINK`, `GAME_UNLINK`, `GAME_LINK_DISMISS`, `GAME_CORRECT` or `GAME_CORRECTION_RESET`; size 20, 40 or 80) | JWT, AUDITOR |
|
||||
| `GET` | `/api/library/link-queue?page=&size=` (`PageDto` of `{a, b, igdb, requests, createdAt}`: both games, most requested first; size 20, 40 or 80) | JWT, AUDITOR |
|
||||
| `DELETE` | `/api/library/link-queue/{appid}/{otherAppid}` (dismiss; `204`, `404 PAIR_NOT_OPEN`) | JWT, AUDITOR |
|
||||
| `POST` | `/api/library/games/{appid}/link-requests` (`{otherAppid}`, both in the caller's library; `204`, `400 SAME_GAME`, `404 GAME_NOT_FOUND`, `409 ALREADY_LINKED` / `PAIR_DISMISSED`, `429 TOO_MANY_REQUESTS`) | JWT |
|
||||
| `PUT` | `/api/library/games/{appid}/status` (status only, keeps rating, notes, priority) | JWT |
|
||||
| `PUT` | `/api/library/games/{appid}/finished-hint` (`{dismissed: ENDING\|PLAYTIME}`, "Not yet"; `204`) | JWT |
|
||||
| `PUT` | `/api/library/games/{appid}/hidden` (`{hidden}`; `409 MANUAL_GAME` for a game added by hand) | JWT |
|
||||
| `PUT` | `/api/library/games/{appid}/hidden` (`{hidden}`; Steam and manual games) | JWT |
|
||||
| `POST` | `/api/library/imports` (`{source: TEXT\|CSV\|SEARCH\|GALAXY\|GROUVEE\|HLTB\|PLAYNITE\|BOOKMARKLET, rows: [{title, platform, playtimeMinutes, status, igdbId}]}`, 1–1000 rows; `202 {id}` in `QUEUED`, `409 IMPORT_RUNNING`, `503 IGDB_OFF`) | JWT |
|
||||
| `GET` | `/api/library/imports` (`{imports}`, the last 20), `/api/library/imports/{id}` (status, progress counts, `ahead` while it waits in a queue, `attempts`; the rows with their candidates while `READY`) | JWT |
|
||||
| `POST` | `/api/library/imports/{id}/save` (`{rows: [{lineNo, igdbId, platform, status, playtimeMinutes}]}`; `202`, then poll until `SAVED`; `409 IMPORT_NOT_READY`) | JWT |
|
||||
@@ -344,7 +345,38 @@ since the sync would bring both back. In one transaction:
|
||||
- `game_alias` points the old key to the new one (kept flat), so a later add of the old IGDB game lands on the new
|
||||
key. `admin_audit` gets one `GAME_MERGE` row: who, both keys, both names and how many rows moved or were combined.
|
||||
|
||||
There is no undo. The game search shows games already in ToBePlayed first, with their catalog name and image, and
|
||||
Merges made since V29 can be undone for 30 days (`POST /api/library/merges/{mergeId}/undo`, AUDITOR). The merge saves
|
||||
a copy in its own transaction: `game_merge_undo` (the deleted shared rows as JSON per table, the link set after the
|
||||
merge, the flattened aliases) and `game_merge_undo_user` (per user: both `user_game` rows as JSON, the history ids and the
|
||||
reward ledger ids the merge moved). The copy holds notes, so it goes with the account. A nightly job
|
||||
(`app.library.merge-undo-purge-cron`) deletes copies older than 30 days; older merges answer `410`.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> MERGED: POST /games/{appid}/merge (copy saved)
|
||||
MERGED --> UNDONE: POST /merges/{id}/undo (30 days, newest first)
|
||||
MERGED --> FINAL: nightly purge after 30 days
|
||||
```
|
||||
|
||||
The undo, in one transaction (the newest merge of a kept key goes first, else `409 NEWER_MERGE`):
|
||||
|
||||
- Shared rows (catalog row, moods, speedrun, time to beat, corrections, broken images, `game_edition`) come back;
|
||||
`game_alias` of the old key is removed and the aliases the merge flattened point to the old key again.
|
||||
- A user with only the old row: the row moves back if it is still on the new key, still added by hand (no Steam
|
||||
takeover) and the user has no old row. It keeps its current status, rating and notes.
|
||||
- A user with both rows: both rows are back if the kept row still exists. If the user did not change status, rating, notes or
|
||||
priority of the kept row since the merge (compared with a copy taken right after the merge), each row gets its own old
|
||||
values. If they did, both rows get the current values. The kept row gets its Steam ownership back when the merge
|
||||
had replaced it and nothing took the row over since. The old status history goes back to the old key; the log
|
||||
entries the undo itself causes are removed.
|
||||
- Ledger rows the merge moved go back to the old key (so does what a moved user earned on the row after the merge), only when the user has the old row
|
||||
again. Nothing is paid twice. A game the user hid after the merge stays hidden on the restored rows.
|
||||
- Links: when the kept key's set is still as the merge left it, the old sets come back with their `set_id`s; if
|
||||
links changed since, they stay and the old key comes back unlinked (the audit detail says "links kept").
|
||||
- Users who deleted their account are not restored. `admin_audit` gets a `GAME_MERGE_UNDO` row; the `GAME_MERGE` row stays.
|
||||
|
||||
`GET /api/library/games/{appid}` follows `game_alias` when the user has no row on the asked key, and answers with the
|
||||
kept `appid` (the frontend then replaces the URL). After an undo the alias is gone. The game search shows games already in ToBePlayed first, with their catalog name and image, and
|
||||
games other users added by hand that IGDB did not return; `inCatalog` marks both (only those can be merged).
|
||||
|
||||
### Editions of one game
|
||||
|
||||
Reference in new issue
Block a user