Repository navigation
Mundus v0.7.98
WORKLOG — Mundus v0.7.98 (v0.7.97 included)
- GitHub Actions build loop + everything it needs
- investigate & fix the "stuck at end of a page" scroll bug
- Character-name auto-highlight must not briefly disappear while the spelling check runs.
- Story flow graph: documents with draft status must show a grey dot (green/yellow for completed/in-review were reported as already correct).
- Sync status hover tooltip: show both the current Mundus's storage and the app-wide total (all Mundus combined).
1. GitHub build loop
- .github/workflows/build.yml (new): tag trigger v* + workflow_dispatch; matrix windows-latest / ubuntu-latest / macos-latest; steps: checkout@v4 → setup-node@v4 (Node 22, npm cache) → npm ci → npm run build → npx electron-builder --publish never (targets already fully configured in electron-builder.json: NSIS+portable / AppImage+deb+rpm / dmg+zip) → artifacts per OS; release job (tag builds only) downloads artifacts and publishes a GitHub Release via softprops/action-gh-release@v2 with auto release notes. CSC_IDENTITY_AUTO_DISCOVERY=false (unsigned by design); permissions: contents: write.
- .gitignore (rewritten): ignores node_modules/dist/dist-electron/release/coverage/tmp/logs/OS noise + secrets (.env*, firebase-applet-config.json — preserved from the original file). build/ deliberately kept tracked (icons + appstream required by electron-builder in CI — the original .gitignore wrongly ignored it).
2. Scroll-stuck bug — root cause + fix
Symptom: sometimes, at the end of a page, scrolling up bounces back down; re-clicking the document in the sidebar fixes it.
Root cause chain (all found in src/components/TextEditor.tsx):
- The manuscript scroller had onScroll={updatePageStats} — every scroll event ran the full pagination engine (normalizeDocumentPages).
- That engine mutates layout: strips every page-start block's virtual-gap margin-top (pre-pass), re-measures blocks (getBoundingClientRect / zoom-scale), re-applies huge gap margins on page-start blocks.
- Mutations while the viewport sits at a page end trigger Chrome scroll anchoring, which re-adjusts scrollTop toward the (moving) page-start block → the adjustment fires another scroll event → step 1 again → a mutation → anchoring → scroll feedback loop that traps the view at the page end. Intermittency explained: interior pages recompute stably (no mutation → no anchoring); boundary blocks can flip-flop on sub-pixel/rounding noise.
- A secondary amplifier: a ResizeObserver on the editor re-runs pagination whenever the pagination itself resizes the editor (boundary flip-flop ⇒ layout thrash).
Fix (3 layers):
- A. onScroll no longer paginates — replaced with new updateCurrentPageFromScroll(): a read-only handler that refreshes the "page N of M" indicator from totalPagesRef + scrollTop (same math as the full pass, functional setPageStats that no-ops when unchanged). Pagination now happens only on input/save/resize/doc-switch/page-format paths, as intended.
- B. #editor-scroll-area { overflow-anchor: none; } (index.css) — opts the scroller out of Chrome scroll anchoring entirely; any legitimate mid-scroll mutation (autosave, image insert) can no longer yank the viewport. No-op where unsupported (Safari).
- C. Oscillation damper — normalizeDocumentPages now reports whether it actually mutated DOM (paginationMutatedRef); the ResizeObserver tracks a mutating-streak (4 passes within 800ms ⇒ 600ms cooldown). Direct input/save pagination bypasses the damper, so typing responsiveness is untouched.
3. Character highlight flicker during spellcheck — root cause + fix
Root cause (src/components/TextEditor.tsx, character-name highlight effect):
- The effect's dependency-change cleanup deleted every registered highlight AND the shared <style> tag immediately, while re-application waited behind the existing 220 ms debounce. Every re-run therefore blacked the highlights out for ≥220 ms.
- Re-run triggers in normal use: a keystroke (document.plainText), any pagination childList mutation (domStructureEpoch — bumped by the v0.7.83 observer after autosave/pagination), and character-card edits (characters). The spellcheck rescan (250 ms), squiggle re-render (~400 ms) and this blackout (0–220 ms) all land in the same post-typing window — which is why users perceived it as "the highlight disappears while the spelling check is being run".
- Confirmed NOT the cause: currentMundusCharacters is useMemo-stable in App.tsx, so ordinary re-renders (syncStatus flips etc.) never re-triggered the effect — the blackout came purely from the nuke-then-debounce pattern.
Fix (atomic swap instead of nuke → debounce gap):
- The dependency-change cleanup now only cancels the pending re-scan timer. Existing highlights stay painted (live Ranges keep tracking their text nodes through edits), and the debounced pass swaps old registrations for new ones inside a single synchronous task (delete + highlights.set + style update) — no paint can ever show the gap.
- Real teardown moments keep immediate removal: feature toggle-off (branch), document switch (new charHighlightPrevDocIdRef check drops the previous document's registrations at once), and unmount (new dedicated useEffect(() => () => removeAllCharHighlightAssets(), [])).
- Teardown logic hoisted to a module-level removeAllCharHighlightAssets() so the effect body and the unmount cleanup share one implementation.
- Side benefit: highlights no longer blink during typing itself either (they used to vanish from the first keystroke until 220 ms after the last one).
- Untouched on purpose: the spellcheck squiggle effect (its cleanup already never deleted spell-errors, which is why squiggles never flickered) and both debounce durations.
4. Status dots on the story flow graph (and timeline)
Root cause: the canonical status scheme lives in the editor's status picker (TextEditor.tsx): complete → emerald, revision ("in review") → amber, draft → neutral grey. The story-flow node dot only had a two-way mapping — doc.status === 'draft' ? 'bg-amber-400' : 'bg-emerald-400' — so drafts showed yellow and in-review scenes showed green (indistinguishable from completed).
Fix:
- StoryFlowGraph.tsx node dot → three-way mapping matching the editor exactly: complete ? 'bg-emerald-500' : revision ? 'bg-amber-500' : 'bg-neutral-400' (dot size/title={doc.status} untouched; the now-unused isDraft const removed).
- Same latent bug fixed in TimelineView.tsx (scene-timeline lane cards used the identical two-way mapping with rings) — kept the ring styling, now emerald-500/amber-500/neutral-400. (Beyond the letter of the request; same defect, fixed for consistency — flagging it explicitly.)
5. Storage tooltip — current Mundus + app-wide total
Root cause/finding: the sidebar's storage approximation only received documents/folders already filtered to the active Mundus (currentMundusDocs/currentMundusFolders in App.tsx). All projects actually share single storage keys (mundus_documents / mundus_folders, each row tagged mundusId), and App state holds the unfiltered lists — so the total was available without any extra reads.
Fix:
- App.tsx passes two new optional props to : allDocuments={documents} and allFolders={folders} (no new storage scans, no async).
- Sidebar.tsx recomputes both scopes with the same byte formula as before (content + plainText + title + 180 B/doc, +90 B/folder, +16 KB base): "This Mundus" keeps the per-project number; "All Mundus" sums every project. Props fall back to the scoped lists when omitted, so any other caller stays valid.
- Tooltip (both the expanded-sidebar hover and the collapsed-sidebar portal copy) now reads:
- header "Cloud Storage" + percent — percent now tracks the app-wide total (that's what counts against the Firestore free tier);
- the progress bar is now live (width: storageBarPercent, clamped 1.5–100%) instead of the old hardcoded w-[1.5%];
- rows: This Mundus: X / All Mundus: Y + Quota: 1.0 GB;
- the offline (Firebase unconfigured) branch shows the same two rows.
- Scope note (by design): the number stays the app's existing approximation of cloud usage (documents + folders + base overhead); characters/codex/timeline payloads and map images are not itemized, exactly as before — only the scope changed.
Full Changelog: v0.7.96...v0.7.98