feat: NCWF-5/6/7/8/9 chess analysis page and engine integration

- NCWF-5: scaffold /analysis route with ChessBoard viewer and navigation
- NCWF-6: FEN / PGN / Game-ID input form with depth selector
- NCWF-7: extend GameApiService with analyzePosition(); add AnalysisService
  with game-wide annotation pipeline; proxy /api/analysis -> :8087
- NCWF-8: EvalTimelineComponent — SVG win-chance chart per ply
- NCWF-9: AnnotatedMoveListComponent — quality labels (!! ! ?! ? ??)
  derived from win-chance delta

Also fix pre-existing app.spec.ts failure (missing provideHttpClient).
Apply project-wide prettier formatting pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janis Eccarius
2026-06-10 15:55:14 +02:00
parent ae952d70b0
commit 8e2afb93f3
115 changed files with 6134 additions and 2378 deletions
+57 -57
View File
@@ -8,71 +8,71 @@ import { Challenge } from '../models/challenge.models';
*/
@Injectable({ providedIn: 'root' })
export class ChallengeEventService {
private readonly incomingChallenges$ = new BehaviorSubject<Challenge[]>([]);
private readonly challengeReceived$ = new Subject<Challenge>();
private readonly challengeAccepted$ = new Subject<Challenge>();
private readonly challengeDeclined$ = new Subject<Challenge>();
private readonly incomingChallenges$ = new BehaviorSubject<Challenge[]>([]);
private readonly challengeReceived$ = new Subject<Challenge>();
private readonly challengeAccepted$ = new Subject<Challenge>();
private readonly challengeDeclined$ = new Subject<Challenge>();
getIncomingChallenges$(): Observable<Challenge[]> {
return this.incomingChallenges$.asObservable();
}
getIncomingChallenges$(): Observable<Challenge[]> {
return this.incomingChallenges$.asObservable();
}
getChallengeReceived$(): Observable<Challenge> {
return this.challengeReceived$.asObservable();
}
getChallengeReceived$(): Observable<Challenge> {
return this.challengeReceived$.asObservable();
}
getChallengeAccepted$(): Observable<Challenge> {
return this.challengeAccepted$.asObservable();
}
getChallengeAccepted$(): Observable<Challenge> {
return this.challengeAccepted$.asObservable();
}
getChallengeDeclined$(): Observable<Challenge> {
return this.challengeDeclined$.asObservable();
}
getChallengeDeclined$(): Observable<Challenge> {
return this.challengeDeclined$.asObservable();
}
/**
* Called when a new challenge is received via WebSocket
*/
onChallengeReceived(challenge: Challenge): void {
const current = this.incomingChallenges$.value;
this.incomingChallenges$.next([...current, challenge]);
this.challengeReceived$.next(challenge);
}
/**
* Called when a new challenge is received via WebSocket
*/
onChallengeReceived(challenge: Challenge): void {
const current = this.incomingChallenges$.value;
this.incomingChallenges$.next([...current, challenge]);
this.challengeReceived$.next(challenge);
}
/**
* Called when a challenge is accepted
*/
onChallengeAccepted(challenge: Challenge): void {
const current = this.incomingChallenges$.value;
this.incomingChallenges$.next(current.filter(c => c.id !== challenge.id));
this.challengeAccepted$.next(challenge);
}
/**
* Called when a challenge is accepted
*/
onChallengeAccepted(challenge: Challenge): void {
const current = this.incomingChallenges$.value;
this.incomingChallenges$.next(current.filter((c) => c.id !== challenge.id));
this.challengeAccepted$.next(challenge);
}
/**
* Called when a challenge is declined or expires
*/
onChallengeRemoved(challengeId: string): void {
const current = this.incomingChallenges$.value;
this.incomingChallenges$.next(current.filter(c => c.id !== challengeId));
}
/**
* Called when a challenge is declined or expires
*/
onChallengeRemoved(challengeId: string): void {
const current = this.incomingChallenges$.value;
this.incomingChallenges$.next(current.filter((c) => c.id !== challengeId));
}
/**
* Remove a challenge from the incoming list
*/
removeChallenge(challengeId: string): void {
this.onChallengeRemoved(challengeId);
}
/**
* Remove a challenge from the incoming list
*/
removeChallenge(challengeId: string): void {
this.onChallengeRemoved(challengeId);
}
/**
* Replace the full incoming list (used by HTTP polling)
*/
setIncomingChallenges(challenges: Challenge[]): void {
this.incomingChallenges$.next(challenges);
}
/**
* Replace the full incoming list (used by HTTP polling)
*/
setIncomingChallenges(challenges: Challenge[]): void {
this.incomingChallenges$.next(challenges);
}
/**
* Clear all incoming challenges (used on logout)
*/
clear(): void {
this.incomingChallenges$.next([]);
}
/**
* Clear all incoming challenges (used on logout)
*/
clear(): void {
this.incomingChallenges$.next([]);
}
}