Compare commits

..

1 Commits

Author SHA1 Message Date
Janis Eccarius b9998b6710 feat: NCWF-5/6/7/8/9 chess analysis page and engine integration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 22:28:30 +02:00
25 changed files with 482 additions and 669 deletions
+5
View File
@@ -19,6 +19,7 @@ If `$ARGUMENTS` already answers some of these, skip those questions.
## Step 2 — Research (if needed) ## Step 2 — Research (if needed)
If the bug involves component logic, a service, or routing: 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}`. - 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. - Check `.spec.ts` files for existing coverage of the broken area.
- Do NOT guess at root cause. Surface findings before drafting. - Do NOT guess at root cause. Surface findings before drafting.
@@ -56,6 +57,7 @@ Environment / Notes
``` ```
Rules: Rules:
- Steps must be minimal and reproducible. - Steps must be minimal and reproducible.
- Expected vs actual: concrete and unambiguous. - Expected vs actual: concrete and unambiguous.
- Omit "Environment / Notes" section if not relevant. - Omit "Environment / Notes" section if not relevant.
@@ -64,6 +66,7 @@ Rules:
Show the draft to the user. Show the draft to the user.
**Use `AskUserQuestion` tool to ask:** **Use `AskUserQuestion` tool to ask:**
- Are steps to reproduce complete and accurate? - Are steps to reproduce complete and accurate?
- Severity: Blocker / Critical / Major / Minor / Trivial? - Severity: Blocker / Critical / Major / Minor / Trivial?
- Any related tickets or recent changes to link? - Any related tickets or recent changes to link?
@@ -73,6 +76,7 @@ Incorporate feedback. Repeat until user approves.
## Step 5 — Determine Project ## Step 5 — Determine Project
> **Project routing rules (always apply these):** > **Project routing rules (always apply these):**
>
> - Frontend code (UI, UX, web app, components, services) → `NCWF` > - Frontend code (UI, UX, web app, components, services) → `NCWF`
> - Backend code (game engine, bots, API, services, coordinator) → `NCS` > - Backend code (game engine, bots, API, services, coordinator) → `NCS`
> - Infrastructure (Kubernetes, pipelines, CI/CD, DB setup, cloud infra) → `NCI` > - Infrastructure (Kubernetes, pipelines, CI/CD, DB setup, cloud infra) → `NCI`
@@ -86,6 +90,7 @@ If ambiguous, ask the user.
## Step 6 — Create Issue ## Step 6 — Create Issue
Call `mcp__youtrack__create_issue` with: Call `mcp__youtrack__create_issue` with:
- `project`: determined in Step 5 - `project`: determined in Step 5
- `summary`: concise title describing what is broken (≤72 chars, sentence case) - `summary`: concise title describing what is broken (≤72 chars, sentence case)
- `description`: full formatted defect report from Step 3 (Markdown) - `description`: full formatted defect report from Step 3 (Markdown)
+6 -1
View File
@@ -19,6 +19,7 @@ If `$ARGUMENTS` already answers some of these, skip those questions.
## Step 2 — Research (if needed) ## Step 2 — Research (if needed)
If the topic involves unfamiliar UI flows, component structure, or technical constraints: If the topic involves unfamiliar UI flows, component structure, or technical constraints:
- Search the repo for relevant code under `src/app/` (use `Grep`/`Bash`). - Search the repo for relevant code under `src/app/` (use `Grep`/`Bash`).
- Use `WebSearch` if the topic involves Angular APIs, external standards or protocols. - Use `WebSearch` if the topic involves Angular APIs, external standards or protocols.
- Do NOT guess. Surface findings before drafting. - Do NOT guess. Surface findings before drafting.
@@ -53,6 +54,7 @@ Implementation Notes
``` ```
Rules: Rules:
- User story line: plain English, present tense, from user's perspective. - User story line: plain English, present tense, from user's perspective.
- Acceptance criteria: testable, unambiguous, one condition each. - Acceptance criteria: testable, unambiguous, one condition each.
- Implementation notes: optional — only include if there are known constraints, related tickets, or design refs. - 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. Show the draft to the user.
**Use `AskUserQuestion` tool to ask:** **Use `AskUserQuestion` tool to ask:**
- Are the acceptance criteria complete and correct? - Are the acceptance criteria complete and correct?
- Any implementation constraints to add? - Any implementation constraints to add?
- Priority (if known)? - Priority (if known)?
@@ -70,6 +73,7 @@ Incorporate feedback. Repeat until user approves.
## Step 5 — Determine Project ## Step 5 — Determine Project
> **Project routing rules (always apply these):** > **Project routing rules (always apply these):**
>
> - Frontend code (UI, UX, web app, components, services) → `NCWF` > - Frontend code (UI, UX, web app, components, services) → `NCWF`
> - Backend code (game engine, bots, API, services, coordinator) → `NCS` > - Backend code (game engine, bots, API, services, coordinator) → `NCS`
> - Infrastructure (Kubernetes, pipelines, CI/CD, DB setup, cloud infra) → `NCI` > - Infrastructure (Kubernetes, pipelines, CI/CD, DB setup, cloud infra) → `NCI`
@@ -83,6 +87,7 @@ If still ambiguous, ask the user.
## Step 6 — Create Issue ## Step 6 — Create Issue
Call `mcp__youtrack__create_issue` with: Call `mcp__youtrack__create_issue` with:
- `project`: determined in Step 5 - `project`: determined in Step 5
- `summary`: concise title derived from the "I want to" clause (≤72 chars, sentence case) - `summary`: concise title derived from the "I want to" clause (≤72 chars, sentence case)
- `description`: full formatted story from Step 3 (Markdown) - `description`: full formatted story from Step 3 (Markdown)
@@ -97,7 +102,7 @@ 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`: Collect any issue IDs the user mentions. For each, determine the correct relation and call `mcp__youtrack__link_issues`:
| Situation | Relation to use | | Situation | Relation to use |
|-----------|----------------| | --------------------------------------- | --------------- |
| This story must be done before another | `blocks` | | This story must be done before another | `blocks` |
| Another story must be done before this | `is blocked by` | | Another story must be done before this | `is blocked by` |
| Stories share domain or are related | `relates to` | | Stories share domain or are related | `relates to` |
+9 -1
View File
@@ -7,10 +7,12 @@ This is the **NowChess-Frontend** repo. Sprint mode defaults to project `NCWF`.
## Step 1 — Determine Scope ## Step 1 — Determine Scope
**Single-issue mode** (`$ARGUMENTS` set): **Single-issue mode** (`$ARGUMENTS` set):
- Call `mcp__youtrack__get_issue` on `$ARGUMENTS`. - Call `mcp__youtrack__get_issue` on `$ARGUMENTS`.
- Proceed with that issue only. - Proceed with that issue only.
**Sprint mode** (`$ARGUMENTS` empty): **Sprint mode** (`$ARGUMENTS` empty):
- Call `mcp__youtrack__search_issues` with query `project: NCWF Sprints: {current sprint} #Unresolved`. - 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}`. - If query returns 0 results, use `AskUserQuestion` to ask for the sprint name, then retry with `project: NCWF Sprints: {name}`.
- Collect all returned issues. - 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 ## Step 2 — Build Issue Tree
For each top-level issue from Step 1: 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. 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). 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. 3. Recursively fetch subtasks until all leaves are known.
@@ -29,12 +32,13 @@ For each top-level issue from Step 1:
## Step 3 — Estimate Leaf Nodes ## Step 3 — Estimate Leaf Nodes
For each leaf node: For each leaf node:
1. Read: summary, description, acceptance criteria, implementation notes. 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. 2. If scope is unclear, search codebase (`Grep`/`Bash`) under `src/app/` for related files to gauge complexity.
3. Assign estimate using this scale: 3. Assign estimate using this scale:
| Size | Criteria | Estimate | | Size | Criteria | Estimate |
|------|----------|----------| | ------- | ------------------------------------------------------ | -------- |
| Trivial | Style tweak, copy change, 1-file tweak | 30m | | Trivial | Style tweak, copy change, 1-file tweak | 30m |
| Small | 13 files, single component/service, no unknowns | 1h2h | | Small | 13 files, single component/service, no unknowns | 1h2h |
| Medium | 36 files, new component + service wiring, some design | 3h5h | | Medium | 36 files, new component + service wiring, some design | 3h5h |
@@ -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**. YouTrack auto-sums `Zeitschätzung` from subtasks up to parents — **do not write estimates to parent nodes**.
Compute display-only rolled-up totals: Compute display-only rolled-up totals:
- Parent total = sum of all descendant leaf estimates (including pre-estimated ones). - Parent total = sum of all descendant leaf estimates (including pre-estimated ones).
- Flag any branch where some leaves are missing estimates (partial roll-up). - 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. Legend: `[X]` = display-only roll-up (not written). Plain = will be written to YouTrack.
If sprint mode, show grand total at bottom: If sprint mode, show grand total at bottom:
``` ```
Sprint total: Xd Yh Zm (N issues, M leaves to update) Sprint total: Xd Yh Zm (N issues, M leaves to update)
``` ```
**Use `AskUserQuestion` tool:** **Use `AskUserQuestion` tool:**
- Does the breakdown look right? - Does the breakdown look right?
- Any estimates to adjust before writing to YouTrack? - Any estimates to adjust before writing to YouTrack?
@@ -81,6 +88,7 @@ Incorporate all feedback before proceeding.
## Step 6 — Write Estimates ## Step 6 — Write Estimates
On user approval, write estimates **only to leaf nodes** (bottom-up order): 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. - 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"`. - YouTrack period format: `"30m"`, `"1h 30m"`, `"1d"`, `"2d 4h"`.
- Skip leaves already pre-estimated. - Skip leaves already pre-estimated.
+6 -2
View File
@@ -3,6 +3,7 @@
Automated defect-fix workflow. Ticket ID: `$ARGUMENTS` Automated defect-fix workflow. Ticket ID: `$ARGUMENTS`
This is the **NowChess-Frontend** repo (Angular 20 / TypeScript). Gates: This is the **NowChess-Frontend** repo (Angular 20 / TypeScript). Gates:
- Build: `npm run build` - Build: `npm run build`
- Test: `npm test -- --watch=false --browsers=ChromeHeadless` - Test: `npm test -- --watch=false --browsers=ChromeHeadless`
- Format: `npx prettier --write .` (check with `npx prettier --check .`) - 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 ## Step 2 — Create Worktree
Derive branch name from ticket: Derive branch name from ticket:
- `type` from YouTrack issue type: `bug``fix`, `feature`/`task``feat`, `refactor``refactor`, else `chore` - `type` from YouTrack issue type: `bug``fix`, `feature`/`task``feat`, `refactor``refactor`, else `chore`
- `scope` from affected area (kebab-case, omit if unclear) - `scope` from affected area (kebab-case, omit if unclear)
- `description` from ticket summary: lowercase, kebab-case, max 40 chars, drop articles - `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. **Complex** (3+ files, multiple concerns, or estimated > 1 hour): create subtasks before coding.
To create subtasks: 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"). 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: 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. - `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. 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. - 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.** - **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 ## Step 5 — Review
@@ -76,6 +79,7 @@ Display findings grouped by severity.
## Step 5b — Apply Review Findings ## Step 5b — Apply Review Findings
If the review produced any findings (any severity): If the review produced any findings (any severity):
1. Implement all agreed fixes. 1. Implement all agreed fixes.
2. Run `npm run build` — must be green. 2. Run `npm run build` — must be green.
3. Run `npm test -- --watch=false --browsers=ChromeHeadless` — must be green. 3. Run `npm test -- --watch=false --browsers=ChromeHeadless` — must be green.
@@ -126,7 +130,7 @@ Files changed:
After commenting, ask the user if `$ARGUMENTS` should be linked to any other issues not already linked: After commenting, ask the user if `$ARGUMENTS` should be linked to any other issues not already linked:
| Situation | Relation | | Situation | Relation |
|-----------|---------| | ----------------------------------- | --------------- |
| This fix blocks another open ticket | `blocks` | | This fix blocks another open ticket | `blocks` |
| Another ticket must ship first | `is blocked by` | | Another ticket must ship first | `is blocked by` |
| Related defect or story | `relates to` | | Related defect or story | `relates to` |
+5
View File
@@ -4,6 +4,7 @@ Automated feature-implementation workflow. Ticket ID: `$ARGUMENTS`
This is the **NowChess-Frontend** repo (Angular 20 / TypeScript). In-project = This is the **NowChess-Frontend** repo (Angular 20 / TypeScript). In-project =
`NCWF`. Gates: `NCWF`. Gates:
- Build: `npm run build` - Build: `npm run build`
- Test: `npm test -- --watch=false --browsers=ChromeHeadless` - Test: `npm test -- --watch=false --browsers=ChromeHeadless`
- Format: `npx prettier --write .` (check with `npx prettier --check .`) - 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 ## Step 3 — Create Worktree
Derive branch name from the root ticket `$ARGUMENTS`: Derive branch name from the root ticket `$ARGUMENTS`:
- `type` from YouTrack issue type: `feature`/`task``feat`, `refactor``refactor`, `bug``fix`, else `chore` - `type` from YouTrack issue type: `feature`/`task``feat`, `refactor``refactor`, `bug``fix`, else `chore`
- `scope` from affected area (kebab-case, omit if unclear) - `scope` from affected area (kebab-case, omit if unclear)
- `description` from ticket summary: lowercase, kebab-case, max 40 chars, drop articles - `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 ## Step 6b — Apply Review Findings
If the review produced any findings (any severity): If the review produced any findings (any severity):
1. Implement all agreed fixes. 1. Implement all agreed fixes.
2. Run `npm run build` — must be green. 2. Run `npm run build` — must be green.
3. Run `npm test -- --watch=false --browsers=ChromeHeadless` — 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: Final report to the user, in two sections:
### Blocked in-project tasks ### Blocked in-project tasks
List any `NCWF` tasks that could **not** be implemented, with the blocker(s) 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.) that stopped them. (These can be re-run with this command once blockers clear.)
### Cross-project tasks (NCS / NCI / other) ### Cross-project tasks (NCS / NCI / other)
For every out-of-project task discovered in the tree (whether it was a subtask For every out-of-project task discovered in the tree (whether it was a subtask
or a blocker), output one entry: or a blocker), output one entry:
+7 -1
View File
@@ -12,6 +12,7 @@ Extract and display: summary, description, acceptance criteria, implementation n
## Step 2 — Research (if needed) ## Step 2 — Research (if needed)
If the story involves unfamiliar UI flows or technical constraints: If the story involves unfamiliar UI flows or technical constraints:
- Search repo for relevant code under `src/app/` (`Grep`/`Bash`). - Search repo for relevant code under `src/app/` (`Grep`/`Bash`).
- Use `WebSearch` for Angular APIs, external standards or protocols. - Use `WebSearch` for Angular APIs, external standards or protocols.
- Do NOT guess. Surface findings before proposing splits. - 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 ## Step 3 — Propose Split
Analyse the story and propose a set of subtasks. Rules: Analyse the story and propose a set of subtasks. Rules:
- Each subtask = one unit of work, completable independently or in sequence. - Each subtask = one unit of work, completable independently or in sequence.
- No subtask should exceed ~2 days of work. - No subtask should exceed ~2 days of work.
- Name subtasks in imperative mood (e.g. "Add board theme service", "Render theme picker component"). - 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: Show proposed subtask list to user (titles only) and ask:
**Use `AskUserQuestion` tool:** **Use `AskUserQuestion` tool:**
- Does the split look right? - Does the split look right?
- Any subtasks to add, remove, or merge? - Any subtasks to add, remove, or merge?
- Should any subtask be assigned to a specific person? - 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: Rules:
- Steps/Tasks: concrete, ordered where order matters. - 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). - 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. - 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 ## Step 6 — Create Subtasks
For each subtask call `mcp__youtrack__create_issue` with: For each subtask call `mcp__youtrack__create_issue` with:
- `project`: from Step 5 - `project`: from Step 5
- `summary`: subtask title (≤72 chars, sentence case) - `summary`: subtask title (≤72 chars, sentence case)
- `description`: full formatted description from Step 4 (Markdown) - `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 ## Step 6b — Inter-Subtask Links
If subtasks must be done in sequence (one depends on output of another), add ordering 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). - 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: Ask the user to confirm sequencing before adding these links:
@@ -95,7 +101,7 @@ 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: Scan `$ARGUMENTS` description and implementation notes for any referenced issue IDs not already linked. For each:
| Situation | Relation | | Situation | Relation |
|-----------|---------| | ---------------------------------------- | --------------- |
| Parent story blocks another epic/story | `blocks` | | Parent story blocks another epic/story | `blocks` |
| Story depends on another epic completing | `is blocked by` | | Story depends on another epic completing | `is blocked by` |
| Related story in same domain | `relates to` | | Related story in same domain | `relates to` |
+40 -31
View File
@@ -1,68 +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) ## 0.0.0 (2026-05-12)
### Features ### Features
* added bot, light and dark mode ([2de003e](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/2de003e497baee72f998d0d805ca1e58aababe48)) - 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)) - 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-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)) - 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 ### Bug Fixes
* build issues ([36d72fd](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/36d72fd6cda41be51d28f8ac307dcdbcd31afa91)) - 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)) - 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 ([4da882f](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/4da882f481ba7a008aac868fb37de7cb2bafea5d))
* GITIGNORE ([8df2d05](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/8df2d0550ab17c9afb2d19c414eac700a75add02)) - GITIGNORE ([8df2d05](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/8df2d0550ab17c9afb2d19c414eac700a75add02))
* npm ([c11c1d4](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/c11c1d4dce9de4bd5b463e891eebf961b37feb04)) - 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 .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)) - 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)) - 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)) - 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) ## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.1.0...0.0.0) (2026-05-12)
### Features ### 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) ## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.0...0.0.0) (2026-05-12)
### Bug Fixes ### 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) ## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.1...0.0.0) (2026-05-12)
### Bug Fixes ### 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) ## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.2...0.0.0) (2026-05-14)
### Bug Fixes ### 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) ## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.3...0.0.0) (2026-05-15)
### Bug Fixes ### Bug Fixes
* build error ([51a363a](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/51a363a2432be111b804082df362975047dc8080)) - 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)) - 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.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) ## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.5...0.0.0) (2026-06-02)
### Bug Fixes ### 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.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) ## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.7...0.0.0) (2026-06-10)
### Bug Fixes ### 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))
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.2.8...0.0.0) (2026-06-10)
### Features
* bots ([#9](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/9)) ([48959da](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/48959daae36e709ea7782ca04fdde699854f8e66))
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.3.0...0.0.0) (2026-06-17)
### Features
* NCWF-5/6/7/8/9 chess analysis page and engine integration ([#11](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/11)) ([f9420e5](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/f9420e5848d8724bcb0e9cf08f08b871c91cf4ba))
+4 -13
View File
@@ -18,9 +18,7 @@
"builder": "@angular/build:application", "builder": "@angular/build:application",
"options": { "options": {
"browser": "src/main.ts", "browser": "src/main.ts",
"polyfills": [ "polyfills": ["zone.js"],
"zone.js"
],
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"assets": [ "assets": [
{ {
@@ -38,9 +36,7 @@
"output": "/assets/ChessAssets" "output": "/assets/ChessAssets"
} }
], ],
"styles": [ "styles": ["src/styles.css"]
"src/styles.css"
]
}, },
"configurations": { "configurations": {
"production": { "production": {
@@ -106,10 +102,7 @@
"test": { "test": {
"builder": "@angular/build:karma", "builder": "@angular/build:karma",
"options": { "options": {
"polyfills": [ "polyfills": ["zone.js", "zone.js/testing"],
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json", "tsConfig": "tsconfig.spec.json",
"assets": [ "assets": [
{ {
@@ -127,9 +120,7 @@
"output": "/assets/ChessAssets" "output": "/assets/ChessAssets"
} }
], ],
"styles": [ "styles": ["src/styles.css"]
"src/styles.css"
]
} }
} }
} }
+2 -5
View File
@@ -55,7 +55,6 @@ tags:
description: Export a game as FEN or PGN description: Export a game as FEN or PGN
paths: paths:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Game lifecycle # Game lifecycle
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -467,7 +466,6 @@ paths:
# ============================================================================= # =============================================================================
components: components:
securitySchemes: securitySchemes:
bearerAuth: bearerAuth:
type: http type: http
@@ -517,7 +515,6 @@ components:
$ref: '#/components/schemas/ApiError' $ref: '#/components/schemas/ApiError'
schemas: schemas:
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Requests # Requests
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
@@ -551,7 +548,7 @@ components:
pgn: pgn:
type: string type: string
description: PGN text (headers and move list) description: PGN text (headers and move list)
example: "1. e4 e5 2. Nf3 Nc6 *" example: '1. e4 e5 2. Nf3 Nc6 *'
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# Game state # Game state
@@ -587,7 +584,7 @@ components:
pgn: pgn:
type: string type: string
description: PGN move text for the full game so far description: PGN move text for the full game so far
example: "1. e4" example: '1. e4'
turn: turn:
type: string type: string
enum: [white, black] enum: [white, black]
+2 -2
View File
@@ -1,4 +1,4 @@
window.__RUNTIME_CONFIG__ = { window.__RUNTIME_CONFIG__ = {
API_URL: "${API_URL}", API_URL: '${API_URL}',
WEBSOCKET_URL: "${WEBSOCKET_URL}" WEBSOCKET_URL: '${WEBSOCKET_URL}',
}; };
+1 -1
View File
@@ -17,5 +17,5 @@ export const routes: Routes = [
{ path: 'bots', component: BotsComponent }, { path: 'bots', component: BotsComponent },
{ path: 'analysis', component: AnalysisComponent }, { path: 'analysis', component: AnalysisComponent },
{ path: 'game/:gameId', component: GameComponent }, { path: 'game/:gameId', component: GameComponent },
{ path: '**', redirectTo: '' } { path: '**', redirectTo: '' },
]; ];
+1 -1
View File
@@ -7,7 +7,7 @@ describe('App', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [App], imports: [App],
providers: [provideRouter([]), provideHttpClient()] providers: [provideRouter([]), provideHttpClient()],
}).compileComponents(); }).compileComponents();
}); });
+161 -49
View File
@@ -1,5 +1,4 @@
<nav class="nc-nav"> <nav class="nc-nav">
<!-- Logo --> <!-- Logo -->
<div class="nc-logo" (click)="goToHome()" role="button" tabindex="0"> <div class="nc-logo" (click)="goToHome()" role="button" tabindex="0">
<div class="nc-logo-mark"></div> <div class="nc-logo-mark"></div>
@@ -10,8 +9,16 @@
@if (currentUser) { @if (currentUser) {
<div class="nc-links"> <div class="nc-links">
<button type="button" class="nc-link"> <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
stroke-linecap="round" stroke-linejoin="round"> width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.7"
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" />
<circle cx="12" cy="12" r="3" /> <circle cx="12" cy="12" r="3" />
</svg> </svg>
@@ -25,15 +32,26 @@
<!-- Right cluster --> <!-- Right cluster -->
<div class="nc-right"> <div class="nc-right">
@if (currentUser; as user) { @if (currentUser; as user) {
<!-- Notifications bell --> <!-- Notifications bell -->
<div class="nc-dropdown-wrap" data-dropdown="notif"> <div class="nc-dropdown-wrap" data-dropdown="notif">
<button type="button" class="nc-bell" [class.is-open]="notifOpen" (click)="toggleNotif($event)" <button
aria-label="Notifications"> type="button"
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="nc-bell"
stroke-linecap="round" stroke-linejoin="round"> [class.is-open]="notifOpen"
(click)="toggleNotif($event)"
aria-label="Notifications"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" /> <path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" />
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" /> <path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
</svg> </svg>
@@ -55,11 +73,21 @@
@for (challenge of incomingChallenges; track challenge.id) { @for (challenge of incomingChallenges; track challenge.id) {
<div class="nc-notif-row is-unread"> <div class="nc-notif-row is-unread">
<div class="nc-notif-icon"> <div class="nc-notif-icon">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" <svg
stroke-linecap="round" stroke-linejoin="round"> width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="14.5" y1="17.5" x2="3" y2="6" /> <line x1="14.5" y1="17.5" x2="3" y2="6" />
<path d="M13 19l6 -6" /><path d="M16 16l4 4" /> <path d="M13 19l6 -6" />
<path d="M19 21l2 -2" /><path d="M15 5l4 4" /> <path d="M16 16l4 4" />
<path d="M19 21l2 -2" />
<path d="M15 5l4 4" />
<path d="M21 3l-3 1l-4 4" /> <path d="M21 3l-3 1l-4 4" />
</svg> </svg>
</div> </div>
@@ -69,24 +97,49 @@
{{ getTimeControlDisplay(challenge) }} game. {{ getTimeControlDisplay(challenge) }} game.
</div> </div>
<div class="nc-notif-meta"> <div class="nc-notif-meta">
{{ challenge.challenger.rating }} · {{ challenge.timeControl.type ?? 'Custom' }} · {{ getExpirationInfo(challenge) }} {{ challenge.challenger.rating }} ·
{{ challenge.timeControl.type ?? 'Custom' }} ·
{{ getExpirationInfo(challenge) }}
</div> </div>
<div class="nc-notif-actions"> <div class="nc-notif-actions">
<button type="button" class="nc-btn-accept" <button
type="button"
class="nc-btn-accept"
[disabled]="acceptingId === challenge.id || !!decliningId" [disabled]="acceptingId === challenge.id || !!decliningId"
(click)="acceptChallenge($event, challenge)"> (click)="acceptChallenge($event, challenge)"
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" >
stroke-linecap="round" stroke-linejoin="round"> <svg
width="11"
height="11"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.4"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12" /> <polyline points="20 6 9 17 4 12" />
</svg> </svg>
{{ acceptingId === challenge.id ? '...' : 'Accept' }} {{ acceptingId === challenge.id ? '...' : 'Accept' }}
</button> </button>
<button type="button" class="nc-btn-decline" <button
type="button"
class="nc-btn-decline"
[disabled]="!!acceptingId || decliningId === challenge.id" [disabled]="!!acceptingId || decliningId === challenge.id"
(click)="declineChallenge($event, challenge)"> (click)="declineChallenge($event, challenge)"
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" >
stroke-linecap="round" stroke-linejoin="round"> <svg
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /> width="10"
height="10"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.4"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg> </svg>
{{ decliningId === challenge.id ? '...' : 'Decline' }} {{ decliningId === challenge.id ? '...' : 'Decline' }}
</button> </button>
@@ -97,7 +150,9 @@
</div> </div>
<div class="nc-notif-footer"> <div class="nc-notif-footer">
<button type="button" class="nc-view-all" (click)="goToChallenges()">View all challenges</button> <button type="button" class="nc-view-all" (click)="goToChallenges()">
View all challenges
</button>
</div> </div>
</div> </div>
} }
@@ -105,23 +160,47 @@
<!-- Games quick-access --> <!-- Games quick-access -->
<button type="button" class="nc-games-btn" (click)="goToGames()"> <button type="button" class="nc-games-btn" (click)="goToGames()">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" <svg
stroke-linecap="round" stroke-linejoin="round"> width="13"
<path d="M14.5 17.5L3 6"/> height="13"
<path d="M13 19l6-6"/><path d="M16 16l4 4"/> viewBox="0 0 24 24"
<path d="M19 21l2-2"/><path d="M15 5l4 4"/> fill="none"
<path d="M21 3l-3 1-4 4"/> stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M14.5 17.5L3 6" />
<path d="M13 19l6-6" />
<path d="M16 16l4 4" />
<path d="M19 21l2-2" />
<path d="M15 5l4 4" />
<path d="M21 3l-3 1-4 4" />
</svg> </svg>
Games Games
</button> </button>
<!-- Profile --> <!-- Profile -->
<div class="nc-dropdown-wrap" data-dropdown="profile"> <div class="nc-dropdown-wrap" data-dropdown="profile">
<button type="button" class="nc-profile" [class.is-open]="profileOpen" (click)="toggleProfile($event)"> <button
type="button"
class="nc-profile"
[class.is-open]="profileOpen"
(click)="toggleProfile($event)"
>
<div class="nc-avatar nc-avatar-sm">{{ getInitial() }}</div> <div class="nc-avatar nc-avatar-sm">{{ getInitial() }}</div>
<span class="nc-profile-name">{{ user.username }}</span> <span class="nc-profile-name">{{ user.username }}</span>
<svg class="nc-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" <svg
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> class="nc-chevron"
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9" /> <polyline points="6 9 12 15 18 9" />
</svg> </svg>
</button> </button>
@@ -139,8 +218,16 @@
<div class="nc-menu-group"> <div class="nc-menu-group">
<button type="button" class="nc-menu-item" (click)="goToProfile()"> <button type="button" class="nc-menu-item" (click)="goToProfile()">
<span class="nc-menu-icon"> <span class="nc-menu-icon">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" <svg
stroke-linecap="round" stroke-linejoin="round"> width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /> <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" /> <circle cx="12" cy="7" r="4" />
</svg> </svg>
@@ -150,11 +237,21 @@
<button type="button" class="nc-menu-item" (click)="goToChallenges()"> <button type="button" class="nc-menu-item" (click)="goToChallenges()">
<span class="nc-menu-icon"> <span class="nc-menu-icon">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" <svg
stroke-linecap="round" stroke-linejoin="round"> width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="14.5" y1="17.5" x2="3" y2="6" /> <line x1="14.5" y1="17.5" x2="3" y2="6" />
<path d="M13 19l6 -6" /><path d="M16 16l4 4" /> <path d="M13 19l6 -6" />
<path d="M19 21l2 -2" /><path d="M15 5l4 4" /> <path d="M16 16l4 4" />
<path d="M19 21l2 -2" />
<path d="M15 5l4 4" />
<path d="M21 3l-3 1l-4 4" /> <path d="M21 3l-3 1l-4 4" />
</svg> </svg>
</span> </span>
@@ -163,8 +260,16 @@
<button type="button" class="nc-menu-item" (click)="toggleTheme($event)"> <button type="button" class="nc-menu-item" (click)="toggleTheme($event)">
<span class="nc-menu-icon"> <span class="nc-menu-icon">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" <svg
stroke-linecap="round" stroke-linejoin="round"> width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" /> <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
</svg> </svg>
</span> </span>
@@ -176,8 +281,16 @@
<div class="nc-menu-group"> <div class="nc-menu-group">
<button type="button" class="nc-menu-item danger" (click)="logout()"> <button type="button" class="nc-menu-item danger" (click)="logout()">
<span class="nc-menu-icon"> <span class="nc-menu-icon">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" <svg
stroke-linecap="round" stroke-linejoin="round"> width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /> <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16 17 21 12 16 7" /> <polyline points="16 17 21 12 16 7" />
<line x1="21" y1="12" x2="9" y2="12" /> <line x1="21" y1="12" x2="9" y2="12" />
@@ -189,21 +302,20 @@
</div> </div>
} }
</div> </div>
} @else { } @else {
<!-- Logged-out auth buttons --> <!-- Logged-out auth buttons -->
<button type="button" class="nc-auth-btn" (click)="openLoginDialog()">Login</button> <button type="button" class="nc-auth-btn" (click)="openLoginDialog()">Login</button>
<button type="button" class="nc-auth-btn nc-auth-btn--primary" (click)="openRegisterDialog()">Register</button> <button type="button" class="nc-auth-btn nc-auth-btn--primary" (click)="openRegisterDialog()">
Register
</button>
} }
</div> </div>
</nav> </nav>
@if (showLoginDialog) { @if (showLoginDialog) {
<app-login-dialog (onClose)="closeLoginDialog()" (onSuccess)="onLoginSuccess()" /> <app-login-dialog (onClose)="closeLoginDialog()" (onSuccess)="onLoginSuccess()" />
} }
@if (showRegisterDialog) { @if (showRegisterDialog) {
<app-register-dialog (onClose)="closeRegisterDialog()" (onSuccess)="onRegisterSuccess()" /> <app-register-dialog (onClose)="closeRegisterDialog()" (onSuccess)="onRegisterSuccess()" />
} }
+20 -15
View File
@@ -18,7 +18,7 @@ import { Challenge } from '../../models/challenge.models';
standalone: true, standalone: true,
imports: [CommonModule, LoginDialogComponent, RegisterDialogComponent], imports: [CommonModule, LoginDialogComponent, RegisterDialogComponent],
templateUrl: './toolbar.component.html', templateUrl: './toolbar.component.html',
styleUrl: './toolbar.component.css' styleUrl: './toolbar.component.css',
}) })
export class ToolbarComponent implements OnInit { export class ToolbarComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
@@ -46,9 +46,7 @@ export class ToolbarComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.destroyRef.onDestroy(() => this.stopPolling()); this.destroyRef.onDestroy(() => this.stopPolling());
this.authService.currentUser$ this.authService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((user) => {
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(user => {
this.currentUser = user; this.currentUser = user;
if (user) { if (user) {
this.challengeWs.connect(); this.challengeWs.connect();
@@ -63,18 +61,21 @@ export class ToolbarComponent implements OnInit {
this.authDialogService.dialogState$ this.authDialogService.dialogState$
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(state => { .subscribe((state) => {
this.showLoginDialog = state === 'login'; this.showLoginDialog = state === 'login';
this.showRegisterDialog = state === 'register'; this.showRegisterDialog = state === 'register';
}); });
this.themeService.darkMode$ this.themeService.darkMode$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((isDark) => {
.pipe(takeUntilDestroyed(this.destroyRef)) this.isDarkMode = isDark;
.subscribe(isDark => { this.isDarkMode = isDark; }); });
this.challengeEventService.getIncomingChallenges$() this.challengeEventService
.getIncomingChallenges$()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(challenges => { this.incomingChallenges = challenges; }); .subscribe((challenges) => {
this.incomingChallenges = challenges;
});
} }
private startPolling(): void { private startPolling(): void {
@@ -91,7 +92,7 @@ export class ToolbarComponent implements OnInit {
private fetchChallenges(): void { private fetchChallenges(): void {
this.challengeService.listChallenges().subscribe({ this.challengeService.listChallenges().subscribe({
next: response => { next: (response) => {
const incoming = response.in ?? response.incoming ?? []; const incoming = response.in ?? response.incoming ?? [];
this.challengeEventService.setIncomingChallenges(incoming); this.challengeEventService.setIncomingChallenges(incoming);
@@ -104,7 +105,7 @@ export class ToolbarComponent implements OnInit {
} }
} }
} }
} },
}); });
} }
@@ -233,12 +234,14 @@ export class ToolbarComponent implements OnInit {
if (this.acceptingId || this.decliningId) return; if (this.acceptingId || this.decliningId) return;
this.acceptingId = challenge.id; this.acceptingId = challenge.id;
this.challengeService.acceptChallenge(challenge.id).subscribe({ this.challengeService.acceptChallenge(challenge.id).subscribe({
next: accepted => { next: (accepted) => {
this.acceptingId = null; this.acceptingId = null;
this.challengeEventService.onChallengeAccepted(accepted); this.challengeEventService.onChallengeAccepted(accepted);
if (accepted.gameId) void this.router.navigate(['/game', accepted.gameId]); if (accepted.gameId) void this.router.navigate(['/game', accepted.gameId]);
}, },
error: () => { this.acceptingId = null; } error: () => {
this.acceptingId = null;
},
}); });
} }
@@ -251,7 +254,9 @@ export class ToolbarComponent implements OnInit {
this.decliningId = null; this.decliningId = null;
this.challengeEventService.removeChallenge(challenge.id); this.challengeEventService.removeChallenge(challenge.id);
}, },
error: () => { this.decliningId = null; } error: () => {
this.decliningId = null;
},
}); });
} }
} }
@@ -64,18 +64,6 @@
} }
.page-title { font-size: 22px; font-weight: 700; margin: 0; letter-spacing: -0.02em; } .page-title { font-size: 22px; font-weight: 700; margin: 0; letter-spacing: -0.02em; }
.page-actions { display: flex; align-items: center; gap: 8px; }
.btn-servers {
display: inline-flex; align-items: center; gap: 6px;
padding: 7px 14px; border-radius: 8px;
border: 1px solid var(--nc-border-strong);
background: transparent; color: var(--nc-text-muted);
font-size: 13px; font-weight: 600; cursor: pointer;
transition: color 0.15s, border-color 0.15s;
}
.btn-servers:hover { color: var(--nc-text); border-color: var(--nc-text-muted); }
.btn-new { .btn-new {
display: inline-flex; align-items: center; gap: 6px; display: inline-flex; align-items: center; gap: 6px;
padding: 7px 14px; border-radius: 8px; border: none; padding: 7px 14px; border-radius: 8px; border: none;
@@ -339,74 +327,3 @@
color: var(--nc-success); letter-spacing: 0.06em; text-transform: uppercase; color: var(--nc-success); letter-spacing: 0.06em; text-transform: uppercase;
} }
.pairing-ongoing svg { animation: pulse 1.4s ease-in-out infinite; } .pairing-ongoing svg { animation: pulse 1.4s ease-in-out infinite; }
/* Official bot join section */
.join-divider {
display: flex; align-items: center; gap: 10px;
margin: 20px 0 14px;
}
.join-divider::before, .join-divider::after {
content: ''; flex: 1; height: 1px; background: var(--nc-border);
}
.join-divider-label {
font-size: 10px; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.08em; color: var(--nc-text-dim); white-space: nowrap;
}
.official-bot-grid {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px;
margin-bottom: 4px;
}
.official-bot-btn {
display: flex; align-items: center; justify-content: center; gap: 6px;
padding: 9px 4px; border-radius: 8px; border: 1px solid var(--nc-border);
background: var(--nc-surface); font-size: 12px; font-weight: 700;
cursor: pointer; transition: border-color 0.15s, background 0.15s, color 0.15s;
color: var(--nc-text-muted); text-transform: capitalize;
}
.official-bot-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.official-btn-easy:hover:not(:disabled) { border-color: var(--nc-success); color: var(--nc-success); background: rgba(94,229,161,0.07); }
.official-btn-medium:hover:not(:disabled) { border-color: var(--nc-warn); color: var(--nc-warn); background: rgba(255,209,102,0.07); }
.official-btn-hard:hover:not(:disabled) { border-color: var(--nc-neon); color: var(--nc-neon); background: rgba(255,69,200,0.07); }
.official-btn-expert:hover:not(:disabled) { border-color: var(--nc-danger); color: var(--nc-danger); background: rgba(255,122,122,0.07); }
/* Servers dialog */
.servers-dialog { max-width: 480px; }
.servers-list {
display: flex; flex-direction: column; gap: 6px;
margin-bottom: 20px;
}
.server-row {
display: flex; align-items: center; gap: 10px;
padding: 10px 12px; border-radius: 8px;
border: 1px solid var(--nc-border); background: var(--nc-surface);
}
.server-info { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.server-label { font-size: 13px; font-weight: 600; color: var(--nc-text); }
.server-url {
font-size: 11px; color: var(--nc-text-dim);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
font-family: monospace;
}
.server-remove-btn {
background: none; border: none; cursor: pointer;
color: var(--nc-text-dim); padding: 4px; border-radius: 6px;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0; transition: color 0.15s, background 0.15s;
font-size: 13px;
}
.server-remove-btn:hover:not(:disabled) { color: var(--nc-danger); background: rgba(255,122,122,0.1); }
.server-remove-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.server-add-form {
border-top: 1px solid var(--nc-border);
padding-top: 16px;
display: flex; flex-direction: column; gap: 0;
}
.server-add-heading {
font-size: 11px; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.08em; color: var(--nc-text-muted);
margin: 0 0 14px;
}
@@ -16,16 +16,7 @@
<header class="page-header"> <header class="page-header">
<div class="page-title-row"> <div class="page-title-row">
<h1 class="page-title">Tournaments</h1> <h1 class="page-title">Tournaments</h1>
<div class="page-actions">
@if (currentUser) { @if (currentUser) {
<button type="button" class="btn-servers" (click)="openServersDialog()">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/>
<line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/>
</svg>
Servers
</button>
<button type="button" class="btn-new" (click)="openCreateDialog()"> <button type="button" class="btn-new" (click)="openCreateDialog()">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"> stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
@@ -35,7 +26,6 @@
</button> </button>
} }
</div> </div>
</div>
<div class="tabs" role="tablist"> <div class="tabs" role="tablist">
<button type="button" class="tab-btn" [class.active]="tab === 'started'" (click)="setTab('started')"> <button type="button" class="tab-btn" [class.active]="tab === 'started'" (click)="setTab('started')">
Live Live
@@ -202,12 +192,12 @@
<span class="dialog-brand">Join with a bot</span> <span class="dialog-brand">Join with a bot</span>
<button type="button" class="dialog-close" (click)="closeJoinDialog()">×</button> <button type="button" class="dialog-close" (click)="closeJoinDialog()">×</button>
</div> </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> <p class="join-hint">Select one of your bots to enter this tournament. Its token will be rotated to authenticate the join.</p>
@if (botsLoading) { @if (botsLoading) {
<div class="dialog-loading"><span class="pulse"></span>Loading bots…</div> <div class="dialog-loading"><span class="pulse"></span>Loading bots…</div>
} @else if (userBots.length === 0) { } @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> <p class="join-empty">You have no bots yet. Go to <strong>Bots</strong> in the nav to create one first.</p>
} @else { } @else {
<div class="bot-pick-list"> <div class="bot-pick-list">
@for (bot of userBots; track bot.id) { @for (bot of userBots; track bot.id) {
@@ -228,97 +218,6 @@
@if (joinError) { @if (joinError) {
<div class="dialog-error">{{ joinError }}</div> <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>
}
{{ d | titlecase }}
</button>
}
</div>
@if (officialJoinError) {
<div class="dialog-error">{{ officialJoinError }}</div>
}
</div>
</div>
}
@if (showServersDialog) {
<div class="dialog-overlay" (click)="closeServersDialog()">
<div class="dialog-card servers-dialog" (click)="$event.stopPropagation()">
<div class="dialog-head">
<span class="dialog-brand">Tournament servers</span>
<button type="button" class="dialog-close" (click)="closeServersDialog()">×</button>
</div>
<p class="join-hint">External tournament servers aggregated into this view. Tournaments from all servers appear in the list.</p>
@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 yet.</p>
} @else {
<div class="servers-list">
@for (s of servers; track s.id) {
<div class="server-row">
<div class="server-info">
<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="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>
</div> </div>
} }
@@ -1,13 +1,11 @@
import { Component, DestroyRef, OnInit, inject } from '@angular/core'; import { Component, DestroyRef, OnInit, inject } from '@angular/core';
import { CommonModule, TitleCasePipe } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Router, RouterLink } from '@angular/router'; import { Router, RouterLink } from '@angular/router';
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { TournamentService } from '../../services/tournament.service'; import { TournamentService } from '../../services/tournament.service';
import { AuthService } from '../../services/auth.service'; import { AuthService } from '../../services/auth.service';
import { BotService } from '../../services/bot.service'; import { BotService } from '../../services/bot.service';
import { OfficialBotService } from '../../services/official-bot.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 } from '../../models/tournament.models'; import { Tournament, TournamentResult, RoundPairings } from '../../models/tournament.models';
import { CurrentUser } from '../../models/auth.models'; import { CurrentUser } from '../../models/auth.models';
@@ -17,7 +15,7 @@ type StatusTab = 'started' | 'created' | 'finished';
@Component({ @Component({
selector: 'app-tournaments', selector: 'app-tournaments',
standalone: true, standalone: true,
imports: [CommonModule, RouterLink, FormsModule, ReactiveFormsModule, TitleCasePipe], imports: [CommonModule, RouterLink, ReactiveFormsModule],
templateUrl: './tournaments.component.html', templateUrl: './tournaments.component.html',
styleUrl: './tournaments.component.css' styleUrl: './tournaments.component.css'
}) })
@@ -27,8 +25,6 @@ export class TournamentsComponent implements OnInit {
private readonly authService = inject(AuthService); private readonly authService = inject(AuthService);
private readonly fb = inject(FormBuilder); private readonly fb = inject(FormBuilder);
private readonly botService = inject(BotService); private readonly botService = inject(BotService);
private readonly officialBotService = inject(OfficialBotService);
private readonly tournamentServerService = inject(TournamentServerService);
private readonly router = inject(Router); private readonly router = inject(Router);
loading = true; loading = true;
@@ -62,19 +58,6 @@ export class TournamentsComponent implements OnInit {
joiningBotId: string | null = null; joiningBotId: string | null = null;
joinError: string | null = null; joinError: string | null = null;
readonly officialDifficulties = ['easy', 'medium', 'hard', 'expert'] as const;
joiningOfficialDifficulty: string | null = null;
officialJoinError: string | null = null;
showServersDialog = false;
servers: ExternalTournamentServer[] = [];
serversLoading = false;
newServerLabel = '';
newServerUrl = '';
addingServer = false;
addServerError: string | null = null;
removingServerId: string | null = null;
ngOnInit(): void { ngOnInit(): void {
this.authService.currentUser$ this.authService.currentUser$
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
@@ -175,7 +158,7 @@ export class TournamentsComponent implements OnInit {
this.joinDialogTournamentId = tournamentId; this.joinDialogTournamentId = tournamentId;
this.joinError = null; this.joinError = null;
this.botsLoading = true; this.botsLoading = true;
this.botService.listOfficial() this.botService.list()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({ .subscribe({
next: bots => { this.userBots = bots; this.botsLoading = false; }, next: bots => { this.userBots = bots; this.botsLoading = false; },
@@ -187,39 +170,15 @@ export class TournamentsComponent implements OnInit {
this.joinDialogTournamentId = null; this.joinDialogTournamentId = null;
this.joiningBotId = null; this.joiningBotId = null;
this.joinError = null; this.joinError = null;
this.joiningOfficialDifficulty = null;
this.officialJoinError = null;
}
joinWithOfficialBot(difficulty: string): void {
if (!this.joinDialogTournamentId || this.joiningOfficialDifficulty || this.joiningBotId) return;
this.joiningOfficialDifficulty = difficulty;
this.officialJoinError = null;
const tid = this.joinDialogTournamentId;
this.officialBotService.joinTournament(tid, difficulty).subscribe({
next: () => {
this.joiningOfficialDifficulty = null;
this.closeJoinDialog();
this.tournamentService.get(tid)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(updated => {
this.created = this.created.map(x => x.id === tid ? updated : x);
this.started = this.started.map(x => x.id === tid ? updated : x);
if (this.selectedTournament?.id === tid) this.selectedTournament = updated;
});
},
error: err => {
this.joiningOfficialDifficulty = null;
this.officialJoinError = err.error?.error ?? 'Failed to join with official bot.';
}
});
} }
joinWithBot(bot: Bot): void { joinWithBot(bot: Bot): void {
if (!this.joinDialogTournamentId || this.joiningBotId) return; if (!this.joinDialogTournamentId || this.joiningBotId) return;
this.joiningBotId = bot.id; this.joiningBotId = bot.id;
this.joinError = null; this.joinError = null;
this.tournamentService.join(this.joinDialogTournamentId, bot.id, bot.name).subscribe({ this.botService.rotateToken(bot.id).subscribe({
next: token => {
this.tournamentService.joinWithBotToken(this.joinDialogTournamentId!, token).subscribe({
next: () => { next: () => {
this.joiningBotId = null; this.joiningBotId = null;
const tid = this.joinDialogTournamentId!; const tid = this.joinDialogTournamentId!;
@@ -237,60 +196,14 @@ export class TournamentsComponent implements OnInit {
this.joinError = err.error?.message ?? err.error?.error ?? 'Failed to join tournament.'; this.joinError = err.error?.message ?? err.error?.error ?? 'Failed to join tournament.';
} }
}); });
}
openServersDialog(): void {
this.newServerLabel = '';
this.newServerUrl = '';
this.addServerError = null;
this.showServersDialog = true;
this.serversLoading = true;
this.tournamentServerService.list()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: res => { this.servers = res.servers; this.serversLoading = false; },
error: () => { this.serversLoading = false; }
});
}
closeServersDialog(): void {
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 => { error: () => {
this.addingServer = false; this.joiningBotId = null;
this.addServerError = err.error?.error ?? 'Failed to add server.'; this.joinError = 'Failed to get bot token.';
} }
}); });
} }
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 { private loadTournaments(): void {
this.tournamentService.list() this.tournamentService.list()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
-5
View File
@@ -7,16 +7,11 @@ import { Bot, BotWithToken } from '../models/bot.models';
export class BotService { export class BotService {
private readonly http = inject(HttpClient); private readonly http = inject(HttpClient);
private readonly base = '/api/account/bots'; private readonly base = '/api/account/bots';
private readonly officialBase = '/api/account/official-bots';
list(): Observable<Bot[]> { list(): Observable<Bot[]> {
return this.http.get<Bot[]>(this.base); return this.http.get<Bot[]>(this.base);
} }
listOfficial(): Observable<Bot[]> {
return this.http.get<Bot[]>(this.officialBase);
}
create(name: string): Observable<BotWithToken> { create(name: string): Observable<BotWithToken> {
return this.http.post<BotWithToken>(this.base, { name }); return this.http.post<BotWithToken>(this.base, { name });
} }
+6 -4
View File
@@ -7,7 +7,7 @@ import {
GameState, GameState,
GameStreamEvent, GameStreamEvent,
LegalMovesResponse, LegalMovesResponse,
PlayerInfo PlayerInfo,
} from '../models/game.models'; } from '../models/game.models';
import { AnalysisRequest, AnalysisResponse } from '../models/analysis.models'; import { AnalysisRequest, AnalysisResponse } from '../models/analysis.models';
import { StreamHandlerService } from './stream-handler.service'; import { StreamHandlerService } from './stream-handler.service';
@@ -29,11 +29,11 @@ export class GameApiService {
const playerColor = Math.random() > 0.5 ? 'white' : 'black'; const playerColor = Math.random() > 0.5 ? 'white' : 'black';
const playerInfo: PlayerInfo = { const playerInfo: PlayerInfo = {
id: `player-${Date.now()}`, id: `player-${Date.now()}`,
displayName: 'You' displayName: 'You',
}; };
const botInfo: PlayerInfo = { const botInfo: PlayerInfo = {
id: `bot-${difficulty}`, id: `bot-${difficulty}`,
displayName: `Bot (${difficulty})` displayName: `Bot (${difficulty})`,
}; };
const payload = const payload =
@@ -57,7 +57,9 @@ export class GameApiService {
if (square) { if (square) {
params = params.set('square', square); params = params.set('square', square);
} }
return this.http.get<LegalMovesResponse>(`${this.apiBase}${this.apiPath}/${gameId}/moves`, { params }); return this.http.get<LegalMovesResponse>(`${this.apiBase}${this.apiPath}/${gameId}/moves`, {
params,
});
} }
importFen(fen: string): Observable<GameFull> { importFen(fen: string): Observable<GameFull> {
-22
View File
@@ -1,22 +0,0 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface JoinTournamentResponse {
botId: string;
difficulty: string;
status: string;
}
@Injectable({ providedIn: 'root' })
export class OfficialBotService {
private readonly http = inject(HttpClient);
private readonly base = '/api/bots/official';
joinTournament(tournamentId: string, difficulty: string): Observable<JoinTournamentResponse> {
return this.http.post<JoinTournamentResponse>(`${this.base}/join-tournament`, {
tournamentId,
difficulty,
});
}
}
@@ -1,31 +0,0 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface ExternalTournamentServer {
id: string;
label: string;
url: string;
}
export interface ExternalTournamentServerList {
servers: ExternalTournamentServer[];
}
@Injectable({ providedIn: 'root' })
export class TournamentServerService {
private readonly http = inject(HttpClient);
private readonly base = '/api/tournament/servers';
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}`);
}
}
+4 -2
View File
@@ -40,8 +40,10 @@ export class TournamentService {
return this.http.post<Tournament>(`${this.base}/${id}/start`, null); return this.http.post<Tournament>(`${this.base}/${id}/start`, null);
} }
join(id: string, botId: string, botName: string): Observable<void> { joinWithBotToken(id: string, botToken: string): Observable<void> {
return this.http.post<void>(`${this.base}/${id}/join`, { botId, botName }); return this.http.post<void>(`${this.base}/${id}/join`, null, {
headers: new HttpHeaders({ Authorization: `Bearer ${botToken}` })
});
} }
roundPairings(id: string, round: number): Observable<RoundPairings> { roundPairings(id: string, round: number): Observable<RoundPairings> {
+2 -6
View File
@@ -6,10 +6,6 @@
"outDir": "./out-tsc/app", "outDir": "./out-tsc/app",
"types": [] "types": []
}, },
"include": [ "include": ["src/**/*.ts"],
"src/**/*.ts" "exclude": ["src/**/*.spec.ts"]
],
"exclude": [
"src/**/*.spec.ts"
]
} }
+2 -7
View File
@@ -4,12 +4,7 @@
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./out-tsc/spec", "outDir": "./out-tsc/spec",
"types": [ "types": ["jasmine"]
"jasmine"
]
}, },
"include": [ "include": ["src/**/*.d.ts", "src/**/*.spec.ts"]
"src/**/*.d.ts",
"src/**/*.spec.ts"
]
} }
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0 MAJOR=0
MINOR=4 MINOR=3
PATCH=0 PATCH=0