Compare commits

..

2 Commits

Author SHA1 Message Date
shahdlala66 aeca003191 feat: api for server could be wrong 2026-06-23 09:37:21 +02:00
shahdlala66 8728817231 feat: streaming endpoint 2026-06-23 09:27:57 +02:00
14 changed files with 32 additions and 284 deletions
-3
View File
@@ -41,6 +41,3 @@ Thumbs.db
# Claude Code # Claude Code
/.claude/settings.local.json /.claude/settings.local.json
/.claude/worktrees/ /.claude/worktrees/
# Local clone of the tournament server repo (not tracked)
/tournament-server/
-37
View File
@@ -91,40 +91,3 @@
### Features ### Features
* **tournaments:** remove external server add/remove UI ([412591d](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/412591dfe0119dbec84c3783cd94590810884580)) * **tournaments:** remove external server add/remove UI ([412591d](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/412591dfe0119dbec84c3783cd94590810884580))
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.5.0...0.0.0) (2026-06-23)
### Features
* NCWF-10 streaming endpoint ([#14](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/14)) ([1dabd88](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/1dabd88c6286a7b01d6fe8527aec864b24e21cca))
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.6.0...0.0.0) (2026-06-23)
### Bug Fixes
* api streaming issues ([0621968](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/0621968c3ceddebe01e9c363bda345b5dcccfbbf))
* api url ([2229cfd](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/2229cfd00a7d16daa6a9544c8940e792c4362dfb))
* streaming issues ([bd6d023](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/bd6d02351336ed6adf66244979c6d959f47e318b))
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.6.1...0.0.0) (2026-06-23)
### Bug Fixes
* jwt token issue ([147d7f0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/147d7f0d2ca7a77bb80eb4b73b8d60b00ad2f708))
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.6.2...0.0.0) (2026-06-23)
### Bug Fixes
* route tournament calls through backend gateway ([890c3fc](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/890c3fcecccec89e643180725e2a601f84fa5d99))
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.6.3...0.0.0) (2026-06-28)
### Bug Fixes
* show actual bot names on watch page instead of Black/White fallbacks ([#15](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/15)) ([76e0e3d](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/76e0e3db7869609e400593ca8f08ea8f72e72f68))
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.6.4...0.0.0) (2026-06-29)
### Bug Fixes
* show finished games on watch page instead of hanging spinner ([#16](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/16)) ([828c2a0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/828c2a03c18c74d191d7181cfa70c824c2beca67))
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.6.5...0.0.0) (2026-06-30)
### Features
* **tournaments:** make finished games in previous rounds clickable ([#17](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/17)) ([c0a98a9](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/c0a98a90dd53e675b5f6655d71bab681a1dcf361))
+3 -24
View File
@@ -458,7 +458,6 @@
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.19.tgz", "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.19.tgz",
"integrity": "sha512-hcB1eUEN8LGcKGc4DlRJ+abS6AYfbEHDZKg8LnXNugkbwI6Ebyh2AUYTDhzZL2S4aH+C8biHKgSYHFCqieCRhA==", "integrity": "sha512-hcB1eUEN8LGcKGc4DlRJ+abS6AYfbEHDZKg8LnXNugkbwI6Ebyh2AUYTDhzZL2S4aH+C8biHKgSYHFCqieCRhA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -475,7 +474,6 @@
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.19.tgz", "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.19.tgz",
"integrity": "sha512-ETkgDKm0l2PuaBubgPJe0ccy8kE75DFu6/zKcz7TUuk3KrKF2OZAopbbjftsUSZGeCNvCdqHzjmcL6hQ6oAOwA==", "integrity": "sha512-ETkgDKm0l2PuaBubgPJe0ccy8kE75DFu6/zKcz7TUuk3KrKF2OZAopbbjftsUSZGeCNvCdqHzjmcL6hQ6oAOwA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -489,7 +487,6 @@
"integrity": "sha512-ET/JjO8s62kAHfgIsGXlvW5VUwLqHm03q1y/2yD7aQW/WdDvssMsvZv7Knl440989vdOFemIGTMwVPakmWqRmA==", "integrity": "sha512-ET/JjO8s62kAHfgIsGXlvW5VUwLqHm03q1y/2yD7aQW/WdDvssMsvZv7Knl440989vdOFemIGTMwVPakmWqRmA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/core": "7.28.3", "@babel/core": "7.28.3",
"@jridgewell/sourcemap-codec": "^1.4.14", "@jridgewell/sourcemap-codec": "^1.4.14",
@@ -522,7 +519,6 @@
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.19.tgz", "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.19.tgz",
"integrity": "sha512-SYnwW+q51bQoPtGFoGovm1P5GK9fMEXsG0lGaEAUapjskblAYyX7hLlM/jgueSojv2SjhqNF8aXR+gjHLhZVNA==", "integrity": "sha512-SYnwW+q51bQoPtGFoGovm1P5GK9fMEXsG0lGaEAUapjskblAYyX7hLlM/jgueSojv2SjhqNF8aXR+gjHLhZVNA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -566,7 +562,6 @@
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.19.tgz", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.19.tgz",
"integrity": "sha512-TRZfatH1B/kreDwFRwtpLEurJQ6044qh6DWpvxzTbugaG5otLQJKTk+1z81/KsJwQqc1+24v+yuywc1LM7aq7w==", "integrity": "sha512-TRZfatH1B/kreDwFRwtpLEurJQ6044qh6DWpvxzTbugaG5otLQJKTk+1z81/KsJwQqc1+24v+yuywc1LM7aq7w==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -633,7 +628,6 @@
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
@@ -1607,7 +1601,6 @@
"integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@inquirer/checkbox": "^4.2.1", "@inquirer/checkbox": "^4.2.1",
"@inquirer/confirm": "^5.1.14", "@inquirer/confirm": "^5.1.14",
@@ -3538,7 +3531,6 @@
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.19.0" "undici-types": "~7.19.0"
} }
@@ -3895,7 +3887,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.10.12", "baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782", "caniuse-lite": "^1.0.30001782",
@@ -4911,7 +4902,6 @@
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"accepts": "^2.0.0", "accepts": "^2.0.0",
"body-parser": "^2.2.1", "body-parser": "^2.2.1",
@@ -5353,7 +5343,6 @@
"integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=16.9.0" "node": ">=16.9.0"
} }
@@ -5850,8 +5839,7 @@
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.9.0.tgz", "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.9.0.tgz",
"integrity": "sha512-OMUvF1iI6+gSRYOhMrH4QYothVLN9C3EJ6wm4g7zLJlnaTl8zbaPOr0bTw70l7QxkoM7sVFOWo83u9B2Fe2Zng==", "integrity": "sha512-OMUvF1iI6+gSRYOhMrH4QYothVLN9C3EJ6wm4g7zLJlnaTl8zbaPOr0bTw70l7QxkoM7sVFOWo83u9B2Fe2Zng==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/jose": { "node_modules/jose": {
"version": "6.2.2", "version": "6.2.2",
@@ -5953,7 +5941,6 @@
"integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@colors/colors": "1.5.0", "@colors/colors": "1.5.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
@@ -6421,7 +6408,6 @@
"integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"cli-truncate": "^4.0.0", "cli-truncate": "^4.0.0",
"colorette": "^2.0.20", "colorette": "^2.0.20",
@@ -7934,7 +7920,6 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
@@ -7970,7 +7955,6 @@
"integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"chokidar": "^4.0.0", "chokidar": "^4.0.0",
"immutable": "^5.0.2", "immutable": "^5.0.2",
@@ -8590,8 +8574,7 @@
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD", "license": "0BSD"
"peer": true
}, },
"node_modules/tuf-js": { "node_modules/tuf-js": {
"version": "4.1.0", "version": "4.1.0",
@@ -8629,7 +8612,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -8759,7 +8741,6 @@
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -9557,7 +9538,6 @@
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
@@ -9576,8 +9556,7 @@
"version": "0.15.1", "version": "0.15.1",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
"integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
"license": "MIT", "license": "MIT"
"peer": true
} }
} }
} }
@@ -9,7 +9,7 @@
<!-- Center links — only when logged in --> <!-- Center links — only when logged in -->
@if (currentUser) { @if (currentUser) {
<div class="nc-links"> <div class="nc-links">
<button type="button" class="nc-link" (click)="goToTournaments()"> <button type="button" class="nc-link">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
stroke-linecap="round" stroke-linejoin="round"> stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" /> <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
+4 -11
View File
@@ -51,20 +51,13 @@ export interface TournamentList {
finished: Tournament[]; finished: Tournament[];
} }
export interface TournamentMatch {
gameId: string;
whiteId: string;
winner?: 'white' | 'black' | 'draw' | null;
status?: GameStatus;
}
export interface TournamentPairing { export interface TournamentPairing {
id?: string; id: string;
round?: number; round: number;
white: TournamentBotRef | null; white: TournamentBotRef | null;
black: TournamentBotRef; black: TournamentBotRef;
matches: TournamentMatch[]; gameId: string | null;
winner?: 'white' | 'black' | 'draw' | null; winner: 'white' | 'black' | 'draw' | null;
} }
export interface RoundPairings { export interface RoundPairings {
@@ -2,10 +2,10 @@
<header class="watch-head"> <header class="watch-head">
<a class="back-link" routerLink="/tournaments">← Tournaments</a> <a class="back-link" routerLink="/tournaments">← Tournaments</a>
<div class="watch-title"> <div class="watch-title">
@if (whiteName && blackName) { @if (snapshot?.white && snapshot?.black) {
<span class="player">{{ whiteName }}</span> <span class="player">{{ snapshot!.white!.name }}</span>
<span class="vs">vs</span> <span class="vs">vs</span>
<span class="player">{{ blackName }}</span> <span class="player">{{ snapshot!.black!.name }}</span>
@if (snapshot?.round) { @if (snapshot?.round) {
<span class="round-tag">Round {{ snapshot!.round }}</span> <span class="round-tag">Round {{ snapshot!.round }}</span>
} }
@@ -24,12 +24,12 @@
<div class="watch-layout"> <div class="watch-layout">
<div class="board-wrap"> <div class="board-wrap">
<div class="clock clock-top" [class.active]="status === 'ongoing' && turn === 'black'"> <div class="clock clock-top" [class.active]="status === 'ongoing' && turn === 'black'">
<span class="clock-label">{{ blackName ?? 'Black' }}</span> <span class="clock-label">{{ snapshot?.black?.name ?? 'Black' }}</span>
<span class="clock-time">{{ formatTime(clock?.blackTime) }}</span> <span class="clock-time">{{ formatTime(clock?.blackTime) }}</span>
</div> </div>
<app-chess-board [fen]="fen"></app-chess-board> <app-chess-board [fen]="fen"></app-chess-board>
<div class="clock clock-bot" [class.active]="status === 'ongoing' && turn === 'white'"> <div class="clock clock-bot" [class.active]="status === 'ongoing' && turn === 'white'">
<span class="clock-label">{{ whiteName ?? 'White' }}</span> <span class="clock-label">{{ snapshot?.white?.name ?? 'White' }}</span>
<span class="clock-time">{{ formatTime(clock?.whiteTime) }}</span> <span class="clock-time">{{ formatTime(clock?.whiteTime) }}</span>
</div> </div>
</div> </div>
@@ -1,7 +1,6 @@
import { Component, DestroyRef, OnInit, inject } from '@angular/core'; import { Component, DestroyRef, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ActivatedRoute, RouterLink } from '@angular/router'; import { ActivatedRoute, RouterLink } from '@angular/router';
import { from } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ChessBoardComponent } from '../../components/chess-board/chess-board.component'; import { ChessBoardComponent } from '../../components/chess-board/chess-board.component';
import { TournamentStreamService } from '../../services/tournament-stream.service'; import { TournamentStreamService } from '../../services/tournament-stream.service';
@@ -26,9 +25,6 @@ export class TournamentWatchComponent implements OnInit {
gameId = ''; gameId = '';
serverUrl = ''; serverUrl = '';
whiteName: string | null = null;
blackName: string | null = null;
fen = INITIAL_FEN; fen = INITIAL_FEN;
turn: 'white' | 'black' = 'white'; turn: 'white' | 'black' = 'white';
status: GameStatus = 'pending'; status: GameStatus = 'pending';
@@ -55,47 +51,14 @@ export class TournamentWatchComponent implements OnInit {
return; return;
} }
from(this.stream.fetchGame(this.serverUrl, this.tournamentId, this.gameId))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: game => {
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) this.stream.streamGame(this.serverUrl, this.tournamentId, this.gameId)
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({ .subscribe({
next: ev => this.apply(ev), next: ev => this.apply(ev),
error: err => { error: err => {
this.connecting = false;
this.error = (err as Error).message ?? 'Stream failed.'; this.error = (err as Error).message ?? 'Stream failed.';
}, }
}); });
} }
@@ -340,13 +340,6 @@
} }
.pairing-ongoing svg { animation: pulse 1.4s ease-in-out infinite; } .pairing-ongoing svg { animation: pulse 1.4s ease-in-out infinite; }
.pairing-review {
font-size: 10px; font-weight: 700; letter-spacing: 0.06em;
text-transform: uppercase; color: var(--nc-neon);
opacity: 0; transition: opacity 0.15s;
}
.pairing-row.is-watchable:hover .pairing-review { opacity: 1; }
/* Official bot join section */ /* Official bot join section */
.join-divider { .join-divider {
display: flex; align-items: center; gap: 10px; display: flex; align-items: center; gap: 10px;
@@ -158,10 +158,9 @@
<div class="state-msg small"><span class="pulse"></span>Loading…</div> <div class="state-msg small"><span class="pulse"></span>Loading…</div>
} @else if (pairings && pairings.pairings.length > 0) { } @else if (pairings && pairings.pairings.length > 0) {
<div class="pairings-list"> <div class="pairings-list">
@for (p of pairings.pairings; track pairingKey(p)) { @for (p of pairings.pairings; track p.id) {
@let gid = firstGameId(p); <div class="pairing-row" [class.is-watchable]="!!p.gameId"
<div class="pairing-row" [class.is-watchable]="!!gid" (click)="p.gameId && watchGame(p.gameId)">
(click)="gid && watchGame(gid)">
<span class="pairing-white">{{ p.white?.name ?? 'Bye' }}</span> <span class="pairing-white">{{ p.white?.name ?? 'Bye' }}</span>
<span class="pairing-vs">vs</span> <span class="pairing-vs">vs</span>
<span class="pairing-black">{{ p.black.name }}</span> <span class="pairing-black">{{ p.black.name }}</span>
@@ -169,7 +168,7 @@
<span class="pairing-result" [class]="'result-' + p.winner"> <span class="pairing-result" [class]="'result-' + p.winner">
{{ p.winner === 'draw' ? '½–½' : p.winner === 'white' ? '10' : '01' }} {{ p.winner === 'draw' ? '½–½' : p.winner === 'white' ? '10' : '01' }}
</span> </span>
} @else if (gid) { } @else if (p.gameId) {
<span class="pairing-ongoing"> <span class="pairing-ongoing">
<svg width="9" height="9" viewBox="0 0 24 24" fill="currentColor"> <svg width="9" height="9" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10"/> <circle cx="12" cy="12" r="10"/>
@@ -186,35 +185,6 @@
</section> </section>
} }
<!-- Previous rounds -->
@if (previousRoundsLoading) {
<div class="state-msg small"><span class="pulse"></span>Loading previous rounds…</div>
}
@for (rp of previousRoundPairings; track rp.round) {
<section class="detail-section">
<h3 class="detail-heading">Round {{ rp.round }} pairings</h3>
<div class="pairings-list">
@for (p of rp.pairings; track pairingKey(p)) {
@let gid = firstGameId(p);
<div class="pairing-row" [class.is-watchable]="!!gid"
(click)="gid && watchGame(gid)">
<span class="pairing-white">{{ p.white?.name ?? 'Bye' }}</span>
<span class="pairing-vs">vs</span>
<span class="pairing-black">{{ p.black.name }}</span>
@if (p.winner) {
<span class="pairing-result" [class]="'result-' + p.winner">
{{ p.winner === 'draw' ? '½–½' : p.winner === 'white' ? '10' : '01' }}
</span>
}
@if (gid) {
<span class="pairing-review">Review</span>
}
</div>
}
</div>
</section>
}
</div> </div>
} }
</div> </div>
@@ -10,7 +10,7 @@ import { BotService } from '../../services/bot.service';
import { OfficialBotService } from '../../services/official-bot.service'; import { OfficialBotService } from '../../services/official-bot.service';
import { TournamentServerService, ExternalTournamentServer } from '../../services/tournament-server.service'; import { TournamentServerService, ExternalTournamentServer } from '../../services/tournament-server.service';
import { Bot } from '../../models/bot.models'; import { Bot } from '../../models/bot.models';
import { Tournament, TournamentResult, RoundPairings, TournamentPairing } from '../../models/tournament.models'; import { Tournament, TournamentResult, RoundPairings } from '../../models/tournament.models';
import { CurrentUser } from '../../models/auth.models'; import { CurrentUser } from '../../models/auth.models';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
@@ -44,8 +44,6 @@ export class TournamentsComponent implements OnInit {
selectedTournament: Tournament | null = null; selectedTournament: Tournament | null = null;
pairings: RoundPairings | null = null; pairings: RoundPairings | null = null;
pairingsLoading = false; pairingsLoading = false;
previousRoundPairings: RoundPairings[] = [];
previousRoundsLoading = false;
showCreateDialog = false; showCreateDialog = false;
createForm: FormGroup = this.fb.group({ createForm: FormGroup = this.fb.group({
@@ -118,35 +116,19 @@ export class TournamentsComponent implements OnInit {
this.tab = tab; this.tab = tab;
this.selectedTournament = null; this.selectedTournament = null;
this.pairings = null; this.pairings = null;
this.previousRoundPairings = [];
} }
selectTournament(t: Tournament): void { selectTournament(t: Tournament): void {
if (this.selectedTournament?.id === t.id) { if (this.selectedTournament?.id === t.id) {
this.selectedTournament = null; this.selectedTournament = null;
this.pairings = null; this.pairings = null;
this.previousRoundPairings = [];
return; return;
} }
this.selectedTournament = t; this.selectedTournament = t;
this.pairings = null; this.pairings = null;
this.previousRoundPairings = [];
if (t.round > 0) { if (t.round > 0) {
this.loadPairings(t.id, t.round); this.loadPairings(t.id, t.round);
} }
if (t.round > 1) {
this.previousRoundsLoading = true;
const rounds = Array.from({ length: t.round - 1 }, (_, i) => i + 1);
forkJoin(rounds.map(r => this.tournamentService.roundPairings(t.id, r)))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: results => {
this.previousRoundPairings = [...results].reverse();
this.previousRoundsLoading = false;
},
error: () => { this.previousRoundsLoading = false; }
});
}
} }
get activeList(): Tournament[] { get activeList(): Tournament[] {
@@ -185,16 +167,6 @@ export class TournamentsComponent implements OnInit {
}); });
} }
firstGameId(p: TournamentPairing): string | null {
return p.matches?.[0]?.gameId ?? null;
}
pairingKey(p: TournamentPairing): string {
const w = p.white?.id ?? 'bye';
const b = p.black?.id ?? 'bye';
return `${w}-${b}-${this.firstGameId(p) ?? ''}`;
}
watchGame(gameId: string): void { watchGame(gameId: string): void {
const tid = this.selectedTournament?.id; const tid = this.selectedTournament?.id;
if (!tid) return; if (!tid) return;
@@ -1,55 +0,0 @@
import { Injectable } from '@angular/core';
interface RegisterResponse {
id: string;
token: string;
}
@Injectable({ providedIn: 'root' })
export class TournamentAuthService {
private readonly inflight = new Map<string, Promise<string>>();
async getToken(serverUrl: string): Promise<string> {
const key = this.cacheKey(serverUrl);
const cached = localStorage.getItem(key);
if (cached) return cached;
const existing = this.inflight.get(key);
if (existing) return existing;
const promise = this.register(serverUrl)
.then(token => {
localStorage.setItem(key, token);
this.inflight.delete(key);
return token;
})
.catch(err => {
this.inflight.delete(key);
throw err;
});
this.inflight.set(key, promise);
return promise;
}
clearToken(serverUrl: string): void {
localStorage.removeItem(this.cacheKey(serverUrl));
}
private async register(serverUrl: string): Promise<string> {
const base = serverUrl.replace(/\/+$/, '');
const localName = localStorage.getItem('username') ?? 'viewer';
const res = await fetch(`${base}/api/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: `${localName}-watch`, isBot: false })
});
if (!res.ok) throw new Error(`tournament-server register failed: ${res.status}`);
const body = (await res.json()) as RegisterResponse;
return body.token;
}
private cacheKey(serverUrl: string): string {
return `tournament-token:${serverUrl.replace(/\/+$/, '')}`;
}
}
+10 -38
View File
@@ -1,48 +1,36 @@
import { Injectable, inject } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { GameStateSnapshot, GameStreamEvent, TournamentStreamEvent } from '../models/tournament.models'; import { GameStreamEvent, TournamentStreamEvent } from '../models/tournament.models';
import { TournamentAuthService } from './tournament-auth.service';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class TournamentStreamService { export class TournamentStreamService {
private readonly auth = inject(TournamentAuthService);
streamTournament(serverUrl: string, tournamentId: string): Observable<TournamentStreamEvent> { streamTournament(serverUrl: string, tournamentId: string): Observable<TournamentStreamEvent> {
return this.ndjson<TournamentStreamEvent>( return this.ndjson<TournamentStreamEvent>(
serverUrl, this.url(serverUrl, `/api/tournament/${tournamentId}/stream`)
`/api/tournament/${tournamentId}/stream`
); );
} }
streamGame(serverUrl: string, tournamentId: string, gameId: string): Observable<GameStreamEvent> { streamGame(serverUrl: string, tournamentId: string, gameId: string): Observable<GameStreamEvent> {
return this.ndjson<GameStreamEvent>( return this.ndjson<GameStreamEvent>(
serverUrl, this.url(serverUrl, `/api/tournament/${tournamentId}/game/${gameId}/stream`)
`/api/tournament/${tournamentId}/game/${gameId}/stream`
); );
} }
async fetchGame(serverUrl: string, tournamentId: string, gameId: string): Promise<GameStateSnapshot> { private url(base: string, path: string): string {
const base = serverUrl.replace(/\/+$/, '');
const token = await this.auth.getToken(serverUrl);
const res = await fetch(`${base}/api/tournament/${tournamentId}/game/${gameId}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) throw new Error(`Game fetch failed: ${res.status}`);
return res.json() as Promise<GameStateSnapshot>;
}
private fullUrl(base: string, path: string): string {
if (!base) return path; if (!base) return path;
return `${base.replace(/\/+$/, '')}${path}`; return `${base.replace(/\/+$/, '')}${path}`;
} }
private ndjson<T>(serverUrl: string, path: string): Observable<T> { private ndjson<T>(url: string): Observable<T> {
return new Observable<T>(subscriber => { return new Observable<T>(subscriber => {
const controller = new AbortController(); const controller = new AbortController();
const token = localStorage.getItem('token');
const headers: Record<string, string> = { Accept: 'application/x-ndjson' };
if (token) headers['Authorization'] = `Bearer ${token}`;
(async () => { (async () => {
try { try {
const res = await this.openWithRetry(serverUrl, path, controller.signal); const res = await fetch(url, { headers, signal: controller.signal });
if (!res.ok || !res.body) { if (!res.ok || !res.body) {
subscriber.error(new Error(`Stream failed: ${res.status} ${res.statusText}`)); subscriber.error(new Error(`Stream failed: ${res.status} ${res.statusText}`));
return; return;
@@ -75,20 +63,4 @@ export class TournamentStreamService {
return () => controller.abort(); return () => controller.abort();
}); });
} }
private async openWithRetry(serverUrl: string, path: string, signal: AbortSignal): Promise<Response> {
const url = this.fullUrl(serverUrl, path);
const token = await this.auth.getToken(serverUrl);
const res = await fetch(url, {
headers: { Accept: 'application/x-ndjson', Authorization: `Bearer ${token}` },
signal,
});
if (res.status !== 401) return res;
this.auth.clearToken(serverUrl);
const fresh = await this.auth.getToken(serverUrl);
return fetch(url, {
headers: { Accept: 'application/x-ndjson', Authorization: `Bearer ${fresh}` },
signal,
});
}
} }
+1
Submodule tournament-server added at ffe36da943
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0 MAJOR=0
MINOR=7 MINOR=5
PATCH=0 PATCH=0