Skip to content

feat(where): the shared control for an alarm's delivery scope - #733

Merged
hokiepokedad2 merged 6 commits into
developfrom
feat/where-ui
Aug 19, 2026
Merged

feat(where): the shared control for an alarm's delivery scope#733
hokiepokedad2 merged 6 commits into
developfrom
feat/where-ui

Conversation

@hokiepokedad2

Copy link
Copy Markdown
Contributor

Draft. First half of the per-alarm scope UI, from the proposal. Depends on #732 for the fields it reads and writes.

What is here

  • alarm-scope.ts — reads override_location_label, override_areas and the radius back into one discriminated value, and writes it out again. This is the load-bearing piece: PoracleNG stores three independent columns and enforces their mutual exclusion with three validation rules, and collapsing them into one choice is what lets the sheet be a radio group instead of three fields plus error copy. The states PoracleNG refuses cannot be expressed.
  • WhereChipComponent — the scope as a sentence fragment ("Within 2 km of Home", "Only in Terrigal, Erina"), and the way into the sheet. The inherited case is deliberately quiet since it is on nearly every card.
  • WhereSheetComponent — the three modes. Its area picker lists the user's own geofences next to admin areas, which is only honest because feat: per-alarm delivery scope, including user-drawn geofences #732 writes those past PoracleNG's user-selectable filter.
  • PlacesService — saved places as a signal, because the sheet, the Places screen and the cards read the same list and a place added in one has to appear in the others.

Two decisions worth reviewing

Nothing says "override". People think "where", not override_location_label. The framing throughout is that every alarm already had an answer to this question and we are making it visible, not adding a new capability.

Clearing sends empty values, never null. Null means "not stated, keep what is stored" on the write path (#731), so sending null would make an override impossible to take off. scopeToFields is where that lives, and it has a test saying so.

Tests

14 cases on the scope utilities, including two that encode real judgement calls rather than mechanics:

  • An empty area list reads back as inherited, not as an empty restriction. An alarm restricted to no areas would match nothing, and the cleared state is stored as an empty column.
  • A row carrying both a place and areas resolves to areas. PoracleNG refuses to store both, but an older client might have; areas is the more restrictive reading, so the alarm cannot silently widen.

Component specs for the chip and sheet come with the wiring, when there is a card to render them in.

Still to do

Wiring into the nine alarm dialogs and their card templates, a Places screen under the user menu (with the 409 referencingRules flow, which is the interesting part of deleting a place), the AlertDefaultsService extension for a default place, and the mega-PVP segmented control. Locale keys are in all eleven files with English text pending translation.

Screenshots

None yet — the sheet has no entry point until the dialogs are wired. Worth a look then rather than now.

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.
@github-actions github-actions Bot added the feat label Aug 19, 2026
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.
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.
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.
@hokiepokedad2
hokiepokedad2 marked this pull request as ready for review August 19, 2026 04:14
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.
@hokiepokedad2
hokiepokedad2 merged commit 4351412 into develop Aug 19, 2026
6 checks passed
@hokiepokedad2
hokiepokedad2 deleted the feat/where-ui branch August 19, 2026 05:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant