Files
MindSift/internal/storage/seed.go
T
sandrodachandClaude Opus 5.5 73fad1c5db
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m36s
feat(tasks): assign tasks in shared lists, unified people search, list avatars
- Tasks in shared task lists can be assigned to one or more people with access
  (task_assignees); assignments are pruned whenever access is lost
- UserAutocomplete: type-to-search everywhere, nothing before typing, max 3 results,
  first match preselected
- Task list and list tabs: avatar stack of members and round monochrome counter
- Shared list items remember who added them (list_items.created_by)
- Doku/ui-patterns.md documents the shared UI patterns

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:26:55 +01:00

211 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package storage
import (
"time"
"github.com/sandrodach/mindsift/internal/domain"
)
// seedBlock describes one outliner bullet of the sample notes: nesting level, block type and text.
type seedBlock struct {
level int
typ string
text string
checked bool
lang string
}
func seedBlocks(spec []seedBlock) []domain.DocumentBlock {
out := make([]domain.DocumentBlock, len(spec))
for i, b := range spec {
typ := b.typ
if typ == "" {
typ = "bullet"
}
out[i] = domain.DocumentBlock{ID: domain.NewID("blk"), Type: typ, Content: b.text, Checked: b.checked, Lang: b.lang, Level: b.level}
}
return out
}
type seedTask struct {
content string
priority string
due int // days from today; 0 = no due date unless dueToday
dueToday bool
status string // kanban column
notes string
subtasks []string
doneSubs int
}
// SeedStarterContent fills a new account with realistic sample data (optional at setup, always in demo mode):
// inbox thoughts, two task lists with Kanban states, shopping and packing lists and a few linked notes.
func (s *Store) SeedStarterContent(user *domain.User) error {
day := func(offset int) string { return time.Now().AddDate(0, 0, offset).Format("2006-01-02") }
inbox := []struct{ content, priority, source string }{
{"Zündkerzen und Luftfilter für den Rasenmäher bestellen #garage #wartung", "normal", "manual"},
{"Urlaubsidee: Wanderhütte in Südtirol für Oktober buchen #urlaub #familie", "high", "voice"},
{"Kellerfeuchtigkeit über 68 % gestiegen – Entfeuchter prüfen #smarthome #alarm", "urgent", "webhook"},
{"Buchtipp von Jonas: „Die Kunst des klaren Denkens“ #lesen", "low", "manual"},
{"Geburtstagsgeschenk für Oma – vielleicht ein Fotobuch vom Sommer? #familie", "normal", "manual"},
{"Idee: Backup-Report jeden Montag per Discord schicken #homelab", "normal", "voice"},
}
for _, t := range inbox {
_, _ = s.CreateThought(user.ID, t.content, t.priority, t.source, "")
}
addTasks := func(listID string, tasks []seedTask) {
for _, t := range tasks {
due := ""
if t.due != 0 || t.dueToday {
due = day(t.due)
}
th, err := s.CreateThoughtWithDetails(user.ID, t.content, t.priority, "manual", due, listID, t.status, "")
if err != nil {
continue
}
if _, err := s.UpdateThoughtTriage(user.ID, th.ID, "keep", "task", "", due, listID); err != nil {
continue
}
subs := make([]domain.SubTask, len(t.subtasks))
for i, title := range t.subtasks {
subs[i] = domain.SubTask{ID: domain.NewID("sub"), Title: title, IsCompleted: i < t.doneSubs, CreatedAt: domain.NowISO()}
}
done := t.status == "done"
status, notes := t.status, t.notes
_, _ = s.UpdateThoughtDetails(user.ID, th.ID, nil, &done, nil, nil, nil, &status, &notes, &subs, nil)
}
}
homelab, err := s.CreateTaskList(user.ID, "", "", "Homelab", "🖥️", "#2563eb")
if err == nil {
addTasks(homelab.ID, []seedTask{
{content: "MindSift-Container auf den neuen Server umziehen #docker", priority: "high", due: 2, status: "in_progress",
notes: "Volume `./data` vorher mit `sqlite3 .backup` sichern.",
subtasks: []string{"Backup der Datenbank ziehen", "docker-compose.yml anpassen", "DNS auf neue IP umstellen", "Alten Container stoppen"}, doneSubs: 2},
{content: "Reverse-Proxy: Nginx-Timeouts für Long-Polling erhöhen #nginx", priority: "normal", due: 5, status: "todo"},
{content: "USV-Batterie tauschen lassen #hardware", priority: "urgent", due: -1, status: "todo",
notes: "Händler hat das Ersatzteil seit Montag da."},
{content: "Wartet auf Lieferung: zweite SSD für das RAID #hardware", priority: "normal", due: 7, status: "waiting"},
{content: "Gitea-Runner auf Version 0.2 aktualisieren #devops", priority: "low", status: "done"},
{content: "Uptime-Monitoring für alle Dienste einrichten #monitoring", priority: "normal", due: 10, status: "todo",
subtasks: []string{"Uptime Kuma installieren", "Benachrichtigung per Discord", "Statusseite veröffentlichen"}, doneSubs: 1},
})
}
house, err := s.CreateTaskList(user.ID, "", "", "Haus & Garten", "🏡", "#16a34a")
if err == nil {
addTasks(house.ID, []seedTask{
{content: "Hecke schneiden, bevor die Vögel brüten #garten", priority: "normal", due: 3, status: "todo"},
{content: "Heizungswartung beim Installateur anfragen #wartung", priority: "high", dueToday: true, status: "in_progress"},
{content: "Steuerunterlagen für 2026 sortieren #finanzen", priority: "normal", due: 14, status: "todo",
subtasks: []string{"Handwerkerrechnungen", "Spendenquittungen", "Fahrtkosten"}},
{content: "Fahrradschloss nachkaufen", priority: "low", status: "done"},
})
}
shop, err := s.CreateList(user.ID, "Wocheneinkauf", "🛒", "shopping")
if err == nil {
items := []struct{ content, category, qty string }{
{"Hafermilch (Barista Edition)", "Kühlregal", "3x"},
{"Griechischer Joghurt", "Kühlregal", "500 g"},
{"Bergkäse", "Kühlregal", "200 g"},
{"Äpfel (Elstar)", "Obst & Gemüse", "1 kg"},
{"Cherrytomaten", "Obst & Gemüse", "2 Schalen"},
{"Avocados", "Obst & Gemüse", "3x"},
{"Bauernbrot", "Backwaren", "1x"},
{"Espressobohnen (100 % Arabica)", "Kaffee & Tee", "1 kg"},
{"Pfefferminztee", "Kaffee & Tee", "1x"},
{"Spülmaschinentabs", "Drogerie", "1 Pack"},
}
for i, it := range items {
item, err := s.CreateListItem(shop.ID, it.content, it.category, it.qty, i, user.ID)
if err == nil && (i == 1 || i == 6 || i == 8) {
_, _ = s.ToggleListItem(item.ID, true, user.ID, user.DisplayName)
}
}
}
packing, err := s.CreateList(user.ID, "Packliste Südtirol", "🎒", "shopping")
if err == nil {
for i, it := range []struct{ content, category string }{
{"Wanderschuhe", "Kleidung"}, {"Regenjacke", "Kleidung"}, {"Fleecepullover", "Kleidung"},
{"Erste-Hilfe-Set", "Ausrüstung"}, {"Stirnlampe", "Ausrüstung"}, {"Trinkflaschen", "Ausrüstung"},
{"Personalausweise", "Dokumente"}, {"Hüttenschlafsack", "Ausrüstung"},
} {
_, _ = s.CreateListItem(packing.ID, it.content, it.category, "", i, user.ID)
}
}
_, _ = s.CreateFolder(user.ID, "Projekte")
_, _ = s.CreateFolder(user.ID, "Privat")
_, _ = s.CreateDocument(user.ID, "", "Homelab 2.0 – Umzug auf den neuen Server", "🚀", "Projekte", seedBlocks([]seedBlock{
{typ: "callout", text: "Ziel: Bis Ende des Monats laufen alle Dienste auf dem neuen Mini-PC – leiser, sparsamer und mit echtem Backup. Aufgaben dazu stehen in der Liste #homelab."},
{typ: "h2", text: "Warum überhaupt?"},
{level: 1, text: "Der alte Server zieht 65 W im Leerlauf, der neue nur 9 W"},
{level: 2, text: "Ersparnis ca. 120 € im Jahr bei aktuellem Strompreis"},
{level: 1, text: "Backups liefen bisher nur manuell – das soll sich ändern #backup"},
{level: 1, text: "Endlich Platz für die Dienste, die seit Monaten auf der Wunschliste stehen"},
{typ: "h2", text: "Checkliste Umzug"},
{level: 1, typ: "todo", text: "Vorbereitung"},
{level: 2, typ: "todo", text: "Proxmox installieren und Updates einspielen", checked: true},
{level: 2, typ: "todo", text: "Netzwerk: feste IP und VLAN für Dienste", checked: true},
{level: 2, typ: "todo", text: "ZFS-Pool mit zwei SSDs anlegen"},
{level: 1, typ: "todo", text: "Dienste umziehen"},
{level: 2, typ: "todo", text: "MindSift (Daten liegen komplett in einer SQLite-Datei)", checked: true},
{level: 2, typ: "todo", text: "Gitea mit Runner und Container-Registry"},
{level: 2, typ: "todo", text: "Home Assistant inklusive Zigbee-Stick"},
{level: 1, typ: "todo", text: "Alten Server sicher löschen und verkaufen"},
{typ: "h2", text: "Dienste & Ports"},
{level: 1, text: "**MindSift** – Notizen, Aufgaben und Listen für die ganze Familie #mindsift"},
{level: 2, text: "läuft hinter Nginx, WebSocket-Upgrade und Long-Polling sind freigegeben"},
{level: 2, text: "Demo-Konten nur im Testsystem, nie öffentlich"},
{level: 1, text: "**Gitea** – Code, CI und Images, z. B. `git.example.com` #devops"},
{level: 1, text: "**Home Assistant** – Sensoren melden Alarme per Webhook direkt in die MindSift-Inbox #smarthome"},
{level: 2, text: "Beispiel: Kellerfeuchtigkeit über 65 % → Gedanke mit Priorität „dringend“"},
{typ: "h3", text: "Compose-Ausschnitt"},
{level: 1, typ: "code", lang: "yaml", text: "services:\n mindsift:\n image: git.example.com/sandrodach/mindsift:v0.2\n restart: unless-stopped\n volumes: [\"./data:/data\"]\n environment:\n - JWT_SECRET=${JWT_SECRET}"},
{typ: "h2", text: "Offene Fragen"},
{level: 1, text: "Reicht eine USV für Router und Server zusammen?"},
{level: 1, text: "Offsite-Backup: Hetzner Storage Box oder Festplatte bei den Eltern? Siehe [[Ideen-Sammlung]]"},
}), []string{"homelab", "projekt"}, true)
_, _ = s.CreateDocument(user.ID, "", "Urlaub Südtirol", "🏔️", "Privat", seedBlocks([]seedBlock{
{typ: "h2", text: "Reiseplan Oktober"},
{level: 1, text: "Anreise Samstag, früh los wegen Brenner-Stau"},
{level: 1, text: "Hütte bei Villnöß – Bestätigung kommt per Mail"},
{level: 2, text: "Halbpension, Hunde erlaubt"},
{typ: "h2", text: "Touren-Ideen"},
{level: 1, typ: "todo", text: "Adolf-Munkel-Weg unter den Geislerspitzen", checked: true},
{level: 1, typ: "todo", text: "Seceda mit der Seilbahn, oben Höhenweg"},
{level: 1, typ: "todo", text: "Regentag: Ötzi-Museum in Bozen #familie"},
{typ: "callout", text: "Packliste liegt unter Listen → Packliste Südtirol."},
}), []string{"urlaub"}, false)
_, _ = s.CreateDocument(user.ID, "", "Ideen-Sammlung", "💡", "", seedBlocks([]seedBlock{
{text: "Offsite-Backup verschlüsselt mit restic #backup"},
{level: 1, text: "Vorteil: Snapshots, Deduplizierung, läuft auf jedem Ziel"},
{text: "Familien-Wiki statt WhatsApp-Gruppe für Rezepte und Termine #familie"},
{text: "Wochenrückblick jeden Sonntag: Inbox leeren, Aufgaben für die Woche planen #routine"},
}), []string{"ideen"}, false)
return nil
}
// seedDemoShares shares part of the admin's sample data with the demo user, so both demo accounts can try
// out editing a note, a task list and a shopping list together.
func (s *Store) seedDemoShares(admin, member *domain.User) {
var docID, taskListID, listID string
_ = s.db.QueryRow(`SELECT id FROM documents WHERE user_id = ? AND title LIKE 'Homelab 2.0%'`, admin.ID).Scan(&docID)
_ = s.db.QueryRow(`SELECT id FROM task_lists WHERE user_id = ? AND title = 'Homelab'`, admin.ID).Scan(&taskListID)
_ = s.db.QueryRow(`SELECT id FROM lists WHERE user_id = ? AND title = 'Wocheneinkauf'`, admin.ID).Scan(&listID)
if docID != "" {
_, _ = s.CreateDocShare(admin.ID, docID, member.ID, "write")
}
if taskListID != "" {
_, _ = s.AddTaskListShare(taskListID, "user", member.ID, "editor", admin.ID)
}
if listID != "" {
_, _ = s.AddListShare(admin.ID, listID, "user", member.ID, "editor")
}
}