release: v0.2 – collaboration docs, richer demo data, new screenshots
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m30s

- 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>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-28 15:32:31 +01:00
1 parent f26f286fb8
commit 4e39b367bd
25 files changed
+590 -373

No files matched your search

+210
View File
@@ -0,0 +1,210 @@
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")
}
}