Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ export const HELP_SECTIONS: HelpSection[] = [
subtitleKey: 'HELP.SECTION_ALERTS_LOGOUT_SUB',
titleKey: 'HELP.SECTION_ALERTS_LOGOUT',
},
{
id: 'quiet',
contentKey: 'HELP.CONTENT_QUIET',
icon: 'notifications_paused',
iconColor: '#5c6bc0',
subtitleKey: 'HELP.SECTION_QUIET_SUB',
titleKey: 'HELP.SECTION_QUIET',
},
{
id: 'faq',
contentKey: 'HELP.CONTENT_FAQ',
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Documentation

- **The PoracleNG version compatibility page has been rewritten, and its premise replaced.** It described PoracleNG as having two long-lived branches and this site supporting both; develop merged and shipped, so the real question is 5.1.0 versus 5.2.0 and newer. It now covers how support is decided, why a database migration number is a better thing to gate on than a version string, which features need a newer server, how to add another, and two traps worth knowing: PoracleNG's v1 API is unchanged on 5.2.1 -- the new error format and status codes in its release notes apply to v2 only -- and its OpenAPI document numbers the days of the week differently from the scheduler that reads them.
- **The documentation has been audited against the code and brought up to date with everything that landed this cycle.** Nine changes shipped and none of them were written down, so the pages a self-hoster reads described a site with ten alarm types, five quest reward tabs, no costume filter and no way to quiet anything. Corrected throughout: the alarm type tables and feature lists now carry all eleven types, the test-alert coverage arithmetic is right in the three places that state it, the scope chip is on seven card types rather than six, and the claim that a Pokemon edit keeps its rule id is gone -- it stopped being true when edits moved to the newer write surface. **Quiet periods have a page of their own**, covering what they are, how they differ from *Pause Alerts*, why one rule cannot be quieted on its own, and that a restart of the Poracle processor clears them. Costume filters, PokéCoin rewards, Pokéstop Events, rule descriptions and repeating schedule rules are all written up where they belong, each with the version or database migration it needs.
- **Every version requirement in the docs said 5.2.1 and every gate in the code says 5.2.0.** 5.2.1 is the release production happens to run, not the release that added the features, so a self-hoster on exactly 5.2.0 was told four working features were unavailable to them. Corrected on the compatibility page, and the page now names the three constants it is describing so the next drift is visible.
- **The in-app help covers the new features too, in all eleven languages.** It gained a Quiet Periods section, an Active Hours section -- which the help had never explained despite the quest summary page referring readers to it twice -- and entries for costume filters, PokéCoin rewards, Pokéstop Events and the rule description on an alarm card. The FAQ's "why am I not getting alerts" list now includes a quiet period as a cause, since that one produces silence with no banner to spot, and gained an entry for the controls that appear only on a new enough Poracle server, which is the likeliest reason something the guide describes is not on your screen.
- **Screenshots refreshed.** The dashboard, the Pokemon list, the profiles page, the quest dialog and the server versions card all showed a site that no longer exists, and new ones were taken for the Pokéstop Events dialog, the quiet sheet, the PokéCoins tab and a repeating schedule rule.
- **The PoracleNG version compatibility page has been rewritten, and its premise replaced.** It described PoracleNG as having two long-lived branches and this site supporting both; develop shipped as 5.2.1 and merged, so the real question is 5.1.0 versus 5.2.1 and newer. It now covers how support is decided, why a database migration number is a better thing to gate on than a version string, which features need a newer server, how to add another, and two traps worth knowing: PoracleNG's v1 API is unchanged on 5.2.1 -- the new error format and status codes in its release notes apply to v2 only -- and its OpenAPI document numbers the days of the week differently from the scheduler that reads them.

## [2.17.1] - 2026-08-21
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# PoracleWeb.NET

A web application for managing Pokemon GO notification alarms through the [PoracleNG](https://github.com/jfberry/PoracleNG) bot. Users authenticate via Discord OAuth2 or Telegram and configure personalized alert filters (Pokemon, Raids and Eggs, Max Battles, Quests, Invasions, Lures, Nests, Gyms, Fort Changes) through a browser-based UI.
A web application for managing Pokemon GO notification alarms through the [PoracleNG](https://github.com/jfberry/PoracleNG) bot. Users authenticate via Discord OAuth2 or Telegram and configure personalized alert filters (Pokemon, Raids and Eggs, Max Battles, Quests, Invasions, Pokéstop Events, Lures, Nests, Gyms, Fort Changes) through a browser-based UI.

> **PoracleNG 5.1.0 or newer is required.** All alarm management, profile handling, and user operations are proxied through PoracleNG's REST API. On an older server, per-alarm delivery scope, the PVP mega evolution filter and the minimum time-left filter write columns that don't exist: the controls accept input, save, and change nothing. PoracleWeb logs an error at startup and shows the mismatch on the Versions card under Admin > Settings.
>
> Some newer features — quiet periods, Pokéstop Event alarms, PokéCoin quest rewards, costume filters — need a server newer than 5.1.0, and hide themselves rather than failing when they do not find one. Which feature needs what, and how support is decided, is in [PoracleNG Version Compatibility](https://pgan-dev.github.io/PoracleWeb.NET/architecture/poracleng-compatibility/).
>
> [PoracleJS](https://github.com/KartulUdus/PoracleJS) is not a tested or supported configuration — some operations that rely on PoracleNG-specific endpoints will not work.

**[Documentation](https://pgan-dev.github.io/PoracleWeb.NET/)** | **[Changelog](CHANGELOG.md)**
Expand Down Expand Up @@ -44,12 +46,13 @@ See the [Quick Start guide](https://pgan-dev.github.io/PoracleWeb.NET/getting-st

## Features

- **Alarm Management** — Pokemon, Raids and Eggs, Max Battles, Quests, Invasions, Lures, Nests, Gyms, Fort Changes
- **Alarm Management** — Pokemon, Raids and Eggs, Max Battles, Quests, Invasions, Pokéstop Events, Lures, Nests, Gyms, Fort Changes
- **Gym Picker** — Search and target specific gyms for team change, raid, and egg alarms
- **Bulk Operations** — Multi-select with bulk delete and distance update
- **Per-Alarm Delivery Scope** — Aim each alert anywhere in your areas, at only specific areas, or within a radius of your pin or a saved place
- **Saved Places** — Name the points your alerts measure from, so an alarm doesn't have to follow your profile pin
- **Test Alerts** — Send yourself a sample notification for any alarm to check its filters and template
- **Test Alerts** — Send yourself a sample notification for an alarm to check its filters and template (every type except Fort Changes, Max Battles and Pokéstop Events)
- **Quiet Periods** — Silence one gym, area, species or Power Spot for a set time, without touching the alarms themselves
- **Alert Defaults** — Choose where new alerts default to reaching you: your areas, or a radius from your pin or a saved place
- **Custom Geofences** — Draw polygons, auto-served to the Poracle bot via unified feed
- **Geofence Admin Review** — Approve/reject with Discord forum integration
Expand Down
11 changes: 11 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ PoracleWeb.NET publishes three Docker image channels on GHCR. Pick one based on

Image registry: [`ghcr.io/pgan-dev/poracleweb.net`](https://github.com/PGAN-Dev/PoracleWeb.NET/pkgs/container/poracleweb.net).

## What `:beta` now expects of PoracleNG

The minimum is still PoracleNG 5.1.0, but three features on the beta channel need **5.2.0 or newer**:
Pokéstop Events, quiet periods (the mute chip on alarm cards), and Pokécoin quest rewards.

On an older PoracleNG they are simply not there — no page, no chip, no Pokécoins tab, no error and no
explanation. That is deliberate: the capability checks fail closed, and there is nothing a user can do
about their operator's version. So if you are testing against 5.1.0 and one of those three is missing,
that is the expected behaviour and not worth an issue. **Admin → Settings** shows the version the
deployment is talking to.

---

## Switching channels
Expand Down
72 changes: 67 additions & 5 deletions docs/architecture/backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

## Alarm services (PoracleNG API proxy)

All alarm tracking services (`MonsterService`, `RaidService`, `EggService`, `QuestService`, `InvasionService`, `LureService`, `NestService`, `GymService`, `FortChangeService`, `MaxBattleService`) use `IPoracleTrackingProxy` to proxy CRUD operations through the PoracleNG REST API. They do **not** use repositories or direct database access.
Ten alarm tracking services (`MonsterService`, `RaidService`, `EggService`, `QuestService`, `InvasionService`, `LureService`, `NestService`, `GymService`, `FortChangeService`, `MaxBattleService`) use `IPoracleTrackingProxy` to proxy CRUD operations through the PoracleNG REST API. They do **not** use repositories or direct database access.

`PokestopEventService` is the eleventh, and the exception: Pokéstop Events have no v1 route, so it uses `IPoracleIncidentProxy` against `/api/v2/humans/{id}/tracking/incident` instead. It never touches `IPoracleTrackingProxy`.

See [PoracleNG API Proxy](poracleng-proxy.md) for the full architecture, request flow, and how to add new alarm types.

Expand All @@ -24,6 +26,8 @@ PoracleNG's tracking POST endpoint handles both creates and updates. When the re

An edit therefore sends the whole row, and the body is built by serializing the typed model — so every column PoracleWeb has no property for arrives absent and PoracleNG stores the default over what the user had. `TrackingFieldPreserver.PreserveStoredFieldsAsync` runs first on every update: it re-reads the stored row and copies across any property the submitted body lacks. Before it existed, editing an alarm on the web reset `override_location_label`, `override_areas` and `pvp_ranking_evolution` set from the bot (#730). A read failure returns the body untouched rather than failing the edit.

On PoracleNG 5.2.0 and later a pokemon edit takes a different route: `PUT /api/v2/humans/{id}/tracking/pokemon/{uid}`, which is a **full replace**. A field left out of the body is reset to its default rather than left alone, so the same merge that fixed #730 is what stops a v2 edit wiping a stored `min_iv`. The PUT is also delete-then-insert, so the pokemon uid rotates on edit and `ITrackedUidRemapper` moves quick-pick applied state to the new one. See [The v2 pilot](poracleng-proxy.md#the-v2-pilot).

The merge runs *before* the collision guards, because `TrackingUpdateReconciler.CountUpdatableDifferences` only compares properties present in the submission — an unmodelled property could not tell two alarms apart, so the guard refused edits PoracleNG would have accepted. See [PoracleNG API Proxy](poracleng-proxy.md#insert-update-or-duplicate) for what the guards are mirroring.

## Repository layer (non-alarm entities)
Expand Down Expand Up @@ -77,7 +81,7 @@ PoracleNG accepts any positive integer as a raid/egg level, so the picker's `+ A

## Test alert service

`TestAlertService` lets users trigger a sample notification for any configured alarm. It uses `Task.WhenAll` to fetch the alarm (via `IPoracleTrackingProxy`) and the human record (via `IPoracleHumanProxy`) in parallel. It then constructs a realistic mock webhook payload based on the alarm's filter fields (e.g., `pokemon_id`, `raid_level`, `quest_reward`) using the user's location as the event coordinates. The payload is sent to PoracleNG's `POST /api/test` endpoint, which formats and delivers the notification. Rate-limited at 5 requests per 60s per IP via the `test-alert` policy.
`TestAlertService` lets users trigger a sample notification for an alarm of one of the eight types `TestAlertController.ValidTypes` names: `pokemon`, `raid`, `egg`, `quest`, `invasion`, `lure`, `nest`, `gym`. Fort Changes, Max Battles and Pokéstop Events have no test alert, and the controller answers 400 for any other type. It uses `Task.WhenAll` to fetch the alarm (via `IPoracleTrackingProxy`) and the human record (via `IPoracleHumanProxy`) in parallel. It then constructs a realistic mock webhook payload based on the alarm's filter fields (e.g., `pokemon_id`, `raid_level`, `quest_reward`) using the user's location as the event coordinates. The payload is sent to PoracleNG's `POST /api/test` endpoint, which formats and delivers the notification. Rate-limited at 5 requests per 60s per user (falling back to the IP) via the `test-alert` policy.

## Fort change and Max Battle services

Expand Down Expand Up @@ -136,6 +140,21 @@ Wraps HttpClient calls for non-tracking Poracle API operations.
- Used for: fetching config, areas/geofences, templates, sending commands
- Registered via `AddHttpClient<IPoracleApiProxy, PoracleApiProxy>()`

### IPoracleMuteProxy (quiet periods)

Proxies the mute store at `/api/v2/humans/{id}/mutes`. **v2 only** — there is no v1 equivalent, so
`MuteCapabilityService` gates every call and `MuteController` answers the capability alongside the list
rather than from a separate endpoint.

### IPoracleIncidentProxy (Pokéstop Events)

Proxies Pokéstop-event alarm CRUD at `/api/v2/humans/{id}/tracking/incident`, again v2 only. It is
deliberately separate from `IPoracleTrackingProxy`: the v2 wire shape is a different contract (named
envelopes, the clean bitmask split into three booleans, a strict rule object rather than a stored row),
and bending the v1 proxy to serve both would mean a second shape inside every one of its methods.

That makes five Poracle proxies in total: tracking, human, api, mute and incident.

### Config parsing

`PoracleConfig` is parsed from Poracle's JSON configuration. The `defaultTemplateName` field can be a number or string — deserialization handles both via `JsonElement`.
Expand Down Expand Up @@ -169,6 +188,26 @@ an unreachable server answers "unknown" quickly instead of stalling the admin pa
`GET /api/admin/server-profile` serves it (admin only); `?refresh=true` invalidates the cache and the
GitHub update check first.

### Per-feature capability services

Four services sit over the profile and answer one question each:
`SummaryCapabilityService`, `MuteCapabilityService`, `QuestPokecoinCapabilityService` and
`CostumeCapabilityService`. All the same shape — one method, fail closed, no cache of their own, since
`IPoracleServerProfileService` already caches for five minutes and exposes `Invalidate()`. Each picks
the narrowest signal that predicts its feature: `CostumeCapabilityService` reads the migration number
(`monsters.costume` at 6, `raid.costume` at 7), the mute and Pokécoin services read the version, and
both version gates compare against **5.2.0** — the release that added the features, not the 5.2.1
production happens to run.

There is deliberately no central registry. A user-facing control cannot ask the admin-only
`GET /api/admin/server-profile`, so four ordinary authenticated endpoints answer instead:
`GET /api/settings/costume-capability`, `GET /api/quests/capability`,
`GET /api/summary-schedules/capability`, and `GET /api/mutes`, which folds its capability into the list
response because the quiet chip needs both on every alarm page.

See [Version compatibility](poracleng-compatibility.md) for how to choose a signal and what each
feature needs.

## Areas

User areas are managed through `IPoracleHumanProxy.SetAreasAsync()`, and PoracleNG handles the dual-write
Expand Down Expand Up @@ -257,6 +296,25 @@ so the two cannot drift:

It accepts `hours` and `mins` as either numbers or strings, because PoracleNG stores them inconsistently.

#### Repeating entries

An entry can also fire repeatedly across a window, which adds three snake_case fields to the stored
JSON:

```json
{"day": 1, "hours": 9, "mins": 0, "end_hours": 17, "end_mins": 0, "step": 2}
```

That fires at 09:00 and every two hours up to and including 17:00. A single fire omits all three
fields entirely.

`step` must be a whole number of hours no greater than 23, and when it is positive the end time must be
strictly later than the start — a window spanning midnight is not supported. A `step` of zero or less is
a single fire, and any end fields beside it are ignored rather than refused, matching what
`ActiveHourEntry.Fires()` does upstream. The end fields carry `omitempty` in PoracleNG, so a window
ending on the hour arrives with `end_mins` missing: a missing end field reads as 0 and is never an
error.

## Scanner service

The scanner DB (`ScannerDb` connection string) is optional. When not configured, `IScannerService` is not registered and scanner endpoints return appropriate fallback responses.
Expand All @@ -270,7 +328,7 @@ The scanner DB (`ScannerDb` connection string) is optional. When not configured,
| `GET /api/scanner/gyms?search=term&limit=20` | Search gyms by name prefix (`term%`, index-sargable). User input is escaped for LIKE wildcards (`%`, `_`, `\`). Search length 2--100 chars; `limit` clamped to `[1, 50]`. |
| `GET /api/scanner/gyms/{id}` | Return a single gym by its ID (max 128 chars). |

Both endpoints are rate-limited under the `scanner-search` policy (60 requests/min per IP).
Both endpoints are rate-limited under the `scanner-search` policy (60 requests/min per user, falling back to the IP).

Both endpoints resolve the gym's area name by running point-in-polygon checks against cached Koji admin geofences (via `IKojiService.GetAdminGeofencesAsync()`). The first matching fence name is set on the result's `Area` property.

Expand Down Expand Up @@ -338,17 +396,21 @@ Weather data is served via `IScannerService` from the scanner DB (`ScannerWeathe

## Rate limiting

Sensitive endpoints use **per-IP** partitioned rate limiting:
Sensitive endpoints use **partitioned** rate limiting, never one global bucket:

| Policy | Limit | Window | Applied to |
|---|---|---|---|
| `auth` | 30 requests | 60 seconds | Login / callback / token exchange |
| `auth-read` | 120 requests | 60 seconds | Current user, profile switch |
| `test-alert` | 5 requests | 60 seconds | Test-alert sends |
| `mutes` | 60 requests | 60 seconds | Quiet-period reads and writes |
| `geojson-import` | 5 requests | 60 seconds | Admin GeoJSON import |
| `scanner-search` | 60 requests | 60 seconds | Scanner gym search / lookup |

Configured in `Program.cs` using `RateLimitPartition.GetFixedWindowLimiter` keyed by `RemoteIpAddress`.
Configured in `Program.cs` using `RateLimitPartition.GetFixedWindowLimiter`. `auth` keys on
`RemoteIpAddress`; the other five key on the authenticated user, falling back to the IP. The `mutes` limit
is set for reads — the quiet chip is read on every alarm page, and the store is written a few times a
day at most.

!!! danger "Never use global rate limiting for auth"
Global (non-partitioned) `AddFixedWindowLimiter` for auth causes cascading login failures — multiple users share one bucket.
Loading
Loading