Skip to content

merge main - #1

Open
ReuschelCGN wants to merge 188 commits into
ReuschelCGN:develop-testfrom
PGAN-Dev:main
Open

merge main#1
ReuschelCGN wants to merge 188 commits into
ReuschelCGN:develop-testfrom
PGAN-Dev:main

Conversation

@ReuschelCGN

Copy link
Copy Markdown
Owner

No description provided.

hokiepokedad2 and others added 30 commits May 22, 2026 09:55
CI installs Node 22 which ships npm 10.9.7. That version's `npm ci`
strictly requires the nested `chokidar@4.0.3` / `readdirp@4.1.2`
entries that `@angular-devkit/*` packages declare as optional peer
deps. Dependabot regenerates `package-lock.json` with a newer npm
that prunes those entries, producing lockfiles npm 10.9.7 rejects
with EUSAGE — blocking #248, #250, #256, #261, #262.

Aligning CI to npm 11 matches Dependabot's resolution so the
post-rebase lockfile is accepted.
Bumps Microsoft.AspNetCore.Authentication.JwtBearer from 10.0.5 to 10.0.8
Bumps Microsoft.AspNetCore.Mvc.Testing from 10.0.5 to 10.0.8
Bumps Microsoft.AspNetCore.OpenApi from 10.0.5 to 10.0.8
Bumps Microsoft.EntityFrameworkCore from 10.0.5 to 10.0.8
Bumps Microsoft.EntityFrameworkCore.Design from 10.0.5 to 10.0.8
Bumps Microsoft.EntityFrameworkCore.InMemory from 10.0.5 to 10.0.8
Bumps Microsoft.Extensions.Caching.Memory from 10.0.5 to 10.0.8
Bumps Microsoft.Extensions.Configuration.Abstractions from 10.0.5 to 10.0.8
Bumps Microsoft.Extensions.Http from 10.0.5 to 10.0.8
Bumps Microsoft.Extensions.Logging.Abstractions from 10.0.5 to 10.0.8
Bumps Microsoft.NET.Test.Sdk from 18.4.0 to 18.5.1

---
updated-dependencies:
- dependency-name: Microsoft.AspNetCore.Authentication.JwtBearer
  dependency-version: 10.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: microsoft
- dependency-name: Microsoft.AspNetCore.Mvc.Testing
  dependency-version: 10.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: microsoft
- dependency-name: Microsoft.AspNetCore.OpenApi
  dependency-version: 10.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: microsoft
- dependency-name: Microsoft.EntityFrameworkCore
  dependency-version: 10.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: microsoft
- dependency-name: Microsoft.EntityFrameworkCore
  dependency-version: 10.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: microsoft
- dependency-name: Microsoft.EntityFrameworkCore.Design
  dependency-version: 10.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: microsoft
- dependency-name: Microsoft.EntityFrameworkCore.InMemory
  dependency-version: 10.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: microsoft
- dependency-name: Microsoft.Extensions.Caching.Memory
  dependency-version: 10.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: microsoft
- dependency-name: Microsoft.Extensions.Logging.Abstractions
  dependency-version: 10.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: microsoft
- dependency-name: Microsoft.Extensions.Configuration.Abstractions
  dependency-version: 10.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: microsoft
- dependency-name: Microsoft.Extensions.Http
  dependency-version: 10.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: microsoft
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: microsoft
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the angular group with 13 updates in the /Applications/Pgan.PoracleWebNet.App/ClientApp directory:

| Package | From | To |
| --- | --- | --- |
| [@angular/animations](https://github.com/angular/angular/tree/HEAD/packages/animations) | `21.2.8` | `21.2.14` |
| [@angular/cdk](https://github.com/angular/components) | `21.2.6` | `21.2.12` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `21.2.8` | `21.2.14` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `21.2.8` | `21.2.14` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `21.2.8` | `21.2.14` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `21.2.8` | `21.2.14` |
| [@angular/material](https://github.com/angular/components) | `21.2.6` | `21.2.12` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `21.2.8` | `21.2.14` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `21.2.8` | `21.2.14` |
| [@angular/build](https://github.com/angular/angular-cli) | `21.2.7` | `21.2.12` |
| [@angular/cli](https://github.com/angular/angular-cli) | `21.2.7` | `21.2.12` |
| [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `21.2.8` | `21.2.14` |
| [@angular/platform-browser-dynamic](https://github.com/angular/angular/tree/HEAD/packages/platform-browser-dynamic) | `21.2.8` | `21.2.14` |



Updates `@angular/animations` from 21.2.8 to 21.2.14
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.14/packages/animations)

Updates `@angular/cdk` from 21.2.6 to 21.2.12
- [Release notes](https://github.com/angular/components/releases)
- [Changelog](https://github.com/angular/components/blob/main/CHANGELOG.md)
- [Commits](angular/components@v21.2.6...v21.2.12)

Updates `@angular/common` from 21.2.8 to 21.2.14
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.14/packages/common)

Updates `@angular/compiler` from 21.2.8 to 21.2.14
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.14/packages/compiler)

Updates `@angular/core` from 21.2.8 to 21.2.14
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.14/packages/core)

Updates `@angular/forms` from 21.2.8 to 21.2.14
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.14/packages/forms)

Updates `@angular/material` from 21.2.6 to 21.2.12
- [Release notes](https://github.com/angular/components/releases)
- [Changelog](https://github.com/angular/components/blob/main/CHANGELOG.md)
- [Commits](angular/components@v21.2.6...v21.2.12)

Updates `@angular/platform-browser` from 21.2.8 to 21.2.14
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.14/packages/platform-browser)

Updates `@angular/router` from 21.2.8 to 21.2.14
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.14/packages/router)

Updates `@angular/build` from 21.2.7 to 21.2.12
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](angular/angular-cli@v21.2.7...v21.2.12)

Updates `@angular/cli` from 21.2.7 to 21.2.12
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](angular/angular-cli@v21.2.7...v21.2.12)

Updates `@angular/compiler-cli` from 21.2.8 to 21.2.14
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.14/packages/compiler-cli)

Updates `@angular/platform-browser-dynamic` from 21.2.8 to 21.2.14
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.14/packages/platform-browser-dynamic)

---
updated-dependencies:
- dependency-name: "@angular/animations"
  dependency-version: 21.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/build"
  dependency-version: 21.2.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/cdk"
  dependency-version: 21.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/cli"
  dependency-version: 21.2.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/common"
  dependency-version: 21.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/compiler"
  dependency-version: 21.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/compiler-cli"
  dependency-version: 21.2.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/core"
  dependency-version: 21.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/forms"
  dependency-version: 21.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/material"
  dependency-version: 21.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-browser"
  dependency-version: 21.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-browser-dynamic"
  dependency-version: 21.2.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/router"
  dependency-version: 21.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@jest/globals](https://github.com/jestjs/jest/tree/HEAD/packages/jest-globals) from 30.3.0 to 30.4.1.
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.4.1/packages/jest-globals)

---
updated-dependencies:
- dependency-name: "@jest/globals"
  dependency-version: 30.4.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
)

Bumps [jsdom](https://github.com/jsdom/jsdom) from 28.1.0 to 29.1.1.
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Commits](jsdom/jsdom@v28.1.0...v29.1.1)

---
updated-dependencies:
- dependency-name: jsdom
  dependency-version: 29.1.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the jest group with 3 updates in the /Applications/Pgan.PoracleWebNet.App/ClientApp directory: [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest), [jest-environment-jsdom](https://github.com/jestjs/jest/tree/HEAD/packages/jest-environment-jsdom) and [jest-preset-angular](https://github.com/thymikee/jest-preset-angular).


Updates `jest` from 30.3.0 to 30.4.2
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.4.2/packages/jest)

Updates `jest-environment-jsdom` from 30.3.0 to 30.4.1
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.4.1/packages/jest-environment-jsdom)

Updates `jest-preset-angular` from 16.1.4 to 16.1.5
- [Release notes](https://github.com/thymikee/jest-preset-angular/releases)
- [Changelog](https://github.com/thymikee/jest-preset-angular/blob/main/CHANGELOG.md)
- [Commits](thymikee/jest-preset-angular@v16.1.4...v16.1.5)

---
updated-dependencies:
- dependency-name: jest
  dependency-version: 30.4.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: jest
- dependency-name: jest-environment-jsdom
  dependency-version: 30.4.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: jest
- dependency-name: jest-preset-angular
  dependency-version: 16.1.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: jest
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the eslint group with 3 updates in the /Applications/Pgan.PoracleWebNet.App/ClientApp directory: [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin), [eslint-plugin-perfectionist](https://github.com/azat-io/eslint-plugin-perfectionist) and [prettier](https://github.com/prettier/prettier).


Updates `@typescript-eslint/eslint-plugin` from 8.58.1 to 8.59.4
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.59.4/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.58.1 to 8.59.4
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.59.4/packages/parser)

Updates `@typescript-eslint/utils` from 8.58.1 to 8.59.4
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.59.4/packages/utils)

Updates `eslint-plugin-perfectionist` from 5.8.0 to 5.9.0
- [Release notes](https://github.com/azat-io/eslint-plugin-perfectionist/releases)
- [Changelog](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/changelog.md)
- [Commits](azat-io/eslint-plugin-perfectionist@v5.8.0...v5.9.0)

Updates `prettier` from 3.8.2 to 3.8.3
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](prettier/prettier@3.8.2...3.8.3)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.58.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: eslint
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.58.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: eslint
- dependency-name: "@typescript-eslint/utils"
  dependency-version: 8.58.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: eslint
- dependency-name: eslint-plugin-perfectionist
  dependency-version: 5.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: eslint
- dependency-name: prettier
  dependency-version: 3.8.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: eslint
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: HokiePokeDad <hokiepokedad@pogoalerts.net>
The auto-merge-deps workflow listed both `pull_request_target` and
`push:` as triggers. In practice GitHub fired it only on push events —
the last 100+ runs were all `push`, zero were `pull_request_target` —
even though the sibling `pr-labeler.yml` (only `pull_request_target`)
fires correctly. The job-level `if: github.event_name == 'pull_request_target'`
then skipped every step on those push runs, recording each as failure
with 0 successful steps.

Two changes:
- Drop the `push:` trigger so only pull_request_target events run.
- Drop the job-level gate; gate each step instead and add a sentinel
  first step so non-Dependabot PRs still record as success rather
  than 0-step failure.

#231 attempted this with job-level if assuming "all-skipped = success",
but GitHub treats 0-job runs as failure regardless.
The approval body string contained Auto-approved: — an unquoted
colon inside an unquoted YAML scalar. PyYAML rejects this with
`mapping values are not allowed here`, and GitHub Actions appears
to silently fail to register the workflow's `pull_request_target`
trigger as a result (the workflow only ever fires on push events,
the friendly name from `name:` never resolves in the API). Sibling
`pr-labeler.yml` has no such ambiguity and fires correctly.

Dropping the colon from the body resolves both symptoms without
needing to nest YAML quoting.
---
updated-dependencies:
- dependency-name: coverlet.collector
  dependency-version: 10.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: test
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: test
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps Microsoft.EntityFrameworkCore from 10.0.5 to 10.0.8
Bumps Microsoft.EntityFrameworkCore.Design from 10.0.5 to 10.0.8
Bumps Microsoft.EntityFrameworkCore.InMemory from 10.0.5 to 10.0.8
Bumps MySql.EntityFrameworkCore from 10.0.1 to 10.0.7

---
updated-dependencies:
- dependency-name: Microsoft.EntityFrameworkCore
  dependency-version: 10.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ef-core
- dependency-name: Microsoft.EntityFrameworkCore.Design
  dependency-version: 10.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ef-core
- dependency-name: MySql.EntityFrameworkCore
  dependency-version: 10.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ef-core
- dependency-name: Microsoft.EntityFrameworkCore.InMemory
  dependency-version: 10.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ef-core
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hokiepokedad2 <38219945+hokiepokedad2@users.noreply.github.com>
Loose .png screenshots from Playwright MCP sessions and the `.playwright-mcp/`
output directory keep showing up as untracked at the repo root. None of them
belong in the tree (tracked PNGs all live under
`Applications/Pgan.PoracleWebNet.App/ClientApp/public/assets/`). Add a
root-only `/*.png` rule plus `.playwright-mcp/` so `git status` stays clean.
#260) (#279)

LikeEscape (added in #232) used `\` as the SQL LIKE escape character,
and ScannerService.SearchGymsAsync passed `\` to EF.Functions.Like
via `"\\"`. MariaDB's default mode (`NO_BACKSLASH_ESCAPES=OFF`) also
treats `\` as a string-literal escape, so an escaped `\` in the
pattern (which LikeEscape itself produces for user-supplied
backslashes) left the SQL string literal unbalanced and broke gym
search with `near ''\')`.

Switch the escape character to `|`, which has no special meaning in
MariaDB string literals. The LIKE pattern can no longer interact with
quote escaping no matter what the user types. Added a `LikeEscape.EscapeChar`
constant so callers stay in sync.

Tests updated to match the new escape sequences.

Reported by @prof-miles0 in #260.
Bumps the angular group in /Applications/Pgan.PoracleWebNet.App/ClientApp with 13 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/animations](https://github.com/angular/angular/tree/HEAD/packages/animations) | `21.2.14` | `21.2.15` |
| [@angular/cdk](https://github.com/angular/components) | `21.2.12` | `21.2.13` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `21.2.14` | `21.2.15` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `21.2.14` | `21.2.15` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `21.2.14` | `21.2.15` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `21.2.14` | `21.2.15` |
| [@angular/material](https://github.com/angular/components) | `21.2.12` | `21.2.13` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `21.2.14` | `21.2.15` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `21.2.14` | `21.2.15` |
| [@angular/build](https://github.com/angular/angular-cli) | `21.2.12` | `21.2.13` |
| [@angular/cli](https://github.com/angular/angular-cli) | `21.2.12` | `21.2.13` |
| [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `21.2.14` | `21.2.15` |
| [@angular/platform-browser-dynamic](https://github.com/angular/angular/tree/HEAD/packages/platform-browser-dynamic) | `21.2.14` | `21.2.15` |


Updates `@angular/animations` from 21.2.14 to 21.2.15
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.15/packages/animations)

Updates `@angular/cdk` from 21.2.12 to 21.2.13
- [Release notes](https://github.com/angular/components/releases)
- [Changelog](https://github.com/angular/components/blob/main/CHANGELOG.md)
- [Commits](angular/components@v21.2.12...v21.2.13)

Updates `@angular/common` from 21.2.14 to 21.2.15
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.15/packages/common)

Updates `@angular/compiler` from 21.2.14 to 21.2.15
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.15/packages/compiler)

Updates `@angular/core` from 21.2.14 to 21.2.15
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.15/packages/core)

Updates `@angular/forms` from 21.2.14 to 21.2.15
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.15/packages/forms)

Updates `@angular/material` from 21.2.12 to 21.2.13
- [Release notes](https://github.com/angular/components/releases)
- [Changelog](https://github.com/angular/components/blob/main/CHANGELOG.md)
- [Commits](angular/components@v21.2.12...v21.2.13)

Updates `@angular/platform-browser` from 21.2.14 to 21.2.15
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.15/packages/platform-browser)

Updates `@angular/router` from 21.2.14 to 21.2.15
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.15/packages/router)

Updates `@angular/build` from 21.2.12 to 21.2.13
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](angular/angular-cli@v21.2.12...v21.2.13)

Updates `@angular/cli` from 21.2.12 to 21.2.13
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](angular/angular-cli@v21.2.12...v21.2.13)

Updates `@angular/compiler-cli` from 21.2.14 to 21.2.15
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.15/packages/compiler-cli)

Updates `@angular/platform-browser-dynamic` from 21.2.14 to 21.2.15
- [Release notes](https://github.com/angular/angular/releases)
- [Changelog](https://github.com/angular/angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular/commits/v21.2.15/packages/platform-browser-dynamic)

---
updated-dependencies:
- dependency-name: "@angular/animations"
  dependency-version: 21.2.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/cdk"
  dependency-version: 21.2.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/common"
  dependency-version: 21.2.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/compiler"
  dependency-version: 21.2.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/core"
  dependency-version: 21.2.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/forms"
  dependency-version: 21.2.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/material"
  dependency-version: 21.2.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-browser"
  dependency-version: 21.2.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/router"
  dependency-version: 21.2.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/build"
  dependency-version: 21.2.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/cli"
  dependency-version: 21.2.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/compiler-cli"
  dependency-version: 21.2.15
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: angular
- dependency-name: "@angular/platform-browser-dynamic"
  dependency-version: 21.2.15
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: angular
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
#259) (#280)

* feat(raids): redesign level selector with named tiers + custom palette (#259)

The raid/egg add dialog had three level-pickers (raid checkboxes, egg
checkboxes, boss-level dropdown) all driven by a hardcoded
`levels = [1, 2, 3, 4, 5, 6]` array, even though PoracleNG accepts
any positive integer. Users with Elite Raids (level 7) or custom
server schemes had to configure those via the bot's `!command`
interface; the UI silently locked them out.

Replace all three sites with a new `<app-level-selector>` shared
component — a Material 3 chip listbox in three sections:

- STANDARD: T1-T5
- SPECIAL:  Mega (6), Elite (7), plus "Any" (9000) when showAny=true
- CUSTOM:   any user-added integer, persisted per-user in localStorage

Power users add a custom level via an inline "+ Add level" affordance
that transforms into a numeric input. The chip then persists across
dialog opens (one-click selection on subsequent alarms) and seeds
itself from saved alarm data on open, so editing an existing
level-42 alarm renders the chip pre-selected rather than orphaned.

Single source of truth for label resolution lives in
`core/models/raid-level.models.ts`. `resolveLevel(value)` maps any
integer to the right LevelOption — adopted by raid-list cards too,
so the dialog and the cards now speak the same vocabulary
("Elite", not "Level 7" vs "7" on different surfaces).

Edge cases:
- 0 / negatives / non-integers in custom input -> inline validation error
- 9000 in custom input -> snaps to the "Any" chip (no duplicate)
- duplicate of built-in -> flashes existing chip + selects, no new entry
- 20-entry LRU cap on the localStorage palette

i18n: new `RAIDS.LEVEL.*` keys added in all 11 locales with English
fallbacks for non-en (translation volunteers can localize later,
per discussion #211).

Bonus correctness: the boss tab used to default level=0 ("any") but
PoracleNG's canonical wildcard sentinel is 9000. New alarms now use
9000; old alarms with 0 continue to work and edit fine.

37 new unit tests across the model, store, pipe, and component.

Closes #259, reported by @prof-miles0.

* fix(raids): address #259 review — compact layout, per-type palette, unblock save

Follow-up on d58752e (the initial #259 redesign), addressing visual,
correctness, and backend-validation issues found during testing.

UI / UX
- Collapse the three-section (Standard/Special/Custom) chip layout
  into one wrapping row per picker. Categories are encoded in chip
  content (T1 / "Mega · 6" / "42 ⊗") rather than container labels;
  cuts dialog height roughly in half.
- Replace the heavy mat-form-field "+ Add custom level" with a
  chip-sized inline numeric input. Enter commits, Esc cancels,
  blur commits. Help text only renders when the input is open or
  there's a validation error.
- Override Material 3 selected-chip font-weight so the selected
  state actually pops; bind `hideSingleSelectionIndicator` to
  `!multiple` so multi-select chips get a leading checkmark.
- Cap card star icons to levels 1-7 (was 1-100) — alarms at
  level 23 no longer render 23 stars in the card.

Per-type palette
- Adding a custom level on the raid picker was leaking it into the
  egg and boss pickers. `CustomLevelStore` now keys palettes by
  `paletteKey` ("raid" / "egg" / "boss"), each persisted to its own
  localStorage slot. Required `paletteKey` input on
  `<app-level-selector>`.

Any chip surfaced where PoracleNG actually honors it
- Raid + boss pickers show the `Any` chip (PoracleNG treats
  level=9000 as the wildcard sentinel — see trackingRaid.go).
- Egg picker deliberately omits Any: PoracleNG's trackingEgg.go
  only validates level >= 1 with no wildcard semantic, so an "Any
  egg" alarm at 9000 would simply never fire.

Server-side fix that was blocking custom-level alarms
- `[Range(0, 10)]` on `RaidCreate.Level`, `RaidUpdate.Level`,
  `EggCreate.Level`, `EggUpdate.Level` was rejecting custom
  integers (8+) and the new Any=9000 sentinel with 400 Bad
  Request before they could reach PoracleNG. Relaxed to
  `[Range(0, int.MaxValue)]` matching PoracleNG's actual range.

Label vocabulary consistency
- Edit dialog (raid/egg) now uses the same `resolveLevel` resolver
  as the cards via the new `LevelLabelPipe` — an alarm at level 7
  reads "Elite" on the card AND in the edit dialog (was "Level 7"
  in the dialog before). Egg image alt-text in the card list now
  uses the pipe too.

Error UX
- Removing a custom chip (`⊗`) opens a 3-second snackbar with
  Undo — accidental click is recoverable; intentional removal
  still wipes the palette entry.

State-machine clarity
- `addInputOpen: signal(boolean)` replaced with an explicit
  `addMode: signal<'closed' | 'open'>` and named `isAddClosed()` /
  `isAddOpen()` getters in the template — removes the `!` negation
  pattern that prior renders sometimes appeared to misread.

Tests
- Updated `custom-level-store.service.spec.ts` for the keyed API.
- Updated `level-selector.component.spec.ts` to set `paletteKey`
  per test and assert per-key isolation.
- 697/697 frontend tests pass, 1063/1063 backend tests pass.

i18n
- Added `RAIDS.LEVEL.REMOVED` and `COMMON.UNDO` keys in all 11
  locales (English placeholder text for non-en — translation
  volunteers per discussion #211).

* fix(raids): align level selector with WatWowMap masterfile (19 named levels)

Builds on the v2 review-pass (2c2f0aa). Follow-up driven by the issue
reporter pointing to the canonical Pokémon GO raid level vocabulary in
the WatWowMap masterfile — there are 19 named raid types (1-Star
through Coordinated 2), not 7, and the prior UI labeled level 7 as
"Elite" when the masterfile says it's "Mega Legendary".

Backend
- `GET /api/masterdata/raid-levels` returns the canonical list with
  per-level integer, category, and singular/plural English names.
- New `IRaidLevelService` / `RaidLevelService` returns a baked-in
  snapshot of the masterfile. A TODO documents how to swap the
  implementation for a live fetch from
  raw.githubusercontent.com/WatWowMap/Masterfile-Generator without
  changing the wire contract.
- 6 new unit tests cover the service + controller endpoint.

Frontend
- `RaidLevelService` (Angular) calls the new API on first dialog use,
  caches the result in a signal. Falls back to `KNOWN_LEVELS`
  baked-in constants when the network fails or before resolve.
- `raid-level.models.ts` rewritten around 19 canonical levels keyed
  by `RAIDS.LEVEL.RAID_1` through `RAID_19` (plus `_PLURAL` variants).
  Removed the bogus `T1`-`T5` / `MEGA` / `ELITE` keys.
- `LevelSelectorComponent` simplified to a `pickerType` input
  (`'raid' | 'egg' | 'boss'`). Inputs:
    • raid  → primary chips 1-7, overflow menu for 8-19, Any chip, +Add
    • egg   → star tiers (1-5) only, +Add (no overflow, no Any)
    • boss  → single-select with the same primary + overflow as raid
  "More raid types…" overlay menu (mat-menu) surfaces the 12 less
  common levels without crowding the chip row.
- i18n: 19 singular + 19 plural keys in all 11 locales, with English
  placeholders for the 10 non-en locales (volunteers per #211).
- Card star icons now render only for the literal 1-5 "N Star Raid"
  tier (was 1-7, producing ~23 stars for custom-level alarms).
- Label vocabulary consistency: alarm at level 7 now reads "Mega
  Legendary Raid" on the card and in the edit dialog (was "Elite"
  on the card, "Level 7" in the edit dialog).

Forward compatibility
- Any positive integer remains addable via the `+ Add` chip. When
  the WatWowMap masterfile adds raid_20+ in the future, the backend
  service can pick it up automatically (once the live-fetch path is
  wired); existing custom alarms at that level continue to work.

Tests: 711/711 frontend, 1069/1069 backend. Lint + prettier clean.

* fix(raids): shorten labels, ephemeral palette, review fixes

Follow-up on 12be778 (v3 masterfile alignment). User feedback +
internal PR review revealed several issues; this commit addresses
them.

User feedback
- Custom levels typed via `+ Add` were persisting across modal
  close AND page refresh because the per-type palette was backed
  by localStorage. Deleted CustomLevelStore entirely; LevelSelector
  now tracks the palette in a local `customPalette` signal that
  lives for the component lifetime only. Refresh or close-and-reopen
  wipes typed-but-not-saved chips. Existing alarms still seed the
  palette through the `[value]` input.
- Chip labels were too long ("Mega Legendary Raid"), and the same
  string caused "All Mega Legendary Raid Raids" double-Raid in card
  titles. Dropped the "Raid" suffix from the 19 RAID_N keys in all
  11 locales — chips now read "Mega Legendary", "Legendary",
  "1 Star", etc. The card-title template
  (`RAIDS.ALL_LEVEL_RAIDS = "All {{level}} Raids"`) supplies the
  noun once; result reads natural English. Also dropped the unused
  `pluralKey` from `LevelOption` and the `_PLURAL` i18n keys (the
  shortened `RAID_N` strings work in both card and chip contexts).
  Backend `RaidLevelInfo.Name` is now the modifier form
  ("Mega Legendary") while `NamePlural` retains the full
  "Mega Legendary Raids" for any future standalone use.

Review fixes (MUST FIX)
- Snackbar undo subscription in `LevelSelector.removeCustom` now
  pipes through `takeUntilDestroyed(this.destroyRef)` so closing
  the dialog mid-toast can't fire the callback against a destroyed
  component.
- `raid-list.getRaidLevelName` was bypassing the live
  `RaidLevelService.byValue()` and using the baked-in `KNOWN_LEVELS`
  constant — cards would drift from the dialog if the API ever
  extended the canonical list. Cards now consult the service first,
  fall back to the baked-in resolver. `raid-list.ngOnInit` primes
  the cache so the list page doesn't depend on a dialog open.
- `LevelLabelPipe` now detects ngx-translate's "key not found"
  pass-through (translated string === key) and falls back to
  "Level {n}" instead of leaking "RAIDS.LEVEL.RAID_20" into the UI.
  Graceful degradation for future masterfile additions before
  locales catch up.

Review fixes (SHOULD FIX)
- `raid-edit-dialog.formatLevel` deleted — the dialog now injects
  `LevelLabelPipe` and calls `.transform()`, eliminating the
  duplicate label-resolution logic. Single source of truth.

Analyzers
- xUnit2032 in `MasterDataControllerRaidLevelsTests`: switched
  `Assert.IsAssignableFrom<T>` to `Assert.IsType<T>(..., exactMatch:
  false)`.
- CA1707 in `RaidLevelServiceTests` and the new controller test:
  test method names renamed to PascalCase to match the project's
  preferred style and silence the analyzer.

Dev workflow
- New `proxy.conf.json` forwards `/api/*` and `/auth/*` from the
  Angular dev server to the API. `environment.development.ts`
  apiUrl set to `''` so all HTTP calls are same-origin from the
  browser's view — works identically for `ng serve` + proxy and
  for the production single-port deployment. OAuth flows survive
  the proxy because Host is preserved.

Tests: 700/700 frontend (+1 fallback test), 1069/1069 backend.
Lint + prettier + dotnet format (scoped) all clean.

* docs: cover the level-selector redesign + ng-serve proxy workflow

- features/alarms.md: replace the generic "tier" wording on Raids/Eggs
  rows with pointers to a new "Raid level selector" subsection that
  documents the chip layout, the 19 masterfile-defined raid types,
  primary vs overflow split per pickerType, the ephemeral custom-add
  affordance, and the wildcard sentinel.
- architecture/backend.md: add a "Raid level service" section
  documenting IRaidLevelService, GET /api/masterdata/raid-levels, the
  baked-in fallback + live-fetch upgrade path, and the [Range] relax
  that lets PoracleNG-accepted custom integers pass validation. Also
  register the singleton in the service lifetimes table.
- architecture/frontend.md: document LevelSelectorComponent + the
  Angular RaidLevelService consumer (signal cache, baked-in fallback,
  LevelLabelPipe missing-key fallback), and the ephemeral palette
  behavior.
- getting-started/development-setup.md: the "proxies API requests"
  claim is now accurate thanks to the committed proxy.conf.json —
  describe it explicitly (changeOrigin: false to preserve Host for
  OAuth callbacks), note the empty apiUrl + same-origin dev flow,
  and document the --port override for matching a non-default
  Discord OAuth redirect URI.
The standalone `dotnet run` DataProtection fallback writes keys to
Applications/Pgan.PoracleWebNet.Api/data/ (Program.cs uses ./data when
DATA_DIR is unset), and the published Angular bundle is copied into the
API host at Applications/Pgan.PoracleWebNet.Api/wwwroot/. Both are
regenerated build/runtime output and were showing up as untracked.

The existing Data/dataprotection-keys/ rule only covered the Data
project path, not the API host path. Added both so a local build/run
leaves a clean working tree.
Poracle's `pvp_ranking_cap` field was never surfaced by PoracleWeb.NET, so
every PvP alarm defaulted to matching all caps (L50 + L51) server-side.
New users got flooded with L51 noise even when admins configured
`tracking.defaultUserTrackingLevelCap = 50` in Poracle.

This wires `pvp_ranking_cap` end-to-end and mirrors the PoracleWeb PHP
passthrough pattern — no new admin setting; the cap list and default
come from Poracle's existing `/api/config/poracleWeb` response
(`pvpCaps`, `defaultPvpCap`).

Backend:
- `Monster` / `MonsterCreate` / `MonsterUpdate` / `MonsterEntity`:
  new `PvpRankingCap` field (0 = all caps).
- `AlarmMappingExtensions.ToMonster` + `ApplyUpdate`: mapping + null-skip.
- `PoracleConfig.PvpCaps` (list) and `DefaultPvpCap` (int), parsed in
  `PoracleApiProxy.GetConfigAsync`. Accepts number or string caps
  (PoracleJS is inconsistent on this).
- `QuickPickService.SafeMonsterFilterKeys`: allow `pvpRankingCap` so
  quick-pick authors can pin a cap per definition.

Frontend:
- `Monster.pvpRankingCap`, `PoracleServerConfig` interface.
- New `PoracleConfigService` caches `/api/config` behind a signal.
- `pokemon-add-dialog`: `mat-button-toggle-group` under the PvP league
  field with `All` / `L{cap}` options. Pre-fills from `defaultPvpCap`.
  Italic "Default · from Poracle config" hint disappears the moment
  the user changes the selection. Hidden entirely when Poracle offers
  only one cap.
- `pokemon-edit-dialog`: same toggle group; shows stored cap on load.

Tests (+1066 backend, +658 frontend):
- Mapping tests cover `PvpRankingCap` in `ToMonster`, null-skip
  `ApplyUpdate`, and explicit-overwrite cases.
- `ConfigControllerTests` verify `PvpCaps` + `DefaultPvpCap` flow
  through the controller unmodified.
- `monster.service.spec.ts` fixtures updated with the new field.
)

* feat(raids): RSVP notification mode for raid and egg alarms (#233)

Surface the existing `rsvpChanges` field as a three-option mode toggle
(Matches only / Matches + RSVP / RSVP only) in the raid & egg add/edit
dialogs, with a matching pill on alarm cards.

The field, mapping, and dialog form binding already exist on main; this
adds the missing UI control and the third mode value.

- New self-contained shared components: rsvp-toggle (FormControl input)
  and rsvp-pill (numeric value input).
- Widen [Range(0, 1)] -> [Range(0, 2)] on RsvpChanges in RaidCreate,
  RaidUpdate, EggCreate, EggUpdate so mode 2 isn't rejected with HTTP 400.
- Wire rsvp-toggle into both dialogs (raid + egg) bound to the
  rsvpChanges form control; rsvp-pill onto raid/egg cards.
- RSVP i18n keys added to all 11 locales (incl. new pl/sv/da).
- RsvpRangeValidationTests covering the 0..2 range and null updates.

Salvaged from #235, dropping that branch's raid-dialog section-refactor
which conflicted with the #280 level-selector restructure already on main.

* feat(raids): set edit-in-place bit when RSVP updates are enabled

PoracleNG's `clean` field is a bitmask (bit 1 = auto-delete, bit 2 =
edit-in-place). Raid/egg RSVP updates are PoracleNG's first edit-tracking
consumer: with the edit bit set, RSVP count changes edit the existing
alert in place; without it, each change sends a brand-new message.

Verified against PoracleNG main (processor/internal/dts/renderer.go):
the per-user editKey is gated on db.IsEdit(user.Clean), so RSVP mode
alone is not enough — the edit bit must accompany it.

- raid/egg add + edit dialogs: OR in `clean` bit 2 when rsvpChanges >= 1.
- edit dialog: read the auto-delete toggle from `clean & 1` (was
  `clean === 1`) so it round-trips correctly once bit 2 can be set.
- raid-list: new isAutoDelete() helper masks bit 1 for the card
  auto-delete badge (Angular templates can't express bitwise &).
- CHANGELOG + docs note the edit-in-place coupling.

* style(raids): align RSVP toggle + pill with the app design system

The RSVP components were copied from a pre-#280 branch and used a bespoke
look (stacked icon+label+description button rows, hardcoded indigo pill)
that didn't match the current Material 3 language.

rsvp-toggle: rebuilt to mirror the #237 PvP-cap pattern — a <fieldset>
with an uppercase <legend> section header, a plain full-width segmented
mat-button-toggle-group (no icons/no inline descriptions), and a single
hint below that shows the selected mode's description (so the "RSVP only
silences without a scanner" caveat stays visible). Uses --text-muted /
--text-secondary tokens instead of raw opacity.

rsvp-pill: now a .clean-tag-style status badge themed with --mat-sys-primary
/ --mat-sys-on-primary (was hardcoded #3f51b5). Moved out of the stat grid
into card-top-actions so it sits beside the auto-delete tag as a sibling
status indicator.

Specs updated for the new structure; docs reworded (themed badge, not
"indigo pill"). prettier/lint/jest(712)/ng build all green.

* chore: keep Dockerfile.local out of the PR (local test artifact)

* style(raids): square off the RSVP toggle ends (--mat-button-toggle-shape)

M3 defaults the segmented button group to a pill radius; override the
shape token to 4px so the end segments read as a crisp Material control
rather than a rounded pill.

* fix(raids): stop RSVP toggle overflow + vertically center labels

- fieldset defaulted to min-inline-size: auto and overflowed its container
  (right edge clipped); force min-inline-size: 0 / width 100% / border-box.
- flex-center the button-toggle label content so single-line options align
  with the wrapped 'Matches + RSVP updates' segment.

* fix(raids): RSVP toggle overflow (drop fieldset, border-box) + drop dead i18n key

- Replace the <fieldset>/<legend> with a plain block + border-box group so
  the segmented control fits the dialog width (right edge was clipped); the
  toggle group keeps its aria-label for the accessible name.
- Remove the now-unused RSVP_HINT key from all 11 locales (the restyle
  replaced the static hint with the per-mode description).

* fix(raids): widen Clean [Range] to bitmask 0-7 so RSVP edit bit saves

The RSVP edit-in-place coupling sets clean bit 2 (clean = 2 or 3), but
Clean was still [Range(0, 1)] on RaidCreate/RaidUpdate/EggCreate/EggUpdate
-> creating/editing an RSVP alarm failed model validation with HTTP 400
('failed to create alarm'). clean is a PoracleNG bitmask (auto-delete |
edit-in-place | summary); widened to [Range(0, 7)]. Added validation tests.

* style(raids): hide RSVP toggle selection checkmark for more label room

Single-select group, so the M3 checkmark only ate ~24px and pushed labels
to wrap. hideSingleSelectionIndicator drops it; the accent fill + bold
already signal the selected mode.
node:22-alpine bundles npm 10.9.x, which rejects the npm-11-generated
package-lock.json with EUSAGE (pruned chokidar/readdirp optional peers),
so docker build / compose up --build failed from source. CI already pins
npm 11 for this; mirror it in the Dockerfile's angular-build stage.
…292 PR1) (#294)

PoracleNG reads `clean` as a 3-bit bitmask (1=auto-delete, 2=edit-in-place,
4=summary) but PoracleWeb treated it as a boolean for 8 of 10 alarm types,
which (a) 400'd bot-set values >1 via [Range(0,1)] and (b) clobbered bits
2/4 on every web edit. This is the invisible correctness fix; the lure-edit
and quest-summary user controls follow in PR2.

- New CleanFlags helper (Core.Models) + clean-flags.ts twin with
  Preserve(existing, mask, changes) read-modify-write; unit tests for both.
- Widen Clean [Range(0,1)] -> [Range(0,7)] on the 16 Monster/Quest/Invasion/
  Lure/Nest/Gym/MaxBattle/FortChange Create+Update models (Raid/Egg already done).
- CleaningService: preserve unknown bits on bulk-clean toggle; bit-aware AllClean.
- All 10 alarm types' edit dialogs preserve unknown bits on save and read
  auto-delete via the bit; list cards + profile overview gate the badge via
  an isAutoDelete() method (Angular templates can't parse bitwise &).
- Fix the raid/egg RSVP save dropping bit 4; fix the quick-pick apply clobber.
- Tests: CleanFlags, clean-range validation (8 types), CleaningService bit
  preservation, mapping round-trip. Backend 1324 pass; frontend 743 pass.
…PR2) (#295)

* feat(alarms): lure edit-in-place + quest daily-summary controls (#292 PR2)

Surfaces the two remaining meaningful clean bits as user controls, building
on PR1's CleanFlags helper and preservation fixes.

- Lure add+edit dialogs: "Edit message in place" toggle (clean bit 2),
  default off, edit-dialog inits from isEdit() and save composes via
  preserve(clean, AUTO_DELETE|EDIT, ...) so the summary bit survives.
- Quest add+edit dialogs: "Daily summary" toggle (clean bit 4), default off,
  same preserve pattern; hint notes it needs a summary schedule on the bot.
- Card badges: lure edit (--mat-sys-secondary), quest summary
  (--mat-sys-tertiary), gated via methods, matching .clean-tag/.rsvp-tag.
- i18n: LURES.EDIT_* / QUESTS.SUMMARY_* added + translated in all 11 locales.
- Only lure/quest get controls (the only types whose PoracleNG processor
  reads the bit). New dialog specs cover init-from-bit + save-preserves-bits.

Frontend: prettier/eslint/build clean, jest 781 pass. No backend change.

* docs(#292): document clean delivery modes (alarms doc, in-app help, CLAUDE.md)

- docs/features/alarms.md: new 'Delivery & message modes' section covering
  the clean bitmask (auto-delete / edit-in-place / daily-summary), which
  types support each, and that PoracleWeb preserves bot-set bits (gh-pages).
- In-app Help (HELP.CONTENT_DELIVERY): appended an 'Edit in place & summaries'
  block describing the lure edit + quest daily-summary modes, translated in
  all 11 locales.
- CLAUDE.md: dev note on the clean bitmask + CleanFlags helper + the Angular
  'templates can't parse &' gotcha.

* docs(rsvp): document RSVP notification mode (alarms doc + in-app help, 11 locales)

Mirrors the #292 delivery-mode docs for RSVP (#233), authored by a
tech-writer swarm:
- docs/features/alarms.md: new '### RSVP updates (raids & eggs)' subsection
  under Delivery & message modes, explaining the 3 modes + edit-in-place
  coupling + the scanner caveat.
- In-app Help (HELP.CONTENT_DELIVERY): appended an 'RSVP updates' block in
  all 11 locales, alongside the edit/summary block.

Jest 781 pass; all 11 i18n files valid JSON.
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 7 to 8.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](peter-evans/create-pull-request@v7...v8)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [marocchino/sticky-pull-request-comment](https://github.com/marocchino/sticky-pull-request-comment) from 2 to 3.
- [Release notes](https://github.com/marocchino/sticky-pull-request-comment/releases)
- [Commits](marocchino/sticky-pull-request-comment@v2...v3)

---
updated-dependencies:
- dependency-name: marocchino/sticky-pull-request-comment
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 9.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](actions/github-script@v7...v9)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps Microsoft.NET.Test.Sdk from 18.5.1 to 18.6.0

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: microsoft
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the eslint group in /Applications/Pgan.PoracleWebNet.App/ClientApp with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.59.4` | `8.60.1` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.59.4` | `8.60.1` |
| [@typescript-eslint/utils](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils) | `8.59.4` | `8.60.1` |
| [eslint-import-resolver-typescript](https://github.com/import-js/eslint-import-resolver-typescript) | `4.4.4` | `4.4.5` |
| [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) | `5.5.5` | `5.5.6` |


Updates `@typescript-eslint/eslint-plugin` from 8.59.4 to 8.60.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.60.1/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.59.4 to 8.60.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.60.1/packages/parser)

Updates `@typescript-eslint/utils` from 8.59.4 to 8.60.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.60.1/packages/utils)

Updates `eslint-import-resolver-typescript` from 4.4.4 to 4.4.5
- [Release notes](https://github.com/import-js/eslint-import-resolver-typescript/releases)
- [Changelog](https://github.com/import-js/eslint-import-resolver-typescript/blob/master/CHANGELOG.md)
- [Commits](import-js/eslint-import-resolver-typescript@v4.4.4...v4.4.5)

Updates `eslint-plugin-prettier` from 5.5.5 to 5.5.6
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/main/CHANGELOG.md)
- [Commits](prettier/eslint-plugin-prettier@v5.5.5...v5.5.6)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.60.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: eslint
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.60.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: eslint
- dependency-name: "@typescript-eslint/utils"
  dependency-version: 8.60.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: eslint
- dependency-name: eslint-import-resolver-typescript
  dependency-version: 4.4.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: eslint
- dependency-name: eslint-plugin-prettier
  dependency-version: 5.5.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: eslint
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](actions/setup-node@v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
)

Surface the Poracle `notes` field on channel-type users in the admin user
list so admins can disambiguate channels that share the same name across
different servers. PoracleJS/PoracleNG can auto-fill notes with the Discord
guild name and channel category; that column already existed on the humans
table but was dropped at every layer.

Reuses data PoracleNG already provides — no new database queries and no live
Discord API calls:
- Single-user reads come from the PoracleNG human JSON (HumanService
  .DeserializeHuman now reads `notes`).
- The admin bulk list maps the `notes` column through the existing read
  (Human model + EntityMappingExtensions); projected by GET /api/admin/users
  and GET /api/admin/users/by-id.

Frontend renders notes as a muted second line under the name with a tooltip,
and the admin search box matches against it. A notesLabel() normalizer trims
whitespace and strips a surrounding quote layer so PoracleJS/NG's quoted-empty
`""` sentinel (and any JSON-quoted note) doesn't render a stray value.

Tests: extend HumanEntity.ToModel mapping test and the admin.service spec mock.
)

* feat: add admin setting to disable user-submitted geofences (#297)

From discussion #214: let operators turn off the custom/user-drawn
geofence feature via a new `disable_user_geofences` site setting,
reusing the existing `disable_*` feature-gate pattern.

Backend
- DisableFeatureKeys.UserGeofences constant.
- Gate the "provide a geofence" actions on UserGeofenceController
  (create, submit-for-review, GeoJSON import) with
  [RequireFeatureEnabled], plus a defense-in-depth
  IFeatureGate.EnsureEnabledAsync guard in UserGeofenceService
  .CreateAsync (also covers import, which funnels through it) and
  .SubmitForReviewAsync.
- Reads, delete, activate/deactivate and the admin review queue stay
  ungated so existing geofences keep working and are still served by
  /api/geofence-feed.
- SettingsMigrationService CategoryMap + BooleanKeys carry the key.

Frontend
- Hide the My Geofences nav item (disableKey) and guard the
  /geofences route (disabledFeatureGuard) -> redirect to dashboard
  with the existing ERROR.FEATURE_DISABLED toast; the 403 interceptor
  handles direct API hits.
- Admin settings toggle in the Features group.
- ADMIN_SETTINGS.DISABLE_USER_GEOFENCES_* label/description added and
  translated across all 11 locales.

Tests: UserGeofenceService gate tests for CreateAsync and
SubmitForReviewAsync; existing test setup updated for the new
IFeatureGate dependency.

* feat: hide admin "User Geofences" review queue when disabled

Extend disable_user_geofences to also hide the admin-facing geofence
review queue: add the disableKey to the /admin/geofence-submissions
nav item, make the adminNavItems computed honour isFeatureDisabled
(it previously ignored disableKey), and guard the route with
disabledFeatureGuard. Enabling the toggle now hides the whole feature
end to end. The admin review backend stays ungated so any pre-existing
submissions aren't bricked and reappear if the feature is re-enabled.
hokiepokedad2 and others added 30 commits August 9, 2026 14:18
Companion to poracleng-enhancement-requests.md, which tracks the v1-era
workaround gaps. This one evaluates whether v2 (PR #139, "huma") closes
them and concludes it does not: the trusted-setAreas hack, admin
list-all-humans, batch name resolution and full-purge delete all remain
blocked, so PoracleContext and the ten alarm entities cannot be deleted
without three explicit upstream asks.

Claude-Session: https://claude.ai/code/session_01CYyjpx2HzaZxMnYamkyoXX
…#624, #626) (#635)

GenerateTokenWithReplacedProfile ended in the WriteToken overload that applies
the configured 24-hour default, so every profile switch and every /api/auth/me
profile resync restarted the clock. An OIDC login issues a deliberately short
30-minute access token precisely so revocation propagates within about one
lifetime; one profile switch turned it into a day. A user who switched profile
once a day never expired at all. The re-issue now carries the principal's own
exp through.

The isAdmin claim was copied verbatim by the same method, and nothing anywhere
revalidates it -- every admin check is a claim read. Removing someone from the
admin list therefore had no effect for as long as they kept switching profile.
Each re-issue now resolves it live.

Resolution moved out of AuthController, where login was the only caller, into
IUserRoleResolver. That also fixes the other half: since #601 the admin
endpoints resolved delegated webhooks from PoracleWeb's own table alone, while
the JWT claim is the union of that table and PoracleNG's delegate list. A
delegate configured in PoracleJS saw the My Webhooks nav item, an empty page,
and a 403 from impersonate. Both now ask the same question.

Claude-Session: https://claude.ai/code/session_01CYyjpx2HzaZxMnYamkyoXX
…#636)

storeTokens wrote the refresh token only when the new login supplied one and
never removed an existing value, so a Discord or Telegram login on a browser
that had held an OIDC session inherited that session's refresh token. The
refresh interceptor then saw hasRefreshToken() true, posted the stale token,
and replaced the JWT with one minted for the previous user. handleAuthResponse
bypassed the store entirely, which is how the Telegram path got there.

The 401 path cleared tokens but left AuthService holding a user and an
impersonation flag, because an interceptor cannot inject AuthService without
constructing it -- and constructing it fires /api/auth/me. Teardown therefore
lives on the token store, which announces it; AuthService listens and forgets
the user. Without that the app rendered the login page inside the signed-in
shell, complete with a "Viewing as" banner whose Stop button entered
stopImpersonating(), found no admin token, and returned in silence.

The login page also keyed its redirect off currentUser rather than the token,
so it sent the user to /dashboard, authGuard sent them back, and because the
router reuses the component the OIDC auto-redirect never ran.

Claude-Session: https://claude.ai/code/session_01CYyjpx2HzaZxMnYamkyoXX
…637) (#638)

Seeding the built-in quick picks has been throwing since #604 added save-time
filter validation: all-invasions and invasion-leader carry empty filter sets
on purpose, because ApplyInvasionAsync fans them out across grunt types at
apply time, and the sample alarm built from those empty filters has no
grunt_type. SeedDefaultsAsync goes through the same method, so it aborted at
sort order 50 -- both the first-visit auto-seed and Reset to Defaults left a
partial preset list. A definition with no filters now has nothing to check.

The admin Settings page hid any group whose keys had no row yet, and a fresh
install seeds exactly one. Alarm Types, Features, Administration and Analytics
were therefore unreachable, and since this page is the only writer, the rows
could never appear.

Reset to Defaults deleted global definitions without clearing applied state,
unlike the single-pick delete, so every user kept a row naming a definition
that no longer exists -- never listed, never cleaned, and the alarms it owned
lost their Remove button.

The quick pick dialog decided scope from who was saving rather than from the
pick, so an admin editing their own personal pick republished it to everyone;
the admin save also had no ownership guard and would take over anybody's.

The admin Users tab kept only Discord accounts, so a Telegram user appeared in
neither list and no admin action could reach them.

Toggling Discord off and Telegram on in one save raced the anti-lockout guard,
which reads the other key from the database. Enables are applied first.

Deleting every global quick pick was undone by an auto-reseed on the next
visit, with no way to keep an empty list.

Claude-Session: https://claude.ai/code/session_01CYyjpx2HzaZxMnYamkyoXX
The template selector's Default option is an empty string, and nine of the ten
edit dialogs sent `values.template || null`. ApplyUpdate skips nulls by
design, so choosing Default was accepted, reported as saved, and thrown away.
max-battle already sent '' and worked, which showed the intent.

MonsterService sent { distance } to an endpoint that binds [FromBody] int, so
"Update Distance (all)" 400'd every time on the Pokemon page while working on
all nine other types. Its own spec asserted the broken shape, which is how it
survived.

Bulk "Update Distance" was an unguarded await on every list: a refusal cleared
nothing, reloaded nothing, and showed nothing -- the same outcome as success.
The server names which alarm a radius would take over; that message was thrown
away twice, since the interceptor has no 409 case either. Three lists had no
failure string at all, so LURES/NESTS/GYMS.SNACK_FAILED_DISTANCE is added in
all eleven locales.

Raids' Select All added every egg key too, while the toolbar sits above the
tab group and shows one count -- so Delete took alarms the user could not see.

The Areas page treated a failed read of the current selection as "no areas
selected", then offered to save. One failed request destroyed every
subscription the user had.

Claude-Session: https://claude.ai/code/session_01CYyjpx2HzaZxMnYamkyoXX
The geofence feed served only active and pending_review rows, so an admin
rejecting a submission dropped the polygon out of PoracleJS's view while the
name stayed in humans.area and every profiles.area, and the card still read
Active. Alerts stopped with no notice, and the fence could not be resubmitted
while still consuming one of the owner's ten slots. CLAUDE.md already said
rejected "remains private with review notes"; now it does.

Rename had no status guard. After approval Koji owns the fence under
PromotedName and the area lists hold that name, so renaming rewrote the
subscription to a name nothing serves and quietly unsubscribed the owner from
a live public area. It also treated the dialog's parentId of 0 as a request to
clear the region, rather than as "nothing selected" -- and the dialog always
sent 0, because it looked the current region up by `name` when creation had
stored `displayName`.

POST /api/profiles binds the domain model, where no validation attributes
live, and wrote the submitted area list and coordinates straight to the
profile row. Coordinates now carry the same bounds as the location endpoint,
and another user's private geofence name is dropped from the area list --
PoracleNG's own setAreas filter would have stripped it, but this path does not
go through setAreas.

The admin UI gated Review on pending_review while ApprovableStatuses has
always included rejected, so a mistaken rejection was irreversible from the
SPA. The region selector declared a selectedValue input and never read it, so
the approval dialog showed an empty picker while submitting the seeded id.
Deleting a profile did not refresh the session, leaving the profileNo claim
naming a profile that no longer exists.

Claude-Session: https://claude.ai/code/session_01CYyjpx2HzaZxMnYamkyoXX
…653)

About one in five defects found in the audit sweeps was caused by an earlier
fix in the same campaign. They cluster into two shapes.

The first is tightening a rule without enumerating who depended on the loose
one: #601 locked out PoracleJS-configured delegates, #604 broke seeding on two
presets that carry empty filters deliberately, #616 left a Stop button that
did nothing, and the alarm collision guard over-refused three times running.
An invasion grunt-type allowlist would have joined them -- blanche, npc 0 and
player team leader are all live in production -- and was only caught because
the database happened to get queried first. That should not be luck, so
querying production before tightening is now the written rule.

The second is fixing one member of a set of ten and leaving the other nine.

Also records why tests written alongside a fix do not prove it: three fixes
validated the domain model rather than the DTO the controller binds and passed
anyway, and one spec asserted the broken request shape outright.

Claude-Session: https://claude.ai/code/session_01CYyjpx2HzaZxMnYamkyoXX
…) (#655)

save() rebuilt the filter object from scratch and skipped falsy values, so a
stored 0 was lost. The built-in nundo preset is { minIv: 0, maxIv: 0 } -- that
is the whole of it -- and an admin changing only its description saved a
definition with no IV bounds at all. "Nundo" then meant "any Pokemon" for
every user who applied it afterwards, with nothing to show anything had
changed.

Rebuilding rather than merging also dropped any stored key the dialog has no
control for. Nothing in live data hits that today, but quick-pick-apply reads
filters['clean'] as its base bitmask and no form exposes it.

The obvious fix -- keep every value including 0 -- would have caused the
mirror-image bug, because most of these controls default to 0 meaning "not
set": every newly created pick would carry an explicit minIv, pokemonId and
pvpRankingLeague of 0. So a 0 is kept when it was already stored and skipped
when it was not, and the create path behaves exactly as before.

Claude-Session: https://claude.ai/code/session_01CYyjpx2HzaZxMnYamkyoXX
)

Found by a regression lens -- an audit whose only question is what the last
day's merges broke. Every one of these was caused by a fix shipped today.

The worst: UserRoleResolver could not tell "resolved: not an admin" from
"could not resolve", and #635 wired that into every token re-issue. A
momentary PoracleNG outage during a profile switch therefore stripped admin
for the rest of the session and cached the answer for a minute. Before #635
the claim was copied forward and an outage could not demote anyone. The
resolver now reports unresolved, callers pass null to leave the claim alone,
and a degraded answer is never cached.

The rename status guard from #652 threw InvalidOperationException past a
controller that catches three other types, so it surfaced as a 500 -- and the
button was still rendered on every status. The profile-create area filter from
the same PR denied approved geofences, which are public Koji areas anyone may
select, so a legitimate area silently vanished from the new profile.

#638 added an ownership guard to SaveAdminPickAsync and fixed seeding in the
same commit, without noticing that seeding goes through that method: one
user-scoped pick holding a built-in id aborted the seed partway, which is the
failure the same commit had just fixed. Its localStorage seed marker was a
per-browser flag for an installation-level fact, so a second admin reseeded
anyway and a failed seed latched it forever; it is a site setting now, written
by the endpoint that does the seeding.

#614 bounded FortChangeCreate.ChangeTypes and left FortChangeUpdate beside it,
and set MaxGruntTypeLength to 35 while calling it "the column width upstream"
-- the column is varchar(255). An invented limit in the one fix whose whole
rationale was refusing the impossible instead.

Claude-Session: https://claude.ai/code/session_01CYyjpx2HzaZxMnYamkyoXX
Second run of the regression lens, over the fixes the first run produced.

The worst is mine from an hour ago. ProfileController.Create validates the
location and the area list AFTER calling AddProfileAsync, so both refusals
answer 400 with the profile already created -- and because addProfile ignores
`area`, that orphan comes up carrying the active profile's entire area list
and location, the inheritance #563 exists to prevent. A retry makes another.
So #647 replaced "a 201 that silently dropped areas" with "a 400 plus a
profile nobody mentioned". Both checks now run before the create, and the
create body carries the filtered list.

The quick-pick seed marker moved to a site setting, and the local signal that
reads it is only refreshed at app init -- so within the seeding session the key
stayed absent and deleting the last pick restored all thirty. That is #634
again, minus the synchronous localStorage write that used to mask it. There
was also no backfill, so an installation seeded under the old marker would
reseed once after upgrading. Both closed, and the guard finally has the
frontend tests it shipped without, which is why this got through.

UserRoleResolver tracked two of its three sources, so a poracle_web blip
returned a confident answer with an incomplete webhook list and cached it.

The new sentinel also rendered in the admin settings page as an editable
unknown setting, because that list is maintained separately from the
controller's; migration_completed is in both and only one was updated.

Claude-Session: https://claude.ai/code/session_01CYyjpx2HzaZxMnYamkyoXX
…672) (#673)

Third run of the regression lens. Two findings, both small -- the curve across
the three passes is 8, 5, 2, and this is the first pass with no regression of
consequence and no missed siblings.

The sameType gate added for #669 cleared every stored key on a type change,
not just the departing type's filters. `clean` is read by the apply dialog as
its base bitmask and is exposed by no form, so changing a pick's type silently
reset its auto-delete, edit and summary bits -- the preservation #654 added,
undone two commits later by the fix for #669. Type-agnostic keys now survive;
`clean` is the only one.

The backfill docstring is also corrected. It claimed to spare an admin who had
deliberately deleted the presets before upgrading; that admin has no global
picks, so there is nothing to infer from and they still get one reseed. The
inline comment said so honestly while the docstring above it did not. The
behaviour is right -- the old marker lived in a browser the server cannot see
-- so only the documentation changes.

Claude-Session: https://claude.ai/code/session_01CYyjpx2HzaZxMnYamkyoXX
…674) (#675)

Fourth run of the regression lens. One finding, and it is against the comment
the third run's fix shipped with rather than against its logic.

TYPE_AGNOSTIC_FILTER_KEYS asserted that `clean` was the only key belonging to
no alarm type. The codebase says otherwise in its own whitelist --
SafeMonsterFilterKeys lists form, gender, clean, template, distance, ping --
and Ping, Template and Distance are properties of all ten alarm models. They
meet the exact test that justified preserving `clean`: read by a consumer,
exposed by no per-type form, meaningful everywhere. `ping` is never overridden
at apply time, because the apply dialog has no ping control.

So changing a pick's type dropped its ping target and template. Not reachable
in production today -- all thirty live definitions are the built-in presets and
none carries any of the four keys -- but the claim was wrong in the direction
that loses a user's settings, and the next definition created outside the admin
dialog would have hit it.

Claude-Session: https://claude.ai/code/session_01CYyjpx2HzaZxMnYamkyoXX
The lens found sixteen defects across five passes and existed only as an
ad-hoc prompt typed each time, plus one sentence of prose in CLAUDE.md. It
would have had to be re-derived from scratch next session, and the details
that made it work are not obvious: scoping each pass to one batch of fixes,
seeding it with the shapes it keeps finding, and telling it explicitly that a
clean result is the desired outcome so it does not manufacture findings.

.claude/ was ignored wholesale. Now only its contents are, with commands/
re-included -- git cannot re-include anything beneath an excluded directory,
so the pattern had to become .claude/* for the negation to take effect.
settings.local.json, worktrees/ and the task lock stay local.

Claude-Session: https://claude.ai/code/session_01CYyjpx2HzaZxMnYamkyoXX
#677)

* fix: the nightly prune was deleting released images, including :latest

GHCR currently holds `beta` and 28 `develop-<sha>` tags and nothing else. No
`latest`, no `2.13.0`, no version tags at all -- so every documented
`docker compose up` fails to pull for anyone not already running an image.
docker-publish.yml ran successfully on the v2.13.0 release and pushed those
tags; the nightly prune then removed them.

Cause: actions/delete-package-versions matches `ignore-versions` against a
package version's NAME, and for container packages the name is the digest,
never the tag. The ignore list protected nothing, so `min-versions-to-keep: 10`
deleted every version older than the ten most recent pushes whatever it was
tagged. Twenty merges to develop after a release was enough to take the
release with it. The pattern was also written for `v`-prefixed semver while
docker/metadata-action strips the `v`, so it would not have matched even if
ignore-versions worked on tags.

Rewritten as github-script, matching the prune-pr-images job above it: read
metadata.container.tags and delete a version only when EVERY tag on it looks
like develop-<sha>. A develop push tags one digest both `beta` and
`develop-<sha>`, and a release tags one digest `latest`, `X.Y.Z`, `X.Y` and
`<sha>` -- checking every tag is what keeps those safe. Ordering is sorted
explicitly rather than assumed from the API, since which images get deleted
depends entirely on it.

Claude-Session: https://claude.ai/code/session_01CYyjpx2HzaZxMnYamkyoXX

* docs: changelog entry for the prune fix

Claude-Session: https://claude.ai/code/session_01CYyjpx2HzaZxMnYamkyoXX
…elease (#678)

A release publishes :latest and redeploys production, so whatever the docs
claim at that moment is what self-hosters act on. Auditing the whole surface
against the code turned up a lot that had drifted, including several things
this session's own fixes had invalidated.

Install and setup instructions that did not work: the stable tag is :X.Y.Z,
not :vX.Y.Z, because docker/metadata-action strips the leading v; the release
page has no prebuilt archives to download, only source; `npm start` was plain
`ng serve` with no proxy, so the documented dev workflow 404'd every /api call
(fixed in package.json rather than in prose, since the docs described the
intent); and the dev Discord redirect URI is the dev server's origin, not the
API's, because the callback is built from the incoming Host header.

Behaviour this session changed: the By Boss tab no longer has a level picker,
a rejected geofence keeps alerting, approved and pending geofences cannot be
renamed, telegram_bot is now a fallback for the login widget, and
quick_picks_seeded exists. Test alerts were documented for all ten alarm types
and support eight. Profile duplication was documented as NOT copying areas
when #503 made it copy them -- in two docs and in the in-app help, in eleven
locales.

The in-app help also still described a Ping / Role Mentions control deleted in
#494, listed Fort Changes as cleanable when the API refuses it, and called the
admin Users tab Discord-only after #632 made it show every account. All eleven
locale files carried each of those.

Architecture docs claimed PoracleNG handles the area dual-write (it does not,
for user geofences), that direct DB access is only for admin bulk operations
(profile rename and the area writer are neither), and documented a
ValidateActiveHours method that moved to Core.Models/ActiveHoursValidator.

CI/CD docs said docker-publish.yml runs on push to main. It runs on release
published and pushes to develop -- and on release it also SSHes to DEPLOY_HOST
and redeploys production, which was documented nowhere and contradicts the
watchtower note in CLAUDE.md.

Contributors were told to clone and had no indication that work happens on
develop; the branch table now lives in the docs tree rather than only README,
because mkdocs cannot resolve links outside docs_dir.

Claude-Session: https://claude.ai/code/session_01CYyjpx2HzaZxMnYamkyoXX
…679)

The screenshot dated from April and showed Pause Alerts, Switch Profile,
Areas & Location, Cleaning, Accent Theme and Logout. The menu has since gained
Alert Defaults and a Language submenu, both of which are documented features --
the language selector is described elsewhere as living in this exact menu.

Captured against a local mock API rather than a real instance. The .env on this
machine points at production: booting the real API would run MigrateAsync and
the settings migration against the production database, and any screenshot
would put a real person's name, location and area list into a public repo. The
mock serves synthetic demo data matching the original's framing (DocUser, Work
Profile, the same viewport), so only the menu differs. Same 1440x900 as every
other help image.

Two visible differences from the old shot are configuration, not drift: the
location card has no map thumbnail because the mock serves no tileserver URL
and the img is conditionally rendered, and the toolbar has no custom-link icon
because that is an operator-set site setting.

The logout section also never mentioned "Sign out everywhere", the single
logout entry that appears when the provider supports it. Added in all eleven
locales, each quoting its own MENU.LOGOUT_EVERYWHERE label so the help matches
the button the reader is looking at. Verified rendering in English and German
in a browser, not just in the JSON.

Claude-Session: https://claude.ai/code/session_01CYyjpx2HzaZxMnYamkyoXX
I claimed a few hours ago that publishing a release SSHes to DEPLOY_HOST and
redeploys prod directly, "not watchtower", and changed CLAUDE.md to match. That
was wrong, and it is the exact failure this codebase's own guidance warns about:
I read the SSH step in docker-publish.yml and asserted a conclusion wider than
the evidence, without checking whether the secret it depends on exists.

It does not. `gh secret list` returns only the changelog app credentials, so the
step hits its `if [ -z "$DEPLOY_HOST" ] ... exit 0` guard on every release and
does nothing. Prod is deployed by watchtower polling :latest every 60 seconds,
exactly as the original note said. Verified on the host: the watchtower
container is up with WATCHTOWER_SCOPE=poracleweb and a 60s interval.

The SSH step is still worth documenting, but as what it is -- an opt-in hook for
anyone who prefers a push deploy to a polling agent.

Claude-Session: https://claude.ai/code/session_01CYyjpx2HzaZxMnYamkyoXX
Co-authored-by: hokiepokedad2 <38219945+hokiepokedad2@users.noreply.github.com>
* docs: document PROXY_KNOWN_PROXIES/PROXY_KNOWN_NETWORKS

The 2.14.0 proxy-trust fix (#583) stopped honouring X-Forwarded-Proto from
undeclared proxies. Instances that had never set these variables started
building OAuth callback URLs as http://, and Discord rejected sign-in with
an invalid redirect_uri. The variables existed but appeared in no example
file, guide or reference.

Adds them to .env.example, the configuration reference, the reverse-proxy
section of standalone-setup, and a troubleshooting entry naming the exact
Discord error.

* feat: let deployments set the OAuth callback URL with PUBLIC_URL

Callback URLs were derived from Request.Scheme/Host at nine sites in
AuthController, so the only way to influence them was to declare the reverse
proxy -- an indirect lever for a value the identity provider requires you to
register anyway. Discord:RedirectUri looked like the setting for this but was
read by nothing.

PUBLIC_URL names the origin outright, feeding one SelfOrigin() helper used by
both callback sites per provider, so the authorize request and the token
exchange cannot drift apart. Unset behaviour is byte-identical to before, which
keeps multi-hostname installs working -- deriving it from CORS_ORIGIN was
considered and rejected, since the SPA calls the API same-origin and that value
is never browser-validated, so it can name a host nobody uses.

The cookie Secure flag deliberately still follows the actual request: forcing it
from an https PUBLIC_URL would silently drop oauth_state for anyone reaching the
instance over plain http on a LAN.

Adds a warning when a callback is about to go out as http:// while an untrusted
X-Forwarded-Proto: https is present -- the exact failing case, named, with both
fixes in the message.

Startup rejects an unusable PUBLIC_URL rather than emitting a callback the
provider refuses without explanation.

* docs: reference #689 in the changelog entries
)

Bumps [jsdom](https://github.com/jsdom/jsdom) from 29.1.1 to 30.0.1.
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Commits](jsdom/jsdom@v29.1.1...v30.0.1)

---
updated-dependencies:
- dependency-name: jsdom
  dependency-version: 30.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
No target-branch was set on any of the four ecosystems, so all of them
defaulted to the repository default branch. Dependency bumps therefore landed
straight on main -- released code -- without being built as :beta or running on
the dev instance first, which contradicts the branching model in CLAUDE.md
where main only moves when a release is merged.

It had already drifted: 7366906 (jsdom) sits on main and not on develop.

It also routed every bump through the merge queue that exists only on main,
which is where #683 and #686 are currently stuck despite being approved with
all required checks green.
…#691)

The Discord and SSO setup pages describe the callback as derived from the
incoming request, which stopped being the whole story when PUBLIC_URL landed.
Both are where someone registers the URI with their provider, so that is where
the reverse-proxy caveat belongs.
Every help screenshot with the toolbar in it read "PoGO Alerts Network",
and login.png showed it again as the 32px splash heading. PoracleWeb is
self-hosted, so a self-hoster reading Help saw another network's name
beside instructions about their own install.

They now read "DM Alerts" -- not an invented placeholder but the app's
own fallback, the string app.ts and AUTH.SITE_TITLE_DEFAULT produce when
custom_title is unset. So the screenshots show what an unbranded install
actually shows.

Re-capturing was not possible (the shots are of a populated production
account), so the title was replaced in place: the gradient behind it is
rebuilt by interpolating between clean rows above and below the text, and
the replacement glyphs were rendered by the same browser engine that took
the screenshots. The two typefaces differ and both were confirmed by
measuring the original ink: the toolbar title is Roboto 500 16px (151px
wide, Roboto measures 152, Plus Jakarta Sans 164) and the login heading is
Plus Jakarta Sans 300 32px with 1px tracking (343px wide, PJS measures 346,
Roboto 315). Placement is fitted by minimising residual against the
original rather than assumed. sidenav.png is untouched -- it is cropped
below the toolbar and has no title in it.

The second half: the shots are captured at 1440px and drawn in a ~770px
column, so the detail each one exists to point at rendered at half size.
Clicking a screenshot now opens it full size in a dialog.

The section bodies are injected with [innerHTML] from the translation
bundles, so no per-image binding is possible -- click and keydown are
delegated from the host, and an afterRenderEffect marks each screenshot
focusable with role=button. It re-runs on language change and on search,
both of which replace that HTML. Enter and Space open the viewer, so the
feature is not mouse-only.

Closes #694
The map fitted the bounds of every fence in the geofence feed. On a single-city
instance that looks like a sensible default; prod's feed carries 830 fences from
Kapolei, Hawaii to Summerland Point, Australia, so the fit is the whole planet.
At that zoom no polygon is more than a pixel and none of them can be clicked --
the page's one job. The feed only grows, so no zoom constant fixes this.

The opening view is now ranked: the areas you have selected, then your own
geofences, then your pinned location at zoom 11, and only then everything. A fit
is capped at zoom 13 so a small user-drawn geofence does not open at street
level with no context to work against.

Two details carry the regression risk, so the ranking lives in a pure function
with its own tests:

A fit only ever upgrades. The Areas page loads the feed and the selection from
independent requests -- the map is routinely on screen before the selection
lands -- so without an upgrade the map would fit everything, mark itself done,
and never recover, intermittently, depending on which response won.

Because a fit only upgrades, a selection changing at the same rank cannot move
the map, so toggling an area still only restyles it. Re-fitting on every toggle
would yank the map out from under the cursor.

Closes #693
The other half of #694. All 41 shots under docs/screenshots/ that show a
toolbar read "PoGO Alerts Network", and login.png showed it again as the
splash heading, so the documentation illustrated how to host your own
instance with pictures of somebody else's.

They now read "DM Alerts" -- the app's own fallback when custom_title is
unset, so the docs show what a fresh install shows.

Same in-place replacement as #696: the gradient behind the text is rebuilt
by interpolating between clean rows above and below it, and the glyphs were
rendered by the browser engine that took the shots. Three layouts, each
measured rather than assumed:

- 34 desktop captures at 1440x900 plus the 1440x64 toolbar strip, title at
  x97-247 y19-30. Nine *-add-dialog.png carry the same title under the
  dialog scrim; the replacement is drawn in the dimmed colour sampled from
  the original rather than in white.
- 6 mobile captures at 390x844. The toolbar is shorter and drops the
  subtitle, so the title sits two rows lower at x85-235 y21-32.
- login.png, splash heading at x549-891 y350-374.

sidenav.png is untouched -- it is cropped below the toolbar.

Closes #697
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants