feat(library): add question queue for open game decisions

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.
This commit is contained in:
lobo committed 2026-10-09 09:31:48 +02:00
1 parent 0ddb345a41
commit 6cd92f3e83
18 files changed
+934 -2

No files matched your search

+21
View File
@@ -86,6 +86,25 @@ 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.
- **Question queue** (Gitea #32): `GET /api/library/questions` lists one open question per game: Start it? (BACKLOG with
playtime), Finished? (BACKLOG or PLAYING, playtime at least IGDB's normal time to beat, else 10 h; never for
IGDB-endless games), Dropped? (PLAYING, no Steam playtime in the last 2 weeks and no sign of play for 90 days) and
Rate it? (FINISHED or COMPLETED without a rating). The order is the priority. `OpenQuestion.of` is a pure rule over
`QuestionFacts`, which one SQL query loads for the whole library (`counted_user_game`, so a linked set is one card).
Every rule starts from the status, so a game the sync already finished or started drops out by itself. A "No" is
stored in `user_game_question` (V41): Start it? and Finished? come back after 5 more hours of playtime, Dropped?
after another 90 idle days, a Rate it? "Skip" never. `POST /api/library/questions/{appid}`
(`{question, yes, rating?}`) checks that the question is still open (`409 QUESTION_GONE`, `404 GAME_NOT_FOUND`,
`400 INVALID_ANSWER` for a yes to RATE without a rating) and then calls `BacklogService`, the same write path as the
game page. `GET /api/library/questions/count` uses the same code as the list, so the badge equals the cards.
```mermaid
flowchart LR
Facts["counted_user_game + playtime days + status log + time to beat + user_game_question"] --> Rule["OpenQuestion.of"]
Rule --> Card["question card"]
Answer["answer"] --> Backlog["BacklogService"] --> UserGame["user_game"] --> Trigger["V14 trigger: status log"] --> Settle["RewardService.settle"]
Answer -->|no| Snooze["user_game_question"]
```
- **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
@@ -207,6 +226,8 @@ ArchUnit role rules (`RoleAnnotationRules`) and `src/test/resources/contract/aut
| `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 |
| `GET` | `/api/library/questions`, `/api/library/questions/count` (`{results: [{appid, name, imageUrl, question, status, playtimeMinutes, normallyMinutes, lastActivity}]}`, `{count}`) | JWT |
| `POST` | `/api/library/questions/{appid}` (`{question, yes, rating?}`; `200 {next}`, `400 INVALID_ANSWER`, `404 GAME_NOT_FOUND`, `409 QUESTION_GONE`) | JWT |
| `PUT` | `/api/library/games/{appid}/finished-hint` (`{dismissed: ENDING\|PLAYTIME}`, "Not yet"; `204`) | 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 |