@bobfrankston/rmfmail 1.0.620 → 1.0.627

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
@@ -787,6 +787,11 @@ Was part of S1 "local-first reconciliation refactor" until 2026-04-23 when S1's
787
787
 
788
788
  | Item | Version |
789
789
  |---|---|
790
+ | **WebView timing forwarded to daemon log + cleanups** — Daemon log shows boot is **~204 ms to showService** (2026-05-09 14:23 run). The "long startup" delay is entirely WebView2 init + JS module cascade + first paint after the daemon hands off. The `[wv N ms]` / `[preview N ms]` / `[search N ms]` marks were `console.log`-only — invisible to the daemon log. Now forwarded via `mailxapi.logClientEvent` so they land alongside `[boot N ms]` for full timeline. Plus: `android.html` removed (renamed `.deleted`); `silent=true` on logit URLs (Android + WebView error handlers) so verbose marks don't spam jserv UI; build clean. Run mailx, check the log — actual breakdown of where the time goes is now visible. | v1.0.626 |
791
+ | **Preview-path timing checkpoints** — User report 2026-05-09: "still seeing very long delays on previews that should be instantaneous? It finally displayed but why?" The boot timing answers cold-start; this answers click-to-preview. New `[preview N ms] showMessage entry / parsedCache HIT|MISS / IPC done (cached, N bytes html) / iframe srcdoc set / iframe load (rendered)` marks. Slow paths now have stage-by-stage breakdown — IPC vs parser vs iframe-paint becomes visible without speculation. | v1.0.625 |
792
+ | **Search history dropdown (recent queries)** — User request 2026-05-09: "the search bar should be a dropdown offering me recent searches so I don't need to type them again. Just recent dropdowns is fine for now without editing and remove from history. Editing is implicit in bringing up a search and being able to edit it." Native HTML5 `<datalist id="search-history">` bound to `#search-input` via `list=` attribute — browser picker handles dropdown UX with no custom code. Backed by localStorage `mailx-search-history` (cap 25, move-to-front dedup). Recorded only when `doSearch(immediate=true)` fires (Enter / scope change) so debounced keystrokes don't pollute the list. Picking a past entry brings it into the input where the user can edit and re-Enter — the edited version becomes the new top entry. | v1.0.624 |
793
+ | **List/search/startup polish + measurement** — User stack 2026-05-09: (a) "long startup — would be nice to see a countup time in addition to the spinner." Live elapsed-seconds counter (ticks 100 ms) appended to `#startup-status`; auto-stops when overlay is removed. (b) "deleted entries reappeared after switching windows." `removeMessagesAndReconcile` now `listCache.clear()`s — pre-fix the per-folder snapshot cache repainted the deleted rows on next visit. (c) "switched to All Inboxes but search is still showing — being ignored?" Both folder handlers (regular + unified) now call `clearSearchMode()` AND clear the input — previously only the input was cleared, leaving `searchMode = true` so subsequent loads were rerouted. (d) "search is suddenly slow." Added `[search N ms] start / IPC begin / IPC done (n/total)` console marks so the actual cost is visible. Pair with `[boot N ms]` and `[wv N ms]` for a full timing picture. | v1.0.623 |
794
+ | **PWA convergence: one HTML, runtime bootstrap detect, MAUI points at index.html** — User insight 2026-05-09: "If I run on a Pixel tablet it should look like the desktop... You're missing the entire point of the PWA approach!" Two HTML files (`index.html` desktop, `android.html` MAUI) had been forking — different toolbars, different button labels, different search controls, different reset wording. CSS responsive layout already drives wide-vs-narrow; the HTML fork was forcing phone-style on tablets regardless of viewport. Convergence: `index.html` now carries the union of platform-runtime concerns (Android import map for in-WebView module resolution, logit error handlers, single boot router that branches on `!window.mailxapi` — present on desktop because msger pre-injects via initScript, absent on Android until `initAndroid()` runs). MAUI's `MainPage.xaml.cs` flipped `webView.Source` to `client/index.html`. `android.html` left in place as a safety net for now; delete after a successful tablet/phone smoke test. Pixel tablet → desktop layout (CSS wide breakpoint). Phone → narrow layout. Browser → desktop layout. Same code path. Plus per-stage boot timestamps `[wv N ms]` from html-parse-start through first rAF, paired with the daemon's `[boot N ms]` lines from v1.0.611 — actual measurement, not theorizing. | v1.0.621 |
790
795
  | **Header encoded-words: uniform decode at the storage seam** — User reports 2026-05-09: ".eml file with strange header" (chase / spam .eml had `From: "=?UTF-8?B?...?=" <s@x>` — encoded-word inside quoted-string, RFC-strictly forbidden but common in the wild). Bob: "you aren't parsing encoded header fields correctly. It's not subject that is encoded. You should uniformly process header escapes early in the parsing." Right answer: not per-callsite patches. Added `decodeHeaderWords` helper at the top of `mailx-store/db.ts` using `libmime.decodeWords`; called once in `upsertMessage` for `subject`, `from.name`, `to[].name`, `cc[].name`. Every storage path (IMAP fetch, Gmail API, manual append, eml replay) now decodes uniformly. Safe on already-decoded text — no `=?` markers → no-op. Old rows in DB keep their bad names until re-fetched; new fetches and any sync action that re-upserts the row pick up the fix. | v1.0.618 |
791
796
  | **List selection: clicks during in-flight IPC no longer bounce** — User report 2026-05-09: "I just selected the 10:03 message and it showed and then bounced back to the 15:49. I have to be quick to capture the 10:03 before it bounces away." Root cause: `loadUnifiedInbox` and `loadMessages` captured `savedUid` at function start, then awaited the IPC. If you clicked a different row during the await, the new selection painted briefly, then `renderMessages` (post-IPC) cleared it and `restoreSelection` restored the *captured* old uid — your click was overwritten. Fix: read the selected uid LIVE — once before each `restoreSelection` call (just before `renderMessages` clears the DOM). The user's mid-flight click survives the re-render. | v1.0.617 |
792
797
  | **Allowlist click: refresh in-memory cache after unblock** — User report 2026-05-09: "I pressed unblock but again it failed. With chase the next run found it unblocked. You added it to allowed but don't know it!" Persistence WAS working — next launch saw the chase.com domain in allowlist. The current-session re-render path was broken: `loadRemote` swapped the iframe but never updated `currentMessage` or the WebView-side `parsedCache` (added this morning for second-view speed). Any subsequent showMessage call hit the stale blocked-version cache and re-rendered the banner. Now `loadRemote` writes the unblocked message into both `currentMessage` and `parsedCachePut(accountId, uid, full)` so future renders agree with what the user just saw. | v1.0.616 |
package/client/app.js CHANGED
@@ -39,6 +39,7 @@ function propagateAppName() {
39
39
  }
40
40
  }
41
41
  propagateAppName();
42
+ window.__btick && window.__btick("app.ts module body executing");
42
43
  let baseTitle = APP_NAME;
43
44
  let lastSeenCount = 0;
44
45
  let badgeCount = 0;
@@ -336,8 +337,11 @@ initFolderTree(folderTree, (accountId, folderId, folderName, specialUse) => {
336
337
  currentFolderSpecialUse = specialUse;
337
338
  currentAccountId = accountId;
338
339
  currentFolderId = folderId;
340
+ // Drop search state on folder switch — input alone wasn't enough,
341
+ // searchMode stayed true and the next loadMessages was rerouted.
339
342
  if (searchInput)
340
343
  searchInput.value = "";
344
+ clearSearchMode();
341
345
  markAsSeen();
342
346
  releaseFocus();
343
347
  loadMessages(accountId, folderId, 1, specialUse);
@@ -347,6 +351,14 @@ initFolderTree(folderTree, (accountId, folderId, folderName, specialUse) => {
347
351
  }, () => {
348
352
  // Unified inbox handler
349
353
  currentFolderSpecialUse = "inbox";
354
+ // Clear search state — switching folders should drop any active filter.
355
+ // Pre-fix: the search input retained its old value AND search mode
356
+ // stayed active, so "All Inboxes" appeared to be ignoring its own
357
+ // contents (Bob 2026-05-09: "I just switched to all inboxes but the
358
+ // search is still showing even though all the entries are there.").
359
+ if (searchInput)
360
+ searchInput.value = "";
361
+ clearSearchMode();
350
362
  releaseFocus();
351
363
  loadUnifiedInbox();
352
364
  setTitle(`${APP_NAME} - All Inboxes`);
@@ -371,6 +383,8 @@ initMessageList((_accountId, _uid, _folderId) => {
371
383
  }
372
384
  });
373
385
  initViewer();
386
+ window.__btick && window.__btick("init* done — first interactive moment");
387
+ requestAnimationFrame(() => window.__btick && window.__btick("first rAF after init"));
374
388
  // Status bar: show focused message UID/folder for debugging. The list
375
389
  // fires mailx-focus-changed with the focused envelope (or null). Pure
376
390
  // observation — this listener never drives the viewer.
@@ -1476,6 +1490,48 @@ document.addEventListener("mailx-share-intent", ((e) => {
1476
1490
  let searchTimeout;
1477
1491
  const searchInput = document.getElementById("search-input");
1478
1492
  const searchScope = document.getElementById("search-scope");
1493
+ // Recent-searches dropdown — backed by localStorage, populated into the
1494
+ // existing <datalist id="search-history"> so the browser's native picker
1495
+ // shows on focus/click. No edit / delete UI; editing is implicit (pick a
1496
+ // past query, modify, hit Enter — the modified version is recorded).
1497
+ // Cap at 25 to keep the list usable.
1498
+ const SEARCH_HISTORY_KEY = "mailx-search-history";
1499
+ const SEARCH_HISTORY_MAX = 25;
1500
+ function loadSearchHistory() {
1501
+ try {
1502
+ return JSON.parse(localStorage.getItem(SEARCH_HISTORY_KEY) || "[]");
1503
+ }
1504
+ catch {
1505
+ return [];
1506
+ }
1507
+ }
1508
+ function saveSearchHistory(list) {
1509
+ try {
1510
+ localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(list));
1511
+ }
1512
+ catch { /* private mode */ }
1513
+ }
1514
+ function refreshSearchHistoryDatalist() {
1515
+ const dl = document.getElementById("search-history");
1516
+ if (!dl)
1517
+ return;
1518
+ const items = loadSearchHistory();
1519
+ dl.innerHTML = items.map(q => `<option value="${q.replace(/"/g, "&quot;")}"></option>`).join("");
1520
+ }
1521
+ function recordSearchHistory(query) {
1522
+ const trimmed = query.trim();
1523
+ if (!trimmed)
1524
+ return;
1525
+ const cur = loadSearchHistory();
1526
+ // Move-to-front dedup: if already there, pull it; then prepend.
1527
+ const filtered = cur.filter(q => q !== trimmed);
1528
+ filtered.unshift(trimmed);
1529
+ if (filtered.length > SEARCH_HISTORY_MAX)
1530
+ filtered.length = SEARCH_HISTORY_MAX;
1531
+ saveSearchHistory(filtered);
1532
+ refreshSearchHistoryDatalist();
1533
+ }
1534
+ refreshSearchHistoryDatalist();
1479
1535
  function doSearch(immediate = false) {
1480
1536
  const query = searchInput.value.trim();
1481
1537
  if (query.length === 0) {
@@ -1507,6 +1563,10 @@ function doSearch(immediate = false) {
1507
1563
  }
1508
1564
  loadSearchResults(query, effectiveScope, currentAccountId, currentFolderId, includeTrash);
1509
1565
  setTitle(`${APP_NAME} - Search: ${query}`);
1566
+ // Only record on `immediate=true` (Enter / scope change) — debounced
1567
+ // typing would otherwise add every keystroke as its own entry.
1568
+ if (immediate)
1569
+ recordSearchHistory(query);
1510
1570
  }
1511
1571
  // Track current folder for scoped search
1512
1572
  let currentAccountId = "";