From b350de1c7993342fa9ef371f0c828330131fef66 Mon Sep 17 00:00:00 2001 From: lobo Date: Sat, 26 Sep 2026 07:37:44 +0200 Subject: [PATCH] docs(readme): add the architecture overview --- readme.md | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/readme.md b/readme.md index 2004723..5e1cd20 100644 --- a/readme.md +++ b/readme.md @@ -71,18 +71,80 @@ accepted only by `GET /api/public/profiles/library` in the `X-Profile-Ticket` he ## Architecture A modular monolith. Each module under `core_service.modules` is a vertical slice that could move to its own -service later: +service later. + +```mermaid +flowchart LR + Browser["Frontend in the browser"] + subgraph CoreService["ToBePlayed-Core"] + Security["configuration.security
JwtAuthenticationFilter"] + subgraph Library["library"] + LibApi["api"] --> LibService["service"] --> LibRepo["repo"] + end + Stats["stats"] + Steam["steam_adapter"] + Igdb["igdb_adapter"] + LibService --> Steam + LibService --> Igdb + end + Browser -->|"tbp_access cookie"| Security + Security -->|"JWKS by kid"| Auth["ToBePlayed-Auth"] + Steam --> SteamApi["Steam Web API"] + Igdb --> IgdbApi["IGDB API"] + Igdb -->|"OAuth token"| Twitch["Twitch"] + LibRepo --> Db[("PostgreSQL tbp_core")] + Stats --> Db +``` | Module | Content | |---|---| -| `library` | Sync, backlog, playtime history, wishlist, public profile data | +| `library` | Sync, backlog, manual games, playtime history, wishlist, achievements, public profile data | | `stats` | Public numbers for the landing page | | `steam_adapter` | Steam Web API client | -| `igdb_adapter` | IGDB client (Twitch OAuth) | +| `igdb_adapter` | IGDB client (Twitch OAuth): time to beat, game tags for moods, game search | Inside a module: `api` (controllers, DTOs), `service` (business logic), `domain` (JPA entities) and `repo` -(Spring Data repositories). ArchUnit enforces cycle freedom, controller-to-repository direction, naming and module -isolation in `src/test/java/core_service/architecture/ArchitectureTest.java`. +(repositories). `src/test/java/core_service/architecture/ArchitectureTest.java` checks these rules on every build: + +- Modules do not depend on each other. Only `library` may use `steam_adapter` and `igdb_adapter`. +- No cycles between `modules`, `shared` and `configuration`. +- `api` never uses `repo`, and `repo` never uses `api`. +- Every class in `repo` ends with `Repository`. +- `shared` never uses `modules`, and `modules` never use `configuration`. +- A `*Service` class in a module's `service` package has `@Service`. A `*Controller` class in `api` has + `@RestController`, `@ControllerAdvice` or `@RestControllerAdvice`. + +### Sync flow + +1. `GET /api/library/games`, `/games-count` and `/picker` call `LibrarySyncService.syncIfStale`. It starts a sync + when the last one is older than 24 hours, older than 15 minutes after a failure, or was for another Steam ID. + `POST /api/library/sync` starts one at once, but at most every 60 seconds (else `429` with `Retry-After`). +2. A nightly job (23:55, Europe/Berlin) syncs every account used in the last 14 days, one after the other. +3. `LibrarySyncWorker` runs in the background. `user_steam_sync` lets only one run per user start. +4. The worker loads the wishlist and the owned games from Steam. It updates `steam_game` and `user_game`, adds + the playtime growth of the day to `user_game_playtime_day`, and soft-deletes Steam games the user no longer + owns. When Steam returns no games, the library stays as it is. +5. `TimeToBeatService` and `MoodService` ask IGDB for missing or old time-to-beat and mood data, for Steam games + and manual games. +6. After the commit, `LibrarySyncedEvent` starts `AchievementSyncService` in the background. + +The frontend polls `GET /api/library/sync-status` until the run ends. + +### Manual games next to Steam games + +A game the user adds by hand comes from IGDB (`ManualGameService`). Steam games and manual games share +`steam_game` and `user_game` and one key, the column `appid` (see `GameKeys`): + +- `appid > 0`: the Steam appid. A manual game gets it when IGDB knows the game on Steam. +- `appid < 0`: minus the IGDB id, for a game that is not on Steam. Steam appids are always positive, so the two + never meet. + +`user_game.source` (`GameSource`) says who owns the row. The sync never changes or removes a `MANUAL` row, with +one exception: when the user later owns the game on Steam under the same appid, the sync takes the row over: it becomes +`STEAM`, and status, rating and notes stay. Only `MANUAL` rows can be edited (`PUT /api/library/games/{appid}/manual`) +or deleted (`DELETE /api/library/games/{appid}`). Time to beat uses the Steam appid for positive keys and the IGDB +id for negative keys. Search and add check both keys of a game. A game added under minus its IGDB id can still get +a second row when IGDB learns its Steam appid later and the user owns it on Steam. ## Run locally