diff --git a/.claude/commands/create-defect.md b/.claude/commands/create-defect.md
index 82e19e9..935c7a5 100644
--- a/.claude/commands/create-defect.md
+++ b/.claude/commands/create-defect.md
@@ -19,6 +19,7 @@ If `$ARGUMENTS` already answers some of these, skip those questions.
## Step 2 — Research (if needed)
If the bug involves component logic, a service, or routing:
+
- Search repo for relevant code (`Grep`/`Bash`) under `src/app/{components,services,models,core,pages}`.
- Check `.spec.ts` files for existing coverage of the broken area.
- Do NOT guess at root cause. Surface findings before drafting.
@@ -56,6 +57,7 @@ Environment / Notes
```
Rules:
+
- Steps must be minimal and reproducible.
- Expected vs actual: concrete and unambiguous.
- Omit "Environment / Notes" section if not relevant.
@@ -64,6 +66,7 @@ Rules:
Show the draft to the user.
**Use `AskUserQuestion` tool to ask:**
+
- Are steps to reproduce complete and accurate?
- Severity: Blocker / Critical / Major / Minor / Trivial?
- Any related tickets or recent changes to link?
@@ -73,6 +76,7 @@ Incorporate feedback. Repeat until user approves.
## Step 5 — Determine Project
> **Project routing rules (always apply these):**
+>
> - Frontend code (UI, UX, web app, components, services) → `NCWF`
> - Backend code (game engine, bots, API, services, coordinator) → `NCS`
> - Infrastructure (Kubernetes, pipelines, CI/CD, DB setup, cloud infra) → `NCI`
@@ -86,6 +90,7 @@ If ambiguous, ask the user.
## Step 6 — Create Issue
Call `mcp__youtrack__create_issue` with:
+
- `project`: determined in Step 5
- `summary`: concise title describing what is broken (≤72 chars, sentence case)
- `description`: full formatted defect report from Step 3 (Markdown)
diff --git a/.claude/commands/create-story.md b/.claude/commands/create-story.md
index 25edb03..8ede476 100644
--- a/.claude/commands/create-story.md
+++ b/.claude/commands/create-story.md
@@ -19,6 +19,7 @@ If `$ARGUMENTS` already answers some of these, skip those questions.
## Step 2 — Research (if needed)
If the topic involves unfamiliar UI flows, component structure, or technical constraints:
+
- Search the repo for relevant code under `src/app/` (use `Grep`/`Bash`).
- Use `WebSearch` if the topic involves Angular APIs, external standards or protocols.
- Do NOT guess. Surface findings before drafting.
@@ -53,6 +54,7 @@ Implementation Notes
```
Rules:
+
- User story line: plain English, present tense, from user's perspective.
- Acceptance criteria: testable, unambiguous, one condition each.
- Implementation notes: optional — only include if there are known constraints, related tickets, or design refs.
@@ -61,6 +63,7 @@ Rules:
Show the draft to the user.
**Use `AskUserQuestion` tool to ask:**
+
- Are the acceptance criteria complete and correct?
- Any implementation constraints to add?
- Priority (if known)?
@@ -70,6 +73,7 @@ Incorporate feedback. Repeat until user approves.
## Step 5 — Determine Project
> **Project routing rules (always apply these):**
+>
> - Frontend code (UI, UX, web app, components, services) → `NCWF`
> - Backend code (game engine, bots, API, services, coordinator) → `NCS`
> - Infrastructure (Kubernetes, pipelines, CI/CD, DB setup, cloud infra) → `NCI`
@@ -83,6 +87,7 @@ If still ambiguous, ask the user.
## Step 6 — Create Issue
Call `mcp__youtrack__create_issue` with:
+
- `project`: determined in Step 5
- `summary`: concise title derived from the "I want to" clause (≤72 chars, sentence case)
- `description`: full formatted story from Step 3 (Markdown)
@@ -96,14 +101,14 @@ After creation, ask the user (use `AskUserQuestion` if interactive, otherwise in
Collect any issue IDs the user mentions. For each, determine the correct relation and call `mcp__youtrack__link_issues`:
-| Situation | Relation to use |
-|-----------|----------------|
-| This story must be done before another | `blocks` |
-| Another story must be done before this | `is blocked by` |
-| Stories share domain or are related | `relates to` |
-| This is a child of an epic/story | `subtask of` |
-| This is a parent grouping subtasks | `parent for` |
-| This depends on another ticket's output | `depends on` |
+| Situation | Relation to use |
+| --------------------------------------- | --------------- |
+| This story must be done before another | `blocks` |
+| Another story must be done before this | `is blocked by` |
+| Stories share domain or are related | `relates to` |
+| This is a child of an epic/story | `subtask of` |
+| This is a parent grouping subtasks | `parent for` |
+| This depends on another ticket's output | `depends on` |
If the user mentions an issue in the story description or implementation notes (e.g. "see NCS-42", "after NCWF-12 is done"), auto-detect and suggest linking it — confirm before creating the link.
diff --git a/.claude/commands/estimate-issues.md b/.claude/commands/estimate-issues.md
index cc8ed37..153f696 100644
--- a/.claude/commands/estimate-issues.md
+++ b/.claude/commands/estimate-issues.md
@@ -7,10 +7,12 @@ This is the **NowChess-Frontend** repo. Sprint mode defaults to project `NCWF`.
## Step 1 — Determine Scope
**Single-issue mode** (`$ARGUMENTS` set):
+
- Call `mcp__youtrack__get_issue` on `$ARGUMENTS`.
- Proceed with that issue only.
**Sprint mode** (`$ARGUMENTS` empty):
+
- Call `mcp__youtrack__search_issues` with query `project: NCWF Sprints: {current sprint} #Unresolved`.
- If query returns 0 results, use `AskUserQuestion` to ask for the sprint name, then retry with `project: NCWF Sprints: {name}`.
- Collect all returned issues.
@@ -18,6 +20,7 @@ This is the **NowChess-Frontend** repo. Sprint mode defaults to project `NCWF`.
## Step 2 — Build Issue Tree
For each top-level issue from Step 1:
+
1. Fetch full details via `mcp__youtrack__get_issue`: summary, description, acceptance criteria, Type, existing `Zeitschätzung`, linked issues.
2. Identify subtasks from links with relation `subtask of` (i.e. issues where the fetched issue is the parent).
3. Recursively fetch subtasks until all leaves are known.
@@ -29,17 +32,18 @@ For each top-level issue from Step 1:
## Step 3 — Estimate Leaf Nodes
For each leaf node:
+
1. Read: summary, description, acceptance criteria, implementation notes.
2. If scope is unclear, search codebase (`Grep`/`Bash`) under `src/app/` for related files to gauge complexity.
3. Assign estimate using this scale:
-| Size | Criteria | Estimate |
-|------|----------|----------|
-| Trivial | Style tweak, copy change, 1-file tweak | 30m |
-| Small | 1–3 files, single component/service, no unknowns | 1h–2h |
-| Medium | 3–6 files, new component + service wiring, some design | 3h–5h |
-| Large | 6+ files, cross-feature, non-trivial state/routing | 1d–2d |
-| XL | New feature area, major refactor, research spike | 3d–5d |
+| Size | Criteria | Estimate |
+| ------- | ------------------------------------------------------ | -------- |
+| Trivial | Style tweak, copy change, 1-file tweak | 30m |
+| Small | 1–3 files, single component/service, no unknowns | 1h–2h |
+| Medium | 3–6 files, new component + service wiring, some design | 3h–5h |
+| Large | 6+ files, cross-feature, non-trivial state/routing | 1d–2d |
+| XL | New feature area, major refactor, research spike | 3d–5d |
4. Record: estimate + one-line reasoning.
5. Skip leaf if it already has `Zeitschätzung` set — note it as pre-estimated.
@@ -49,6 +53,7 @@ For each leaf node:
YouTrack auto-sums `Zeitschätzung` from subtasks up to parents — **do not write estimates to parent nodes**.
Compute display-only rolled-up totals:
+
- Parent total = sum of all descendant leaf estimates (including pre-estimated ones).
- Flag any branch where some leaves are missing estimates (partial roll-up).
@@ -68,11 +73,13 @@ Epic NCWF-10: Board UI overhaul [4h 30m] ← rolled up
Legend: `[X]` = display-only roll-up (not written). Plain = will be written to YouTrack.
If sprint mode, show grand total at bottom:
+
```
Sprint total: Xd Yh Zm (N issues, M leaves to update)
```
**Use `AskUserQuestion` tool:**
+
- Does the breakdown look right?
- Any estimates to adjust before writing to YouTrack?
@@ -81,6 +88,7 @@ Incorporate all feedback before proceeding.
## Step 6 — Write Estimates
On user approval, write estimates **only to leaf nodes** (bottom-up order):
+
- For each leaf with a new estimate, call `mcp__youtrack__update_issue` with field `Zeitschätzung` = approved estimate.
- YouTrack period format: `"30m"`, `"1h 30m"`, `"1d"`, `"2d 4h"`.
- Skip leaves already pre-estimated.
diff --git a/.claude/commands/fix-defect.md b/.claude/commands/fix-defect.md
index e46441c..71aee61 100644
--- a/.claude/commands/fix-defect.md
+++ b/.claude/commands/fix-defect.md
@@ -3,6 +3,7 @@
Automated defect-fix workflow. Ticket ID: `$ARGUMENTS`
This is the **NowChess-Frontend** repo (Angular 20 / TypeScript). Gates:
+
- Build: `npm run build`
- Test: `npm test -- --watch=false --browsers=ChromeHeadless`
- Format: `npx prettier --write .` (check with `npx prettier --check .`)
@@ -15,6 +16,7 @@ Extract and display: summary, description, steps to reproduce, Priority, Subsyst
## Step 2 — Create Worktree
Derive branch name from ticket:
+
- `type` from YouTrack issue type: `bug` → `fix`, `feature`/`task` → `feat`, `refactor` → `refactor`, else `chore`
- `scope` from affected area (kebab-case, omit if unclear)
- `description` from ticket summary: lowercase, kebab-case, max 40 chars, drop articles
@@ -42,6 +44,7 @@ After root cause confirmed, assess scope:
**Complex** (3+ files, multiple concerns, or estimated > 1 hour): create subtasks before coding.
To create subtasks:
+
1. Break fix into discrete, independently-completable tasks (e.g. "Fix selection state in BoardComponent", "Add spec for flip behaviour", "Update GameService move stream").
2. For each subtask call `mcp__youtrack__create_issue` with:
- `project`: based on subtask content — do **not** inherit from parent. Frontend/UI → `NCWF`; backend code → `NCS`; Kubernetes/pipelines/CI-CD/infrastructure → `NCI`. If ambiguous, ask user.
@@ -66,7 +69,7 @@ Then proceed to Step 4, implementing subtasks in order.
5. Run `npx prettier --check .` — **blocking, foreground only** (never `run_in_background`). Wait for exit code 0. Must be green.
- If it fails, fix all issues and re-run until exit code 0.
- **Do NOT proceed to Step 5 until the build, tests and format check all pass.**
-If any step fails, iterate until all pass.
+ If any step fails, iterate until all pass.
## Step 5 — Review
@@ -76,6 +79,7 @@ Display findings grouped by severity.
## Step 5b — Apply Review Findings
If the review produced any findings (any severity):
+
1. Implement all agreed fixes.
2. Run `npm run build` — must be green.
3. Run `npm test -- --watch=false --browsers=ChromeHeadless` — must be green.
@@ -125,12 +129,12 @@ Files changed:
After commenting, ask the user if `$ARGUMENTS` should be linked to any other issues not already linked:
-| Situation | Relation |
-|-----------|---------|
-| This fix blocks another open ticket | `blocks` |
-| Another ticket must ship first | `is blocked by` |
-| Related defect or story | `relates to` |
-| Duplicate of another defect | `duplicates` |
+| Situation | Relation |
+| ----------------------------------- | --------------- |
+| This fix blocks another open ticket | `blocks` |
+| Another ticket must ship first | `is blocked by` |
+| Related defect or story | `relates to` |
+| Duplicate of another defect | `duplicates` |
Scan the ticket description and comments for any issue IDs that were mentioned but not yet linked. Suggest those automatically.
diff --git a/.claude/commands/implement-feature.md b/.claude/commands/implement-feature.md
index fa2edb1..fb02ce1 100644
--- a/.claude/commands/implement-feature.md
+++ b/.claude/commands/implement-feature.md
@@ -4,6 +4,7 @@ Automated feature-implementation workflow. Ticket ID: `$ARGUMENTS`
This is the **NowChess-Frontend** repo (Angular 20 / TypeScript). In-project =
`NCWF`. Gates:
+
- Build: `npm run build`
- Test: `npm test -- --watch=false --browsers=ChromeHeadless`
- Format: `npx prettier --write .` (check with `npx prettier --check .`)
@@ -40,6 +41,7 @@ are collected and reported at the end with a ready-to-run prompt.
## Step 3 — Create Worktree
Derive branch name from the root ticket `$ARGUMENTS`:
+
- `type` from YouTrack issue type: `feature`/`task` → `feat`, `refactor` → `refactor`, `bug` → `fix`, else `chore`
- `scope` from affected area (kebab-case, omit if unclear)
- `description` from ticket summary: lowercase, kebab-case, max 40 chars, drop articles
@@ -83,6 +85,7 @@ Display findings grouped by severity.
## Step 6b — Apply Review Findings
If the review produced any findings (any severity):
+
1. Implement all agreed fixes.
2. Run `npm run build` — must be green.
3. Run `npm test -- --watch=false --browsers=ChromeHeadless` — must be green.
@@ -139,10 +142,12 @@ Call `ExitWorktree` with `discard_changes: true` to delete the worktree.
Final report to the user, in two sections:
### Blocked in-project tasks
+
List any `NCWF` tasks that could **not** be implemented, with the blocker(s)
that stopped them. (These can be re-run with this command once blockers clear.)
### Cross-project tasks (NCS / NCI / other)
+
For every out-of-project task discovered in the tree (whether it was a subtask
or a blocker), output one entry:
diff --git a/.claude/commands/split-story.md b/.claude/commands/split-story.md
index 40bcf1e..1ebe9b1 100644
--- a/.claude/commands/split-story.md
+++ b/.claude/commands/split-story.md
@@ -12,6 +12,7 @@ Extract and display: summary, description, acceptance criteria, implementation n
## Step 2 — Research (if needed)
If the story involves unfamiliar UI flows or technical constraints:
+
- Search repo for relevant code under `src/app/` (`Grep`/`Bash`).
- Use `WebSearch` for Angular APIs, external standards or protocols.
- Do NOT guess. Surface findings before proposing splits.
@@ -19,6 +20,7 @@ If the story involves unfamiliar UI flows or technical constraints:
## Step 3 — Propose Split
Analyse the story and propose a set of subtasks. Rules:
+
- Each subtask = one unit of work, completable independently or in sequence.
- No subtask should exceed ~2 days of work.
- Name subtasks in imperative mood (e.g. "Add board theme service", "Render theme picker component").
@@ -26,6 +28,7 @@ Analyse the story and propose a set of subtasks. Rules:
Show proposed subtask list to user (titles only) and ask:
**Use `AskUserQuestion` tool:**
+
- Does the split look right?
- Any subtasks to add, remove, or merge?
- Should any subtask be assigned to a specific person?
@@ -57,6 +60,7 @@ What must be true for this subtask to be considered complete:
```
Rules:
+
- Steps/Tasks: concrete, ordered where order matters.
- Definition of Done: adjust per subtask — not all subtasks need the same criteria (e.g. a research spike has different DoD than an implementation task).
- Keep description short — one paragraph max.
@@ -74,6 +78,7 @@ If a subtask's project is ambiguous, ask the user before creating it.
## Step 6 — Create Subtasks
For each subtask call `mcp__youtrack__create_issue` with:
+
- `project`: from Step 5
- `summary`: subtask title (≤72 chars, sentence case)
- `description`: full formatted description from Step 4 (Markdown)
@@ -84,6 +89,7 @@ Then call `mcp__youtrack__link_issues` to link each created subtask to `$ARGUMEN
## Step 6b — Inter-Subtask Links
If subtasks must be done in sequence (one depends on output of another), add ordering links:
+
- For each dependency pair call `mcp__youtrack__link_issues` with relation `is blocked by` (subtask B is blocked by subtask A).
Ask the user to confirm sequencing before adding these links:
@@ -94,12 +100,12 @@ Ask the user to confirm sequencing before adding these links:
Scan `$ARGUMENTS` description and implementation notes for any referenced issue IDs not already linked. For each:
-| Situation | Relation |
-|-----------|---------|
-| Parent story blocks another epic/story | `blocks` |
+| Situation | Relation |
+| ---------------------------------------- | --------------- |
+| Parent story blocks another epic/story | `blocks` |
| Story depends on another epic completing | `is blocked by` |
-| Related story in same domain | `relates to` |
-| This story duplicates or supersedes | `duplicates` |
+| Related story in same domain | `relates to` |
+| This story duplicates or supersedes | `duplicates` |
Suggest links to the user and call `mcp__youtrack__link_issues` on confirmation.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 643029b..ca63010 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,58 +1,77 @@
+## [0.3.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.8...0.3.0) (2026-06-10)
+
+### Features
+
+- NCWF-5 Scaffold post-game analysis page at /analysis with board viewer and route
+- NCWF-6 FEN / PGN / Game-ID input form for custom analysis
+- NCWF-7 Integrate backend analysis API (POST /api/analysis/position) via GameApiService
+- NCWF-8 Per-move evaluation timeline SVG chart
+- NCWF-9 Annotated move list with quality labels (!! ! ?! ? ??)
+
## 0.0.0 (2026-05-12)
### Features
-* added bot, light and dark mode ([2de003e](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/2de003e497baee72f998d0d805ca1e58aababe48))
-* added web view 1v1 ([1828fa3](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/1828fa3275ddb8ce6bb90a9f6a970ec428ebce3a))
-* NCS-63 User account implementation ([#2](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/2)) ([ff75c8c](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/ff75c8ce2fad54137f04a14c15bc1d4a38fa9bb8))
-* NCS-75 Frontend Deployment Dockerfile ([#4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/4)) ([bd7ec58](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/bd7ec581e38b5d8e775741bf16e19b4dc67b979e))
+- added bot, light and dark mode ([2de003e](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/2de003e497baee72f998d0d805ca1e58aababe48))
+- added web view 1v1 ([1828fa3](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/1828fa3275ddb8ce6bb90a9f6a970ec428ebce3a))
+- NCS-63 User account implementation ([#2](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/2)) ([ff75c8c](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/ff75c8ce2fad54137f04a14c15bc1d4a38fa9bb8))
+- NCS-75 Frontend Deployment Dockerfile ([#4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/4)) ([bd7ec58](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/bd7ec581e38b5d8e775741bf16e19b4dc67b979e))
### Bug Fixes
-* build issues ([36d72fd](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/36d72fd6cda41be51d28f8ac307dcdbcd31afa91))
-* cleaner components seperation ([8b090e4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/8b090e4d96c64c0adb253e3aefad7930108ccfb9))
-* gitignore ([4da882f](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/4da882f481ba7a008aac868fb37de7cb2bafea5d))
-* GITIGNORE ([8df2d05](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/8df2d0550ab17c9afb2d19c414eac700a75add02))
-* npm ([c11c1d4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/c11c1d4dce9de4bd5b463e891eebf961b37feb04))
-* removed .vs ([2833ead](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/2833ead7be3b47ee5c188d2d21b7326cb3cb3f26))
-* removed cache files ([7ee59c4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/7ee59c434bf137a08fd560bbc02ceefbcfd90f04))
-* size of pieces and removed text view of the game state ([c60d00f](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/c60d00f9d25247504845654615065fbccd7fe448))
-* structure ([3e8c7c4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/3e8c7c4057e55aeec7cee8c24f6751ff24912c93))
+- build issues ([36d72fd](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/36d72fd6cda41be51d28f8ac307dcdbcd31afa91))
+- cleaner components seperation ([8b090e4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/8b090e4d96c64c0adb253e3aefad7930108ccfb9))
+- gitignore ([4da882f](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/4da882f481ba7a008aac868fb37de7cb2bafea5d))
+- GITIGNORE ([8df2d05](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/8df2d0550ab17c9afb2d19c414eac700a75add02))
+- npm ([c11c1d4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/c11c1d4dce9de4bd5b463e891eebf961b37feb04))
+- removed .vs ([2833ead](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/2833ead7be3b47ee5c188d2d21b7326cb3cb3f26))
+- removed cache files ([7ee59c4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/7ee59c434bf137a08fd560bbc02ceefbcfd90f04))
+- size of pieces and removed text view of the game state ([c60d00f](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/c60d00f9d25247504845654615065fbccd7fe448))
+- structure ([3e8c7c4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/3e8c7c4057e55aeec7cee8c24f6751ff24912c93))
+
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.1.0...0.0.0) (2026-05-12)
### Features
-* NCS-69 Challenge request ([#3](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/3)) ([bad7366](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/bad7366bdbb048c20218257b30ac22efc9ecb6db))
+- NCS-69 Challenge request ([#3](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/3)) ([bad7366](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/bad7366bdbb048c20218257b30ac22efc9ecb6db))
+
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.0...0.0.0) (2026-05-12)
### Bug Fixes
-* NCWF-1 401 ([#5](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/5)) ([f8f93ef](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/f8f93efff48f1d7198023fed45b675c2e225df36))
+- NCWF-1 401 ([#5](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/5)) ([f8f93ef](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/f8f93efff48f1d7198023fed45b675c2e225df36))
+
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.1...0.0.0) (2026-05-12)
### Bug Fixes
-* NCWF-1 401 ([#6](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/6)) ([6d1e06d](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/6d1e06dfd606b93d029e9c9b84eea6f8b3b6294e))
+- NCWF-1 401 ([#6](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/6)) ([6d1e06d](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/6d1e06dfd606b93d029e9c9b84eea6f8b3b6294e))
+
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.2...0.0.0) (2026-05-14)
### Bug Fixes
-* added missing challenge routes ([61000f8](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/61000f8a22aff8b524664a756cc933834365f923))
+- added missing challenge routes ([61000f8](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/61000f8a22aff8b524664a756cc933834365f923))
+
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.3...0.0.0) (2026-05-15)
### Bug Fixes
-* build error ([51a363a](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/51a363a2432be111b804082df362975047dc8080))
-* NCWF-2 bugs and desing fixes ([#7](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/7)) ([c02414e](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/c02414ea40177b05a5e62dcf68dcb44efa6d3740))
+- build error ([51a363a](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/51a363a2432be111b804082df362975047dc8080))
+- NCWF-2 bugs and desing fixes ([#7](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/7)) ([c02414e](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/c02414ea40177b05a5e62dcf68dcb44efa6d3740))
+
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.4...0.0.0) (2026-06-01)
+
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.5...0.0.0) (2026-06-02)
### Bug Fixes
-* NCWF-4 Token Issues ([#8](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/8)) ([95eff42](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/95eff42dfe6d9c23ede08c7297614369a1b00d9f))
+- NCWF-4 Token Issues ([#8](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/8)) ([95eff42](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/95eff42dfe6d9c23ede08c7297614369a1b00d9f))
+
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.6...0.0.0) (2026-06-06)
+
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.7...0.0.0) (2026-06-10)
### Bug Fixes
-* route play-vs-bot to /vs-bot endpoint ([a620735](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/a62073511f2ac912ceb0f6b4730bef37545dd8ea))
+- route play-vs-bot to /vs-bot endpoint ([a620735](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/a62073511f2ac912ceb0f6b4730bef37545dd8ea))
diff --git a/angular.json b/angular.json
index 53f40a6..d6c8d66 100644
--- a/angular.json
+++ b/angular.json
@@ -18,9 +18,7 @@
"builder": "@angular/build:application",
"options": {
"browser": "src/main.ts",
- "polyfills": [
- "zone.js"
- ],
+ "polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": [
{
@@ -38,9 +36,7 @@
"output": "/assets/ChessAssets"
}
],
- "styles": [
- "src/styles.css"
- ]
+ "styles": ["src/styles.css"]
},
"configurations": {
"production": {
@@ -106,10 +102,7 @@
"test": {
"builder": "@angular/build:karma",
"options": {
- "polyfills": [
- "zone.js",
- "zone.js/testing"
- ],
+ "polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "tsconfig.spec.json",
"assets": [
{
@@ -127,9 +120,7 @@
"output": "/assets/ChessAssets"
}
],
- "styles": [
- "src/styles.css"
- ]
+ "styles": ["src/styles.css"]
}
}
}
diff --git a/docs/board-api-spec.yaml b/docs/board-api-spec.yaml
index 61bf241..b6947ef 100644
--- a/docs/board-api-spec.yaml
+++ b/docs/board-api-spec.yaml
@@ -55,7 +55,6 @@ tags:
description: Export a game as FEN or PGN
paths:
-
# ---------------------------------------------------------------------------
# Game lifecycle
# ---------------------------------------------------------------------------
@@ -467,7 +466,6 @@ paths:
# =============================================================================
components:
-
securitySchemes:
bearerAuth:
type: http
@@ -517,7 +515,6 @@ components:
$ref: '#/components/schemas/ApiError'
schemas:
-
# -------------------------------------------------------------------------
# Requests
# -------------------------------------------------------------------------
@@ -551,7 +548,7 @@ components:
pgn:
type: string
description: PGN text (headers and move list)
- example: "1. e4 e5 2. Nf3 Nc6 *"
+ example: '1. e4 e5 2. Nf3 Nc6 *'
# -------------------------------------------------------------------------
# Game state
@@ -587,7 +584,7 @@ components:
pgn:
type: string
description: PGN move text for the full game so far
- example: "1. e4"
+ example: '1. e4'
turn:
type: string
enum: [white, black]
diff --git a/proxy.conf.json b/proxy.conf.json
index f243619..e65b255 100644
--- a/proxy.conf.json
+++ b/proxy.conf.json
@@ -1,4 +1,9 @@
{
+ "/api/analysis": {
+ "target": "http://localhost:8087",
+ "secure": false,
+ "changeOrigin": true
+ },
"/api/tournament": {
"target": "http://localhost:8089",
"secure": false,
diff --git a/public/env.template.js b/public/env.template.js
index 72477ac..165a040 100644
--- a/public/env.template.js
+++ b/public/env.template.js
@@ -1,4 +1,4 @@
window.__RUNTIME_CONFIG__ = {
- API_URL: "${API_URL}",
- WEBSOCKET_URL: "${WEBSOCKET_URL}"
+ API_URL: '${API_URL}',
+ WEBSOCKET_URL: '${WEBSOCKET_URL}',
};
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index 54f7663..63b01eb 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -6,6 +6,7 @@ import { ChallengesComponent } from './pages/challenges/challenges.component';
import { GamesComponent } from './pages/games/games.component';
import { TournamentsComponent } from './pages/tournaments/tournaments.component';
import { BotsComponent } from './pages/bots/bots.component';
+import { AnalysisComponent } from './pages/analysis/analysis.component';
export const routes: Routes = [
{ path: '', component: WelcomeComponent },
@@ -14,6 +15,7 @@ export const routes: Routes = [
{ path: 'challenges', component: ChallengesComponent },
{ path: 'tournaments', component: TournamentsComponent },
{ path: 'bots', component: BotsComponent },
+ { path: 'analysis', component: AnalysisComponent },
{ path: 'game/:gameId', component: GameComponent },
- { path: '**', redirectTo: '' }
+ { path: '**', redirectTo: '' },
];
diff --git a/src/app/app.spec.ts b/src/app/app.spec.ts
index 2f546f6..91e7669 100644
--- a/src/app/app.spec.ts
+++ b/src/app/app.spec.ts
@@ -1,12 +1,13 @@
import { TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
+import { provideHttpClient } from '@angular/common/http';
import { App } from './app';
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
- providers: [provideRouter([])]
+ providers: [provideRouter([]), provideHttpClient()],
}).compileComponents();
});
diff --git a/src/app/components/annotated-move-list/annotated-move-list.component.css b/src/app/components/annotated-move-list/annotated-move-list.component.css
new file mode 100644
index 0000000..6062a45
--- /dev/null
+++ b/src/app/components/annotated-move-list/annotated-move-list.component.css
@@ -0,0 +1,108 @@
+:host {
+ display: block;
+}
+
+.empty {
+ font-size: 11px;
+ color: rgba(255, 255, 255, 0.4);
+ padding: 12px 16px;
+ font-family: var(--nc-mono, monospace);
+ letter-spacing: 0.06em;
+}
+
+.move-grid {
+ display: grid;
+ grid-template-columns: 28px 1fr 1fr;
+ gap: 2px 4px;
+ padding: 8px 12px;
+ font-family: var(--nc-mono, monospace);
+ font-size: 12px;
+}
+
+.mv-num {
+ color: rgba(255, 255, 255, 0.3);
+ font-size: 10px;
+ display: flex;
+ align-items: center;
+ letter-spacing: 0.04em;
+}
+
+.mv {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 8px;
+ cursor: pointer;
+ border-radius: 2px;
+ color: rgba(255, 255, 255, 0.8);
+ transition:
+ background 0.12s,
+ color 0.12s;
+}
+
+.mv:hover {
+ background: rgba(255, 255, 255, 0.06);
+ color: #fff;
+}
+
+.mv.active {
+ background: rgba(255, 69, 200, 0.18);
+ color: #fff;
+}
+
+.mv-empty {
+ cursor: default;
+ color: rgba(255, 255, 255, 0.25);
+}
+
+.mv-empty:hover {
+ background: transparent;
+ color: rgba(255, 255, 255, 0.25);
+}
+
+.mv-san {
+ flex: 1;
+}
+
+.mv-placeholder {
+ opacity: 0.4;
+}
+
+/* Quality badges */
+.mv-badge {
+ font-size: 10px;
+ font-weight: 700;
+ padding: 1px 4px;
+ border-radius: 2px;
+ flex-shrink: 0;
+}
+
+.q-brilliant .mv-badge,
+.mv-badge.q-brilliant {
+ color: #5ee5a1;
+ background: rgba(94, 229, 161, 0.15);
+}
+
+.q-best .mv-badge,
+.mv-badge.q-best {
+ color: #5ee5a1;
+ background: rgba(94, 229, 161, 0.1);
+}
+
+.q-inaccuracy .mv-badge,
+.mv-badge.q-inaccuracy {
+ color: #ffb13a;
+ background: rgba(255, 177, 58, 0.15);
+}
+
+.q-mistake .mv-badge,
+.mv-badge.q-mistake {
+ color: #ff7a7a;
+ background: rgba(255, 122, 122, 0.15);
+}
+
+.q-blunder .mv-badge,
+.mv-badge.q-blunder {
+ color: #ff4444;
+ background: rgba(255, 68, 68, 0.18);
+}
diff --git a/src/app/components/annotated-move-list/annotated-move-list.component.html b/src/app/components/annotated-move-list/annotated-move-list.component.html
new file mode 100644
index 0000000..111aa14
--- /dev/null
+++ b/src/app/components/annotated-move-list/annotated-move-list.component.html
@@ -0,0 +1,48 @@
+@if (moves.length === 0) {
+
No annotated moves yet.
+} @else {
+
+ @for (pair of pairs; track $index) {
+
{{ $index + 1 }}
+
+
+ {{ pair.white?.san ?? '' }}
+ @if (qualityLabel(pair.white)) {
+ {{
+ qualityLabel(pair.white)
+ }}
+ }
+
+
+
+ @if (pair.black) {
+ {{ pair.black.san }}
+ @if (qualityLabel(pair.black)) {
+ {{
+ qualityLabel(pair.black)
+ }}
+ }
+ } @else {
+ …
+ }
+
+ }
+
+}
diff --git a/src/app/components/annotated-move-list/annotated-move-list.component.ts b/src/app/components/annotated-move-list/annotated-move-list.component.ts
new file mode 100644
index 0000000..b7d7a66
--- /dev/null
+++ b/src/app/components/annotated-move-list/annotated-move-list.component.ts
@@ -0,0 +1,83 @@
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { AnnotatedMove, MoveQuality } from '../../models/analysis.models';
+
+interface AnnotatedPair {
+ white: AnnotatedMove | null;
+ black: AnnotatedMove | null;
+}
+
+const QUALITY_LABELS: Record = {
+ brilliant: '!!',
+ best: '!',
+ good: '',
+ inaccuracy: '?!',
+ mistake: '?',
+ blunder: '??',
+};
+
+const QUALITY_CLASSES: Record = {
+ brilliant: 'q-brilliant',
+ best: 'q-best',
+ good: 'q-good',
+ inaccuracy: 'q-inaccuracy',
+ mistake: 'q-mistake',
+ blunder: 'q-blunder',
+};
+
+@Component({
+ selector: 'app-annotated-move-list',
+ standalone: true,
+ imports: [],
+ templateUrl: './annotated-move-list.component.html',
+ styleUrl: './annotated-move-list.component.css',
+})
+export class AnnotatedMoveListComponent {
+ @Input({ required: true }) moves: AnnotatedMove[] = [];
+ @Input() activePly: number | null = null;
+ @Output() plySelected = new EventEmitter();
+
+ get pairs(): AnnotatedPair[] {
+ const result: AnnotatedPair[] = [];
+ for (let i = 0; i < this.moves.length; i += 2) {
+ result.push({
+ white: this.moves[i] ?? null,
+ black: this.moves[i + 1] ?? null,
+ });
+ }
+ return result;
+ }
+
+ qualityLabel(move: AnnotatedMove | null): string {
+ if (!move?.quality) return '';
+ return QUALITY_LABELS[move.quality] ?? '';
+ }
+
+ qualityClass(move: AnnotatedMove | null): string {
+ if (!move?.quality) return '';
+ return QUALITY_CLASSES[move.quality] ?? '';
+ }
+
+ isWhiteActive(pairIndex: number): boolean {
+ return this.activePly === pairIndex * 2;
+ }
+
+ isBlackActive(pairIndex: number): boolean {
+ return this.activePly === pairIndex * 2 + 1;
+ }
+
+ selectWhite(pairIndex: number): void {
+ this.plySelected.emit(pairIndex * 2);
+ }
+
+ selectBlack(pairIndex: number, black: AnnotatedMove | null): void {
+ if (!black) return;
+ this.plySelected.emit(pairIndex * 2 + 1);
+ }
+
+ formatEval(move: AnnotatedMove | null): string {
+ if (!move || move.evalAfter === null) return '';
+ const v = move.evalAfter;
+ const sign = v > 0 ? '+' : '';
+ return `${sign}${v.toFixed(2)}`;
+ }
+}
diff --git a/src/app/components/eval-timeline/eval-timeline.component.css b/src/app/components/eval-timeline/eval-timeline.component.css
new file mode 100644
index 0000000..fc29110
--- /dev/null
+++ b/src/app/components/eval-timeline/eval-timeline.component.css
@@ -0,0 +1,53 @@
+:host {
+ display: block;
+ width: 100%;
+}
+
+.timeline-wrap {
+ width: 100%;
+ overflow: hidden;
+}
+
+.timeline-svg {
+ display: block;
+ width: 100%;
+ height: 80px;
+}
+
+.midline {
+ stroke: rgba(255, 255, 255, 0.12);
+ stroke-width: 1;
+ stroke-dasharray: 4 4;
+}
+
+.area-white {
+ fill: rgba(255, 255, 255, 0.18);
+}
+
+.area-black {
+ fill: rgba(20, 20, 30, 0.55);
+}
+
+.eval-line {
+ fill: none;
+ stroke: var(--nc-neon, #ff45c8);
+ stroke-width: 1.5;
+ stroke-linejoin: round;
+ stroke-linecap: round;
+}
+
+.active-marker {
+ stroke: var(--nc-warning, #ffb13a);
+ stroke-width: 1.5;
+ stroke-dasharray: 3 3;
+ opacity: 0.8;
+}
+
+.empty {
+ font-size: 11px;
+ color: rgba(255, 255, 255, 0.4);
+ padding: 12px 0;
+ font-family: var(--nc-mono, monospace);
+ letter-spacing: 0.06em;
+ text-align: center;
+}
diff --git a/src/app/components/eval-timeline/eval-timeline.component.html b/src/app/components/eval-timeline/eval-timeline.component.html
new file mode 100644
index 0000000..1589d21
--- /dev/null
+++ b/src/app/components/eval-timeline/eval-timeline.component.html
@@ -0,0 +1,53 @@
+@if (points.length === 0) {
+ No evaluation data yet.
+} @else {
+
+
+
+}
diff --git a/src/app/components/eval-timeline/eval-timeline.component.ts b/src/app/components/eval-timeline/eval-timeline.component.ts
new file mode 100644
index 0000000..9099dd9
--- /dev/null
+++ b/src/app/components/eval-timeline/eval-timeline.component.ts
@@ -0,0 +1,73 @@
+import { Component, Input, OnChanges } from '@angular/core';
+import { AnnotatedMove } from '../../models/analysis.models';
+
+interface TimelinePoint {
+ x: number;
+ y: number;
+ eval: number;
+ san: string;
+ plyIndex: number;
+}
+
+const CLAMP = 5; // clamp eval to ±5 pawns for display
+const HEIGHT = 80;
+const WIDTH = 600;
+
+@Component({
+ selector: 'app-eval-timeline',
+ standalone: true,
+ imports: [],
+ templateUrl: './eval-timeline.component.html',
+ styleUrl: './eval-timeline.component.css',
+})
+export class EvalTimelineComponent implements OnChanges {
+ @Input({ required: true }) moves: AnnotatedMove[] = [];
+ @Input() activePly: number | null = null;
+
+ points: TimelinePoint[] = [];
+ evalPolyline = '';
+ polylineWhite = '';
+ polylineBlack = '';
+ svgWidth = WIDTH;
+ svgHeight = HEIGHT;
+
+ ngOnChanges(): void {
+ this.buildChart();
+ }
+
+ activeX(): number | null {
+ if (this.activePly === null) return null;
+ const pt = this.points[this.activePly];
+ return pt ? pt.x : null;
+ }
+
+ private buildChart(): void {
+ if (this.moves.length === 0) {
+ this.points = [];
+ this.evalPolyline = '';
+ this.polylineWhite = '';
+ this.polylineBlack = '';
+ return;
+ }
+
+ const total = this.moves.length;
+ this.points = this.moves.map((m, i) => {
+ const evalValue = m.evalAfter ?? 0;
+ const clamped = Math.max(-CLAMP, Math.min(CLAMP, evalValue));
+ const x = (i / Math.max(total - 1, 1)) * WIDTH;
+ // y=0 => white winning (top), y=HEIGHT => black winning (bottom)
+ const y = ((CLAMP - clamped) / (CLAMP * 2)) * HEIGHT;
+ return { x, y, eval: evalValue, san: m.san, plyIndex: i };
+ });
+
+ const coordStr = this.points.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ');
+ this.evalPolyline = coordStr;
+
+ const mid = HEIGHT / 2;
+ const first = this.points[0];
+ const last = this.points[this.points.length - 1];
+
+ this.polylineWhite = `${first.x.toFixed(1)},${mid} ${coordStr} ${last.x.toFixed(1)},${mid}`;
+ this.polylineBlack = this.polylineWhite;
+ }
+}
diff --git a/src/app/components/toolbar/toolbar.component.html b/src/app/components/toolbar/toolbar.component.html
index 8522bd5..d859706 100644
--- a/src/app/components/toolbar/toolbar.component.html
+++ b/src/app/components/toolbar/toolbar.component.html
@@ -1,5 +1,4 @@