save_documents debounces 400ms; migrateDataDir used to fire without
waiting for it, so a save still in flight to the old folder could
land after migrate had already moved documents.json away, silently
recreating it in the abandoned folder and stranding the last edit.
replaces new Function() in the main window; per-script and per-call
interrupt deadlines, memory and stack limits, graceful skip if the
wasm module fails to load. csp script-src drops 'unsafe-eval' for
'wasm-unsafe-eval'.
- Track window focus via performance.now() instead of suppressing all backdrop
clicks for 300ms after the modal appears. This fixes over-suppression (Ctrl+W
close-sheet now dismisses on immediate click) while keeping protection for
genuine stray refocus-clicks (native dialog closing, tray window refocus).
- Serialize concurrent askModal calls via a promise chain so a second call
queues instead of clobbering the first's handlers (was: hang on concurrent
modal show).
- Use monotonic performance.now() throughout to avoid clock-jump edge cases
with Date.now().
a click that focuses the window lands on the modal backdrop too, so
the update-available prompt after an unattended auto-check could get
dismissed by the very click that brought the window to front, before
it was ever seen
Both Tauri commands took a raw path string straight from the frontend
and wrote to it with no validation — any code able to call invoke()
(a compromised or malicious extension script, which run with full page
privileges) could write to any path the OS user has access to, not just
one chosen through the save dialog. Move the save-file dialog into the
Rust command itself so the only path ever used is one the native dialog
returned; the frontend now only sends the content to save.
Boot had grown into one long function covering settings load, engine
and workspace setup, and all the UI wiring. Split along those natural
phase boundaries; boot() itself is now just the three awaits in order.
The settings panel grew into tabs (General/Format/Data/Updates) but
the binding code was still one 170-line function. Split it to match:
bindFormattingSettings, bindHotkeySettings, bindBehaviorSettings,
bindDataDirSettings, bindSettingsNavigation.
fetchNeededHistoricalRates awaited each date's fetch sequentially even
though they're independent requests — a sheet referencing several
historical dates paid for that latency serially instead of in parallel.
Toolbar buttons (☰ 🔍 ⚙ ✕ ─ ⤓ +) and the untitled doc-title/search
inputs had no accessible name beyond a title tooltip, which screen
readers don't reliably announce.
The manual rebind flow in Settings already toasts hotkeyFailed when a
combo is taken by another app, but the initial registerHotkey() call
at boot discarded its success/failure result — if the saved hotkey
couldn't be registered (already bound elsewhere, etc.), there was no
error and no indication the shortcut just wasn't active.
Tauri's window-level file-drop hook (needed for dragging a .numi file
in from Explorer) intercepts WebView2's own drag-and-drop session, so
native HTML5 draggable="true" elements never get a valid drop target
inside the webview — the OS always shows "not allowed", confirmed
with an actual mouse-drag simulation before this fix.
Replaced with plain pointer tracking (mousedown/mousemove/mouseup),
the same pattern already used for the results-column divider — full
control over the cursor and the drag-over indicator, no dependency on
the webview's native DnD stack, and it doesn't touch the unrelated
OS file-drop feature at all.
Move-up/move-down felt disconnected from the result — clicking a
button and having two rows silently swap position doesn't read as
motion. Dropped both buttons for native HTML5 drag-and-drop: drag a
sheet, an accent line shows where it'll land, drop to reorder. Still
respects the pinned/unpinned grouping — dragging across that boundary
is a no-op, same as the old up/down buttons.
#market-info had no CSS at all — different font size and color from
#rates-info right next to it, and its own hardcoded English "Market"
text ignored the language setting. Merged its styling into the same
rule as #rates-info and gave it the same .spin pulse while a fetch
is in flight, so a market refresh is as visible as a rates refresh.
Updates tab, below the check-update button. Reads from
tauri.conf.json via getVersion() in a real build (the source of
truth for what's actually installed); falls back to package.json's
version under vite dev, where there's no Tauri runtime to ask.
The flat settings list had grown to 14 fields plus footer buttons —
split it into General / Format / Data / Updates tabs (same pill style
as the pinned-doc sidebar, active tab gets the selection color).
Wired the date-format setting into the engine, and reworked update
checks per an actual bug report: a boot-only check is easy to miss on
a tray app that can run for weeks without a cold restart, and every
failure was swallowed silently with no logging at all. Now: failures
are logged (console.warn) and successful "no update" checks say why
(console.info, current version vs. latest published release) so the
next report has something to go on; checks also rerun every 6 hours;
Settings has both a manual "Check for updates" button (reports back
via toast) and a toggle to turn automatic checking off entirely —
manual checks keep working either way.
Both were self-contained enough to live in their own modules —
src/ui/search.ts (Ctrl+F overlay, text and result-value queries) and
src/ui/export.ts (render the sheet + results to a PNG, clipboard or
file). Trims main.ts by about 150 lines.
Also fixes a real bug while it's in there: the PNG export's clipboard
fallback did btoa(String.fromCharCode(...bytes)), which blows the
call stack on large sheets (spreading a big array as call arguments).
Chunks the conversion in 0x8000-byte pieces instead.
v7/finance/quote returns 401 now — Yahoo stopped issuing anonymous
clients the cookie+crumb pair it requires. v8 chart still answers
without auth, one symbol per request instead of one bulk call.
Also dropped SBER/GAZP from the tracked symbols; Yahoo delisted
Russian tickers in 2022, so those two always missed anyway.
api.frankfurter.app 301-redirects to frankfurter.dev now, and the old
host is flaky over reqwest/rustls-tls — talk to the new one directly
(Rust backend, the vite-dev web fallback, and the CSP allowlist).
Separately, a sheet reopened on boot with "on 2024-01-01" in it never
fetched historical rates at all: the fetch only ran from switchDoc()
and the editor's onChange, and the editor's initial text is seeded
directly, bypassing both.
Ctrl+F opens an overlay that searches every document's lines, not just
the active one, and jumps to the hit on click. Always-on-top is a
plain window pin, off by default, toggleable in settings.
Signed by a new key (public half embedded in tauri.conf.json, private
half + password live in GitHub secrets). CI now signs release artifacts
and publishes latest.json; the app checks on boot and prompts to
install and restart.
- vocab: EN+RU aliases for abs/log/ln/round; RU already had sqrt/cbrt
- engine: "half"/"a half"/"one half"/"половина" → 0.5; "one and a half"/"полтора"/"полторы" → 1.5 as named constants; widen const union in parser/tokenizer/evaluator/registry
- export: ⤓ button in statusbar dropdown — copy sheet, save .sum, export .txt, print/PDF; write_text_file Tauri command; print media CSS hides chrome
Write plain text, get live results per line. English + Russian, units,
live currency rates with crypto, percents, dates and timezones, bitwise
ops, variables and sheet totals. Tauri 2 + TypeScript engine +
CodeMirror 6; daily backups and a deleted-sheets bin.
Inspired by Numi for macOS (numi.app); built from scratch for Windows.