Skip to content

chore(release): v1.2.0-BETA.1 - #104

Closed
catapult-counterweight[bot] wants to merge 69 commits into
betafrom
dev
Closed

catapult-counterweight[bot] wants to merge 69 commits into
betafrom
dev

Conversation

@catapult-counterweight

@catapult-counterweight catapult-counterweight Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Sors cette PR du draft pour publier la release ; elle fusionnera quand les checks seront verts. Pour la programmer, indique ci-dessous une date AAAA-MM-JJ HH:MM, en heure de Paris.

  • Le bot met à jour le changelog

Sortie programmée :

Features

  • api: let an admin game mapping ignore its game by default (939704b)
  • web: ignore a game by default from the admin game mappings (aac9d6e)
  • let an admin game mapping ignore its game by default (4dab0bc)
  • api: tell the settings whether the title's conditions and drops are offered (b1f56be)
  • api: expose each DTDD topic's severity in the public game detail (5f54d94)
  • api: track each DTDD key's remaining quota and let bulk work keep a monthly reserve (0b11316)
  • api: list the DTDD caches for the admins and refetch them under the rate limit (8ed75a3)
  • web: add an admin tab for the DTDD caches with a refetch per entry and a full one (6f7012c)
  • api: give the admins everything the DTDD caches keep about one entry (458d771)
  • web: open a DTDD cache entry in full from the admin tab (525ecd0)
  • api: keep each cached DTDD topic's severity and show it to the admins (67508d9)
  • web: show each DTDD topic's severity in a wider entry dialog (0bb16dc)
  • an admin tab for the DTDD caches, with each topic's severity (4f681f3)
  • api: let a DTDD topic's severity keep its TW off below the mapping's least severe level (2092d4c)
  • api: let the admins read and set each DTDD topic's least severe level on a TW (a3656a7)
  • web: edit a TW's DTDD topics and their least severe level from the admin tab (9c099d8)
  • api: tell a streamer why each of a binding's TWs is on (6e8c455)
  • web: show a streamer why each of a binding's TWs is on, in a details dialog (2c1e5d9)
  • let a DTDD topic's severity decide its TW, and tell streamers why each TW is on (45ffb48)
  • api: run one TW recalculation at a time, follow and cancel it, waiting out Steam's rate limit (1ae844a)
  • web: show the TW recalculation's progress in the admin tab, and let it be stopped (9f3102f)
  • follow the TW recalculation and respect Steam's rate limit (3a9d91d)

Fixes

  • api: explain each title condition and the drops in the help, only when offered (d85167e)
  • web: name the title's conditions and drop variables under its fields when offered (b5ad1b1)
  • api: read DTDD's new topic severity instead of failing every item call (8478f18)
  • api: decide the title help's parts in a service, not in the controller (859daae)
  • show the title's conditions and drop variables in its help when offered (11e0f6e)
  • api: read DTDD's new topic severity instead of failing every item call (63a3ae7)
  • web: write the DTDD caches' API paths in their calls (025d804)
  • api: leave the Steam store alone after a 429, for its Retry-After or a doubling wait (4bbbc96)
  • api: resolve a Steam game's TWs again once the store's rate limit ends (d7170cb)

Refactoring

  • api: inject the binding service's event publisher through its constructor (e13f736)

Other

  • merge dev into feat/dtdd-cache-admin (375f54a)
  • web: cover the DTDD caches calls of the real and mock API services (bc3ce92)
  • merge dev into feat/dtdd-cache-admin (e6a9391)
  • merge dev into feat/tw-dtdd-severity (9ee1eef)
  • merge feat/dtdd-cache-admin into feat/tw-dtdd-severity (62bf3a7)
  • web: cover the TW details and DTDD topics calls of the real and mock API services (6cf1834)
  • merge feat/dtdd-cache-admin into feat/tw-dtdd-severity (939f5f2)
  • merge feat/tw-dtdd-severity into feat/tw-rebuild-progress (bc730cc)
  • common: cover the admin DTOs' shorter constructors and older payloads (e9b65e8)

catapult-counterweight Bot and others added 27 commits November 10, 2026 14:17
## Problème

Wallpaper Engine (et toute appli de fond d'écran) est détecté comme jeu
en cours pendant tout le stream : Catapult change la catégorie, et
chaque streamer doit ignorer le binding à la main.

## Changements

- **Mapping admin « ignoré par défaut »**
(`game_mapping.ignored_by_default`) : la catégorie Twitch devient
facultative. Sans catégorie, les bindings gardent celle d'IGDB
(AUTO/INCOMPLETE), seul l'ignore est appliqué.
- **Activer le flag** ignore les bindings AUTO / INCOMPLETE / MAPPED du
jeu ; les nouvelles détections arrivent ignorées. Avec « appliquer aux
lives », les chaînes live sur le jeu passent tout de suite sur leur
catégorie « aucun jeu ».
- **Le désactiver, supprimer ou déplacer le mapping** désignore ce qu'il
avait ignoré. L'ignore n'est touché que quand le flag change.
- **Choix du streamer** (`game_binding.ignored_override`) : posé quand
il ignore / désignore lui-même ; le mapping ne revient plus dessus. Les
MANUAL ne sont jamais touchés. « Réinitialiser » efface le marqueur et
réapplique la règle du mapping.
- **Migration V104** : les bindings déjà ignorés sont marqués
`ignored_override` (avant, seul le streamer pouvait ignorer).
- **Panel admin** : interrupteur dans le formulaire, badge « Ignoré par
défaut » dans la liste, exemple Wallpaper Engine dans le mock.
- `ignoredByDefault` est un `Boolean` boxé dans `GameMappingRequest` /
`GameMappingDto` (Jackson 3 refuse un primitif absent), `null` =
`false`.

## Tests

`AdminGameMappingServiceTest`, `BindingServiceMappingTest`,
`MockGameMappingsStoreTest`, `admin-game-mappings.test.js`. Pas de
`bootRun` lancé : à faire avant merge.

## Modules à redémarrer

- `catapult-api` (migration V104 au démarrage)
- `catapult-web`
@catapult-counterweight catapult-counterweight Bot added the release PR de promotion (bot) label Nov 12, 2026
## Problème

Un 429 du Steam Store était avalé comme « jeu sans descripteurs » :
pendant un recalcul des TW, des séries de jeux étaient enregistrées
**sans leurs TW Steam**. Le recalcul n'avait aucun suivi, et plusieurs
pouvaient tourner en même temps.

## Changements

- **Steam Store** : sur un 429, plus d'appel jusqu'au `Retry-After`
(secondes ou date HTTP), sinon 60 s doublées jusqu'à 10 min tant que les
429 s'enchaînent ; remis à zéro à la première réponse.
- **Détection** : un jeu Steam résolu pendant la limite garde ce qu'on
a, et ses TW sont recalculés automatiquement juste après la fin du
`Retry-After` (sans bloquer la détection), puis poussés au dashboard ;
jamais pour un choix du streamer ou d'un admin.
- **Recalcul des TW** : un seul à la fois (409 sinon), annulable ; un
jeu sans signaux Steam n'est jamais enregistré, il attend la fin de la
limite et est réessayé (échec après 10 attentes).
- **Onglet admin TW** : barre de progression (traités / total, ignorés,
échecs, « Steam limite les requêtes : reprise à … »), rafraîchie toutes
les 3 s, bouton Arrêter.

## Points d'attention

- Conflit attendu avec la PR « TW dtdd severity » sur
`TwResolverService` et `BindingService` : résolu au merge.

## Tests

`SteamStoreServiceTest` (429, backoff, Retry-After), resolver,
`BindingServiceSteamLimitTest`, `TwSteamRetryTest`,
`TwBackfillServiceTest` (progression, attente Steam, annulation, un seul
à la fois), contrôleurs, rendu, mock, JS. `WiringConventionsTest`
vérifié.

## Modules à redémarrer

- `catapult-api`
- `catapult-web`
When a client disconnected mid-write, the outbox completed the emitter after the
IOException; Tomcat had already run onError, so the dispatch threw an
IllegalStateException out of the writing virtual thread. Leave the stream to the
container after an IOException, and complete quietly on the other paths.
Until a v2, the bot opens a single dev -> main promotion: its version is the
last stable bumped by the promoted commits (no -BETA.N tag), and a stable or a
hotfix is brought back to dev straight from main. The chain lives in
.github/release/lib/flow.mjs (BETA = false); the beta pairs stay accepted by
the scripts and workflows, so setting it back to true restores dev -> beta -> main.
… dev

The bot enables auto-merge (merge commit, pinned to the head it opened) on each
backport it opens, so dev catches up with main as soon as the checks pass
instead of waiting for a manual merge. If GitHub refuses it, the PR stays open
and its body asks for the manual merge as before.
A push to a branch with an open pull request ran build, test, commitlint and
release-scripts twice (push, then pull_request). The four workflows now run on
pull requests only, into any base so stacked PRs keep their checks: dev's head
is covered by its dev -> main promotion PR, main's by the backport main -> dev,
and renovate-merge.yml still dispatches build and test on dev. A new push to a
pull request cancels the run it supersedes. The obsolete master branch filter
is gone.
The Helix calls concatenate the streamer's id into the query, which Spring
takes as the URI template: one uri tag per streamer until Micrometer's cap
(100) dropped every new one. Tag the template's path only.
IgdbWebsiteKeys keeps a game's Twitch website URL under "twitch" when IGDB has
no Twitch external_game, and the binding used it as the game_id: Twitch then
refused every update ("The ID in game_id is not valid"). Only accept a numeric
id there, falling back to IGDB's lookup and the name search; log the game_id
sent with Twitch's errors.
## Pourquoi

Jusqu'à une éventuelle v2, on saute l'étape `beta` : les releases
passent directement de `dev` à `main`. Les PR de report qui ramènent une
release sur `dev` doivent se fusionner seules, et la CI lançait
plusieurs jobs en double (push puis pull_request sur la même branche).

## Changements

### 1. `chore(release)` : promotion directe `dev` → `main`
- Nouveau `.github/release/lib/flow.mjs` : `BETA = false` décrit la
chaîne de promotion (`dev → main`). Pour réactiver beta, il suffit de
remettre `BETA = true` et de remettre `beta` dans les branches de push
de `promote.yml` : les scripts et les conditions des workflows acceptent
toujours les paires beta.
- Version : `dev → main` prend le dernier stable incrémenté selon les
commits conventionnels (`nextDirectStable`), sans tag `-BETA.N`.
Prochaine release attendue : `v1.2.0`. Si une beta plus haute est encore
en attente, sa cible est reprise.
- `ready.mjs` traite `dev → main` comme un stable (bump de
`build.gradle.kts` sur `dev`, auto-merge épinglé).
- `publish.mjs` : après un stable ou un hotfix, le report se fait
directement `main → dev`.
- `schedule.mjs` : les sorties programmées concernent `dev → main`.
`--run` prend la cible dans `flow.mjs`, et `--target` ne sert plus que
de repli : l'ancienne copie de `release-schedule.yml` encore sur `main`
(qui passe `--target beta`) reste donc correcte jusqu'à la prochaine
release.
- Le tableau de bord liste aussi les promotions `dev → main`.
- Workflows : `promote.yml` (push sur `dev` uniquement, cible calculée
par le script), `release-ready.yml`, `release-schedule.yml`,
`publish.yml` et `commitlint.yml` (job ignoré sur `dev → main` et `main
→ dev`) acceptent la nouvelle paire.

### 2. `chore(release)` : auto-merge des reports vers `dev`
- Chaque PR de report ouverte par le bot reçoit l'auto-merge : commit de
merge, épinglé sur la tête ouverte, avec le titre `chore(release):
backport vX.Y.Z to dev` comme sujet. Ce sujet est déjà ignoré par la
promotion.
- Si GitHub refuse l'auto-merge, la PR reste ouverte et sa description
demande la fusion manuelle, comme avant.
- `allow_auto_merge` est déjà activé sur le dépôt : aucun réglage
modifié.

### 3. `ci` : moins de jobs en double
- `build`, `test`, `commitlint` et `release-scripts` ne tournent plus
que sur `pull_request` (vers n'importe quelle base, donc les PR empilées
gardent leurs checks). Il n'y a plus de trigger `push`.
- La tête de `dev` est vérifiée par sa PR de promotion `dev → main`.
Celle de `main` l'est par le report `main → dev`.
- `renovate-merge.yml` continue de lancer `build` et `test` sur `dev`
par `workflow_dispatch`.
- `concurrency` avec `cancel-in-progress` pour les PR : un nouveau push
annule le run qu'il remplace. Les runs lancés à la main ne sont jamais
annulés.
- Le filtre obsolète sur `master` est supprimé.
- Les noms des checks requis (`build`, `test`, `commitlint`,
`release-scripts`) ne changent pas : le ruleset n'a pas été touché.

## Modules à redémarrer

Modules à redémarrer : aucun (CI uniquement).

## À reporter sur `main` / à faire après merge
- `release-schedule.yml` est modifié ici. Il arrivera sur `main` avec la
prochaine promotion `dev → main` et restera ainsi identique sur les deux
branches. Pas besoin de le reporter à la main : `main` n'a pas bougé
depuis, la fusion sera propre. D'ici là, le cron de `main` utilise
l'ancienne copie, que le script gère.
- La PR draft #104 (`dev → beta`, `v1.2.0-BETA.1`) est désormais
obsolète : la fermer après le merge. Le prochain push sur `dev` ouvrira
la promotion `dev → main`.
- Le report #101 (`beta → dev`, `v1.1.0`) est toujours ouvert. Tant
qu'il l'est, `ready.mjs` refuse la promotion vers `main` : le fusionner
(commit de merge) ou activer son auto-merge.
- La branche `beta` peut rester en place (inutilisée) pour une
réactivation éventuelle.
Correctifs tirés des logs de prod (images `1.1.0`, pods `catapult-api`
et `catapult-web`, ~39 h de logs, aucun redémarrage).

## 1. SSE : `complete()` après l'erreur du conteneur (catapult-web)

```
Exception in thread "" java.lang.IllegalStateException: A non-container (application) thread attempted to use the AsyncContext after an error had occurred and the call to AsyncListener.onError() had returned. This is not allowed to avoid race conditions.
	at org.apache.catalina.core.AsyncContextImpl.check(AsyncContextImpl.java:666)
	...
	at org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter.complete(ResponseBodyEmitter.java:280)
	at fr.enimaloc.catapult.controller.SseStreams$Outbox.write(SseStreams.java:237)
```

**Cause** : quand le client se déconnecte pendant une écriture,
`emitter.send()` lève une `IOException` ; Tomcat appelle alors `onError`
et termine la requête lui-même. L'outbox appelait quand même
`emitter.complete()` (ce que la javadoc de Spring déconseille après une
erreur d'envoi) ; Tomcat refuse ce dispatch depuis un thread applicatif,
et l'exception sortait non rattrapée du thread virtuel d'écriture.

**Correctif** : plus de `complete()` après une `IOException` (le
conteneur s'en charge) ; les autres `complete()` de l'outbox (fermeture
demandée, flux trop lent, `IllegalStateException` à l'envoi) passent par
un `complete()` qui ignore le refus d'un flux déjà terminé par le
conteneur.

## 2. Cardinalité du tag `uri` de `http.client.requests` (catapult-api)

```
WARN  .s.b.m.m.MaximumAllowableTagsMeterFilter : Reached the maximum number of 'uri' tags for 'http.client.requests'. Are you using 'uriVariables'?
```

**Cause** : les appels Helix concatènent l'id du streamer dans la query
(`"/helix/channels?broadcaster_id=" + id`, `/streams?user_id=`,
`/moderation/moderators?...`, etc.). Spring prend cette chaîne pour le
gabarit d'URI, d'où un tag par streamer, jusqu'au plafond de 100 au-delà
duquel toute nouvelle URI est écartée des métriques (y compris les
appels légitimes).

**Correctif** : une `ClientRequestObservationConvention` (reprise par
l'auto-configuration RestClient de Spring Boot) qui retire la query
string du tag `uri`. Les fichiers Steam Store / Xbox
(`SteamStoreServiceImpl`, `XboxStoreServiceImpl`, `XboxGameGetter`,
`XboxRawQueries`, `SteamRawQueries`, `WebClientConfig`) ne sont pas
modifiés : leurs ids dans le **chemin** sont traités par la branche des
limites de débit apprises. Reste encore un id dans le chemin en dehors
de ces fichiers : `GitLabClient` / `GitLabRawQueries` (`/issues/" +
iid`), un appel admin à faible volume.

## 3. `The ID in game_id is not valid` (catapult-api)

```
ERROR f.e.c.service.twitch.TwitchServiceImpl : Twitch API error for user 74e0f0cf-…: 400 BAD_REQUEST 400 Bad Request: "{"error":"Bad Request","status":400,"message":"The ID in game_id is not valid."}"
```
(2 fois le 11/11, au début du live puis sur une détection.)

**Cause probable** : `BindingService` lit l'id de catégorie Twitch dans
`websites.get("twitch")` du cache IGDB. Or `IgdbWebsiteKeys#links` y
range aussi l'**URL** du site Twitch d'un jeu (type de site 6) ; l'`uid`
d'un external_game Twitch ne l'écrase que si IGDB en a un. Pour un jeu
sans external_game Twitch mais avec un lien Twitch, la liaison partait
avec une URL comme `game_id`, refusée à chaque mise à jour. La valeur
exacte envoyée n'apparaît pas dans le log, donc ce n'est pas confirmé
sur ce cas précis.

**Correctif** : seul un id numérique est retenu à cet endroit ; sinon on
passe à `igdbService.findTwitchGameId` puis à la recherche par nom. Le
log d'erreur indique désormais le `game_id` envoyé, ce qui permettra de
confirmer. Les liaisons déjà enregistrées avec une URL restent à
re-résoudre (ou à corriger depuis l'admin).

## Vu dans les logs, non corrigé (bruit attendu ou déjà traité)

- `DTDD call failed: UnrecognizedPropertyException: Unrecognized field
"severity"` : déjà corrigé sur `dev` (8478f18), pas encore en prod.
- `Detection skipped … Steam did not answer in time` (9/24 h) : relève
de la branche Steam (limites de débit).
- EventSub `Connection reset`, keepalive timeouts, 429 `number of
websocket transports` : déjà gérés (reconnexion / nouvelle session).
- Minecraft `500 /friends`, `503 /presence` : erreurs ponctuelles du
service externe.
- Au démarrage de catapult-web : `I/O error on GET … catapult-api:8081`
pendant que l'API démarrait.

## Tests

`SseStreamsTest` (3 nouveaux cas + 1 adapté),
`HttpClientUriTagConventionTest`,
`BindingServiceTest#resolveOrCreate_ignoresATwitchWebsiteUrl_forTheCategoryId`.
Les tests Testcontainers / full-context ne tournent pas dans mon
environnement : la CI tranche.

**Modules à redémarrer : catapult-api, catapult-web**
… first limit

Each write runs in its own transaction, apart from the caller's.
## Limites de débit apprises

Catapult devine la limite des plateformes qui ne la documentent pas, à
partir de leurs 429. Il la respecte avant d'appeler et apprend le
meilleur délai pour reprendre. Le cas déclencheur est le Steam Store
pendant le recalcul des TW.

### Fonctionnement
- **Accroche** : un intercepteur sur le `RestClient` partagé.
`ExternalApiObservations.observe` reste dédié aux métriques. Les hôtes à
limite documentée sont exclus (`rate-limit.learning.excluded-hosts`) :
Web API Steam (laissée à `SteamRateLimiter`), Twitch, IGDB, DTDD,
GitLab, Battle.net.
- **Route** = méthode + hôte + gabarit d'URI. Les segments qui
ressemblent à des identifiants deviennent `{id}`, même dans une URL
concaténée. Au-delà de 50 routes, les nouvelles routes d'un hôte sont
regroupées dans `/{other}`.
- **Portée mixte** : chaque hôte a un budget partagé par ses routes. Une
route passe à sa propre limite quand elle prend un 429 alors que l'hôte
est loin de sa limite et que ses autres routes répondent encore. Un
admin peut aussi forcer la portée.
- **Limite** : nombre d'appels dans la fenêtre (10 s, 1 min, 5 min ou 1
h) qui correspond à l'attente. Elle est resserrée par les 429, au plus
divisée par deux par un même 429, et remontée de 10 % après 1 h sans
429. Elle n'est apprise que si la fenêtre a été comptée entièrement, ce
qui évite d'en déduire 1 appel par fenêtre juste après un redémarrage.
Le compteur est remis à zéro après chaque blocage.
- **Délai de reprise** (429 sans `Retry-After`) : le premier appel après
l'attente sert de test. S'il réussit, le délai suivant est 80 % du délai
courant. S'il échoue, le délai double. Il ne descend jamais sous le
dernier délai qui a échoué, oublié après 24 h. Un `Retry-After` est
suivi à la lettre.
- **Priorité** : un appel live passe sous la limite et renonce après 2
s, avec un 429 local marqué `X-Catapult-Local-Limit` et jamais appris.
Le travail de masse (recalcul des TW) garde 20 % de marge, attend son
tour et reste annulable.
- **Persistance** : table `learned_rate_limits` (V107), resynchronisée
entre les pods toutes les 60 s. Les compteurs d'appels restent propres à
chaque pod.
- **Steam Store** : le blocage ad hoc (attente de 60 s qui double) est
remplacé. `rateLimitedUntil()` s'appuie désormais sur le limiteur, ce
qui garde `TwSteamRetry` et le recalcul fonctionnels.
- **Xbox Store** : appels passés en gabarit d'URI.

### Admin
- API : `GET /api/internal/admin/rate-limits`, `PUT|DELETE
…/rate-limits/entry?id=` (correction ou remise à zéro). Les routes sont
ajoutées à `access-matrix.csv`.
- Nouvel onglet « Limites de débit » : chaque hôte puis ses routes, avec
limite, délai de reprise, nombre de 429, appels dans la fenêtre et
attente en cours. On peut corriger (appels/fenêtre, délai, portée ; un
champ vide reste à l'apprentissage) et remettre à zéro. Une correction
n'est plus modifiée par l'apprentissage tant qu'elle n'est pas remise à
zéro.

### Tests
Tests unitaires de l'apprentissage, du limiteur (blocage d'hôte,
correction figée, annulation BULK, séparation hôte/route, repli quand la
base est indisponible), de l'intercepteur, du Steam Store, des
contrôleurs, du rendu et du JS.
Localement, seuls échouent les tests Testcontainers/full-context
habituels du sandbox.

**Modules à redémarrer : catapult-api (migration V107) et
catapult-web.**
Report de `v1.1.0` (#100) de `beta` vers `dev`.

Fusionne cette PR par commit de merge pour ramener la release sur `dev`
; tant qu'elle est ouverte, la promotion depuis et vers `dev` est
refusée.
@enimaloc

Copy link
Copy Markdown
Owner

Fermée : la promotion sur beta est abandonnée (dev part désormais directement vers main, #109).

@enimaloc enimaloc closed this Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release PR de promotion (bot)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant