Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| efb129c323 | |||
| 828c2a03c1 | |||
| 8603222ab4 | |||
| 76e0e3db78 | |||
| 0ac61032bd | |||
| 890c3fcecc | |||
| a98b5534b7 | |||
| 147d7f0d2c |
@@ -103,3 +103,23 @@
|
|||||||
* api streaming issues ([0621968](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/0621968c3ceddebe01e9c363bda345b5dcccfbbf))
|
* 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))
|
* 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))
|
* 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))
|
||||||
|
|||||||
Generated
+24
-3
@@ -458,6 +458,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -474,6 +475,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -487,6 +489,7 @@
|
|||||||
"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",
|
||||||
@@ -519,6 +522,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -562,6 +566,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -628,6 +633,7 @@
|
|||||||
"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",
|
||||||
@@ -1601,6 +1607,7 @@
|
|||||||
"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",
|
||||||
@@ -3531,6 +3538,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -3887,6 +3895,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"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",
|
||||||
@@ -4902,6 +4911,7 @@
|
|||||||
"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",
|
||||||
@@ -5343,6 +5353,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -5839,7 +5850,8 @@
|
|||||||
"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",
|
||||||
@@ -5941,6 +5953,7 @@
|
|||||||
"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",
|
||||||
@@ -6408,6 +6421,7 @@
|
|||||||
"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",
|
||||||
@@ -7920,6 +7934,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -7955,6 +7970,7 @@
|
|||||||
"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",
|
||||||
@@ -8574,7 +8590,8 @@
|
|||||||
"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",
|
||||||
@@ -8612,6 +8629,7 @@
|
|||||||
"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"
|
||||||
@@ -8741,6 +8759,7 @@
|
|||||||
"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",
|
||||||
@@ -9538,6 +9557,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -9556,7 +9576,8 @@
|
|||||||
"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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (snapshot?.white && snapshot?.black) {
|
@if (whiteName && blackName) {
|
||||||
<span class="player">{{ snapshot!.white!.name }}</span>
|
<span class="player">{{ whiteName }}</span>
|
||||||
<span class="vs">vs</span>
|
<span class="vs">vs</span>
|
||||||
<span class="player">{{ snapshot!.black!.name }}</span>
|
<span class="player">{{ blackName }}</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">{{ snapshot?.black?.name ?? 'Black' }}</span>
|
<span class="clock-label">{{ blackName ?? '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">{{ snapshot?.white?.name ?? 'White' }}</span>
|
<span class="clock-label">{{ whiteName ?? 'White' }}</span>
|
||||||
<span class="clock-time">{{ formatTime(clock?.whiteTime) }}</span>
|
<span class="clock-time">{{ formatTime(clock?.whiteTime) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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';
|
||||||
@@ -25,6 +26,9 @@ 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';
|
||||||
@@ -51,14 +55,47 @@ 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.';
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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(/\/+$/, '')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,36 +1,48 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { GameStreamEvent, TournamentStreamEvent } from '../models/tournament.models';
|
import { GameStateSnapshot, 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>(
|
||||||
this.url(serverUrl, `/api/tournament/${tournamentId}/stream`)
|
serverUrl,
|
||||||
|
`/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>(
|
||||||
this.url(serverUrl, `/api/tournament/${tournamentId}/game/${gameId}/stream`)
|
serverUrl,
|
||||||
|
`/api/tournament/${tournamentId}/game/${gameId}/stream`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private url(base: string, path: string): string {
|
async fetchGame(serverUrl: string, tournamentId: string, gameId: string): Promise<GameStateSnapshot> {
|
||||||
|
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>(url: string): Observable<T> {
|
private ndjson<T>(serverUrl: string, path: 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 fetch(url, { headers, signal: controller.signal });
|
const res = await this.openWithRetry(serverUrl, path, 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;
|
||||||
@@ -63,4 +75,20 @@ 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Injectable, inject } from '@angular/core';
|
|||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { Tournament, TournamentList, RoundPairings } from '../models/tournament.models';
|
import { Tournament, TournamentList, RoundPairings } from '../models/tournament.models';
|
||||||
import { environment } from '../../environments/environment';
|
|
||||||
|
|
||||||
export interface CreateTournamentForm {
|
export interface CreateTournamentForm {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -15,7 +14,7 @@ export interface CreateTournamentForm {
|
|||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class TournamentService {
|
export class TournamentService {
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
private readonly base = `${(environment.tournamentServerUrl ?? '').replace(/\/+$/, '')}/api/tournament`;
|
private readonly base = '/api/tournament';
|
||||||
|
|
||||||
list(): Observable<TournamentList> {
|
list(): Observable<TournamentList> {
|
||||||
return this.http.get<TournamentList>(this.base);
|
return this.http.get<TournamentList>(this.base);
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
MAJOR=0
|
MAJOR=0
|
||||||
MINOR=6
|
MINOR=6
|
||||||
PATCH=1
|
PATCH=5
|
||||||
|
|||||||
Reference in New Issue
Block a user