diff --git a/CHANGELOG.md b/CHANGELOG.md index 169a7c8..cb98907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V ## [Unreleased] +## [0.47.0] - 2026-11-01 + +### Added +- Verlinken-Fenster in Notizen: Ab 3 Buchstaben stehen unter den passenden Notizen auch passende **Punkte aus allen Notizen** (bis zu 8, Suchbegriff markiert, mit Notiztitel). Ein Klick verlinkt direkt diesen Punkt. +- Beispieldaten (Demo und auf Wunsch bei der Einrichtung): neuer Ordner „MindSift-Hilfe“ mit kurzen Anleitungen (Erste Schritte, Notizen, Aufgaben, Listen, Tastenkürzel, Spracheingabe, Teilen, API), eine Notiz „Zoom ausprobieren“ mit zehn Ebenen, eine tiefere Küchenplanung in „Hausrenovierung 2027“ und Beispiele für verknüpfte To-dos: „Backup-Konzept“ (Häkchen als Aufgabe mit Unteraufgaben) und die als Notiz vertiefte Aufgabe „Uptime-Monitoring“. + ## [0.46.0] - 2026-11-01 ### Added @@ -744,7 +750,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.46.0...dev +[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.47.0...dev +[0.47.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.46.0...v0.47.0 [0.46.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.45.2...v0.46.0 [0.45.2]: https://git.dsit.me/sandrodach/MindSift/compare/v0.45.1...v0.45.2 [0.45.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.45.0...v0.45.1 diff --git a/README.md b/README.md index 7f586ff..d71eb6e 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.46.0-16a34a?style=flat-square)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/Version-v0.47.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 6fd0cb4..23e6052 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mindsift-frontend", - "version": "0.46.0", + "version": "0.47.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mindsift-frontend", - "version": "0.46.0", + "version": "0.47.0", "license": "Unlicense", "dependencies": { "@fontsource-variable/inter": "5.3.0", diff --git a/frontend/package.json b/frontend/package.json index 291c465..3d50e36 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mindsift-frontend", "private": true, - "version": "0.46.0", + "version": "0.47.0", "license": "Unlicense", "type": "module", "scripts": { diff --git a/frontend/src/components/CommandPalette.tsx b/frontend/src/components/CommandPalette.tsx index 78d3078..6c8fd88 100644 --- a/frontend/src/components/CommandPalette.tsx +++ b/frontend/src/components/CommandPalette.tsx @@ -10,6 +10,7 @@ import { } from 'lucide-react'; import { SearchHit } from '../lib/types'; import { api } from '../lib/api'; +import { SnippetRenderer } from './SnippetRenderer'; interface CommandPaletteProps { isOpen: boolean; @@ -19,32 +20,6 @@ interface CommandPaletteProps { onOpenList: (listId: string) => void; } -// Safely render snippet containing ... without dangerouslySetInnerHTML -const SnippetRenderer: React.FC<{ snippet: string }> = ({ snippet }) => { - const parts = useMemo(() => { - if (!snippet) return []; - return snippet.split(/([\s\S]*?<\/mark>)/g); - }, [snippet]); - - return ( - - {parts.map((part, i) => { - if (part.startsWith('') && part.endsWith('')) { - const text = part.slice(6, -7); - return ( - - {text} - - ); - } - return {part}; - })} - - ); -}; export const CommandPalette: React.FC = ({ isOpen, diff --git a/frontend/src/components/SnippetRenderer.tsx b/frontend/src/components/SnippetRenderer.tsx new file mode 100644 index 0000000..0fd8938 --- /dev/null +++ b/frontend/src/components/SnippetRenderer.tsx @@ -0,0 +1,28 @@ +import React, { useMemo } from 'react'; + +// Safely render snippet containing ... without dangerouslySetInnerHTML +export const SnippetRenderer: React.FC<{ snippet: string }> = ({ snippet }) => { + const parts = useMemo(() => { + if (!snippet) return []; + return snippet.split(/([\s\S]*?<\/mark>)/g); + }, [snippet]); + + return ( + + {parts.map((part, i) => { + if (part.startsWith('') && part.endsWith('')) { + const text = part.slice(6, -7); + return ( + + {text} + + ); + } + return {part}; + })} + + ); +}; diff --git a/frontend/src/features/workspace/LinkModal.tsx b/frontend/src/features/workspace/LinkModal.tsx index 6864dde..180ac32 100644 --- a/frontend/src/features/workspace/LinkModal.tsx +++ b/frontend/src/features/workspace/LinkModal.tsx @@ -14,6 +14,7 @@ import { import { api } from '../../lib/api'; import { Document, DocumentBlock } from '../../lib/types'; import { buildBlockLink } from '../../lib/wikiLink'; +import { SnippetRenderer } from '../../components/SnippetRenderer'; export interface LinkModalProps { isOpen: boolean; @@ -134,6 +135,50 @@ export const LinkModal: React.FC = ({ ); }, [pickBlocks, blockSearch]); + // Bullets of all notes matching the search (full-text index, like the global search), from 3 letters + type BlockHit = { docId: string; blockId: string; title: string; icon?: string; snippet: string; content: string }; + const [blockHits, setBlockHits] = useState([]); + const [blockHitsLoading, setBlockHitsLoading] = useState(false); + useEffect(() => { + const q = search.trim(); + if (!isOpen || tab !== 'note' || pickDoc || q.length < 3) { + setBlockHits([]); + setBlockHitsLoading(false); + return; + } + let cancelled = false; + setBlockHitsLoading(true); + const timer = window.setTimeout(() => { + api + .searchAll(q) + .then((res) => { + if (cancelled) return; + const docsById = new Map((res.documents || []).map((d) => [d.id, d])); + const hits = (res.hits || []) + .filter((h) => h.type === 'document' && h.blockId && h.docId) + .slice(0, 8) + .map((h) => { + const block = docsById.get(h.docId!)?.blocks?.find((b) => b.id === h.blockId); + return { + docId: h.docId!, + blockId: h.blockId!, + title: h.title, + icon: h.icon, + snippet: h.snippet, + content: block?.content ?? h.snippet.replace(/<\/?mark>/g, ''), + }; + }); + setBlockHits(hits); + }) + .catch(() => !cancelled && setBlockHits([])) + .finally(() => !cancelled && setBlockHitsLoading(false)); + }, 250); + return () => { + cancelled = true; + window.clearTimeout(timer); + }; + }, [search, isOpen, tab, pickDoc]); + const handleSelectNote = (doc: { title: string }) => { const noteTitle = doc.title.trim() || 'Notiz'; onInsertLink(`[[${noteTitle}]]`); @@ -399,6 +444,33 @@ export const LinkModal: React.FC = ({ )) )} + + {/* Matching bullets of all notes: a click links that bullet */} + {search.trim().length >= 3 && (blockHitsLoading || blockHits.length > 0) && ( + <> +
+ Punkte + {blockHitsLoading && } +
+
+ {blockHits.map((h) => ( + + ))} +
+ + )} )} diff --git a/internal/api/handlers_lists.go b/internal/api/handlers_lists.go index 1b0d907..7fb114d 100644 --- a/internal/api/handlers_lists.go +++ b/internal/api/handlers_lists.go @@ -38,9 +38,9 @@ func (s *Server) handleGetListByID(w http.ResponseWriter, r *http.Request) { func (s *Server) handleCreateList(w http.ResponseWriter, r *http.Request) { userID := auth.GetUserID(r.Context()) var req struct { - Title string `json:"title"` + Title string `json:"title"` Icon *string `json:"icon"` // "" = no icon; missing = the default - ListType string `json:"listType"` + ListType string `json:"listType"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Ungültiges JSON") diff --git a/internal/api/handlers_tasklists.go b/internal/api/handlers_tasklists.go index fbe26d7..98625a4 100644 --- a/internal/api/handlers_tasklists.go +++ b/internal/api/handlers_tasklists.go @@ -25,11 +25,11 @@ func (s *Server) handleGetTaskLists(w http.ResponseWriter, r *http.Request) { func (s *Server) handleCreateTaskList(w http.ResponseWriter, r *http.Request) { userID := auth.GetUserID(r.Context()) var req struct { - SpaceID string `json:"spaceId"` - TeamID string `json:"teamId"` - Title string `json:"title"` + SpaceID string `json:"spaceId"` + TeamID string `json:"teamId"` + Title string `json:"title"` Icon *string `json:"icon"` // "" = no icon; missing = the default - Color string `json:"color"` + Color string `json:"color"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Ungültiges JSON") diff --git a/internal/storage/docsync_test.go b/internal/storage/docsync_test.go index efc0a05..7b63b5a 100644 --- a/internal/storage/docsync_test.go +++ b/internal/storage/docsync_test.go @@ -3,6 +3,7 @@ package storage import ( "errors" "path/filepath" + "strings" "testing" "time" @@ -398,13 +399,22 @@ func TestDemoAccountsGetSampleDataAndShares(t *testing.T) { t.Fatalf("expected tasks and inbox thoughts, got %d tasks, %d inbox", tasks, inbox) } // Running it again (container restart) must not seed twice + var before, docs int + _ = s.db.QueryRow(`SELECT COUNT(*) FROM documents WHERE user_id = ?`, admin.ID).Scan(&before) if err := s.EnsureDemoAccounts(); err != nil { t.Fatal(err) } - var docs int _ = s.db.QueryRow(`SELECT COUNT(*) FROM documents WHERE user_id = ?`, admin.ID).Scan(&docs) - if docs != 4 { - t.Fatalf("restart seeded again: %d notes", docs) + if docs != before || docs < 10 { + t.Fatalf("restart seeded again or sample notes missing: %d notes before, %d after", before, docs) + } + // The linked example: a task tied to a note checkbox, with the checkboxes below as subtasks + var subtasks string + if err := s.db.QueryRow(`SELECT subtasks FROM thoughts WHERE user_id = ? AND converted_type = 'document_block' AND content LIKE 'Backup-Konzept%'`, admin.ID).Scan(&subtasks); err != nil { + t.Fatalf("linked sample task missing: %v", err) + } + if !strings.Contains(subtasks, "systemd-Timer") || !strings.Contains(subtasks, "blockId") { + t.Fatalf("checkboxes below should be linked subtasks: %s", subtasks) } } diff --git a/internal/storage/seed.go b/internal/storage/seed.go index 35db99b..7e8e3d2 100644 --- a/internal/storage/seed.go +++ b/internal/storage/seed.go @@ -194,7 +194,17 @@ func (s *Store) SeedStarterContent(user *domain.User) error { {level: 4, text: "Varianten"}, {level: 5, text: "A: Zeile mit Insel – 14.800 €"}, {level: 6, text: "Insel braucht eigenen Stromkreis, Elektriker fragen"}, + {level: 7, text: "Elektriker Huber, Termin im März"}, + {level: 8, text: "Was er wissen muss"}, + {level: 9, text: "Kochfeld 7,4 kW → Starkstrom (400 V) an der Insel"}, + {level: 9, text: "Steckdosen auf beiden Seiten der Insel, eine davon mit USB"}, + {level: 9, typ: "todo", text: "Unterverteilung fotografieren und vorab schicken"}, + {level: 8, text: "Kosten"}, + {level: 9, text: "Schätzung 900–1.200 €, abhängig vom Bodenaufbau"}, {level: 6, text: "Kochfeld mit integriertem Abzug, kein Hängeschrank nötig"}, + {level: 7, text: "Umluft oder Abluft?"}, + {level: 8, text: "Abluft braucht einen Mauerdurchbruch in der Außenwand"}, + {level: 8, text: "Umluft: Filter alle 12 Monate tauschen #wartung"}, {level: 5, text: "B: L-Form ohne Insel – 11.200 €"}, {level: 6, text: "mehr Stauraum, aber der Essplatz wird kleiner"}, {level: 4, typ: "todo", text: "Gegenangebot vom zweiten Studio einholen #renovierung"}, @@ -227,6 +237,8 @@ func (s *Store) SeedStarterContent(user *domain.User) error { {text: "Wochenrückblick jeden Sonntag: Inbox leeren, Aufgaben für die Woche planen #routine"}, }), []string{"ideen"}, false) + s.seedLinkedTodos(user, homelab) + s.seedHelpNotes(user) return nil } diff --git a/internal/storage/seed_help.go b/internal/storage/seed_help.go new file mode 100644 index 0000000..7b2e518 --- /dev/null +++ b/internal/storage/seed_help.go @@ -0,0 +1,159 @@ +package storage + +import "github.com/sandrodach/mindsift/internal/domain" + +// seedLinkedTodos shows the link between notes and tasks both ways: a checklist in a note whose main +// checkbox became a task (the checkboxes below are its subtasks), and a task deepened into a note. +func (s *Store) seedLinkedTodos(user *domain.User, homelab *domain.TaskList) { + listID := "" + if homelab != nil { + listID = homelab.ID + } + blocks := seedBlocks([]seedBlock{ + {typ: "callout", text: "Das Häkchen „Backup-Konzept umsetzen“ ist mit einer Aufgabe in der Liste Homelab verknüpft (Badge „Erweitert“). Die Häkchen direkt darunter sind ihre Unteraufgaben: Abhaken hier oder in der Aufgabe – beide Seiten bleiben gleich."}, + {typ: "todo", text: "Backup-Konzept umsetzen #backup"}, + {level: 1, typ: "todo", text: "restic-Repository auf der Storage Box anlegen", checked: true}, + {level: 1, typ: "todo", text: "Nächtlicher Lauf per systemd-Timer"}, + {level: 1, typ: "todo", text: "Wiederherstellung einmal komplett testen"}, + {level: 1, text: "Normale Punkte gehören nicht zur Aufgabe – hier ist Platz für Gedanken zum Ablauf"}, + {level: 2, text: "Aufbewahrung: 7 tägliche, 4 wöchentliche, 6 monatliche Stände"}, + {typ: "h3", text: "So geht's"}, + {level: 1, text: "Ein Häkchen über „Erweitert“ (Menü am Häkchen) zur Aufgabe machen"}, + {level: 1, text: "Neue Häkchen direkt eine Ebene darunter werden automatisch Unteraufgaben"}, + {level: 1, text: "Umgekehrt: In den Aufgaben „Als Notiz vertiefen“ – siehe die Notiz zur Aufgabe „Uptime-Monitoring“"}, + }) + doc, err := s.CreateDocument(user.ID, "", "Backup-Konzept", "💾", "Projekte", blocks, []string{"homelab"}, false) + if err != nil || doc == nil { + return + } + task, err := s.CreateTaskFromDocBlock(user.ID, doc.ID, blocks[1].ID, blocks[1].Content, false) + if err != nil || task == nil { + return + } + s.ReconcileDocTaskLinks(doc.ID) + high, status := "high", "in_progress" + _, _ = s.UpdateThoughtDetails(user.ID, task.ID, nil, nil, nil, &high, &listID, &status, nil, nil, nil) + + // A task of the list deepened into a note: the note starts with its checkbox and subtasks + var monitoringID string + _ = s.db.QueryRow(`SELECT id FROM thoughts WHERE user_id = ? AND content LIKE 'Uptime-Monitoring%'`, user.ID).Scan(&monitoringID) + if monitoringID != "" { + _, _ = s.UpdateThoughtTriage(user.ID, monitoringID, "keep", "document", "", "", "") + } +} + +// seedHelpNotes: a short guide to MindSift as notes, so the demo explains itself while showing the block +// types (headings, callouts, numbered lists, code, links to notes). +func (s *Store) seedHelpNotes(user *domain.User) { + _, _ = s.CreateFolder(user.ID, "MindSift-Hilfe") + help := func(title, icon string, spec []seedBlock) { + _, _ = s.CreateDocument(user.ID, "", title, icon, "MindSift-Hilfe", seedBlocks(spec), []string{"hilfe"}, false) + } + help("Erste Schritte", "👋", []seedBlock{ + {typ: "callout", text: "Willkommen! Diese Notizen erklären MindSift. Alles hier ist Beispielinhalt – ändern, verschieben und löschen ist ausdrücklich erlaubt. #hilfe"}, + {typ: "h2", text: "Die Idee in drei Schritten"}, + {typ: "numbered", text: "**Erfassen:** Alles, was dir einfällt, sofort in die Leiste oben (Quick Dump) – Taste `C` oder `/`, am Handy das +."}, + {typ: "numbered", text: "**Sortieren:** In der Triage gehst du die Gedanken später durch: verwerfen, Aufgabe, Notiz oder Listeneintrag."}, + {typ: "numbered", text: "**Vertiefen:** Aufgaben bekommen Unteraufgaben und Termine, Notizen werden zu Gliederungen, Listen teilst du mit der Familie."}, + {typ: "h2", text: "Weiterlesen"}, + {level: 1, text: "[[Notizen & Outliner]] – Punkte, Ebenen, Zoom und Verlinken"}, + {level: 1, text: "[[Zoom ausprobieren]] – eine Gliederung zehn Ebenen tief"}, + {level: 1, text: "[[Aufgaben & Kanban]] – Listen, Spalten, Papierkorb"}, + {level: 1, text: "[[Listen & Einkauf]] – gemeinsam einkaufen und packen"}, + {level: 1, text: "[[Tastenkürzel]] – schneller arbeiten"}, + {level: 1, text: "[[Spracheingabe]] – diktieren statt tippen"}, + {level: 1, text: "[[Teilen & gemeinsam bearbeiten]]"}, + {level: 1, text: "[[API & Smart Home]] – Home Assistant und KI-Agenten anbinden"}, + }) + help("Notizen & Outliner", "📝", []seedBlock{ + {typ: "h2", text: "Punkte und Ebenen"}, + {level: 1, text: "`Enter` legt einen neuen Punkt an, `Tab` rückt ein, `Shift+Tab` rückt aus"}, + {level: 1, text: "Punkte mit Unterpunkten lassen sich auf- und zuklappen (Pfeil links beim Drüberfahren)"}, + {level: 1, text: "Am Punkt (•) ziehen verschiebt den ganzen Ast"}, + {level: 1, text: "Klick auf den Punkt (•) **zoomt** in den Ast – probier es in [[Zoom ausprobieren]]"}, + {typ: "h2", text: "Formatieren"}, + {level: 1, text: "Am Zeilenanfang: `- ` Punkt, `1. ` Nummer, `[] ` Häkchen, `# ` Überschrift"}, + {level: 1, text: "`/` öffnet das Menü: /todo, /h1, /code, /callout, /image, /audio, /diktat, /extract …"}, + {level: 1, text: "**fett** mit `Strg+B`, *kursiv* mit `Strg+I`, Link mit `Strg+K`"}, + {typ: "h2", text: "Verlinken"}, + {level: 1, text: "`[[Notiztitel]]` verlinkt eine Notiz, z. B. [[Erste Schritte]]"}, + {level: 1, text: "Auch einzelne Punkte lassen sich verlinken: im Verlinken-Fenster nach Text suchen, gefundene Punkte stehen unter den Notizen"}, + {level: 1, text: "`#Hashtags` sammeln Fundstellen über alle Notizen, z. B. #hilfe"}, + {typ: "h2", text: "Markieren und löschen"}, + {level: 1, text: "Mit der Maus über mehrere Punkte ziehen markiert Text über Punktgrenzen; Entf löscht wie in einem Texteditor"}, + {level: 1, text: "`Strg+Z` nimmt jede eigene Änderung zurück"}, + {typ: "callout", text: "Gelöschte Notizen landen im Papierkorb unten in der Ordnerleiste. Wie lange sie dort bleiben, steht unter Kontoeinstellungen → Aufbewahrung."}, + }) + help("Zoom ausprobieren", "🔍", []seedBlock{ + {typ: "callout", text: "Klick auf einen Punkt (•) zeigt nur diesen Ast. Oben erscheint der Pfad – ein Klick darauf geht Ebene für Ebene zurück."}, + {text: "Ebene 1: Projekt „Garten neu anlegen“"}, + {level: 1, text: "Ebene 2: Beete"}, + {level: 2, text: "Ebene 3: Hochbeet am Zaun"}, + {level: 3, text: "Ebene 4: Bepflanzung"}, + {level: 4, text: "Ebene 5: Frühjahr"}, + {level: 5, text: "Ebene 6: Salat und Radieschen"}, + {level: 6, text: "Ebene 7: Sorten"}, + {level: 7, text: "Ebene 8: Pflücksalat „Lollo Rosso“"}, + {level: 8, text: "Ebene 9: Aussaat ab Mitte März"}, + {level: 9, text: "Ebene 10: alle zwei Wochen nachsäen – so gibt es den ganzen Sommer Salat 🥗"}, + {level: 7, text: "Ebene 8: Radieschen „Eiszapfen“"}, + {level: 5, text: "Ebene 6: Kräuter"}, + {level: 6, text: "Ebene 7: Schnittlauch, Petersilie, Basilikum"}, + {level: 4, text: "Ebene 5: Sommer"}, + {level: 5, text: "Ebene 6: Tomaten und Zucchini"}, + {level: 2, text: "Ebene 3: Kräuterspirale"}, + {level: 1, text: "Ebene 2: Rasen"}, + {level: 2, typ: "todo", text: "Ebene 3: Vertikutieren im April"}, + }) + help("Aufgaben & Kanban", "✅", []seedBlock{ + {typ: "h2", text: "Ansichten"}, + {level: 1, text: "**Liste** mit den Reitern Offen, Erledigt und Papierkorb; Gruppe und Sortierung stehen rechts darüber"}, + {level: 1, text: "**Kanban** mit eigenen Spalten je Aufgabenliste (Spalten anpassen: Name, Farbe, Erledigt-Spalte)"}, + {level: 1, text: "„Alle Aufgaben“ zeigt die Spalten aller Listen zusammengeführt"}, + {typ: "h2", text: "Arbeiten mit Aufgaben"}, + {level: 1, text: "Klick öffnet die Details: Priorität, Fälligkeit mit Uhrzeit, Wiederholung, Unteraufgaben, Notizen (mit Mikrofon)"}, + {level: 1, text: "Ziehen sortiert um oder wechselt die Spalte; `Strg+Z` nimmt Löschen, Verschieben und Abhaken zurück"}, + {level: 1, text: "Aufgaben und Notizen lassen sich verknüpfen – Beispiel: [[Backup-Konzept]]"}, + {typ: "h2", text: "Aufräumen"}, + {level: 1, text: "Erledigte wandern nach einer Frist in den Papierkorb, der sich später selbst leert"}, + {level: 1, text: "Die Fristen stellst du unter Kontoeinstellungen → Aufbewahrung ein"}, + }) + help("Listen & Einkauf", "🛒", []seedBlock{ + {text: "Kategorien entstehen aus Hashtags: `Hafermilch #Kühlregal` landet unter „Kühlregal“"}, + {text: "Mengen ins eigene Feld, z. B. `500 g` oder `3x`"}, + {text: "Geteilte Listen zeigen Abhaken bei allen sofort – ideal für den Wocheneinkauf"}, + {text: "Abgehakte Einträge verschwinden nach einer Woche von selbst (einstellbar unter Aufbewahrung)"}, + }) + help("Tastenkürzel", "⌨️", []seedBlock{ + {typ: "h2", text: "Überall"}, + {level: 1, text: "`C` oder `/` – Quick Dump"}, + {level: 1, text: "`Strg+K` – globale Suche über Notizen, Aufgaben und Listen"}, + {typ: "h2", text: "Triage"}, + {level: 1, text: "`H` / `←` verwerfen, `L` / `→` Aufgabe, `T` mit Termin, `D` Notiz, Leertaste später"}, + {typ: "h2", text: "Notizen"}, + {level: 1, text: "`Tab` / `Shift+Tab` ein- und ausrücken, `Strg+Z` / `Strg+Shift+Z` rückgängig und wiederholen"}, + {level: 1, text: "`Strg+Shift+F` Fokus-Modus – nur die Notiz, in Lesebreite"}, + {typ: "h2", text: "Aufgaben"}, + {level: 1, text: "`Strg+Z` nimmt Löschen, Verschieben und Abhaken zurück"}, + }) + help("Spracheingabe", "🎙️", []seedBlock{ + {text: "Mikrofone gibt es im Quick Dump, beim Anlegen von Aufgaben und Listeneinträgen, in den Detailnotizen einer Aufgabe und als `/diktat` in Notizen"}, + {text: "Am Handy: das + gedrückt halten startet sofort eine Aufnahme"}, + {text: "Mit eigenem Whisper-Server bleibt die Aufnahme auf deinem Server; unter Kontoeinstellungen → Spracheingabe wählst du Whisper oder die Erkennung des Browsers"}, + {typ: "callout", text: "In dieser Demo nutzt MindSift die Spracherkennung des Browsers."}, + }) + help("Teilen & gemeinsam bearbeiten", "🤝", []seedBlock{ + {text: "Notizen, Ordner, Aufgabenlisten und Listen lassen sich mit Personen oder Gruppen teilen – zum Bearbeiten oder nur zum Lesen"}, + {text: "In einer geteilten Notiz schreiben mehrere gleichzeitig; wer in einem Punkt tippt, hält ihn kurz für sich"}, + {level: 1, text: "Probier es aus: „Homelab 2.0“ ist mit dem Demo-Konto **user** geteilt – in einem zweiten Browser als user/user anmelden"}, + {text: "Aufgaben in geteilten Listen lassen sich Personen zuweisen und beobachten"}, + }) + help("API & Smart Home", "🔌", []seedBlock{ + {text: "Unter Kontoeinstellungen → API legst du Schlüssel mit genau den Rechten an, die ein Gerät braucht"}, + {typ: "h3", text: "Heute fällige Aufgaben abfragen"}, + {typ: "code", lang: "bash", text: "curl -H \"X-API-Key: mindsift_sec_…\" \\\n \"https://mindsift.example.com/api/v1/tasks?due=today&status=open\""}, + {typ: "h3", text: "KI-Agenten (MCP)"}, + {text: "Claude Code, Codex und andere Agenten lesen und pflegen Aufgaben und Listen über `/api/v1/mcp`"}, + {text: "Was ein Schlüssel anlegt oder ändert, trägt eine kleine Badge mit seinem Namen"}, + }) +}