Skip to content

fix: flush pending preview and align companion version metadata - #3

Merged
lukisch merged 20 commits into
mainfrom
perf/debounce-preview
Oct 3, 2026
Merged

lukisch merged 20 commits into
mainfrom
perf/debounce-preview

Conversation

@lukisch

@lukisch lukisch commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Änderung

  • Die Vorschau wartet beim Tippen den bestehenden 180-ms-Single-Shot-Timer ab. Beim Tabwechsel wird ein ausstehender Stand sofort gerendert; _render_preview() stoppt den Timer am Einstieg, damit kein zweiter verzögerter Render folgt.
  • Der Web Companion exportiert appVersion nun mit der Projektversion 1.0.5. Die beiden Beispiele in EXPORTFORMAT.md und der Versionsvertragstest stimmen damit überein.
  • Das Session-Schema cleanmarkdown-session-v1 und das Importverhalten bleiben unverändert.

Prüfungen

  • Lokale vollständige Testsuite: 236 bestanden, 1 Symlink-Test wegen fehlender Windows-Berechtigung übersprungen.
  • Fokussierte Vorschau- und Web-Companion-Tests: 13 bestanden.
  • Ruff: bestanden; Python-Syntaxprüfung mit compile() in 36 Dateien: bestanden.
  • GitHub Actions für f4092f3414831c41f686495b7df79c007b7a33a7: Tests und Source-Platform-Smoke bestanden.

Lukas Geiger and others added 19 commits September 24, 2026 21:49
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 lukisch left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@lukisch lukisch changed the title perf: debounce preview re-render while typing fix: flush pending preview and align companion version metadata Oct 3, 2026
@lukisch
lukisch merged commit 0aea162 into main Oct 3, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant