feat: link dialog finds bullets of all notes; richer sample data with help notes, deep nesting and linked to-dos
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m46s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m46s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ae96aa5f2b
commit
ca56220e68
12 files changed
+303
-40
No files matched your search
+8
-1
@@ -6,6 +6,12 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [0.46.0] - 2026-11-01
|
||||||
|
|
||||||
### Added
|
### 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.
|
- 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.
|
- 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.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.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
|
[0.45.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.45.0...v0.45.1
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
[](#)
|
[](#)
|
||||||
[](#-schnellstart)
|
[](#-schnellstart)
|
||||||
[](#-deine-daten-gehören-dir)
|
[](#-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.
|
**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.
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"version": "0.46.0",
|
"version": "0.47.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"version": "0.46.0",
|
"version": "0.47.0",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter": "5.3.0",
|
"@fontsource-variable/inter": "5.3.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "mindsift-frontend",
|
"name": "mindsift-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.46.0",
|
"version": "0.47.0",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { SearchHit } from '../lib/types';
|
import { SearchHit } from '../lib/types';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
|
import { SnippetRenderer } from './SnippetRenderer';
|
||||||
|
|
||||||
interface CommandPaletteProps {
|
interface CommandPaletteProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -19,32 +20,6 @@ interface CommandPaletteProps {
|
|||||||
onOpenList: (listId: string) => void;
|
onOpenList: (listId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safely render snippet containing <mark>...</mark> without dangerouslySetInnerHTML
|
|
||||||
const SnippetRenderer: React.FC<{ snippet: string }> = ({ snippet }) => {
|
|
||||||
const parts = useMemo(() => {
|
|
||||||
if (!snippet) return [];
|
|
||||||
return snippet.split(/(<mark>[\s\S]*?<\/mark>)/g);
|
|
||||||
}, [snippet]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className="text-muted text-xs line-clamp-2 leading-relaxed">
|
|
||||||
{parts.map((part, i) => {
|
|
||||||
if (part.startsWith('<mark>') && part.endsWith('</mark>')) {
|
|
||||||
const text = part.slice(6, -7);
|
|
||||||
return (
|
|
||||||
<mark
|
|
||||||
key={i}
|
|
||||||
className="bg-accent-dim text-accent font-semibold px-0.5 rounded"
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</mark>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <span key={i}>{part}</span>;
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CommandPalette: React.FC<CommandPaletteProps> = ({
|
export const CommandPalette: React.FC<CommandPaletteProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
// Safely render snippet containing <mark>...</mark> without dangerouslySetInnerHTML
|
||||||
|
export const SnippetRenderer: React.FC<{ snippet: string }> = ({ snippet }) => {
|
||||||
|
const parts = useMemo(() => {
|
||||||
|
if (!snippet) return [];
|
||||||
|
return snippet.split(/(<mark>[\s\S]*?<\/mark>)/g);
|
||||||
|
}, [snippet]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="text-muted text-xs line-clamp-2 leading-relaxed">
|
||||||
|
{parts.map((part, i) => {
|
||||||
|
if (part.startsWith('<mark>') && part.endsWith('</mark>')) {
|
||||||
|
const text = part.slice(6, -7);
|
||||||
|
return (
|
||||||
|
<mark
|
||||||
|
key={i}
|
||||||
|
className="bg-accent-dim text-accent font-semibold px-0.5 rounded"
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</mark>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <span key={i}>{part}</span>;
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { api } from '../../lib/api';
|
import { api } from '../../lib/api';
|
||||||
import { Document, DocumentBlock } from '../../lib/types';
|
import { Document, DocumentBlock } from '../../lib/types';
|
||||||
import { buildBlockLink } from '../../lib/wikiLink';
|
import { buildBlockLink } from '../../lib/wikiLink';
|
||||||
|
import { SnippetRenderer } from '../../components/SnippetRenderer';
|
||||||
|
|
||||||
export interface LinkModalProps {
|
export interface LinkModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -134,6 +135,50 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
|||||||
);
|
);
|
||||||
}, [pickBlocks, blockSearch]);
|
}, [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<BlockHit[]>([]);
|
||||||
|
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 handleSelectNote = (doc: { title: string }) => {
|
||||||
const noteTitle = doc.title.trim() || 'Notiz';
|
const noteTitle = doc.title.trim() || 'Notiz';
|
||||||
onInsertLink(`[[${noteTitle}]]`);
|
onInsertLink(`[[${noteTitle}]]`);
|
||||||
@@ -399,6 +444,33 @@ export const LinkModal: React.FC<LinkModalProps> = ({
|
|||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Matching bullets of all notes: a click links that bullet */}
|
||||||
|
{search.trim().length >= 3 && (blockHitsLoading || blockHits.length > 0) && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between text-xs font-semibold text-muted uppercase tracking-wider px-0.5 pt-1">
|
||||||
|
<span>Punkte</span>
|
||||||
|
{blockHitsLoading && <Loader2 className="w-3.5 h-3.5 animate-spin text-accent" />}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5 max-h-56 overflow-y-auto pr-0.5">
|
||||||
|
{blockHits.map((h) => (
|
||||||
|
<button
|
||||||
|
key={`${h.docId}:${h.blockId}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onInsertLink(buildBlockLink(h.title || 'Notiz', h.blockId, h.content))}
|
||||||
|
className="w-full text-left p-2.5 min-h-11 bg-canvas border border-border rounded-card hover:border-accent/60 transition-colors cursor-pointer space-y-0.5"
|
||||||
|
title="Diesen Punkt verlinken"
|
||||||
|
>
|
||||||
|
<SnippetRenderer snippet={h.snippet} />
|
||||||
|
<span className="block text-[10px] text-faint truncate">
|
||||||
|
{h.icon ? `${h.icon} ` : ''}
|
||||||
|
{h.title || 'Unbenannte Notiz'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ func (s *Server) handleGetListByID(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (s *Server) handleCreateList(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleCreateList(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := auth.GetUserID(r.Context())
|
userID := auth.GetUserID(r.Context())
|
||||||
var req struct {
|
var req struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Icon *string `json:"icon"` // "" = no icon; missing = the default
|
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 {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ func (s *Server) handleGetTaskLists(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (s *Server) handleCreateTaskList(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleCreateTaskList(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := auth.GetUserID(r.Context())
|
userID := auth.GetUserID(r.Context())
|
||||||
var req struct {
|
var req struct {
|
||||||
SpaceID string `json:"spaceId"`
|
SpaceID string `json:"spaceId"`
|
||||||
TeamID string `json:"teamId"`
|
TeamID string `json:"teamId"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Icon *string `json:"icon"` // "" = no icon; missing = the default
|
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 {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package storage
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -398,13 +399,22 @@ func TestDemoAccountsGetSampleDataAndShares(t *testing.T) {
|
|||||||
t.Fatalf("expected tasks and inbox thoughts, got %d tasks, %d inbox", tasks, inbox)
|
t.Fatalf("expected tasks and inbox thoughts, got %d tasks, %d inbox", tasks, inbox)
|
||||||
}
|
}
|
||||||
// Running it again (container restart) must not seed twice
|
// 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 {
|
if err := s.EnsureDemoAccounts(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
var docs int
|
|
||||||
_ = s.db.QueryRow(`SELECT COUNT(*) FROM documents WHERE user_id = ?`, admin.ID).Scan(&docs)
|
_ = s.db.QueryRow(`SELECT COUNT(*) FROM documents WHERE user_id = ?`, admin.ID).Scan(&docs)
|
||||||
if docs != 4 {
|
if docs != before || docs < 10 {
|
||||||
t.Fatalf("restart seeded again: %d notes", docs)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -194,7 +194,17 @@ func (s *Store) SeedStarterContent(user *domain.User) error {
|
|||||||
{level: 4, text: "Varianten"},
|
{level: 4, text: "Varianten"},
|
||||||
{level: 5, text: "A: Zeile mit Insel – 14.800 €"},
|
{level: 5, text: "A: Zeile mit Insel – 14.800 €"},
|
||||||
{level: 6, text: "Insel braucht eigenen Stromkreis, Elektriker fragen"},
|
{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: 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: 5, text: "B: L-Form ohne Insel – 11.200 €"},
|
||||||
{level: 6, text: "mehr Stauraum, aber der Essplatz wird kleiner"},
|
{level: 6, text: "mehr Stauraum, aber der Essplatz wird kleiner"},
|
||||||
{level: 4, typ: "todo", text: "Gegenangebot vom zweiten Studio einholen #renovierung"},
|
{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"},
|
{text: "Wochenrückblick jeden Sonntag: Inbox leeren, Aufgaben für die Woche planen #routine"},
|
||||||
}), []string{"ideen"}, false)
|
}), []string{"ideen"}, false)
|
||||||
|
|
||||||
|
s.seedLinkedTodos(user, homelab)
|
||||||
|
s.seedHelpNotes(user)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"},
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in new issue
Block a user