From 828c2a03c18c74d191d7181cfa70c824c2beca67 Mon Sep 17 00:00:00 2001 From: Leon Hermann Date: Mon, 29 Jun 2026 09:47:40 +0200 Subject: [PATCH] fix: show finished games on watch page instead of hanging spinner (#16) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary ▎ ▎ - Opening a finished game caused an infinite spinner because the NDJSON stream never delivers events for finished games — it just hangs waiting for events that will never come ▎ - Fix: fetch the full game state via REST on load (GET .../game/{gameId}), apply it to the board immediately, and only open the stream subscription if the game is still ongoing or pending ▎ ▎ Changes ▎ ▎ - tournament-watch.component.ts — REST fetch now drives the initial state; stream is only started conditionally; extracted applySnapshot(), isFinished(), subscribeToStream() ▎ ▎ Test plan ▎ ▎ - [ ] Finished game: board shows final position, correct status label, no spinner ▎ - [ ] Live game: board shows current position, stream updates continue to work ▎ - [ ] Pending game: stream starts and updates once the game begins --------- Co-authored-by: LQ63 Reviewed-on: https://git.janis-eccarius.de/NowChess/NowChess-Frontend/pulls/16 --- package-lock.json | 27 ++++++++++++++-- .../tournament-watch.component.ts | 32 ++++++++++++++++--- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6c981c5..323270f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -458,6 +458,7 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.19.tgz", "integrity": "sha512-hcB1eUEN8LGcKGc4DlRJ+abS6AYfbEHDZKg8LnXNugkbwI6Ebyh2AUYTDhzZL2S4aH+C8biHKgSYHFCqieCRhA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -474,6 +475,7 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.19.tgz", "integrity": "sha512-ETkgDKm0l2PuaBubgPJe0ccy8kE75DFu6/zKcz7TUuk3KrKF2OZAopbbjftsUSZGeCNvCdqHzjmcL6hQ6oAOwA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -487,6 +489,7 @@ "integrity": "sha512-ET/JjO8s62kAHfgIsGXlvW5VUwLqHm03q1y/2yD7aQW/WdDvssMsvZv7Knl440989vdOFemIGTMwVPakmWqRmA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "7.28.3", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -519,6 +522,7 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.19.tgz", "integrity": "sha512-SYnwW+q51bQoPtGFoGovm1P5GK9fMEXsG0lGaEAUapjskblAYyX7hLlM/jgueSojv2SjhqNF8aXR+gjHLhZVNA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -562,6 +566,7 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.19.tgz", "integrity": "sha512-TRZfatH1B/kreDwFRwtpLEurJQ6044qh6DWpvxzTbugaG5otLQJKTk+1z81/KsJwQqc1+24v+yuywc1LM7aq7w==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -628,6 +633,7 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1601,6 +1607,7 @@ "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@inquirer/checkbox": "^4.2.1", "@inquirer/confirm": "^5.1.14", @@ -3531,6 +3538,7 @@ "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.19.0" } @@ -3887,6 +3895,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -4902,6 +4911,7 @@ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -5343,6 +5353,7 @@ "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -5839,7 +5850,8 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.9.0.tgz", "integrity": "sha512-OMUvF1iI6+gSRYOhMrH4QYothVLN9C3EJ6wm4g7zLJlnaTl8zbaPOr0bTw70l7QxkoM7sVFOWo83u9B2Fe2Zng==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/jose": { "version": "6.2.2", @@ -5941,6 +5953,7 @@ "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -6408,6 +6421,7 @@ "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", @@ -7920,6 +7934,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -7955,6 +7970,7 @@ "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -8574,7 +8590,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tuf-js": { "version": "4.1.0", @@ -8612,6 +8629,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8741,6 +8759,7 @@ "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -9538,6 +9557,7 @@ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -9556,7 +9576,8 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", - "license": "MIT" + "license": "MIT", + "peer": true } } } diff --git a/src/app/pages/tournament-watch/tournament-watch.component.ts b/src/app/pages/tournament-watch/tournament-watch.component.ts index db329f2..9826869 100644 --- a/src/app/pages/tournament-watch/tournament-watch.component.ts +++ b/src/app/pages/tournament-watch/tournament-watch.component.ts @@ -59,19 +59,43 @@ export class TournamentWatchComponent implements OnInit { .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: game => { - this.whiteName = game.white?.name ?? null; - this.blackName = game.black?.name ?? null; + this.applySnapshot(game); + this.connecting = false; + if (!this.isFinished(game.status)) { + this.subscribeToStream(); + } + }, + error: err => { + this.connecting = false; + this.error = (err as Error).message ?? 'Failed to load game.'; }, }); + } + private applySnapshot(game: GameStateSnapshot): void { + this.whiteName = game.white?.name ?? null; + this.blackName = game.black?.name ?? null; + this.snapshot = game; + this.fen = game.fen; + this.turn = game.turn; + this.status = game.status; + this.winner = game.winner; + this.clock = game.clock ?? null; + this.moves = game.moves ? game.moves.split(/\s+/).filter(Boolean) : []; + } + + private isFinished(status: GameStatus): boolean { + return status !== 'pending' && status !== 'ongoing'; + } + + private subscribeToStream(): void { this.stream.streamGame(this.serverUrl, this.tournamentId, this.gameId) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: ev => this.apply(ev), error: err => { - this.connecting = false; this.error = (err as Error).message ?? 'Stream failed.'; - } + }, }); }