Deck opslaan overschrijft externe wijzigingen zonder conflictcontrole #1699

Closed
opened 2026-08-21 22:56:31 +00:00 by brenno · 0 comments
Owner

Probleem

Een presentatie opslaan schrijft de markdown onvoorwaardelijk over het pad heen. Heeft een andere editor, een synchronisatiemap of een git-actie het bestand sinds het openen gewijzigd, dan verdwijnt die wijziging zonder melding.

Dit is de presentatiekant van #1683 (hetzelfde gat in de documentmodus). Apart gehouden omdat de reparatie hier ander materiaal heeft — zie hieronder: de vingerafdruk bestáát al.

Reproductie

  1. Open een deck in OciDeck.
  2. Wijzig en bewaar hetzelfde .md buiten de app (andere editor, git checkout, een synchronisatiemap).
  3. Wijzig iets in OciDeck en kies Opslaan.
  4. De externe wijziging is weg, zonder waarschuwing.

Verwacht

Vóór het overschrijven wordt gecontroleerd of het bestand nog overeenkomt met wat er bij het openen (of bij de vorige opslag) stond. Bij een verschil krijgt de gebruiker de keuze: vergelijken, herladen, of bewust overschrijven.

Technische aanwijzing

FileService._writeProject (lib/services/file/file_service_project.dart) doet writeStringAtomic(File(filePath), markdown) zonder voorafgaande lezing of vergelijking. Alle drie de opslagroutes (saveDeck, saveDeckDetailed, saveDeckAsDetailed) lopen daarlangs.

Wat er al ligt

De vingerafdruk bestaat al. Deck.fileHash (SHA-512 over de bestandsbytes) wordt gezet bij het openen (FileService.openDeck…, via DocumentIntegrity.hashMarkdown(raw)) én na elke schrijfactie (DocumentIntegrity.recordWrittenBytes). Er ontbreekt alleen de vergelijking vóór het schrijven — dit is geen nieuw mechanisme, maar één controle op een waarde die er al is.

Het patroon staat ook al in de repo. _writeChartData (lib/services/file/file_service_open.dart) leest een gekoppeld data/*.json terug, vergelijkt het met de basislijn van bij het openen en weigert mét waarschuwing bij een verschil. Wat daar geldt voor de cijfers van één grafiek, geldt zeker voor het deck zelf.

De git-route heeft het wél. tabs_provider_git.dart kent GitSaveStatus.conflict met een echte samenvoeging. Het gat zit in de gewone lokale-bestandsroute.

Oplossingsrichting

  1. In _writeProject, vóór de schrijfactie: bestaat het bestand, en is deck.fileHash niet leeg, lees dan de bytes en vergelijk de hash. Verschil ⇒ niet schrijven, maar een conflict-uitkomst teruggeven — niet false, want dat leest als een schrijffout.
  2. De aanroeper (DeckNotifier._saveToPath) biedt drie wegen: vergelijken, herladen (eigen wijzigingen weg), overschrijven (die van de ander weg). Nooit stil één daarvan kiezen.
  3. Deel de poort met #1683, zodat document en presentatie dezelfde controle en dezelfde dialoogtekst gebruiken.

Drie valkuilen die in het werk horen

  • Lege fileHash = geen controle. Bij een deck uit geheugen of uit het web (content != null) wordt de hash bij het openen niet berekend. Geen vingerafdruk betekent niets te vergelijken; dan schrijven zonder klagen, niet weigeren.
  • Opslaan als naar een nieuw pad is nooit een conflict — de vergelijking geldt alleen voor het pad waar de hash vandaan komt. Bestaat het doelbestand al, dan is dat de gewone overschrijfvraag van de bestandskiezer, een ander gesprek.
  • Doortypen tijdens het schrijven maakt de hash muf. DeckNotifier._saveToPath zet de teruggegeven deck (met verse fileHash) niet in de state wanneer de gebruiker tijdens het schrijven doortypte (de userEdited-tak, #1473). De volgende opslag zou dan een conflict melden tegen het bestand dat de app zélf zojuist schreef. De hash moet ook in die tak worden bijgewerkt — een valse conflictmelding is precies wat een gebruiker deze controle laat wantrouwen.
  • De sidecars. Naast de .md schrijft _writeProject notities, MIAUW, zegel en dismissals. De controle op de .md dekt die niet. Voor deze ronde: benoem het, dek de .md af, en laat de sidecars een vervolgvraag zijn — een half werkende controle die doet alsof ze alles dekt is erger dan geen.

Regressietest

  • Servicetest: openen, bestand extern wijzigen, opslaan ⇒ geweigerd, bestand op schijf ongewijzigd, conflict-uitkomst bij de aanroeper.
  • Twee opslagen achter elkaar zónder externe wijziging ⇒ geen klacht (de hash-verversing na de eerste opslag).
  • Opslaan-tijdens-typen ⇒ daarna nog een opslag, en géén valse conflictmelding.
  • Widgettest per uitweg: herladen, overschrijven, annuleren.

Kosten

Opslaglaag + DeckNotifier + dialoog, gedeeld met #1683. De dialoog kost ongeveer vijf nieuwe l10n.d('…') ⇒ 31 vertalingen elk (make add-l10n). Reken op een dag voor beide kanten samen; los is dit ongeveer een halve.

Gevonden bij de triage van #1683 (22-08-2026), geverifieerd tegen main (e93ef205c).

## Probleem Een presentatie opslaan schrijft de markdown onvoorwaardelijk over het pad heen. Heeft een andere editor, een synchronisatiemap of een git-actie het bestand sinds het openen gewijzigd, dan verdwijnt die wijziging zonder melding. Dit is de presentatiekant van #1683 (hetzelfde gat in de documentmodus). Apart gehouden omdat de reparatie hier ander materiaal heeft — zie hieronder: de vingerafdruk bestáát al. ## Reproductie 1. Open een deck in OciDeck. 2. Wijzig en bewaar hetzelfde `.md` buiten de app (andere editor, `git checkout`, een synchronisatiemap). 3. Wijzig iets in OciDeck en kies Opslaan. 4. De externe wijziging is weg, zonder waarschuwing. ## Verwacht Vóór het overschrijven wordt gecontroleerd of het bestand nog overeenkomt met wat er bij het openen (of bij de vorige opslag) stond. Bij een verschil krijgt de gebruiker de keuze: vergelijken, herladen, of bewust overschrijven. ## Technische aanwijzing `FileService._writeProject` (`lib/services/file/file_service_project.dart`) doet `writeStringAtomic(File(filePath), markdown)` zonder voorafgaande lezing of vergelijking. Alle drie de opslagroutes (`saveDeck`, `saveDeckDetailed`, `saveDeckAsDetailed`) lopen daarlangs. ## Wat er al ligt **De vingerafdruk bestaat al.** `Deck.fileHash` (SHA-512 over de bestandsbytes) wordt gezet bij het openen (`FileService.openDeck…`, via `DocumentIntegrity.hashMarkdown(raw)`) én na elke schrijfactie (`DocumentIntegrity.recordWrittenBytes`). Er ontbreekt alleen de vergelijking vóór het schrijven — dit is geen nieuw mechanisme, maar één controle op een waarde die er al is. **Het patroon staat ook al in de repo.** `_writeChartData` (`lib/services/file/file_service_open.dart`) leest een gekoppeld `data/*.json` terug, vergelijkt het met de basislijn van bij het openen en weigert mét waarschuwing bij een verschil. Wat daar geldt voor de cijfers van één grafiek, geldt zeker voor het deck zelf. **De git-route heeft het wél.** `tabs_provider_git.dart` kent `GitSaveStatus.conflict` met een echte samenvoeging. Het gat zit in de gewone lokale-bestandsroute. ## Oplossingsrichting 1. In `_writeProject`, vóór de schrijfactie: bestaat het bestand, en is `deck.fileHash` niet leeg, lees dan de bytes en vergelijk de hash. Verschil ⇒ niet schrijven, maar een conflict-uitkomst teruggeven — niet `false`, want dat leest als een schrijffout. 2. De aanroeper (`DeckNotifier._saveToPath`) biedt drie wegen: vergelijken, herladen (eigen wijzigingen weg), overschrijven (die van de ander weg). Nooit stil één daarvan kiezen. 3. Deel de poort met #1683, zodat document en presentatie dezelfde controle en dezelfde dialoogtekst gebruiken. ### Drie valkuilen die in het werk horen - **Lege `fileHash` = geen controle.** Bij een deck uit geheugen of uit het web (`content != null`) wordt de hash bij het openen niet berekend. Geen vingerafdruk betekent niets te vergelijken; dan schrijven zonder klagen, niet weigeren. - **`Opslaan als` naar een nieuw pad is nooit een conflict** — de vergelijking geldt alleen voor het pad waar de hash vandaan komt. Bestaat het doelbestand al, dan is dat de gewone overschrijfvraag van de bestandskiezer, een ander gesprek. - **Doortypen tijdens het schrijven maakt de hash muf.** `DeckNotifier._saveToPath` zet de teruggegeven deck (met verse `fileHash`) *niet* in de state wanneer de gebruiker tijdens het schrijven doortypte (de `userEdited`-tak, #1473). De volgende opslag zou dan een conflict melden tegen het bestand dat de app zélf zojuist schreef. De hash moet ook in die tak worden bijgewerkt — een valse conflictmelding is precies wat een gebruiker deze controle laat wantrouwen. - **De sidecars.** Naast de `.md` schrijft `_writeProject` notities, MIAUW, zegel en dismissals. De controle op de `.md` dekt die niet. Voor deze ronde: benoem het, dek de `.md` af, en laat de sidecars een vervolgvraag zijn — een half werkende controle die doet alsof ze alles dekt is erger dan geen. ## Regressietest - Servicetest: openen, bestand extern wijzigen, opslaan ⇒ geweigerd, bestand op schijf ongewijzigd, conflict-uitkomst bij de aanroeper. - Twee opslagen achter elkaar zónder externe wijziging ⇒ geen klacht (de hash-verversing na de eerste opslag). - Opslaan-tijdens-typen ⇒ daarna nog een opslag, en géén valse conflictmelding. - Widgettest per uitweg: herladen, overschrijven, annuleren. ## Kosten Opslaglaag + `DeckNotifier` + dialoog, gedeeld met #1683. De dialoog kost ongeveer vijf nieuwe `l10n.d('…')` ⇒ 31 vertalingen elk (`make add-l10n`). Reken op een dag voor beide kanten samen; los is dit ongeveer een halve. Gevonden bij de triage van #1683 (22-08-2026), geverifieerd tegen `main` (e93ef205c).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
LibreKAT/Ocideck#1699
No description provided.