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 @@
[](#)
[](#-schnellstart)
[](#-deine-daten-gehören-dir)
-[](CHANGELOG.md)
+[](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"},
+ })
+}