From 5a8922011d5f4debb87f984ef1d9e52558b3827b Mon Sep 17 00:00:00 2001 From: Sandro Date: Sun, 1 Nov 2026 12:29:10 +0100 Subject: [PATCH] feat(tasks): linked tasks sync title and subtasks with their note in both directions Subtasks of a task linked to a note checkbox are tied 1:1 to checkboxes of that note (subtasks[].blockId), wherever they are moved. ReconcileDocTaskLinks now carries tick, title and subtasks from the note to the task and adopts new checkboxes directly under the task; SyncTaskToDoc carries tick, title and subtasks to the note and adds checkboxes for new subtasks. Deleting is not mirrored. Deepening a task ties its subtasks right away; saves without links keep the server's links. The sync response reports taskLinks/subtaskLinks; the editor shows an "Unteraufgabe" badge. v0.36.0. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 8 +- Doku/modules/tasks.md | 3 +- README.md | 2 +- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- .../src/features/workspace/DocumentEditor.tsx | 5 +- .../src/features/workspace/OutlinerBlock.tsx | 22 +- frontend/src/lib/docSync/diff.ts | 8 +- frontend/src/lib/docSync/engine.ts | 9 +- frontend/src/lib/docSync/transport.ts | 3 + frontend/src/lib/types.ts | 4 + internal/api/docsync.go | 4 + internal/api/handlers_documents.go | 6 + internal/api/handlers_thoughts.go | 2 +- internal/domain/models.go | 3 + internal/storage/docsync.go | 37 --- internal/storage/documents.go | 3 +- internal/storage/task_doc_sync.go | 273 ++++++++++++++++++ internal/storage/task_doc_sync_test.go | 153 ++++++++++ internal/storage/thoughts.go | 13 +- 20 files changed, 511 insertions(+), 53 deletions(-) create mode 100644 internal/storage/task_doc_sync.go create mode 100644 internal/storage/task_doc_sync_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index c50fb24..a1f3960 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V ## [Unreleased] +## [0.36.0] - 2026-11-01 + +### Added +- Aufgaben, die mit einer Notiz verknüpft sind („Erweitert“), gleichen jetzt auch den Titel ab, in beide Richtungen. Ihre Unteraufgaben sind mit eigenen Häkchen in der Notiz verknüpft (Badge „Unteraufgabe“): Haken und Text wandern hin und her, auch wenn du das Häkchen in der Notiz verschiebst. Eine neue Unteraufgabe erscheint in der Notiz unter der Aufgabe; ein neues Häkchen direkt eine Ebene unter der Aufgabe wird zur Unteraufgabe. Gelöscht wird nie automatisch: Was du auf einer Seite löschst, bleibt auf der anderen stehen. + ## [0.35.0] - 2026-11-01 ### Added @@ -606,7 +611,8 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V - Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken. - Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung. -[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.35.0...dev +[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.36.0...dev +[0.36.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.35.0...v0.36.0 [0.35.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.34.0...v0.35.0 [0.34.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.33.0...v0.34.0 [0.33.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.32.0...v0.33.0 diff --git a/Doku/modules/tasks.md b/Doku/modules/tasks.md index c435c9a..172b09d 100644 --- a/Doku/modules/tasks.md +++ b/Doku/modules/tasks.md @@ -12,7 +12,8 @@ Eine Aufgabe ist ein `thought` ([database.md](../database.md)) mit `kanbanStatus - **Kanban:** Spalten pro Liste frei konfigurierbar (Name, Farbe, „Erledigt“-Spalte). Farben in „Spalten anpassen“ über den Punkt vor dem Namen: Grau, Anthrazit (Textfarbe des Themes), Grün, Gelb, Blau, Rot, Lila, Akzentfarbe (`columns[].color` = `gray`/`slate`/`green`/`yellow`/`blue`/`red`/`purple`/`accent`; ältere Werte `emerald`/`amber` gelten als Grün/Gelb, Spalten ohne Farbe behalten die ihrer Rolle). Der Punkt erscheint im Spaltenkopf, vor dem Status in der Listenansicht und im Detail-Fenster ([ManageColumnsModal.tsx](../../frontend/src/features/triage/ManageColumnsModal.tsx), `columnDotClass`). Blau und Lila kommen aus `--color-info` / `--color-purple`; Themes können sie setzen, sonst gelten Standardwerte aus `index.css`. Standard: *Zu erledigen*, *In Bearbeitung*, *Wartet auf andere*, *Erledigt*. Eine Karte in eine „Erledigt“-Spalte zu ziehen setzt `isCompleted`. - **Erledigte aufräumen** ([done_cleanup.go](../../internal/storage/done_cleanup.go)): Erledigte Aufgaben werden standardmäßig 30 Tage nach dem Abhaken gelöscht. Die Frist stellt jede Person im Reiter „Erledigt“ ein (7, 30, 90, 365 Tage oder nie, `users.done_retention_days`); für Aufgaben in einer Liste gilt die Frist des Eigentümers, für persönliche die eigene. Den Zeitpunkt setzt ein Trigger (`thoughts.completed_at`), egal auf welchem Weg abgehakt wird; der Server räumt stündlich auf (nicht in der Demo). „Erledigte leeren“ (`POST /tasks/clear-done`) löscht sofort, aber nur, was man bearbeiten darf. Ein mit einer Notiz verknüpftes Häkchen bleibt dabei als normales Häkchen stehen. - Drag & Drop sortiert in der Liste und zwischen Spalten; ein Klick öffnet das Detail-Fenster (Titel, Status, Priorität, Fälligkeit mit optionaler Uhrzeit und Schnellwahl, Liste, Hashtags, Unteraufgaben mit Fortschrittsbalken, Notizen). -- „Als Notiz vertiefen“ legt aus einer Aufgabe eine Notiz an (Titel, Notizen, Unteraufgaben, Fälligkeit, Priorität als Inhalt). Die Aufgabe bleibt in ihrer Liste und wird mit dem ersten Häkchen der Notiz verknüpft (`converted_type = document_block`); ein Gedanke aus der Triage wird dagegen ganz zur Notiz. Aufgaben aus Notiz-Checkboxen (`enhanced-task`) bleiben verknüpft: Abhaken wirkt in beide Richtungen (`SyncDocBlockAndTask`), „In Notiz öffnen“ springt zum Block. +- „Als Notiz vertiefen“ legt aus einer Aufgabe eine Notiz an (Titel, Notizen, Unteraufgaben, Fälligkeit, Priorität als Inhalt). Die Aufgabe bleibt in ihrer Liste und wird mit dem ersten Häkchen der Notiz verknüpft (`converted_type = document_block`); ein Gedanke aus der Triage wird dagegen ganz zur Notiz. Aufgaben aus Notiz-Checkboxen (`enhanced-task`) bleiben verknüpft, „In Notiz öffnen“ springt zum Block. +- **Abgleich verknüpfter Aufgaben** ([task_doc_sync.go](../../internal/storage/task_doc_sync.go)): Haken und Titel gehen in beide Richtungen (Notiz → Aufgabe in `ReconcileDocTaskLinks` nach jeder Notiz-Änderung, Aufgabe → Notiz in `SyncTaskToDoc` nach jeder Aufgaben-Änderung). Unteraufgaben gehören zur Verknüpfung: Jede ist 1:1 mit einem Häkchen derselben Notiz verbunden (`subtasks[].blockId`, Badge „Unteraufgabe“, `DocumentBlock.subtaskOf`), auch wenn es verschoben oder anders eingerückt wird. Neue Unteraufgaben bekommen ein Häkchen direkt unter dem Haupt-Häkchen; neue Häkchen direkt eine Ebene darunter werden Unteraufgaben (tiefer eingerückte bleiben Notiz). Löschen wird nicht abgeglichen: Ein gelöschtes Häkchen löst nur die Verknüpfung, eine gelöschte Unteraufgabe lässt ihr Häkchen stehen. Ein Häkchen, das in eine andere Notiz wandert, bekommt dort eine neue ID und verliert damit die Verknüpfung. Die Sync-Antwort der Notiz liefert `taskLinks` und `subtaskLinks`, damit die Badges sofort stimmen. ## Aufgabenlisten und Freigabe diff --git a/README.md b/README.md index 63ebbad..6f0f6b5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Self-Hosted](https://img.shields.io/badge/100%25-Self--Hosted-2563eb?style=flat-square)](#) [![Docker](https://img.shields.io/badge/Deployment-Single%20Container-2496ed?style=flat-square&logo=docker&logoColor=white)](#-schnellstart) [![SQLite](https://img.shields.io/badge/Daten-SQLite-003b57?style=flat-square&logo=sqlite&logoColor=white)](#-deine-daten-gehören-dir) -[![Version](https://img.shields.io/badge/Version-v0.35.0-16a34a?style=flat-square)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/Version-v0.36.0-16a34a?style=flat-square)](CHANGELOG.md) **MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6853103..e92a5c8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mindsift-frontend", - "version": "0.35.0", + "version": "0.36.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mindsift-frontend", - "version": "0.35.0", + "version": "0.36.0", "license": "Unlicense", "dependencies": { "@fontsource-variable/inter": "5.3.0", diff --git a/frontend/package.json b/frontend/package.json index 6a401a4..faac980 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mindsift-frontend", "private": true, - "version": "0.35.0", + "version": "0.36.0", "license": "Unlicense", "type": "module", "scripts": { diff --git a/frontend/src/features/workspace/DocumentEditor.tsx b/frontend/src/features/workspace/DocumentEditor.tsx index a3c1e16..9ea03c4 100644 --- a/frontend/src/features/workspace/DocumentEditor.tsx +++ b/frontend/src/features/workspace/DocumentEditor.tsx @@ -128,6 +128,7 @@ function sameBlocks(a: DocumentBlock[], b: DocumentBlock[]): boolean { !!x.collapsed !== !!y.collapsed || (x.lang || '') !== (y.lang || '') || (x.taskId || '') !== (y.taskId || '') || + (x.subtaskOf || '') !== (y.subtaskOf || '') || (x.conflictOf || '') !== (y.conflictOf || '') ) { return false; @@ -360,10 +361,12 @@ export const DocumentEditor: React.FC = ({ setBlocks(normalized); } const links = new Map(); + const subtaskLinks = new Map(); for (const b of fetched.blocks || []) { if (b.taskId) links.set(b.id, b.taskId); + if (b.subtaskOf) subtaskLinks.set(b.id, b.subtaskOf); } - engineRef.current?.setTaskLinks(links); + engineRef.current?.setTaskLinks(links, subtaskLinks); setLoading(false); onDocMetadataChange(fetched); diff --git a/frontend/src/features/workspace/OutlinerBlock.tsx b/frontend/src/features/workspace/OutlinerBlock.tsx index e8e3fa4..412e2d1 100644 --- a/frontend/src/features/workspace/OutlinerBlock.tsx +++ b/frontend/src/features/workspace/OutlinerBlock.tsx @@ -1488,6 +1488,24 @@ export const OutlinerBlock: React.FC = ({ )} + {/* Checkbox tied to a subtask of a linked task: tick and text follow the subtask */} + {block.type === 'todo' && !block.taskId && block.subtaskOf && ( +
+ +
+ )} {/* Conflict copy: text that lost against someone else's change */} {block.conflictOf && ( @@ -1619,7 +1637,9 @@ export const OutlinerBlock: React.FC = ({ className="absolute right-0 top-full mt-1 bg-panel border border-border shadow-lg rounded-card p-1 z-30 w-52 text-xs" onClick={(e) => e.stopPropagation()} > - {!block.taskId ? ( + {block.subtaskOf && !block.taskId ? ( +

Unteraufgabe einer verknüpften Aufgabe

+ ) : !block.taskId ? (