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
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ describe('QuestEditDialogComponent', () => {
it('offers the stardust floor, which PoracleNG keeps in reward', () => {
setup({ ...baseQuest, pokemonId: 0, reward: 1000, rewardType: 3 });

expect(component.isStardust).toBe(true);
expect(component.usesRewardSlot).toBe(true);
expect(component.form.controls.stardust.value).toBe(1000);

component.form.controls.stardust.setValue(1500);
Expand All @@ -183,7 +183,7 @@ describe('QuestEditDialogComponent', () => {
setup({ ...baseQuest, reward: 25, rewardType: 7 });

expect(component.hasAmount).toBe(false);
expect(component.isStardust).toBe(false);
expect(component.usesRewardSlot).toBe(false);

component.save();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,6 @@ export class QuestEditDialogComponent {

readonly isPokecoins = this.data.rewardType === POKECOINS;

readonly isStardust = this.data.rewardType === STARDUST;

readonly isWebhook = inject(AuthService).isImpersonating();

saving = signal(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { provideTranslateService, TranslateService } from '@ngx-translate/core';

import { ActiveHoursChipComponent } from './active-hours-chip.component';
import { ActiveHourEntry } from '../../../core/models/active-hours.models';
import { I18nService } from '../../../core/services/i18n.service';

describe('ActiveHoursChipComponent', () => {
let component: ActiveHoursChipComponent;
Expand Down Expand Up @@ -106,6 +107,27 @@ describe('ActiveHoursChipComponent', () => {
expect(component.pills()[0].label).toBe('Weekends 9:00 AM–5:30 PM, every 2h');
});

it('follows a live display-language switch', () => {
// The labels come from translate.instant inside a computed, which only stays current because
// instant reads the translation store's signals -- an implementation detail of @ngx-translate
// v18 that older versions did not have. Switching through I18nService is the path the language
// menu takes, so this fails if either half stops holding.
const i18n = TestBed.inject(I18nService);
const translate = TestBed.inject(TranslateService);
withEnglishRangeStrings();
translate.setTranslation('it', { PROFILES: { ACTIVE_HOURS_RANGE_HOURLY: '{{days}} {{start}}–{{end}}, ogni ora' } }, true);

fixture.componentRef.setInput('activeHours', [{ day: 1, endHours: 17, endMins: 0, hours: 9, mins: 0, step: 1 }] as ActiveHourEntry[]);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.chip-active').textContent).toContain('hourly');

i18n.use('it');
fixture.detectChanges();

expect(fixture.nativeElement.querySelector('.chip-active').textContent).toContain('ogni ora');
expect(component.pills()[0].label).toBe('Mon 9:00 AM–5:00 PM, ogni ora');
});

it('should leave a single fire label untouched', () => {
withEnglishRangeStrings();
fixture.componentRef.setInput('activeHours', [{ day: 1, hours: 9, mins: 0 }] as ActiveHourEntry[]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ export class ActiveHoursChipComponent {

readonly isEmpty = computed(() => this.activeHours().length === 0);

/**
* Reactive to a display-language switch, and by a route worth stating out loud: `translate.instant`
* reads the store's `_currentLang` and `_translations` signals, so calling it inside a `computed`
* registers them as dependencies and switching language invalidates these labels the same way a new
* schedule does. That is true of @ngx-translate v18 and was not true of the versions before it, where
* this shape went stale until something else knocked `activeHours`. The spec switches the language and
* asserts the rendered pill follows; if a future version stops reading those signals it goes red here
* rather than in front of a user.
*/
readonly pills = computed(() =>
this.groups().map(g => ({
label: formatRuleLabel(g, (key, params) => this.translate.instant(key, params)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ describe('cleanRuleSummary', () => {
);
});

it('leaves an underscore that is part of a name alone', () => {
// Poracle interpolates user-chosen strings -- template names, and the areas and saved-place labels
// a scope override renders -- straight into this sentence without escaping them. A place called
// work_gym is not italics, and losing the underscore renames it on the card.
expect(cleanRuleSummary('**Bulbasaur** | distance: 5000m | template: my_template ')).toBe(
'Bulbasaur | distance: 5000m | template: my_template',
);
expect(cleanRuleSummary('**Pikachu** | areas: north_side, east_side ')).toBe('Pikachu | areas: north_side, east_side');
});

it('strips the other Discord emphasis, but only where it is paired', () => {
expect(cleanRuleSummary('_Bulbasaur_ | distance: 5000m')).toBe('Bulbasaur | distance: 5000m');
expect(cleanRuleSummary('__Bulbasaur__ | distance: 5000m')).toBe('Bulbasaur | distance: 5000m');
expect(cleanRuleSummary('*Bulbasaur* | `iv: 90%-100%`')).toBe('Bulbasaur | iv: 90%-100%');
});

it('is empty for the absent, null and blank cases, so the card renders nothing', () => {
expect(cleanRuleSummary(undefined)).toBe('');
expect(cleanRuleSummary(null)).toBe('');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,34 @@
* The upstream string is written for Discord, so it carries markdown emphasis and the loose spacing
* that survives a chat client: `**Bulbasaur** | distance: 5000m | iv: 90%-100% `. The bold always
* wraps the species or the level, which is already the card's heading, so keeping it would double the
* emphasis and fight the `<h3>` rather than help it. Everything here is plain-text transformation
* emphasis and fight the `<h3>` rather than help it. Everything here is plain-text transformation --
* the result is interpolated, never handed to innerHTML.
*
* The stripping is deliberately pair-aware rather than a sweep of `[*_`]`. PoracleNG interpolates
* user-chosen strings into this sentence without escaping them -- `rowtext` builds `**%s**` around a
* template name, an area or a saved-place label -- and `work_gym` is a name, not italics. Discord
* agrees: an underscore flanked by word characters emphasises nothing. So a delimiter is only removed
* where it has a partner, and the underscore forms additionally need a non-word character on the
* outside, which is exactly the boundary Discord applies.
*/
export function cleanRuleSummary(raw: null | string | undefined): string {
if (!raw) return '';

return raw
.replace(/\*\*/g, '')
.replace(/__/g, '')
.replace(/[*_`]/g, '')
.replace(/\s+/g, ' ')
.replace(/\s*\|\s*/g, ' | ')
.replace(/^[\s|]+/, '')
.replace(/[\s|]+$/, '')
.trim();
return (
raw
// Code spans first: inside a backtick pair Discord renders the rest literally, so unwrapping the
// span before looking for emphasis stops a `*` in code being read as a delimiter.
.replace(/`([^`\n]+)`/g, '$1')
.replace(/\*\*(?=\S)([\s\S]*?\S)\*\*/g, '$1')
.replace(/\*(?=\S)([^*\n]*?\S)\*/g, '$1')
.replace(/(?<![\w])__(?=\S)([^_]*?\S)__(?![\w])/g, '$1')
.replace(/(?<![\w])_(?=\S)([^_]*?\S)_(?![\w])/g, '$1')
.replace(/\s+/g, ' ')
.replace(/\s*\|\s*/g, ' | ')
.replace(/^[\s|]+/, '')
.replace(/[\s|]+$/, '')
.trim()
);
}

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

- **Quest alarms can track PokéCoin rewards.** Poracle started accepting them in 5.2.0, so quest alarms gained a sixth reward tab that works the way Stardust does: a minimum amount and no item picker, because Poracle matches this reward on the amount alone. The tab appears only on a Poracle new enough to store it -- an older one refuses the reward type outright -- and a PokéCoin rule you already have, set with the bot or left behind by a downgrade, stays visible and deletable either way.
- **A request an older Poracle cannot serve now says which feature it was and what the server would need.** Nothing throws it yet, so there is no visible change today: it is the answer waiting for the first control that depends on a newer PoracleNG than the one an instance is pointed at. Until now such a request came back either in Poracle's own wording, which names a database column, or as a plain server error. It now answers with the feature named as this site names it and the version or database migration that would serve it, in words, beside the control that asked -- rather than borrowing the disabled-by-your-administrator wording, which would be untrue and would send the reader looking for a switch nobody turned off.

### 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 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.
### Changed

- **A refused alarm explains itself the same way whichever Poracle surface answered.** The v2 write path already read PoracleNG's newer RFC 9457 error bodies and named the individual field it refused; the older v1 path, still the one most installs use, was reading only the older shape and answering a validation refusal as though the server had broken. Both paths now share one reader, and where many fields are refused at once the message names the first few and counts the rest rather than rendering a dozen clauses into a snackbar ([#803](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/803)).
### Added

- **Every alarm card says in a sentence what its rule actually does.** A card gave you the species and a row of filter chips, and working out that a rule meant "Bulbasaur within 5 km, 90% IV or better, level 20 to 35" was a matter of decoding the chips. Poracle already writes that sentence -- it is the same wording the bot answers a `!pokemon` command with -- and was returning it on every read of this page, where it was thrown away. It now sits at the foot of each card, under a hairline, below the chips that still read first when you are scanning forty rules. Long ones are clamped to two lines with a control to open them. On the nine card types Poracle words well; fort-change cards keep their chips, because the sentence Poracle renders for them still has a raw JSON array in the middle of it. The line appears only when the language Poracle writes your alerts in is the language you are reading the site in, so a card never carries two languages at once -- and only on a Poracle new enough to send it, which older instances are not; in both cases the card is exactly what it was before ([#810](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/810)).
- **Quiet one gym, one area or one species for a while, instead of deleting the rule.** A notifications-paused button now sits in the actions row of every alarm card that names something specific -- Pokemon and nest cards name a species, gym, raid and egg cards name a gym, max battle cards name a station -- and on the Areas page, on both the checklist rows and the selected-area chips. Pick a duration from fifteen minutes to a day and that subject goes quiet; the button becomes a live countdown, and pressing it again extends or lifts it. The dashboard grows a Quiet card while anything is silenced, which lists everything including quiet periods set from the Discord bot and offers Resume on each. This is not *Pause Alerts*, which stays what it was: account-wide, indefinite, and saved. A quiet period is one subject for a while, and Poracle holds it in memory -- a restart of the processor clears every one, which the sheet says out loud rather than showing a deadline it cannot keep. Requires PoracleNG 5.2.0 or newer; on anything older the control does not appear at all ([#809](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/809)).
- **Showcases, Kecleon and Gold Stops are trackable in their own right.** They were only ever reachable through the invasion add dialog, which files them as ordinary invasions, so the alert that arrived described a Team Rocket encounter that was not there. There is now a *Pokéstop Events* page: pick the events you want, set a radius or areas, and Poracle formats them with its showcase template. The rules live in the same table invasions do, so the invasion list stops listing them and the dashboard counts them separately — the total across all alarm types is unchanged, but the Invasions figure will drop by however many event alarms you had, and a *Pokéstop Events* figure appears beside it ([#806](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/806)).
Expand All @@ -29,15 +20,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **A refused alarm explains itself the same way whichever Poracle surface answered.** The v2 write path already read PoracleNG's newer RFC 9457 error bodies and named the individual field it refused; the older v1 path, still the one most installs use, was reading only the older shape and answering a validation refusal as though the server had broken. Both paths now share one reader, and where many fields are refused at once the message names the first few and counts the rest rather than rendering a dozen clauses into a snackbar ([#803](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/803)).
- **Pokemon alarm edits are written through PoracleNG 5.2.1’s strict `/api/v2` surface, where the server can tell an edit apart from a takeover.** Nothing changes on screen. What changes is underneath: an edit now addresses the rule by its id, so Poracle refuses outright if the uid is not yours or if the result would duplicate an alarm you already have, instead of PoracleWeb.NET having to work that out from a success response and undo it afterwards. Poracle also explains a rejected filter field by name now, so the message on the dialog says which one. Anything older than 5.2.0 keeps the surface it has always used, unchanged, and so do the other nine alarm types; an edit carrying anything the new surface cannot express takes the old path rather than failing. Set `PORACLE_TRACKING_API_VERSION` to `v1` or `v2` to pin it ([#805](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/805)).

### Fixed

- **An underscore in a name survives the rule sentence on an alarm card.** The sentence was having every `*`, `_` and backtick stripped out of it, not only the ones acting as Discord emphasis. Nothing Poracle renders today contains one, so there is no visible change: this is the case that had not arrived yet. Poracle interpolates template names, areas and saved-place labels into that sentence without escaping them, so an area called `north_side` would have reached the card as `northside`. Emphasis is now removed only where it is paired, and where an underscore sits between two word characters it is left alone -- which is what Discord does with it too ([#819](https://github.com/PGAN-Dev/PoracleWeb.NET/pull/819)).
- **Pokéstop Events works at all.** The page shipped unable to answer a single request: the service behind it was never handed to the application at startup, so opening the page, adding an event or deleting one all failed the same way, with the generic server error. Nothing caught it -- the tests for that code substitute the missing piece, so they passed, and the site compiled and deployed green. A new test now builds every page's dependencies the way the running application does, which is the only place this kind of omission shows up.
- **The PVP rank range is readable again on a dark-themed alarm card.** The band under a PVP alarm showed its league and nothing else, so the ranks you had set looked like they had been dropped. They were being drawn, in white, on a band that stays light in both themes. The league name gained some contrast on the way past ([#800](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/800)).
- **A delegate configured in Poracle's own config can see their webhooks again.** Poracle reports a delegated webhook by the name the operator wrote in `webhook_admins`, and this site matched those strings against the webhook's URL, so a name matched nothing: the *My Webhooks* item appeared in the sidebar, the page it led to was empty, and the button on it would have been refused. Grants are now resolved to the webhook they name, whether the config names it by name or by URL, and a grant that names no webhook at all no longer puts an item in the sidebar ([#797](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/797)).
- **Impersonating a delegate shows what the delegate sees.** *My Webhooks* was hidden inside an impersonation session -- the one place an admin looks to find out why someone is complaining -- while the page and its actions would both have answered. Impersonating a second account from inside that session is refused, since only one token can be held for the way back out ([#797](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/797)).

### 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 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

### Changed
Expand Down
Loading