Commit Graph
5 Commits
Author SHA1 Message Date
jcoffey-dev d1731efdb9 Use American English spelling throughout 2026-09-15 11:45:58 -07:00
jcoffey-dev 635c4c7e52 Keep a settings change made before the first read, and wait for it
Two defects on the path that decides what language the app starts in.

**A change made before the settings file came back was thrown away.**
`queueSettingsPush` returned early while unarmed, dropping the value
instead of holding it, so a language picked in the second or so after a
page load was never written up: it survived until the next reload and no
further. That is a better account of "sometimes it takes several clicks"
than the remount race fixed in #160 — the click that stuck was one made
after the read had finished. Keeping it is safe because hydrate already
refuses to overwrite a key that is still queued.

Proof it was real: before this, no `ihasmail` folder was ever created in
the account's files, because the seed write never fired. After it, the
folder appears.

**Without a cached settings object the tree painted too early.** The
cache is not read on an untrusted device, and it is cleared by the
sign-out that every deploy causes, so in both cases the first frame is
the defaults — and the defaults are English. Anything computed in that
window is computed in the wrong language. The interface recovers, since
it is rebuilt when the catalogue lands, but a string emitted once does
not: this is why the stale-folder toast came out in English on an
otherwise German screen.

So without a cache the authenticated tree now waits for the account's
settings and their catalogue, which costs nothing — there was nothing
worth painting yet. With a cache it does not wait, and the first frame is
as quick as it was.

Neither fix makes the toast German yet: the account settings file is
neither written nor read successfully in the mock, and both failures are
swallowed. That is a third problem, and this commit does not touch it.
2026-08-31 15:03:40 -07:00
jcoffey-dev d255c20215 Keep push alive across a deploy, not just across a week
#143 added a device-local flag recording that background notifications were
switched on in this browser, and made the renewal on app start key off it. It
is not in KEEP_ON_SIGN_OUT, and that is the whole bug: clearSignedInData()
runs on two different endings and only one of them is a sign-out.

The other is a session expiring, which is what a deploy does to every signed-in
browser at once. That path deliberately does not remove the push subscription
-- there is no session left to remove it with -- so the subscription stays
registered at Stalwart and the browser keeps its own. Losing the flag there
left nothing to renew them: push would have gone quiet a week after every
deploy, with the switch in Settings still reading as on because both ends of
the subscription still existed. That is the exact failure #143 was written to
prevent, reintroduced through a different door, and the first deploy carrying
#143 would have been the thing that triggered it.

Signing out for real still forgets it. That happens directly in
unsubscribeThisDevice, next to destroying the subscription, and it happens even
when the server cannot be reached -- a browser that goes on believing it has
push would have renewal resurrect it on the next sign-in.

Both halves are tested now, because they are one invariant seen from two sides:
storage.test.ts covers the flag surviving an expiry, webpush.test.ts covers a
sign-out clearing it with the server unreachable.
2026-08-31 08:20:31 -07:00
jcoffey-dev 0b01956535 Ask whose computer this is, and believe the answer
Sign-out never cleared local storage. It stopped push, flushed settings and
removed the subscription -- that last one reasoned explicitly that a browser
left holding someone's mail becomes somebody else's next -- and then left the
settings cache and the recently-addressed list on disk. That list is other
people's addresses, and nothing ever removed it.

Clearing it on sign-out is now unconditional, because lending a laptop is the
same exposure as a public machine, only quieter. The keep-list is short and
deliberate: lastUser, which only a trusted device writes; the trust flag; and
the random push device id. Everything else goes, so a key added later is
forgotten by default rather than by nobody having thought about it.

"Keep me signed in on this device" defaulted to true, which assumed the answer
most costly to get wrong -- someone on a library machine got a thirty-day
cookie unless they noticed a ticked box. It now asks whose computer this is,
defaults to not yours, and says what each answer does. Untrusted means a
session cookie, nothing written locally, no push subscription, and a five
minute idle sign-out.

The idle timer is there because the alternative does not work: custom
beforeunload text was removed from browsers years ago, and no event fires at
all for walking away from a signed-in screen, which is the case that matters.
A timer needs nobody's cooperation.

Reads are gated as well as writes, since a machine trusted once still has the
residue; an untrusted sign-in purges it outright. The wire keeps calling this
`remember` -- it is persisted in SESSION_FILE, and renaming it would invalidate
every session file on upgrade for a change of vocabulary.

Verified in a browser against the mock, not only in tests: untrusted sign-in
leaves localStorage empty through a full session including folder expansion;
trusted writes settings, recent and lastUser as before; sign-out clears recent
and settings while keeping lastUser; an untrusted sign-in afterwards clears
even that.
2026-08-28 14:15:15 -07:00
jcoffey-dev 645b8b510f ihasmail 2.0: rebuild as Stalwart-first JMAP webmail
Replace the FastAPI/HTMX prototype with a Node/Hono session proxy and a
React 19/Vite SPA. Mail (conversation view, search operators, labels,
sanitised HTML, privacy image proxy, invites, undo send, templates),
calendar (month/week/day/agenda, invites, free/busy, categories,
context menus), contacts (JSContact, groups, vCard), files, Sieve filter
builder (incl. filter-from-message with retroactive apply), vacation,
identities with default + Reply-To, PWA/mobile layout, push via SSE,
in-memory mock Stalwart for dev, Docker + CI.
2026-08-23 01:07:13 -07:00