Both editors on Filters & rules kept their edits in component state, so every way out of the page discarded them silently — a settings link, the app rail, even the Rules/Scripts switch. And as the issue points out, with a long rule list the Save button is scrolled off the bottom, so there was no persistent sign that anything was pending either.
The fix
Ask before leaving. Editors register what they have pending (web/src/lib/unsavedChanges.ts); navigation asks first. The question is Save / Discard / Stay here rather than a yes-or-no "leave without saving?", which would make losing the work the easy answer.
Wouter routes links, redirects and navigate() through a single aroundNav hook, so one guard on the router covers the app rail and the settings nav without either of them knowing an editor exists. The Rules/Scripts switch asks for itself, since it unmounts the editor without ever reaching the router. Reload and tab close fall to beforeunload and the browser's own generic prompt.
The back button is the known gap: by the time popstate arrives the history has already moved, and holding the page would mean pushing an entry back, which breaks the button for everyone with nothing pending.
Pin the save bar. It sticks to the foot of the settings pane with an "Unsaved changes" marker, so the indicator is on screen whether or not the rules fit in the window.
Two things fixed on the way past
Both in the raw script editor:
Saving cleared only sel, but the editor is shown whenever there is a name or a body — so it stayed open with the name field unlocked, and a second save created a duplicate script instead of updating the one just written.
The condition deciding whether the editor is open was a pair of identical nested ifs; it is now the one question it was asking twice.
Verification
npm run typecheck, npm test (492 pass, 6 new) and npm run build all green. Driven by hand against dev:mock as well:
16 rules, one toggled → bar pinned at the foot showing Unsaved changes, rules scrolling behind it
settings nav link → dialog, URL unchanged; Save changes → "Filters saved", then the navigation completes
Rules → Scripts switch → same dialog; Discard changes → lands on Scripts
raw script edited, then Mail on the app rail → dialog with the script's own wording; saving leaves the script's active state exactly as it was, and the edit is there on reopening
One note: sticky bottom: 0 stops at the scroll container's padding box, which left the pane's 64px bottom padding as a strip for rules to scroll through below the bar. The offset is now minus that padding, via a variable so the narrow layout's 80px works too.
Not translated yet
Eight new strings fall back to English in all nine locales. Happy to add translations if you'd rather not wait for a native pass.
Fixes #175.
## The problem
Both editors on **Filters & rules** kept their edits in component state, so every way out of the page discarded them silently — a settings link, the app rail, even the Rules/Scripts switch. And as the issue points out, with a long rule list the Save button is scrolled off the bottom, so there was no persistent sign that anything was pending either.
## The fix
**Ask before leaving.** Editors register what they have pending (`web/src/lib/unsavedChanges.ts`); navigation asks first. The question is Save / Discard / Stay here rather than a yes-or-no "leave without saving?", which would make losing the work the easy answer.
Wouter routes links, redirects and `navigate()` through a single `aroundNav` hook, so one guard on the router covers the app rail and the settings nav without either of them knowing an editor exists. The Rules/Scripts switch asks for itself, since it unmounts the editor without ever reaching the router. Reload and tab close fall to `beforeunload` and the browser's own generic prompt.
The back button is the known gap: by the time `popstate` arrives the history has already moved, and holding the page would mean pushing an entry back, which breaks the button for everyone with nothing pending.
**Pin the save bar.** It sticks to the foot of the settings pane with an "Unsaved changes" marker, so the indicator is on screen whether or not the rules fit in the window.
## Two things fixed on the way past
Both in the raw script editor:
- Saving cleared only `sel`, but the editor is shown whenever there is a name or a body — so it stayed open with the name field unlocked, and a second save created a duplicate script instead of updating the one just written.
- The condition deciding whether the editor is open was a pair of identical nested `if`s; it is now the one question it was asking twice.
## Verification
`npm run typecheck`, `npm test` (492 pass, 6 new) and `npm run build` all green. Driven by hand against `dev:mock` as well:
- 16 rules, one toggled → bar pinned at the foot showing **Unsaved changes**, rules scrolling behind it
- settings nav link → dialog, URL unchanged; **Save changes** → "Filters saved", then the navigation completes
- Rules → Scripts switch → same dialog; **Discard changes** → lands on Scripts
- raw script edited, then Mail on the app rail → dialog with the script's own wording; saving leaves the script's *active* state exactly as it was, and the edit is there on reopening
One note: sticky `bottom: 0` stops at the scroll container's padding box, which left the pane's 64px bottom padding as a strip for rules to scroll through *below* the bar. The offset is now minus that padding, via a variable so the narrow layout's 80px works too.
## Not translated yet
Eight new strings fall back to English in all nine locales. Happy to add translations if you'd rather not wait for a native pass.
**Merged** 2026-09-01 as coffey-labs/ihasmail@1a29784848c7
<sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes #175.
The problem
Both editors on Filters & rules kept their edits in component state, so every way out of the page discarded them silently — a settings link, the app rail, even the Rules/Scripts switch. And as the issue points out, with a long rule list the Save button is scrolled off the bottom, so there was no persistent sign that anything was pending either.
The fix
Ask before leaving. Editors register what they have pending (
web/src/lib/unsavedChanges.ts); navigation asks first. The question is Save / Discard / Stay here rather than a yes-or-no "leave without saving?", which would make losing the work the easy answer.Wouter routes links, redirects and
navigate()through a singlearoundNavhook, so one guard on the router covers the app rail and the settings nav without either of them knowing an editor exists. The Rules/Scripts switch asks for itself, since it unmounts the editor without ever reaching the router. Reload and tab close fall tobeforeunloadand the browser's own generic prompt.The back button is the known gap: by the time
popstatearrives the history has already moved, and holding the page would mean pushing an entry back, which breaks the button for everyone with nothing pending.Pin the save bar. It sticks to the foot of the settings pane with an "Unsaved changes" marker, so the indicator is on screen whether or not the rules fit in the window.
Two things fixed on the way past
Both in the raw script editor:
sel, but the editor is shown whenever there is a name or a body — so it stayed open with the name field unlocked, and a second save created a duplicate script instead of updating the one just written.ifs; it is now the one question it was asking twice.Verification
npm run typecheck,npm test(492 pass, 6 new) andnpm run buildall green. Driven by hand againstdev:mockas well:One note: sticky
bottom: 0stops at the scroll container's padding box, which left the pane's 64px bottom padding as a strip for rules to scroll through below the bar. The offset is now minus that padding, via a variable so the narrow layout's 80px works too.Not translated yet
Eight new strings fall back to English in all nine locales. Happy to add translations if you'd rather not wait for a native pass.
Merged 2026-09-01 as coffey-labs/ihasmail@1a29784848
Rebuilt from: git history, session transcript.