Release: develop → main (v2.16.0) - #755
Queued
hokiepokedad2 wants to merge 36 commits into
Queued
Conversation
…731) Every alarm write built its body by serializing the typed model, so any column PoracleNG has that PoracleWeb never modelled was absent from the request. The write carries a uid, so PoracleNG upserts the row and stores the column default over the user's value. PoracleNG 5.1.0, which prod now runs, added override_location_label, override_areas and pvp_ranking_evolution to the tracking API. Setting any of them with the bot and then pressing Update Distance, toggling auto-delete or editing the alarm on the web erased them. The bulk paths now rewrite the stored rows in place and set only the field being changed. The edit paths merge the stored row into the body before it is sent. Neither enumerates the new fields, so costume (PoracleNG 5.2.0) is covered by the same change. Merging runs before the collision guards on purpose: CountUpdatableDifferences only compares properties present in the submission, so an unmodelled field could not tell two alarms apart and the guard refused edits PoracleNG would have accepted. Also re-read the diff:"update" tags at 5.1.0 as TrackingUpdateReconciler's comment instructs. Every entry still matches; recorded in the doc comment along with why maxbattle's absence from the table is harmless.
* fix: preserve alarm fields PoracleWeb does not model on write (#730) Every alarm write built its body by serializing the typed model, so any column PoracleNG has that PoracleWeb never modelled was absent from the request. The write carries a uid, so PoracleNG upserts the row and stores the column default over the user's value. PoracleNG 5.1.0, which prod now runs, added override_location_label, override_areas and pvp_ranking_evolution to the tracking API. Setting any of them with the bot and then pressing Update Distance, toggling auto-delete or editing the alarm on the web erased them. The bulk paths now rewrite the stored rows in place and set only the field being changed. The edit paths merge the stored row into the body before it is sent. Neither enumerates the new fields, so costume (PoracleNG 5.2.0) is covered by the same change. Merging runs before the collision guards on purpose: CountUpdatableDifferences only compares properties present in the submission, so an unmodelled field could not tell two alarms apart and the guard refused edits PoracleNG would have accepted. Also re-read the diff:"update" tags at 5.1.0 as TrackingUpdateReconciler's comment instructs. Every entry still matches; recorded in the doc comment along with why maxbattle's absence from the table is harmless. * feat: per-alarm delivery scope, including user-drawn geofences (#730) PoracleNG 5.1.0 gives every alarm its own scope: a saved place to measure a radius from, or a set of areas to be confined to, instead of inheriting the profile's pin and area list. This carries it through PoracleWeb. - override_location_label and override_areas on all ten alarm models, their Create and Update DTOs, and the mapping extensions. - Saved places over PoracleNG's /locations API: list, add, delete. Delete surfaces the 409 referencing_rules list instead of flattening it, because the useful answer is which alarms to repoint first. - PoracleNG's three mutual-exclusion rules are mirrored and refused before the write, at the proxy seam so the callers that never touch a controller are covered too (#548, #565). The interesting part is user-drawn geofences. PoracleNG validates override_areas against GetAvailableAreas, which filters on userSelectable; PoracleWeb serves user geofences with userSelectable=false on purpose, so naming one fails the whole request with 400 "area not permitted" -- it rejects rather than stripping, unlike setAreas. Matching never consults that flag: resolveOverride hands the rule's areas to areaOverlap, a name comparison against the fences the spawn fell in. So the permitted names go to PoracleNG, the full list is written straight into the column, and state is reloaded. Verified against 5.1.0's matcher. That lives in UserOwnedOverrideAreaProxy, a decorator over the tracking proxy, rather than in the ten alarm services: the outbound body already carries everything the decision needs, and the service-layer version would have meant ten new constructor parameters and the same logic ten times. Also tightens PreserveUnmodelled from #730: now that the models declare these fields, every serialized alarm carries them as explicit nulls whether or not the caller read them. Null now means "not stated, keep what is stored"; an empty list or empty string is how a scope is cleared -- the same split the models already use for gym_id. Without this, modelling the fields would have reintroduced the erasure #730 exists to prevent.
* feat(where): the shared control for an alarm's delivery scope First half of the per-alarm scope UI. The chip and the sheet exist and are covered; wiring them into the nine alarm dialogs, the card templates and a Places screen is the follow-up. Every alarm has always answered "where should this reach me". Before PoracleNG 5.1.0 the answer was inherited and invisible: one profile pin, one area list, shared by everything. This makes the answer visible and editable per alarm, so the work is stating something that was already there rather than bolting on an override system -- which is also why nothing in the UI says "override". - alarm-scope.ts reads the two override columns and the radius back into one discriminated value, and writes it out again. That is what lets the sheet offer a radio group instead of three fields and three error messages: the states PoracleNG refuses cannot be expressed. - Clearing sends empty values, never null. Null means "not stated, keep what is stored" on the write path, so a null here would make an override impossible to take off. - WhereChipComponent renders the scope as a sentence fragment and is the way into the sheet, so the same control reads and edits the same idea. - WhereSheetComponent offers the three modes. Its area picker lists the user's own geofences alongside admin areas, which is only honest because the backend now writes those past PoracleNG's user-selectable filter. - PlacesService holds the saved places as a signal, since the sheet, the Places screen and the cards all read the same list. Locale keys added to all eleven files, English text throughout pending translation. * feat(where): Places, and a pick-only mode for the location map Second slice of the per-alarm scope UI. "Near a place" is unusable without somewhere to make places, so this is the half that has to land with it. - PlacesDialogComponent, in the user menu beside Alert Defaults: the profile pin, the named places, add and delete. - LocationDialogComponent gains `pickOnly`. It saved the chosen point as the profile pin unconditionally, which is what it was for -- so borrowing it to name a place would have quietly relocated every alarm without an override. Three existing callers are untouched. - Naming reuses ConfirmDialog's promptField, which already does name-with-duplicate-check, rather than adding a third dialog that asks for one string. The existing labels are passed in so it can refuse a duplicate before the request goes out. - Delete surfaces the 409 referencing_rules count. PoracleNG refuses to orphan a label, and the useful thing to say is how many alerts are in the way, not that it failed. Seven component tests, including both delete failure paths and the cancelled naming step. The pick-only assertion is there because the failure it guards against is silent: without it the pin moves and nothing says so. * feat(where): chip on the Pokemon card, and fix the pin-radius reading Wiring the first card found a bug in the model I shipped yesterday. scopeOf collapsed "within N km of my pin" into the inherited reading, so an alarm with a radius and no place rendered as "Anywhere in my areas" -- the opposite of what it does, in the one place the card is supposed to be authoritative. A radius with no place is the behaviour that predates per-alarm scope and it needs its own reading, so the profile mode now carries a distance and describeScope words it separately. That also corrected the sheet. Offering "areas / near a place / specific areas" left no way to express the most common alarm there is. The second option is now "near a point" with a target select whose first entry is the pin, because to a person that is one choice with a target rather than two unrelated modes. Three options still, and they still map onto the three mutual-exclusion rules PoracleNG enforces. - WhereChipComponent replaces the areas-or-distance badge on the Pokemon card. Clicking it opens the sheet, so scope can be changed without opening the whole edit dialog. - The list loads the profile's selected areas, only so the inherited wording can be honest: "anywhere in my areas" is a lie for someone with none. - 11 new tests across the chip and the scope model, including both readings of a bare radius, which is the case that was wrong. * feat(where): scope in the Pokemon add and edit dialogs The add dialog's radius gains a "measured from" selector: the pin, as before, or a saved place. Everything funnels through scopeToFields, so the card chip, the scope sheet and this dialog produce the same wire format from one implementation. The edit dialog deliberately does NOT gain the control. Scope is changed from the card chip, which is one control in one place; a second editor would be two paths that can disagree. What the dialog needed was to stop being able to destroy a scope it cannot describe: - An area-confined alarm has no radius, so the areas-or-distance control cannot express it. The control is hidden for those and the stored distance is sent back unchanged, rather than offering a change that would silently discard the areas. - The dialog shows the scope read-only, so an alarm aimed at a place does not look like a plain radius while being edited. Three add-dialog tests, including the one that matters most: a plain radius must NOT acquire a location override just because the field now exists. Full frontend suite: 1025 passing. * fix(where): import DecimalPipe in the Places dialog The production build resolves template pipes; jest and tsc do not. `| number` on the coordinate rows compiled locally and failed CI with NG8004. Verification for Angular template changes needs `npm run build`, not just the type check and the unit tests.
Cards
- Raids, eggs, quests and max battles share app-alarm-info, so that component
now renders the Where chip instead of its own distance display. One change,
four types, and no chance of the four drifting apart.
- Gyms, lures, nests, invasions and fort changes each carried their own
distance-chip markup; all five now use the chip.
- Every list loads the profile's selected areas, used only to keep the
inherited wording honest.
- Clicking a chip opens the scope sheet, on all ten types. Raids and eggs
share a list, so the tracking type is a parameter rather than two methods.
Edit dialogs
- The areas-or-distance control cannot express an area-confined alarm, and
switching it to a distance would have sent both and been refused by
PoracleNG. It is hidden for those alarms, and every dialog shows the scope
read-only above it.
Translations
- All 37 WHERE strings plus the Places menu entry into de, es, fr, it, nl,
pl, pt, pt-BR, sv and da. The applier asserts each locale carries exactly
the English key set and that every {{placeholder}} survives, since a
dropped one renders as literal braces to the user. RADIUS_KM matching
English in Danish and German, and PLACE_NAME in German, are cognates.
Verified with npm run build, not just tsc and jest -- template errors only
surface in the production build, which is how NG8004 reached CI last time.
Two remaining pieces of per-alarm scope. Default place - AlertDefaultsService remembers a place beside the mode and radius, and the nine add dialogs that lacked it gain the "measured from" selector Pokemon already had. All ten seed from the saved default. - save() drops the place when the mode is areas. A place only means something alongside a radius, and the two are mutually exclusive upstream, so keeping one would seed every new alarm with a scope PoracleNG refuses. - reconcilePlace() forgets a place that no longer exists, so deleting one cannot keep stamping a rejected label onto new alarms. Mega PVP - pvp_ranking_evolution on the Monster model, a Base / Mega / Mega X / Mega Y control in the add and edit dialogs, and a suffix on the card's PVP badge. - Only sent when the rule has a league. Carrying it on a non-PVP alarm would be a filter nobody asked for on a field PoracleNG still reads. - It works whatever include_mega_evolution is set to: that flag only decides the default for rules that do not state a mode. Confirmed against 5.1.0's matcher, where filterMega is gone and mega entries are split per evolution. Translations for all new strings in the ten non-English locales. Two rounds of build-only failures again, both invisible to tsc and jest: mat-option unknown in three dialogs whose imports lacked MatSelectModule, and a spec stub for AlertDefaultsService missing the new accessor.
* feat(where): default place for new alarms, and mega PVP
Two remaining pieces of per-alarm scope.
Default place
- AlertDefaultsService remembers a place beside the mode and radius, and the
nine add dialogs that lacked it gain the "measured from" selector Pokemon
already had. All ten seed from the saved default.
- save() drops the place when the mode is areas. A place only means something
alongside a radius, and the two are mutually exclusive upstream, so keeping
one would seed every new alarm with a scope PoracleNG refuses.
- reconcilePlace() forgets a place that no longer exists, so deleting one
cannot keep stamping a rejected label onto new alarms.
Mega PVP
- pvp_ranking_evolution on the Monster model, a Base / Mega / Mega X / Mega Y
control in the add and edit dialogs, and a suffix on the card's PVP badge.
- Only sent when the rule has a league. Carrying it on a non-PVP alarm would
be a filter nobody asked for on a field PoracleNG still reads.
- It works whatever include_mega_evolution is set to: that flag only decides
the default for rules that do not state a mode. Confirmed against 5.1.0's
matcher, where filterMega is gone and mega entries are split per evolution.
Translations for all new strings in the ten non-English locales.
Two rounds of build-only failures again, both invisible to tsc and jest:
mat-option unknown in three dialogs whose imports lacked MatSelectModule, and
a spec stub for AlertDefaultsService missing the new accessor.
* fix(where): Places belongs in the nav, not the user menu
Places was a dialog next to Alert Defaults. Wrong on two counts.
It is data, not a preference. Alert Defaults is localStorage; Places is
server-side CRUD, exactly like My Geofences, which has its own nav item.
And the Areas page already answers this question: it opens with two method
cards, Areas and Location, where the Location card holds the pin and its
Set/Change button. Named points are just more pins, so hiding them in a
settings menu split one idea across two surfaces.
- The dialog becomes a page at /places, in the settings nav group beside
Areas and My Geofences. The user-menu entry and its string are gone.
- The route carries disabledFeatureGuard('disable_location'), matching
LocationController, which already gates the endpoints it calls. A nav item
alone would have left the page reachable by URL.
- The Areas location card links to it, so the two surfaces reference each
other rather than one being hidden.
Also caught while doing it: NAV.PLACES was missing from all eleven locale
files, English included, so the sidebar would have rendered the literal key.
My earlier parity check could not see it, because it compares locales
against English and English was missing it too. Added a check that reads the
keys templates actually ask for and asserts each exists in en.json. It found
two more, COMMON.ERROR and COMMON.SAVED, both pre-existing in the invasion,
max-battle and quest add dialogs and left alone here.
* docs: fold the superseded Places entry into the accurate one
Both described the same unreleased feature and disagreed about where it
lives. Nothing has shipped, so the log should say where Places is rather
than record the route it took.
…737) Style The page used a mat-list; every comparable page in the app uses a card grid with an accent bar, skeleton loaders and a dashed empty state. It now matches Geofences, which is the closest sibling. The pin appears as the first card rather than a banner above the list, because it IS a place — the unnamed one every alert falls back to — and a separate treatment said otherwise. Its accent and icon are muted, since it is the fallback and not a choice. Dead end The scope picker's empty state read "Add one from the location menu", naming a menu removed when Places became a page. Wrong in all eleven locales. Rather than just correct the words, the select now ends with Add a place: it opens the map picker, asks for a name, saves and selects it. Creating a place is only ever wanted at that exact moment, and sending someone to another screen lost the alarm they were mid-edit on — thirteen steps across two contexts, down to eight in one. Also added a check that reads the keys templates ask for and asserts each exists in en.json, then that every locale carries them. Parity against English cannot catch a key English is missing too, which is how NAV.PLACES went absent from all eleven. It reports two pre-existing gaps, COMMON.ERROR and COMMON.SAVED, left alone here.
Three design reviews, run independently, all reached the same objection to the nav item I added yesterday: Places is a thin, set-and-forget list that bought permanent nav space, and the pin and the named places were still on two different pages. The cross-link I added from the Areas page was the tell — it existed because the content was in the wrong place. - The Places page becomes a section of the Areas page, directly under the Location card that holds the pin. It no longer renders a pin card of its own; the card above it is the pin. - One nav item, relabelled Areas & Places, replacing two. The page title matches. /places redirects rather than 404s. - The notification language block moved from the middle of the page to the end. It has nothing to do with areas and was interrupting the picker. Two of the three reviews argued AGAINST merging Areas and My Geofences, and the third named that as the strongest counter to its own proposal, so Geofences stays its own item. It hosts drawing, submit-for-review and approval states: a workspace, not a list. Left undone deliberately: Areas and My Geofences both edit profiles.area through different controls, chips on one page and slide toggles on the other. Verified rather than taken on trust. That is a deeper incoherence than anything this change touches and wants its own decision.
) Cleared pin read back as 0,0 Poracle stores "no pin" as 0,0, not null. Clearing set the page's own state to null and looked right; the next visit read 0,0 at face value and rendered it as coordinates. The literal was open-coded in six components, each rewriting it, so the rule now lives in one helper and the pin reads through it in both the Areas page and PlacesService. Map opened too far out The initial-view ladder ranked the selection above the pin. A multi-area selection frames a whole region, so the map opened where nothing could be clicked. The pin now outranks it. It does NOT outrank the user's own shapes. Three existing tests failed on the first attempt at this, and they were right to: making the pin win outright would have opened My Geofences on the pin instead of the polygons you came there to edit. The ladder is custom > location > selection > all, which is correct on both pages. Tests updated to state the new intent, plus one for the case the reorder makes newly reachable. My pin The section was labelled Location while the rest of the site says My pin. Alert language Moved from a block on the Areas page into the user menu beneath display language, relabelled as a pair. It was on that page for want of anywhere better, and its description had to end by explaining it was not the other language setting — prose compensating for layout. Adjacent, the distinction is structural. Also removed the user menu's Areas & Location item label, which the nav rename had left stale in all eleven locales; it now shares the nav's own string so a rename cannot orphan it again.
The Delivery tab and the Where sheet asked the same question in two shapes, and the difference was not only cosmetic: the tab had two options where the sheet had three. "Only in specific areas" could not be chosen while creating an alarm, only afterwards from the card chip. Its copy, "Notify within a radius from your location", also predated saved places and was false. ScopePickerComponent is now the control. The sheet is a dialog shell around it; all ten add dialogs render it inline. The sheet's vocabulary won because it is the only one that can express the three states PoracleNG stores, and WHERE.SHEET_TITLE -- "Where should this alert reach you?" -- is the inline heading, so it is literally the same question in both places. It also carries the missing-pin warning: choosing to measure from a pin you have never set produced an alarm that silently matched nothing. Alert language renders flag rows like display language rather than an embedded select. Its state moved into a service so the menu can render rows directly and keep Material's keyboard handling; two menus that sit next to each other only read as different things if they look like siblings. The edit dialogs still hold the old two-option control. Converting them the same way ran into a DOTALL regex eating past its intended block, so I reverted rather than push a half-converted file. They keep working as they were and get the same treatment in a follow-up, done one at a time.
Edit dialogs All ten now render ScopePickerComponent, the same control the add dialogs and the card sheet use. They had kept the old two-option version, which could not express an area-confined alarm — that is why they showed the scope read-only and hid the control for those alarms. With a control that can describe all three states, neither workaround is needed. Done one file at a time after the scripted attempt went wrong last round. The script this time verifies every piece is where it expects before writing anything, and skips the file otherwise: five of eight skipped on the first pass because they compute distance differently, and were converted separately rather than guessed at. One still needed a hand-fix afterwards. WHERE.EDIT_FROM_CARD is gone from all eleven locales. It told people to change the scope from the card, which is no longer the only place. Map The Areas & Places map container set no height and let the child decide, which left it too small to pick an area out of. It now matches My Geofences: 70vh, 500px floor, same margins and framing. Three max-battle edit tests drove the removed form controls; they drive the picker's signal now and their assertions are untouched.
) Four add dialogs asked for COMMON.SAVED and COMMON.ERROR, neither of which exists, so they showed the literal key. The keys they should have used were already present and already translated: INVASIONS/QUESTS/RAIDS SNACK_CREATED_COUNT and MAX_BATTLES.CREATE_SUCCESS/CREATE_FAILED. That was a wrong reference, not a missing translation. The audit that found it also found 121 keys sitting in English in all ten locales. Translated the 38 that are prose a user reads: every sign-in error, the test-alert messages, fort-change and max-battle snackbars, the raid level picker's help and validation, the distance validation, and Undo. Deliberately left in English: proper nouns (Arlo, Sierra, Team Rocket, the team names), game terminology the community uses untranslated (Gigantamax, GMAX, raid tier names, the DTS condition names), unit formats like "{{value}} km", and the webhook URL placeholder. Admin-only settings strings are also still English and are the obvious next batch. The applier asserts each translation keeps every {{placeholder}} the English has, since a dropped one renders literal braces and stays invisible until someone in that language hits that string. Also, from the walkthrough: - The place picker was seeded with 0,0, so it opened in the Atlantic. It now starts from the pin, and is titled for picking a point rather than setting a location, which is what the other caller does. - The missing-pin warning offers to set the pin, in place, rather than only naming the problem. Sending someone to Areas & Places would lose the alarm. - Both language menus carry a line saying what they change. - Removed a literal NUL byte that an earlier escape had written into the scope picker's source, which was making git treat the file as binary.
… input (#743) * fix(i18n): real messages instead of raw keys, and 380 translations Four add dialogs asked for COMMON.SAVED and COMMON.ERROR, neither of which exists, so they showed the literal key. The keys they should have used were already present and already translated: INVASIONS/QUESTS/RAIDS SNACK_CREATED_COUNT and MAX_BATTLES.CREATE_SUCCESS/CREATE_FAILED. That was a wrong reference, not a missing translation. The audit that found it also found 121 keys sitting in English in all ten locales. Translated the 38 that are prose a user reads: every sign-in error, the test-alert messages, fort-change and max-battle snackbars, the raid level picker's help and validation, the distance validation, and Undo. Deliberately left in English: proper nouns (Arlo, Sierra, Team Rocket, the team names), game terminology the community uses untranslated (Gigantamax, GMAX, raid tier names, the DTS condition names), unit formats like "{{value}} km", and the webhook URL placeholder. Admin-only settings strings are also still English and are the obvious next batch. The applier asserts each translation keeps every {{placeholder}} the English has, since a dropped one renders literal braces and stays invisible until someone in that language hits that string. Also, from the walkthrough: - The place picker was seeded with 0,0, so it opened in the Atlantic. It now starts from the pin, and is titled for picking a point rather than setting a location, which is what the other caller does. - The missing-pin warning offers to set the pin, in place, rather than only naming the problem. Sending someone to Areas & Places would lose the alarm. - Both language menus carry a line saying what they change. - Removed a literal NUL byte that an earlier escape had written into the scope picker's source, which was making git treat the file as binary. * fix(where): quick picks, one dialog size, and a picker that reads its input You asked why the two Set Location dialogs looked different. They are the same component — six callers each passed their own width and one said 400px. The component owns its width now and no caller passes one. Quick picks kept their own copy of the old two-option control, and the apply request had no field for a place or a set of areas, so converting the UI alone would have made the control lie. QuickPickApplyRequest carries both now and QuickPickService puts them on every alarm it creates, all nine types. The quick-pick test then caught something worse. ScopePickerComponent seeded itself in the constructor, where a signal input is not yet populated, so it read its own model default and wrote that back over whatever the host passed. Every host's scope was being discarded: the Alert Defaults preference on a new alarm, and an alarm's actual scope when you opened it to edit. Seeding moved to ngOnInit and there is now a spec that fails on the old behaviour. Also from the walkthrough: the place picker opens on your pin rather than 0,0, its title says it is picking a point, the missing-pin warning offers to set the pin in place, and both language menus say what they change. * docs: tidy the changelog list after the merge
Making the dialog one size, I put the width on :host. That sizes the component inside Material's padded surface, so the content box came out wider than its container and the whole dialog got a horizontal scrollbar. The size belongs on mat-dialog-content, with border-box and a max-width, which is what the other dialogs in the app already do. Same single size, no overflow.
I gave the mega control a fieldset with class="pvp-evolution" and never wrote any CSS for it. Browsers style a bare fieldset with a border and inset padding, and it reserved no space beneath itself, so it drew a box around itself and the Best/Worst Rank fields ran into it. The PVP cap control immediately above it already solves this with .pvp-cap-fieldset and .pvp-cap-legend. Reusing those is the fix; there was never a reason for a second set of classes. Applied to both the add and edit dialogs, which carry the same markup.
Driven Playwright against dev rather than reasoning from CSS, which is how the last several of these got shipped broken. PVP tab The rank fields carry a mat-hint that fills the row to its bottom edge, and the next field's label floats above its own border into that band. Measured: the row ended at exactly the y the next field began. .form-row now leaves 12px, in both pokemon dialogs. Scope picker on a phone At 390px the warning's icon was crushed to a sliver and "Set your pin" was squeezed into three lines against the edge. It wraps now, and the 2.25rem indent that lines options up with the radio labels drops to 1.25rem on small screens, where it was costing a fifth of the usable width. Set Location dialog The surface stopped scrolling with the earlier fix, but 2px remained. The Leaflet container is width:100% with a 1px border and no border-box, so it came out 514px against its siblings' 512. That was the whole scrollbar. Places empty state Title said "No places yet" and the line under it opened with "No places yet." Checked at 1400px and 390px: Areas & Places, the alarm dialog's four tabs, the scope picker in all three modes, and the Set Location dialog.
Walking every page at 390px, two overflowed. The geofence header's three buttons measure 404px in a 390px viewport, putting Draw Geofence 30px past the edge; they now wrap, with the FAB on its own line. Help's small screenshots set a bare max-width: 480px, which overrides the base class's max-width: 100%, so three images rendered at 482px and spilled 92px; min(480px, 100%) keeps the cap without letting it exceed the column. Both measured in the browser rather than inferred. The remaining wide elements at that width are the admin tables, which sit in a container that genuinely scrolls, and Material's off-screen inactive tab panel on Raids. Refs #730 Claude-Session: https://claude.ai/code/session_01Nah4N2sGFs1TU2t7DXfKzJ
…ft (#748) pvp_ranking_evolution shipped as a PVP-tab selector, an Angular request field, a card suffix and four passing component specs, against a backend with no such property. Model binding dropped it on the way in and the typed read dropped it on the way out, so the control changed nothing and the suffix could never render. The specs passed because they assert what the component puts in the request, which says nothing about what the API accepts. min_time is the other 5.1.0 filter PoracleWeb never carried: seconds a spawn must still have left when it is found. Exposed as a short list of round durations rather than a seconds box — 301 of production's rules use this field and every one is set to exactly 300. A bot-set value outside the presets is offered as its own option so opening the dialog cannot round or clear it. rarity/max_rarity stay unmodelled on purpose. PoracleNG reads rarity per species from rolling sighting stats, and PoracleWeb only writes species- specific rules, so the filter is a constant on anything it can create: no-op or permanent mute. Zero of 17,420 production rules set it. PokemonFieldCoverageTests records that reasoning and fails the build if PoracleNG grows a column that is neither written nor excused. Refs #730 Claude-Session: https://claude.ai/code/session_01Nah4N2sGFs1TU2t7DXfKzJ
Stardust is the only reward type PoracleNG matches that PoracleWeb could not create. The list already rendered such rules -- it has had a STARDUST_AMOUNT string all along -- so the gap showed up as a reward you could see and not ask for. It gets its own tab, and its floor travels in `reward` rather than `amount`, because singleRewardMatches reads `reward` as the dust threshold for reward type 3 and ignores `amount` there. `amount` is the minimum quantity for the three rewards that come in quantities: items, candy and mega energy. PoracleNG has always stored it and PoracleWeb never sent it, so every rule created here asked for one of whatever it was. The card title now carries it, since a rule that fires only on three-plus and says nothing about it reads as broken. The edit dialog is unchanged on purpose: it does not edit the reward either. Both are identity fields in PoracleNG's diff, so changing one is a different rule rather than an edit of this one. Not added: the quest shiny flag. PoracleNG only matches it when the scanner reports shiny-eligibility on the reward, which cannot be confirmed for this deployment, and the failure mode of guessing wrong is a rule that silently matches nothing. Zero production rules set it. Refs #730 Claude-Session: https://claude.ai/code/session_01Nah4N2sGFs1TU2t7DXfKzJ
Both found by walking the new controls at 390px and 1400px on dev. The "Minimum Time Left" hint wraps to two lines at phone width, and Material reserves one line of subscript space, so it overlapped the SIZE heading below it by 4px. Headings in More Filters now carry a top margin; several locales wrap that hint at desktop width too, so this is not a phone-only fix. Adding the Stardust tab pushed the reward tab group from 418px to 520px in a 464px rail, so Material paginated it and put the newest tab behind an arrow on every screen. Trimming the tab padding fits all five with room to spare; phone width still scrolls, as it did with four. Refs #730 Claude-Session: https://claude.ai/code/session_01Nah4N2sGFs1TU2t7DXfKzJ
Found by creating a Mega X rule on dev and opening it again: the PVP tab showed League, Best/Worst Rank and Min CP, and no mega control at all. The picker was nested inside the level-cap fieldset, whose @if (showCapPicker()) is false on any server that does not advertise PVP level caps -- which is this deployment. So the add dialog offered it, the edit dialog did not, and a mega rule was create-only. It is now a sibling of the cap picker, matching the add dialog, and carries the same hint. No data was at risk: the form control was seeded from the alarm and sent back unchanged, verified against the dev row -- min_time 300 and pvp_ranking_evolution 2 both survived an edit, on the same uid. The regression test compares the two templates rather than the DOM. Material mounts only the active tab body, and driving a tab switch in this zoneless harness costs more than the assertion is worth; the defect was a control in the wrong block, so the block structure is what is asserted. It fails on the old markup. Refs #730 Claude-Session: https://claude.ai/code/session_01Nah4N2sGFs1TU2t7DXfKzJ
Swept every add dialog against its edit twin after the mega picker turned out to be create-only, comparing the form controls each template actually renders. Nine alarm types, three differences, one of them a defect: the quest minimum amount I added hours ago existed only in the add dialog. A card reading "3x Rare Candy" had no way back to the 3. Both editable fields here are thresholds -- they narrow the rule without changing which reward it is about. The reward itself stays fixed, because turning Rare Candy into a Poke Ball describes a different alarm rather than an edit of this one. Stardust is the exception that proves it: PoracleNG reads `reward` as the dust floor for that type, so there it is a threshold and is editable. The other two differences are real and now written down: pokemon `forms` is a fan-out multi-select with no single-alarm equivalent, and max battles names the same flag `gmaxOnly` when creating and `gmax` when editing. alarm-dialog-parity.spec.ts holds the sweep. Each exception carries its reason, and a third test fails if an exception no longer describes a real difference, so the list cannot rot into permission to drift. Verified red by deleting the control it was written for. Refs #730 Claude-Session: https://claude.ai/code/session_01Nah4N2sGFs1TU2t7DXfKzJ
Nine strings sat in the templates as English literals, so the ten non-English
locales rendered them untranslated no matter what the user picked: the two Max
Battle tab hints, both Gigantamax toggle hints, the all-levels note, the
(Gigantamax) option suffix, and Fort Type / Change Types / Fort Change
Tracking. Found by scanning every component template for text nodes with no
translate pipe.
Terminology follows what each locale already uses for "Max Battle" and "fort"
rather than a fresh translation of the term -- de "Max-Kampf", it "Battaglia
Max", sv "Max-strid", and so on -- so the new hints read as part of the page
they sit on.
Two English sentences were also passive next to their toggle ("When enabled,
only notifies about..."). They say the same thing in the active voice now,
which is what got translated.
The PVP level cap's aria-label was hardcoded English as well. It now binds to
POKEMON.PVP_CAP, the same string its visible legend uses, so screen readers get
the reader's language without a duplicate key.
Still English: the four strings on the admin landing page. The whole admin
section is untranslated by an existing decision, and doing one page of it would
read as an oversight rather than a choice.
Refs #730
Claude-Session: https://claude.ai/code/session_01Nah4N2sGFs1TU2t7DXfKzJ
Cross-checked PoracleWeb against PoracleNG main (c5e08cb4, 5.1.0 — the commit production runs) in three directions: every column each of the ten *TrackingAPI structs stores, every filter prefix the Discord commands accept, and the 48 command names themselves. One defect. PoracleNG's fort webhook reports `description` among its edit types, the `!fort` command accepts it and the matcher compares it case-insensitively -- PoracleWeb drew five checkboxes and not that one. Since the stored list is rebuilt from the checkboxes on every save, a rule a user set with the bot lost `description` the next time they touched the radius on the web. Now it has a box, and anything else PoracleNG grows is carried through rather than rebuilt away. Production has 13 fort rules and none of them use `description`, so nothing was lost in the field. FortChangeOptions.ValidChangeTypes was also missing it. Nothing reads that set today, which is the only reason this was latent rather than a refusal: an allowlist that does not match what upstream accepts is the shape that has bitten this codebase repeatedly. TrackingFieldCoverageTests now runs over all ten tracking types instead of two. Each excuse is keyed by type.column, `*.column` covers the ones common to every type, and a stale excuse fails the build. The only columns unaccounted for anywhere are pokemon rarity/max_rarity, whose reason is now accurate: it is a per-species tier, so it is constant on the rules PoracleWeb creates, meaningful only on the 534 track-everything rules the bot owns, and set by nobody. Refs #730 Claude-Session: https://claude.ai/code/session_01Nah4N2sGFs1TU2t7DXfKzJ
* docs: cut changelog for v2.15.2 * docs: cut changelog for v2.15.3 --------- Co-authored-by: poracleweb-net-release[bot] <291134500+poracleweb-net-release[bot]@users.noreply.github.com>
…-develop # Conflicts: # CHANGELOG.md
hokiepokedad2
marked this pull request as draft
August 19, 2026 20:03
Contributor
Author
|
Holding this — not cutting a release yet, on the owner's call. The branch is ready when that changes: CI is green and #757 clears the CHANGELOG conflict. Nothing here has shipped; |
chore: merge main into develop so the release can fast-forward
PoracleWeb assumed 5.1.0 and never checked. Against an older server the features that need it write columns that do not exist: PoracleNG's decoder drops the unknown field, the write returns 200, and the filter does nothing. That is indistinguishable from a bug in PoracleWeb, which is the failure mode this whole release has been about. Two reads, because they answer different questions. /health -- unauthenticated, so no secret and it still answers when the API key is wrong -- gives the release number and PoracleNG's own capability map. The applied migration number from schema_migrations gives what the map does not: which alarm columns exist. 5.1.0 sits at 5; costume lands at 6 and 7 on their develop branch. Not branch detection, deliberately. PoracleNG stamps its branch into the binary but publishes only the version, so a develop build between releases reports the last release's number and cannot be told apart. It is also the wrong question: self-hosters run forks and cherry-picks, and what matters is whether this server can store a given field. Every unknown resolves to unsupported -- unreachable, unparseable version, unreadable schema. The opposite default would offer controls that write nothing. "0.0.0" is treated as unknown rather than ancient, because that is what an un-stamped local build reports and shouting "upgrade" at it would teach admins to ignore the banner. Nothing is gated yet; there is nothing to gate. Every feature in this build works on 5.1.0. This is the mechanism, plus the floor check that has immediate value, and the four hardcoded English strings on the admin landing page are translated on the way past. Refs #730 Claude-Session: https://claude.ai/code/session_01Nah4N2sGFs1TU2t7DXfKzJ
Caught by opening /admin on dev after deploying #758: the card was not there. AdminComponent is not routed. /admin redirects to /admin/users, and the sidebar links to users, webhooks and settings directly, so the landing page has been dead code -- which is also why its four hardcoded English strings survived every translation pass. Nobody could get to them. The card moves to the top of Admin -> Settings, extracted as its own component rather than more markup in a file that is already long, and testable on its own, which the version on an unrouted page was not. The dead page and the five translation keys that existed only for it are deleted. Shipping an invisible control is the same defect this card was built to expose, so: found in a browser, not in a test. The unit tests passed on the dead page too. Refs #730 Claude-Session: https://claude.ai/code/session_01Nah4N2sGFs1TU2t7DXfKzJ
Two projects, two ways of publishing, so two ways of reading the latest. PoracleWeb cuts GitHub releases, so its latest is a tag name. PoracleNG has no releases and no tags -- checked, the API returns an empty list -- and keeps its version as a constant in processor/version.go, so the released number is read from that file on main. That file also settles the branch question I got wrong earlier. I said a develop build reports the last release's number and so cannot be told apart. It does not: version.go is bumped at the start of a cycle, so main reads 5.1.0 while develop already reads 5.2.0. A binary reporting more than main is a development build by definition, and the card names it as one instead of claiming it is out of date. That does not change the gating design. What may I offer is still a capability question -- a fork with costume backported would report an unrelated version -- and this is a maintenance question. They want different signals and now have them. This is the only outbound request PoracleWeb makes. It sends nothing, is cached for six hours against a 60-per-hour anonymous allowance, and disable_update_check switches it off before any call is made rather than discarding the result. Everything unknown -- unreachable, unparseable, a beta channel, a local build -- renders as no line at all, because a false "you are behind" costs more than a missing one. Refs #730 Claude-Session: https://claude.ai/code/session_01Nah4N2sGFs1TU2t7DXfKzJ
The card described the Poracle server and only mentioned PoracleWeb when it had an update to report. So on a current deployment it said nothing about the site, and on dev -- where the channel is "beta" and not comparable to a release -- it said nothing either. The half an admin is most likely to be behind on was the half that never appeared. Both now get a section: version, build reference, and a line that says up to date, behind, a development build, or -- for a rolling channel that cannot be compared -- which release is current. The card is titled Versions rather than Poracle server, because it is about both. Refs #730 Claude-Session: https://claude.ai/code/session_01Nah4N2sGFs1TU2t7DXfKzJ
Spotted while taking a documentation screenshot of the Delivery tab: "Message Settings" was flush against the scope picker, measured 0px of gap. The heading margin added in #750 exempts the first heading in a tab, which is right when that heading starts the tab and wrong otherwise. :first-of-type matches the first h4 among its siblings whatever precedes it, so a heading that follows a control still lost its margin. :first-child is the predicate that matches the intent. Refs #730 Claude-Session: https://claude.ai/code/session_01Nah4N2sGFs1TU2t7DXfKzJ
* fix(ui): first-child, not first-of-type, for dialog headings Spotted while taking a documentation screenshot of the Delivery tab: "Message Settings" was flush against the scope picker, measured 0px of gap. The heading margin added in #750 exempts the first heading in a tab, which is right when that heading starts the tab and wrong otherwise. :first-of-type matches the first h4 among its siblings whatever precedes it, so a heading that follows a control still lost its margin. :first-child is the predicate that matches the intent. Refs #730 Claude-Session: https://claude.ai/code/session_01Nah4N2sGFs1TU2t7DXfKzJ * docs: catch the documentation up with what the site does Audited the README, the 31-page docs site, the in-app help and both screenshot sets against the code. 101 findings: 35 sentences that would actively mislead a reader today, 38 features never documented, 21 stale, 7 cosmetic. The corrections that matter most are the ones that had gone wrong rather than merely missing. The help told people to open "Areas & Location", which is now Areas & Places. Alarm docs described the two-mode "Use Areas or Set Distance" delivery model that the per-alarm scope replaced. The fort change_types list omitted description. Quests had no filter documentation at all, so stardust and minimum amounts had nowhere to be described. internationalization.md sent readers to a page that had been renamed and a language setting that had moved. The in-app guide is corrected in all eleven locales. That content is 37,000 to 42,000 characters of HTML per locale, and the audit found fr.json is condensed rather than faithful, so edits matched sentences by content rather than position. Eleven screenshots were retaken at 1440x900 against the running dev deployment, with demo data seeded directly into that database and removed afterwards -- the account is back to the state it was in. Four are new: the scope picker, the delivery chip on a card, the Places section, and the Versions card. Every factual claim the writing pass made was checked against the code by a separate reviewer. Nineteen failed and were corrected, including one this session had repeated in a PR description: the update check is not the only outbound call PoracleWeb makes -- MasterDataService fetches the masterfile from raw.githubusercontent.com on any visit. Also fixed while reading: docs disagreed with each other about where weather data comes from and which writes bypass the proxy. Refs #730 Claude-Session: https://claude.ai/code/session_01Nah4N2sGFs1TU2t7DXfKzJ
I ran `mkdocs build --strict` to check the docs site still builds, which writes site/ next to docs/, and the commit in #763 swept 125 generated files and 12MB into the repository. Nothing reads them: docs.yml runs mkdocs gh-deploy itself from docs/. Removed and added to .gitignore so the next person checking a docs build does not repeat it. Claude-Session: https://claude.ai/code/session_01Nah4N2sGFs1TU2t7DXfKzJ
The documentation audit reported that disable_user_geofences does not gate
DELETE custom/{id}, which is true: the attribute is on rename, create, submit,
activate, deactivate and import, and the service gates the same five. Delete is
covered by neither.
It should stay that way. Disabling the feature hides the page and refuses new
work, but fences that already exist keep being served in the feed and keep
matching, so a gated delete leaves someone receiving alerts from an area they
can neither edit nor remove.
The alarm types gate their whole controller and get away with it because the
bot is the escape hatch -- !untrack removes an alarm whatever the web says.
Geofences are PoracleWeb-only and none of the bot's 48 commands manages a drawn
polygon, so this endpoint is a user's only route to their own data. Production
holds 42 of them.
No behaviour change. The reason is now in the code, and a test pins the split:
delete ungated, the other six gated, and the class itself must not acquire the
attribute -- which would silently re-gate the delete and the reads with it.
Refs #730
Claude-Session: https://claude.ai/code/session_01Nah4N2sGFs1TU2t7DXfKzJ
They were captured against a running deployment, so the signed-in account's Discord handle and avatar came along: the handle in the user menu, the avatar in the toolbar of every full-page shot. The user menu is retaken under a placeholder name with a neutral silhouette. The other ten are patched in place rather than retaken, because the demo data they show was torn down afterwards and re-seeding it to change 28 pixels is not a trade worth making. The silhouette matches what the app already draws for a user with no Discord picture, so it reads as a default avatar rather than a redaction. Refs #730 Claude-Session: https://claude.ai/code/session_01Nah4N2sGFs1TU2t7DXfKzJ
hokiepokedad2
marked this pull request as ready for review
August 20, 2026 02:20
hokiepokedad2
enabled auto-merge
August 20, 2026 02:21
hokiepokedad2
added this pull request to the merge queue
Aug 20, 2026
Any commits made after this event will not be merged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
35 commits since v2.15.3. Minor bump: this carries new user-facing filters and a new page, not only fixes.
New
/placesredirects there.Fixed
21 entries. Eight were defects in this release's own new code, found by using it rather than reading it: a mega picker that stored nothing, the same picker unreachable when editing, quest minimums that were create-only, a server card shipped on an unrouted page, two pages overflowing a phone screen, and hint text overlapping headings twice.
Documentation
The README, 31 docs pages and the in-app guide were audited against the code — 101 findings, 35 of them sentences that would mislead a reader today. The in-app guide is corrected in all eleven languages and eleven screenshots were retaken. Publishing this release is also what publishes those docs, since
docs.ymldeploys frommain.Compatibility
Requires PoracleNG 5.1.0 or newer, which production already runs. Below that the delivery scope, the mega filter and the time-left filter write columns that don't exist — silently. This release adds the check that says so, in the logs at startup and on the admin card.
Cross-checked against PoracleNG
main(c5e08cb4): every column of all ten tracking types is either written or has a recorded reason, and every filter the Discord commands accept maps to something the web can set.Before publishing
Merging this ships nothing —
docker-publish.ymlbuilds:latestonrelease: published, not on a push to main. Publishing the release is what reaches production, within watchtower's 60-second poll.https://claude.ai/code/session_01Nah4N2sGFs1TU2t7DXfKzJ