Files
MindSift/internal/storage/seed.go
T
sandrodachandClaude Opus 5.5 4e39b367bd
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m30s
release: v0.2 – collaboration docs, richer demo data, new screenshots
- Turn the collaboration plan into module docs (Doku/modules/collaboration.md)
  describing the implemented state; remove Doku/plans
- Header version comes from the build: git tag via APP_VERSION (CI passes
  it for tag builds), otherwise "<package version>-dev"; package 0.2.0
- Sample data (setup option and demo mode): six inbox thoughts, two task
  lists with Kanban states, subtasks and due dates, shopping and packing
  lists, three notes in folders incl. a long nested outline; demo mode
  shares the note and two lists with user/user
- e2e/screenshots.sh renews the README screenshots from demo data
- README: live collaboration, editor sharing, image tags, version badge

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 15:32:31 +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)
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)
}
}
_, _ = 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")
}
}