@bobfrankston/rmfmail 1.2.191 → 1.2.192

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/TODO.md CHANGED
@@ -274,7 +274,7 @@ Small, self-contained items. Pick them up between higher-priority blocks without
274
274
  - **Q149 — Visible prefer/ignore affordances.** (1) ✅ **Done v1.1.55** — compose autocomplete rows now have visible per-row ★ (prefer → `contacts.jsonc#preferred[]`) and ⊘ (ignore → `denylist[]`) buttons, shown on hover; plain click, no dependence on the flaky right-click. (2) ✅ **Done v1.2.190** (2026-07-30) — "⊘ Never use this address" added to the message-viewer From/To/Cc right-click menu (`message-viewer.ts`), routed through `addToDenylist`. (3) Related: route ★ to a Google-Contacts star instead of `preferred[]` once that's decided (see Q151 discussion).
275
275
  - **Q148 — "Open in editor" + live file-watch for the JSONC config editor.** Bob 2026-05-16: the Edit-config-file modal's textarea is cramped; he wants a button revealing the source path so he can edit in a full editor (VS Code), with the modal watching the file and reflecting external changes. Local/cloud split: `config.jsonc` is a real local file (`~/.rmfmail/config.jsonc`) — trivial: a "Reveal source" button + `fs.watch` → re-read the textarea on change. The other files (`accounts.jsonc`, `allowlist.jsonc`, `clients.jsonc`, `contacts.jsonc`) live on Google Drive via the Drive API — **no local path exists**. To edit those externally, "check out" a local working copy (e.g. `~/.rmfmail/config-edit/<name>`), reveal that path, `fs.watch` it, and `cloudWrite` back on change. Modal gains: a "Reveal/Open source" button per file + a watcher that updates the textarea (and warns on a dirty-buffer conflict). ImapManager already runs `fs.watch` on the local config files (`watchConfigFiles()` → `configChanged`) — reuse that channel for the local case.
276
276
  - **Q147 — Message list stalls mid-scroll during sync churn — incremental updates + virtualization.** Bob 2026-05-16: "scrolling the list of messages, it seems stuck and finally gets loose." Design defect, not a symptom to patch. Root cause: `renderMessages` rebuilds the whole list (`new MessageRow` per row, ~10 DOM nodes each) and there is **no virtualization** — infinite scroll appends pages so a large folder holds thousands of live nodes. Sync events (and the every-30s calendar/task refresh storm makes them constant) trigger synchronous list re-render and/or `scrollIntoView` re-anchoring on the **main thread the user is scrolling** → scroll input queues until the burst clears. Existing `{scroll:false}` guards (added 2026-05-14 for the same "won't let me scroll" report) patched the symptom; the defect remains. Fix, two parts: (1) **sync events update rows in place** — a changed flag/date/folder mutates that one `MessageRow`'s DOM; never rebuild the list and never touch scroll/focus on a background reconcile; (2) **virtualize** large folders so only viewport-adjacent rows are in the DOM. Continuation of the row-objects-own-behavior direction (related: S56). Touches `message-list.ts` render + the store-subscription/event handlers.
277
- - **Q146 — All-day events are floating calendar dates, not instants — model fix.** Bob 2026-05-16. Interim fix shipped v1.1.50: `calendarEventToLocal` parses an all-day `start.date` as *local* midnight instead of UTC (`Date.parse` treats date-only as UTC → a June 14 birthday rendered June 13 in US Eastern). That makes it correct *on the current machine in the current timezone* but the underlying model is still wrong: an all-day event — a birthday especially — is a **floating date** ("June 14", true everywhere), not a timestamp. Storing it as `start_ms` epoch ties a date to a timezone; it'll drift again on travel / timezone change / cross-device view. Proper fix: distinguish **timed events** (an instant — `start_ms`, fine) from **date events** (store the `YYYY-MM-DD` string, no timezone). Day-grouping, the day header, and rendering key off the date directly for date-events; only timed events do epoch math. Reminders on all-day events (Google's "1 day before at 9am") still need a timezone anchor — resolve that against the *local* day. Touches `GCalEvent`/`calendarEventToLocal`, `calendar_events` schema (a date column or an `all_day` + date pair), `calendarRowToObject`, the sidebar `CalEvent` + `fetchUpcoming` + `renderEvents` grouping, and the alarm scheduler. Medium model change — the v1.1.50 parse is a stopgap until it lands.
277
+ - **Q146 — All-day events are floating calendar dates, not instants — model fix.** *Update 2026-07-30 (v1.2.192): priority reduced — the practical drift mostly dissolves on inspection: the calendar cache is per-device and re-syncs every 5 min, so each device parses all-day dates in its own timezone and self-corrects minutes after a timezone change. The real bugs in this class were elsewhere and are now fixed: Google Tasks due dates parsed as UTC midnight (a day early + premature overdue in US Eastern), all-day event creation writing the UTC date (a day early east of UTC), and UTC-formatted due labels — see DONE v1.2.192. The floating-date model below remains the right end-state but is a nicety now, not a defect.* Bob 2026-05-16. Interim fix shipped v1.1.50: `calendarEventToLocal` parses an all-day `start.date` as *local* midnight instead of UTC (`Date.parse` treats date-only as UTC → a June 14 birthday rendered June 13 in US Eastern). That makes it correct *on the current machine in the current timezone* but the underlying model is still wrong: an all-day event — a birthday especially — is a **floating date** ("June 14", true everywhere), not a timestamp. Storing it as `start_ms` epoch ties a date to a timezone; it'll drift again on travel / timezone change / cross-device view. Proper fix: distinguish **timed events** (an instant — `start_ms`, fine) from **date events** (store the `YYYY-MM-DD` string, no timezone). Day-grouping, the day header, and rendering key off the date directly for date-events; only timed events do epoch math. Reminders on all-day events (Google's "1 day before at 9am") still need a timezone anchor — resolve that against the *local* day. Touches `GCalEvent`/`calendarEventToLocal`, `calendar_events` schema (a date column or an `all_day` + date pair), `calendarRowToObject`, the sidebar `CalEvent` + `fetchUpcoming` + `renderEvents` grouping, and the alarm scheduler. Medium model change — the v1.1.50 parse is a stopgap until it lands.
278
278
  - **Q145 — From header: RFC 2047 encoded-word not decoded when it spans the whole `name <addr>`.** ✅ **Fixed v1.1.49** — `db.upsertMessage` now runs `decodeHeaderWords` over the `address` of from/to/cc, not just the display `name`. A legit address never carries an encoded-word so it's a no-op there; for the spam case the `=?utf-8?q?…=3C…=40…?=` mailbox decodes to readable text. Bob 2026-05-16, `screenshots/Screenshot 2026-05-16 141730.png`, sample `.eml` at `~/.rmfmail/mailxstore/bobma/1f/1fc0be6eb64d43e88503afaa8e424009.eml`. The From renders raw as `=?utf-8?q?PayPal_Nouveaut=C3=A9s_=3CParts=40samisec=2Ecom=3E?=@exch-smtp-out-01.wtr.livemail.co.uk`. The sender packed the entire display-name-and-address — including the `<`, `@`, `.`, `>` as `=3C =40 =2E =3E` — into ONE encoded-word, then concatenated `@domain` with no whitespace. Decoded it is `PayPal Nouveautés <Parts@samisec.com>`. mailx fails to decode it: the encoded-word isn't whitespace-delimited and sits where an addr-spec is expected, so the address parser (envelope `tokenizeParenList` / header decode) skips RFC 2047 decoding. Technically malformed mail, but Thunderbird decodes it. Fix: run encoded-word decoding on From/To/Cc display strings before address splitting, and tolerate a `=?...?=` token adjacent to non-whitespace. The broken string also propagates into the remote-content "Always: …" allowlist suggestions.
279
279
  - **Q143 — Dynamic per-calendar checkboxes + per-source icons (Google-selected calendars).** ✅ **Implemented v1.1.47** (pending runtime verify). Shipped: `getCalendars()` service method, dynamic checkbox list, blue-dot/🇺🇸/✡️/🎂/monogram icons, per-event source icons, `hiddenCalendars` display-filter, `HOLIDAY_SOURCES` + `showHolidays`/`showJewishHolidays` removed. **Deferred**: multi-source badge (one event from two calendars still shows one icon — last-write-wins on `calendarId`; needs `iCalUID` + a source-set). Bob 2026-05-16, design settled. **Drop the hardcoded model**: today `HOLIDAY_SOURCES` hardcodes two Google calendar IDs and the sidebar has two fixed checkboxes (`showHolidays` / `showJewishHolidays`). Replace with a fully dynamic scheme — Bob curates *which* calendars exist by selecting them in Google Calendar; mailx enumerates the `selected` calendars (`listCalendars().filter(c => c.selected)`, already done in `refreshCalendarEvents`) and the sidebar **generates one checkbox per selected calendar**, each with an icon. Per-mailx visibility persists per calendar ID (replaces the two boolean settings). Calendar-URL input / non-Google calendar kinds are **deferred** (future TODO) — for now only Google-selected calendars.
280
280
  - **Icons**: known types get a themed glyph — US holidays 🇺🇸, Jewish holidays ✡️, Birthdays 🎂 (detect via calendar ID pattern / `eventType`). Every other named calendar gets a **monogram badge**: the calendar name's first letter in a colored circle, color taken from the Google calendar's color (e.g. "Frankston Family" → "F" in a colored circle). The checkbox row and each event row from that calendar show the icon.
@@ -5503,7 +5503,7 @@ function overdueTaskRowHtml(t) {
5503
5503
  if (t.dueMs) {
5504
5504
  const d = new Date(t.dueMs);
5505
5505
  const sameYear = d.getFullYear() === (/* @__PURE__ */ new Date()).getFullYear();
5506
- dueLabel = sameYear ? `${d.getMonth() + 1}/${d.getDate()}` : d.toISOString().slice(0, 10);
5506
+ dueLabel = sameYear ? `${d.getMonth() + 1}/${d.getDate()}` : `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
5507
5507
  }
5508
5508
  return `<div class="cal-side-task cal-side-task-overdue" data-uuid="${escapeHtml5(t.uuid)}">
5509
5509
  <input type="checkbox" class="cal-side-overdue-check" title="Mark done">
@@ -5677,7 +5677,7 @@ async function renderTasks(prefetched) {
5677
5677
  if (t.dueMs) {
5678
5678
  const d = new Date(t.dueMs);
5679
5679
  const sameYear = d.getFullYear() === startOfToday.getFullYear();
5680
- const label = sameYear ? `${d.getMonth() + 1}/${d.getDate()}` : d.toISOString().slice(0, 10);
5680
+ const label = sameYear ? `${d.getMonth() + 1}/${d.getDate()}` : `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
5681
5681
  const overdue = !done && d < startOfToday;
5682
5682
  dueHtml = `<span class="cal-side-task-due${overdue ? " overdue" : ""}" title="${d.toLocaleDateString()}">${label}</span>`;
5683
5683
  } else {