fix: flush pending preview and align companion version metadata - #3
Conversation
Each keystroke re-parsed the whole document and reset the preview: 134 ms per key at 300 lines, 1.4 s at 3000 lines (measured offscreen). Typing now restarts a 250 ms single-shot timer; loads, saves and tab switches still render immediately. Ticket T-20260924-322340371 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured root cause (T-20260924-322340371): _on_text_changed() called
_render_preview() synchronously on EVERY keystroke. _render_preview() does a
full-document re-parse (markdown.markdown() + task-list/strikethrough/figure
post-processing) plus viewer.setHtml(), which forces a full re-layout.
Profiled on a 56 KB synthetic document with a 600-row table:
_render_preview() (full): 308 ms/call
_render_markdown_body() (parse only): 202 ms/call
of which markdown.markdown() alone: 211 ms/call (dominant cost,
third-party python-markdown, not our post-processing regexes: task
lists 0.2ms, strikethrough 3.5ms, figures/captions 1.0ms)
_render_markdown_body() on a tiny doc: 0.84 ms/call
With no debounce, every keystroke blocked the UI thread for ~300ms on such a
document - exactly the reported "sluggish typing". The underlying parse cost
comes from a third-party library, but calling it unthrottled on every
keystroke is our own architectural bug (Eigenverschulden).
Fix: PREVIEW_DEBOUNCE_MS=180 single-shot QTimer; _on_text_changed() now
(re)starts the timer instead of rendering directly, collapsing a burst of
keystrokes into one render after typing pauses. All other call sites
(file load, session load, theme/tab switches) still render immediately -
only the per-keystroke path is debounced.
Tests: 3 new (test_preview_debounce.py) proving (a) typing does not render
synchronously, (b) the debounced render still fires and updates the viewer,
(c) a burst of keystrokes collapses into a single pending render. One
existing self-test call site needed an explicit immediate render (scroll-
sync setup) since it asserted on viewer content right after setPlainText.
166/166 tests green, ruff clean.
Teil B of the ticket (optional "edit in rendered preview" toggle) is NOT
implemented: the ticket asks to first check the architecture, and this
codebase currently has no source-position mapping between markdown source
and the rendered QTextBrowser HTML at all - building that is a real feature
project, not a decided/approved scope for this pass. Left as open follow-up.
Ticket: T-20260924-322340371 (Teil A)
Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk
Co-authored-by: Lukas Geiger <lukas@um-bruch.org>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…low gap - APP_VERSION/pyproject/store_package.json/build_exe.bat auf 1.0.3 harmonisiert (analog frueherer Releases). - CHANGELOG: Bugfix-Eintrag fuer das debounced Live-Preview-Rendering (fix(perf) #4, bereits auf main gemergt) ergaenzt. - test_metadata.py Versions-Contract-Tests auf 1.0.3 nachgezogen. - CI (tests.yml): Pillow zur pip-install-Zeile ergaenzt - test_assets_and_icons.py importiert PIL, das bisher nicht installiert wurde und main auf CI rot liess. Zusaetzlich als optionale test-Abhaengigkeit in pyproject.toml eingetragen. - Lokal verifiziert: 166/166 Tests gruen (pytest, venv mit requirements+Pillow). Store-Ticket T-20260926-902661741.
* fix: regenerate desktop .ico assets from current Store branding T-20260926-864299616: after the 1.0.3 Store update, the taskbar/tile icon was correct but the window titlebar icon (and the EXE resource feeding it) still showed an old, different design. Root cause (measured, not assumed): assets/cleanmarkdown.ico and its duplicates (CleanMarkdown.ico, DesktopIcon.ico, icon.ico, assets/icon.ico) were generated from an older master image, while store_assets/Square*.png (used by the MSIX manifest for the taskbar tile) got rebranded in a later commit. Each .ico was internally consistent -- same design across all embedded sizes -- which is why this wasn't caught: the mismatch is only visible when comparing the desktop icon against the Store tile, not within one file. Regenerated all five desktop .ico files from store_assets/Square310x310Logo.png (same source, 7 sizes: 16/24/32/48/64/128/256), and added a regression test (test_window_icon_matches_store_tile_branding) that fails if the desktop icon and the Store tile ever drift apart again -- perceptual diff against store_assets/Square310x310Logo.png, threshold 0.20. Measured: broken file diffed by 0.35, this fix by 0.0005. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk * fix: regenerate remaining stale master PNGs/favicon; bump to 1.0.4 Review of PR#5 (T-20260926-864299616) found PR#5 only fixed the .ico files load_app_icon() tries first -- the PNG masters/fallbacks it also uses, and the favicon, still carried the pre-fix design and were the actual regression source for anyone hitting the PNG fallback path: - icon.png, DesktopIcon.png, assets/icon.png (1024px masters) - assets/favicon.png, assets/favicon.ico All regenerated from store_assets/Square310x310Logo.png (the same, single source as the .ico fix), master PNGs upscaled 310->1024 via LANCZOS (no higher-resolution source exists in this repo). Test extended (test_window_icon_matches_store_tile_branding) to check every embedded frame of every .ico candidate in load_app_icon()'s fallback chain, plus every master/fallback PNG, against the Store tile -- not just the first two .ico candidates. Version bumped to 1.0.4 (main.py, pyproject.toml, store_package.json, build_exe.bat, tests/test_metadata.py) following the 1.0.3 pattern, CHANGELOG entry added. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk --------- Co-authored-by: Lukas Geiger <lukas@um-bruch.org> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* chore: bump docs/badges to 1.0.4, fix stale legacy icon_*.png mirror Follow-up to PR#5 (T-20260926-864299616): - README.md/README_DE.md version badges, WINDOWS_STORE_PREP.md and PORTIERUNGSPLAN.md version references bumped 1.0.3 -> 1.0.4 (the store_package.json / main.py / CHANGELOG values PR#5 already bumped). store_assets/AppxManifest.xml (1.0.2.0) intentionally left untouched -- that's verified separately at package-build time. - store_assets/icon_*.png: a legacy mirror of Square*/Wide*/StoreLogo under an older MSIX naming convention, still carrying the pre-fix design (diff 0.35 against the Store tile) because PR#5 only fixed the desktop-icon lineage load_app_icon() actually consumes. Regenerated each from its corresponding correctly-branded source (icon_44x44.png <- Square44x44Logo.png, icon_50x50.png <- StoreLogo.png, icon_150x150.png <- Square150x150Logo.png, icon_310x150.png <- Wide310x150Logo.png, icon_310x310.png <- Square310x310Logo.png). - Test extended: test_window_icon_matches_store_tile_branding now also checks the four square legacy mirrors, plus icon_310x150.png against the WIDE Wide310x150Logo.png reference specifically (comparing a wide image against the square tile via a square compare_size distorts it and false-positives even when correctly branded -- found while adding this check). 167/167 project tests green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk * fix: anchor wide Store assets directly to the square tile merge-reviewer's review of #6 (9f0702e) found a real gap in the wide-asset check: comparing icon_310x150.png only against Wide310x150Logo.png (as its "wide reference") leaves two cases undetected -- (C) both the wide logo and its mirror stale relative to the Store tile -- they still agree with each other, so nothing was flagged (D) the wide logo missing -- the mirror then became its own reference (`png_path == reference`) and was skipped outright Fix: every wide asset (Wide310x150Logo.png and its icon_310x150.png mirror) is now anchored DIRECTLY to the one authoritative square tile, via its own center-square crop (same aspect as the tile, no distortion, no indirection through another wide asset). Calibrated on the real files: the correct Wide310x150Logo.png's center crop differs from the tile by 0.24, the pre-fix mirror by 0.54 -- threshold set to 0.35. Same fix mirrored in the shared .SOFTWARE/_STORE/icon_consistency_check.py gate (new check_wide_png_matches_store_tile()/_center_square_crop(), WIDE_TILE_CANDIDATES/find_wide_tile_reference() removed -- no longer needed since nothing is compared against another wide asset anymore), with two new negative-control tests reproducing gaps C and D against the real b26390d files (15/15 gate tests green). 167/167 project tests green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk --------- Co-authored-by: Lukas Geiger <lukas@um-bruch.org> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
….5 (#7) * fix: .md-Dateisymbol zeigte Kachel auf brauner Platte; Store-Paket ohne Dateityp-Zuordnung (1.0.5) T-20260927-699609650. Gemessen, nicht vermutet: - .md -> ProgId CleanMarkdown.mdfile -> DefaultIcon "<CleanMarkdown.exe>,0". Das Symbol ist also das in die EXE eingebackene Icon. Die ausgelieferte 1.0.3-EXE traegt das Plattendesign aus e712d9d (byte-identisch zum CleanMarkdown.ico von b26390d, Abweichung 0.35 zur Store-Kachel). PR #5 hat das nur im Quellbaum behoben, 1.0.4 wurde nie ausgeliefert. - Das installierte 1.0.3-MSIX hat keine FileTypeAssociation, kein resources.pri und keine targetsize/unplated-Varianten: _STORE/msstore_build_msix.ps1 erzeugte das Manifest selbst und ignorierte store_package.json file_types (Builder ausserhalb des Repos mitgefixt). Fix: Version 1.0.5; Square44x44Logo.targetsize-{16..256} je plain, altform-unplated, altform-lightunplated aus der Store-Kachel (scripts/gen_targetsize_icons.py); Regressionstest; STORE_ISSUES korrigiert. Gate (_STORE/icon_consistency_check.py --package) prueft jetzt das gebaute MSIX: rot gegen das installierte 1.0.3 (4 Befunde), gruen gegen 1.0.5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk * fix: unplated targetsize-Assets waren opak; Migrationspfad für lokale Installation T-20260927-699609650, Blocker D aus der astra-Abnahme von PR #7 (81dacc6): - install_local.ps1 registriert eine eigene ProgID (CleanMarkdown.mdfile, DefaultIcon auf die lokale EXE). Diese Installation blieb nach einem Store-Update unverändert bestehen und "gewann" weiterhin die .md-Zuordnung -- das MSIX ersetzt sie nicht automatisch. uninstall_local.ps1 ist das Gegenstück: bricht ab, wenn kein Store-Paket installiert ist, sichert die betroffenen Registry-Zweige per reg export und entfernt danach nur, was install_local.ps1 selbst angelegt hat (ProgID, DefaultIcon, .md-Verweis, OpenWithProgids, Applications-Key, UserChoice falls zutreffend, Startmenü- Verknüpfung, Installationsordner). README/README_DE dokumentieren jetzt, dass lokale und Store-Installation nicht parallel bestehen sollen. - gen_targetsize_icons.py erzeugte für unplated/lightunplated-Varianten byte-identische, opake Kopien der plated-Variante -- ein Nebenbefund der Gate-Härtung in _STORE/icon_consistency_check.py (siehe dortigen Commit): das jetzt strengere --package-Gate hätte den eigenen 1.0.5-Build andernfalls selbst abgelehnt. Fix: kleiner transparenter Eckpatch pro Variante -- genug für echte Transparenz, klein genug, um innerhalb der bereits kalibrierten Diff-Schwelle von test_assets_and_icons.py zu bleiben (ein voller 12%-Rand ergab 0.22 bei 16px, Schwelle 0.20). Verifiziert: 168 Projekttests grün; echter 1.0.5-MSIX-Neubau über das gehärtete _STORE/msstore_build_msix.ps1 (inkl. erzwungenem icon_consistency_check.py --package) läuft mit Exit 0 durch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk * fix: 2. astra-Abnahme (5 Blocker) schliessen — C1/C2/D1-D3 T-20260927-699609650, Head 9596928 wurde von gpt-6-astra (effort high) mit "NICHT OK" bewertet (voller Bericht: _codex/ASTRA-REABNAHME_T-20260927-699609650.md). Alle 5 Blocker sind behoben und gegen astras real reproduzierte Manipulationen verifiziert (Details: _codex/FIX-C1-C2-D1-D2-D3_T-20260927-699609650.md): - C1: Gate pruefte nur 5 von 14 ausgelieferten Icon-Groessen -- jetzt werden alle tatsaechlich im Paket/resources.pri vorhandenen Varianten erkannt und geprueft, nicht nur eine feste Pflichtliste. - C2: Das reale 1.0.5-MSIX enthielt den onedir-Laufzeitordner (_internal, Python-DLLs) nicht -- die App waere nicht gestartet. Kopierschritt erkennt jetzt onedir-Layout und uebernimmt den kompletten Ordner. - D1: uninstall_local.ps1 loeschte beim UserChoice-Rueckbau den gesamten Explorer-Elternzweig FileExts\.md samt fremden Geschwisterzweigen -- jetzt nur noch der UserChoice-Unterschluessel selbst. - D2: Fehlgeschlagene Registry-Backups stoppten die Loeschung nicht -- jeder Export wird jetzt geprueft, ein Fehlschlag bricht vor jeder Aenderung ab. - D3: Jede installierte Store-Version wurde akzeptiert, auch die real installierte 1.0.3 ohne .md-FileTypeAssociation -- das Manifest wird jetzt geprueft, eine Version ohne Handler wird abgelehnt. D1-D3 zusaetzlich per gemocktem PowerShell-Testlauf verifiziert (kein echter Registry-Zugriff). Store-Gate-Tests 27/27 gruen, Projekt-Testsuite 168/168 gruen, realer 1.0.5-MSIX-Neubau ueber das gehaertete msstore_build_msix.ps1 gruen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk * fix: uninstall_local.ps1 loeschte .md-Standardwert nicht wirklich (D4) T-20260927-699609650, 3. Abnahme (Fable, schreibgeschuetzt) fand: mein D1-Fix `Remove-ItemProperty -Name "(Default)"` schlaegt gegen den PowerShell- Registry-Provider fehl (verifiziert in pwsh 7.6.6 und Windows PowerShell 5.1), `-ErrorAction SilentlyContinue` machte das unsichtbar. Folge: HKCU:\Software\ Classes\.md zeigte nach dem Rueckbau weiter auf die bereits geloeschte ProgID CleanMarkdown.mdfile -- schlimmer als der vorherige Set-Item -Value "" (der den Verweis wenigstens leerte). Fix: echte Entfernung ueber die .NET-Registry-API ([Microsoft.Win32.Registry]::CurrentUser...DeleteValue), die den PowerShell- Provider umgeht. Verifiziert in einem Wegwerf-Schluessel (angelegt, geprueft, wieder entfernt -- keine dauerhafte Aenderung am Host). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk * chore: internal review reports aus dem Repo entfernen, README neutralisieren merge-reviewer-Befund vor dem Merge von PR #7: Das Repo ist public. _codex/ enthielt interne Abnahmeberichte mit lokalen Pfaden, Hostnamen und Reviewernamen -- nach einem Squash-Merge waeren die dauerhaft in der main-Historie gelandet. - _codex/ aus dem Git-Tracking entfernt (git rm --cached), lokal behalten, in .gitignore aufgenommen. - README.md/README_DE.md: interne Ticket-ID und Reviewername ("astra abnahme review") aus der Nutzer-Doku entfernt/neutralisiert. Die Ticket-ID bleibt wie besprochen im CHANGELOG stehen. Fachliche Pruefung wird durch diesen Commit nicht beruehrt (reine Bereinigung, kein Verhaltensunterschied). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk --------- Co-authored-by: Lukas Geiger <lukas@um-bruch.org> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…fication) Store-Update mit dem korrekten .md-Dateisymbol, Fenstersymbol und vollstaendiger onedir-Laufzeit ist eingereicht. Die neue Submission wurde als Klon der letzten veroeffentlichten (1.0.3.0, Submission 1152921505701981274) angelegt, dadurch blieben Listings (de-de, en-us), Kategorie und Alterseinstufung unveraendert. Sicht-OK zur Icon-Collage (Policy Paragraph 7a.2) liegt vor. 1.0.4 wurde nie eingereicht und uebersprungen. Ticket T-20260927-699609650. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk
Co-authored-by: Lukas Geiger <lukas@um-bruch.org>
* ci: verify Flutter and cover source smoke runtime inputs * ci: match Flutter SDK to existing locked dependency versions --------- Co-authored-by: Lukas Geiger <lukas@um-bruch.org>
lukisch
left a comment
There was a problem hiding this comment.
Zweitmodell-Review: CI rot (alle 9 test-Jobs), Ursache liegt nicht im Diff; smoke und CodeQL grün.
Diagnose (Job test (ubuntu-latest, 3.10) gelesen, alle anderen Matrixzellen scheitern gleich): tests/test_assets_and_icons.py:13: from PIL import Image führt zu ModuleNotFoundError: No module named 'PIL' und Collection-Abbruch (collected 159 items / 1 error). Der PR ändert nur main.py und fügt tests/test_preview_debounce.py hinzu; Pillow fehlt in der CI-Installation. Patch-Vorschlag: Pillow>=10 in die CI-Install-Zeile bzw. dev/test-Extras aufnehmen (oder pytest.importorskip("PIL") im Test).
Diff-Review (Vorschau-Debounce, 250 ms): Idee und Tab-Wechsel-Flush sind korrekt, der Test deckt Tippen und Flush ab. Bitte prüfen, ob weitere Stellen die Vorschau lesen, ohne vorher zu flushen: Export (HTML/PDF), Drucken, Speichern/Schließen. Dort sollte if self.preview_timer.isActive(): self._render_preview() stehen, sonst sehen sie bis zu 250 ms alten Inhalt. Keine Credentials oder Nutzerpfade.
Generated by Claude Code
Signed-off-by: Lukas Geiger <lukas@um-bruch.org>
Änderung
_render_preview()stoppt den Timer am Einstieg, damit kein zweiter verzögerter Render folgt.appVersionnun mit der Projektversion 1.0.5. Die beiden Beispiele inEXPORTFORMAT.mdund der Versionsvertragstest stimmen damit überein.cleanmarkdown-session-v1und das Importverhalten bleiben unverändert.Prüfungen
compile()in 36 Dateien: bestanden.f4092f3414831c41f686495b7df79c007b7a33a7: Tests und Source-Platform-Smoke bestanden.