Compare commits

..

1 Commits

Author SHA1 Message Date
LQ63 68395f820c fix(ncs-122): send WS token via first-message auth instead of query param
Remove token from WebSocket URL query parameters in ChallengeWebSocketService
and GameApiService. Instead, send {"type":"auth","token":"..."} as the first
text frame after the connection opens, matching the new backend auth protocol.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 10:42:38 +02:00
9 changed files with 140 additions and 124 deletions
-20
View File
@@ -71,23 +71,3 @@
### Bug Fixes
* **auth:** attach Bearer token to /api/bots requests ([#12](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/12)) ([a54957a](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/a54957aa74ef15bf2dd439d386e221ac134c5c5c))
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.4.1...0.0.0) (2026-06-17)
### Bug Fixes
* NCS-122 send WS token via first-message auth instead of query param ([#13](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/13)) ([1d2c217](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/1d2c217da8982d361e2eb7de26f6447171a1dd43))
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.4.2...0.0.0) (2026-06-21)
### Bug Fixes
* **analysis:** fix API field mismatch and enable full game analysis ([ce1fb0d](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/ce1fb0d60b695093495ee0ad824c511dd2db7fbb))
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.4.3...0.0.0) (2026-06-21)
### Bug Fixes
* **tournaments:** load both user bots and official bots in join dialog ([5b5fd6f](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/5b5fd6f027b4aedb951a802725fcd929d514c359))
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.4.4...0.0.0) (2026-06-21)
### Features
* **tournaments:** remove external server add/remove UI ([412591d](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/412591dfe0119dbec84c3783cd94590810884580))
-10
View File
@@ -3,21 +3,11 @@ export interface AnalysisRequest {
depth: number;
}
export interface RawAnalysisResponse {
fen: string;
evaluation: number;
depth: number;
bestMove: string;
mate: number | null;
continuationMoves: string[];
}
export interface AnalysisResponse {
eval: number;
winChance: number;
depth: number;
bestMove: string;
mate: number | null;
continuations: string[];
}
@@ -246,7 +246,7 @@
<!-- Side column -->
<aside class="side">
<!-- Single position analysis result -->
@if (positionAnalysis) {
@if (positionAnalysis && !hasAnnotations) {
<details class="side-card" open>
<summary class="side-card-summary">
<span class="side-card-title">Position Analysis</span>
+6 -12
View File
@@ -2,7 +2,7 @@ import { Component, DestroyRef, inject, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { switchMap, of, forkJoin } from 'rxjs';
import { switchMap, of } from 'rxjs';
import { ChessBoardComponent } from '../../components/chess-board/chess-board.component';
import { EvalTimelineComponent } from '../../components/eval-timeline/eval-timeline.component';
@@ -146,7 +146,6 @@ export class AnalysisComponent implements OnInit {
return;
}
this.analysing = true;
this.positionAnalysis = null;
this.errorMessage = '';
const sans =
this.annotatedMoves.length > 0
@@ -208,7 +207,12 @@ export class AnalysisComponent implements OnInit {
private applyGame(game: GameFull): void {
this.game = game;
this.currentFen = game.state.fen;
// Build a flat FEN history from scratch using moves array
// The server gives us the final FEN. We reconstruct history by
// storing the final FEN; full per-ply history requires per-move API calls
// which is out of scope here — we store what we have and allow analysis to proceed.
this.fenHistory = [game.state.fen];
// Seed annotated moves with san strings, no quality yet
this.annotatedMoves = game.state.moves.map((san) => ({
san,
fen: game.state.fen,
@@ -219,16 +223,6 @@ export class AnalysisComponent implements OnInit {
winChanceBefore: null,
winChanceAfter: null,
}));
this.gameApi
.getFenHistory(game.gameId)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (fens) => {
if (fens.length >= 2) {
this.fenHistory = fens;
}
},
});
}
private analyseSinglePosition(fen: string): void {
@@ -202,57 +202,53 @@
<span class="dialog-brand">Join with a bot</span>
<button type="button" class="dialog-close" (click)="closeJoinDialog()">×</button>
</div>
<p class="join-hint">Select an official (engine-backed) bot to enter this tournament. These are the bots that actually play their moves.</p>
@if (botsLoading) {
<div class="dialog-loading"><span class="pulse"></span>Loading bots…</div>
} @else if (userBots.length === 0) {
<p class="join-empty">No official bots are available. The official-bots engine service must be running to register them.</p>
} @else {
@if (userBots.length === 0) {
<p class="join-empty">You have no bots. <a routerLink="/bots">Create one</a> to join with your own bot.</p>
} @else {
<div class="bot-pick-list">
@for (bot of userBots; track bot.id) {
<button type="button" class="bot-pick-row"
[disabled]="!!joiningBotId || !!joiningOfficialDifficulty"
(click)="joinWithBot(bot)">
<span class="bot-pick-avatar">{{ bot.name.charAt(0).toUpperCase() }}</span>
<span class="bot-pick-name">{{ bot.name }}</span>
<span class="bot-pick-rating">{{ bot.rating }}</span>
@if (joiningBotId === bot.id) {
<span class="bot-pick-spinner"></span>
}
</button>
}
</div>
}
@if (joinError) {
<div class="dialog-error">{{ joinError }}</div>
}
<div class="join-divider">
<span class="join-divider-label">or join with an official bot</span>
<div class="bot-pick-list">
@for (bot of userBots; track bot.id) {
<button type="button" class="bot-pick-row"
[disabled]="!!joiningBotId"
(click)="joinWithBot(bot)">
<span class="bot-pick-avatar">{{ bot.name.charAt(0).toUpperCase() }}</span>
<span class="bot-pick-name">{{ bot.name }}</span>
<span class="bot-pick-rating">{{ bot.rating }}</span>
@if (joiningBotId === bot.id) {
<span class="bot-pick-spinner"></span>
}
</button>
}
</div>
}
@if (officialBots.length === 0) {
<p class="join-empty">No official bots are available. The official-bots engine service must be running to register them.</p>
} @else {
<div class="official-bot-grid">
@for (d of officialDifficulties; track d) {
<button type="button" class="official-bot-btn"
[class]="'official-btn-' + d"
[disabled]="!!joiningOfficialDifficulty || !!joiningBotId"
(click)="joinWithOfficialBot(d)">
@if (joiningOfficialDifficulty === d) {
<span class="pulse"></span>
}
{{ d | titlecase }}
</button>
@if (joinError) {
<div class="dialog-error">{{ joinError }}</div>
}
<div class="join-divider">
<span class="join-divider-label">or join with an official bot</span>
</div>
<div class="official-bot-grid">
@for (d of officialDifficulties; track d) {
<button type="button" class="official-bot-btn"
[class]="'official-btn-' + d"
[disabled]="!!joiningOfficialDifficulty || !!joiningBotId"
(click)="joinWithOfficialBot(d)">
@if (joiningOfficialDifficulty === d) {
<span class="pulse"></span>
}
</div>
{{ d | titlecase }}
</button>
}
</div>
@if (officialJoinError) {
<div class="dialog-error">{{ officialJoinError }}</div>
}
@if (officialJoinError) {
<div class="dialog-error">{{ officialJoinError }}</div>
}
</div>
</div>
@@ -271,7 +267,7 @@
@if (serversLoading) {
<div class="dialog-loading"><span class="pulse"></span>Loading servers…</div>
} @else if (servers.length === 0) {
<p class="join-empty">No external servers registered.</p>
<p class="join-empty">No external servers registered yet.</p>
} @else {
<div class="servers-list">
@for (s of servers; track s.id) {
@@ -280,14 +276,49 @@
<span class="server-label">{{ s.label }}</span>
<span class="server-url">{{ s.url }}</span>
</div>
<button type="button" class="server-remove-btn"
[disabled]="removingServerId === s.id"
(click)="removeServer(s.id)"
title="Remove server">
@if (removingServerId === s.id) { … } @else {
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
<path d="M10 11v6"/><path d="M14 11v6"/>
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
</svg>
}
</button>
</div>
}
</div>
}
<div class="dialog-actions">
<button type="button" class="btn-ghost" (click)="closeServersDialog()">Close</button>
<div class="server-add-form">
<h4 class="server-add-heading">Add server</h4>
<div class="dialog-field">
<label class="dialog-label">Label</label>
<input type="text" class="dialog-input" [(ngModel)]="newServerLabel"
placeholder="e.g. Local Dev Server" />
</div>
<div class="dialog-field">
<label class="dialog-label">URL</label>
<input type="url" class="dialog-input" [(ngModel)]="newServerUrl"
placeholder="http://host:8089" />
</div>
@if (addServerError) {
<div class="dialog-error">{{ addServerError }}</div>
}
<div class="dialog-actions">
<button type="button" class="btn-ghost" (click)="closeServersDialog()">Close</button>
<button type="button" class="btn-primary"
[disabled]="addingServer || !newServerLabel.trim() || !newServerUrl.trim()"
(click)="addServer()">
{{ addingServer ? 'Adding…' : 'Add' }}
</button>
</div>
</div>
</div>
</div>
}
@@ -3,7 +3,6 @@ import { CommonModule, TitleCasePipe } from '@angular/common';
import { Router, RouterLink } from '@angular/router';
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { forkJoin } from 'rxjs';
import { TournamentService } from '../../services/tournament.service';
import { AuthService } from '../../services/auth.service';
import { BotService } from '../../services/bot.service';
@@ -59,7 +58,6 @@ export class TournamentsComponent implements OnInit {
joinDialogTournamentId: string | null = null;
userBots: Bot[] = [];
officialBots: Bot[] = [];
botsLoading = false;
joiningBotId: string | null = null;
joinError: string | null = null;
@@ -71,6 +69,11 @@ export class TournamentsComponent implements OnInit {
showServersDialog = false;
servers: ExternalTournamentServer[] = [];
serversLoading = false;
newServerLabel = '';
newServerUrl = '';
addingServer = false;
addServerError: string | null = null;
removingServerId: string | null = null;
ngOnInit(): void {
this.authService.currentUser$
@@ -172,22 +175,16 @@ export class TournamentsComponent implements OnInit {
this.joinDialogTournamentId = tournamentId;
this.joinError = null;
this.botsLoading = true;
forkJoin({ user: this.botService.list(), official: this.botService.listOfficial() })
this.botService.listOfficial()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: ({ user, official }) => {
this.userBots = user;
this.officialBots = official;
this.botsLoading = false;
},
next: bots => { this.userBots = bots; this.botsLoading = false; },
error: () => { this.botsLoading = false; }
});
}
closeJoinDialog(): void {
this.joinDialogTournamentId = null;
this.userBots = [];
this.officialBots = [];
this.joiningBotId = null;
this.joinError = null;
this.joiningOfficialDifficulty = null;
@@ -243,6 +240,9 @@ export class TournamentsComponent implements OnInit {
}
openServersDialog(): void {
this.newServerLabel = '';
this.newServerUrl = '';
this.addServerError = null;
this.showServersDialog = true;
this.serversLoading = true;
this.tournamentServerService.list()
@@ -257,6 +257,40 @@ export class TournamentsComponent implements OnInit {
this.showServersDialog = false;
}
addServer(): void {
const label = this.newServerLabel.trim();
const url = this.newServerUrl.trim();
if (!label || !url || this.addingServer) return;
this.addingServer = true;
this.addServerError = null;
this.tournamentServerService.register(label, url).subscribe({
next: server => {
this.addingServer = false;
this.servers = [...this.servers, server];
this.newServerLabel = '';
this.newServerUrl = '';
this.loadTournaments();
},
error: err => {
this.addingServer = false;
this.addServerError = err.error?.error ?? 'Failed to add server.';
}
});
}
removeServer(id: string): void {
if (this.removingServerId) return;
this.removingServerId = id;
this.tournamentServerService.remove(id).subscribe({
next: () => {
this.removingServerId = null;
this.servers = this.servers.filter(s => s.id !== id);
this.loadTournaments();
},
error: () => { this.removingServerId = null; }
});
}
private loadTournaments(): void {
this.tournamentService.list()
.pipe(takeUntilDestroyed(this.destroyRef))
+2 -23
View File
@@ -1,7 +1,6 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { environment } from '../../environments/environment';
import {
GameFull,
@@ -10,7 +9,7 @@ import {
LegalMovesResponse,
PlayerInfo,
} from '../models/game.models';
import { AnalysisRequest, AnalysisResponse, RawAnalysisResponse } from '../models/analysis.models';
import { AnalysisRequest, AnalysisResponse } from '../models/analysis.models';
import { StreamHandlerService } from './stream-handler.service';
@Injectable({ providedIn: 'root' })
@@ -79,28 +78,8 @@ export class GameApiService {
return this.http.post<void>(`${this.apiBase}${this.apiPath}/${gameId}/draw/offer`, {});
}
getFenHistory(gameId: string): Observable<string[]> {
return this.http
.get<{ fens: string[] }>(`${this.apiBase}${this.apiPath}/${gameId}/fen-history`)
.pipe(map((r) => r.fens));
}
analyzePosition(request: AnalysisRequest): Observable<AnalysisResponse> {
return this.http
.post<RawAnalysisResponse>(`${this.apiBase}/api/analysis/position`, request)
.pipe(map((raw) => this.mapAnalysisResponse(raw)));
}
private mapAnalysisResponse(raw: RawAnalysisResponse): AnalysisResponse {
const evalPawns = raw.evaluation / 100;
return {
eval: evalPawns,
winChance: 1 / (1 + Math.exp(-0.374 * evalPawns)),
depth: raw.depth,
bestMove: raw.bestMove,
mate: raw.mate,
continuations: raw.continuationMoves ?? [],
};
return this.http.post<AnalysisResponse>(`${this.apiBase}/api/analysis/position`, request);
}
private resolveWsBase(): string {
@@ -20,4 +20,12 @@ export class TournamentServerService {
list(): Observable<ExternalTournamentServerList> {
return this.http.get<ExternalTournamentServerList>(this.base);
}
register(label: string, url: string): Observable<ExternalTournamentServer> {
return this.http.post<ExternalTournamentServer>(this.base, { label, url });
}
remove(id: string): Observable<void> {
return this.http.delete<void>(`${this.base}/${id}`);
}
}
+2 -2
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=5
PATCH=0
MINOR=4
PATCH=1