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>
211 lines
11 KiB
Go
211 lines
11 KiB
Go
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, ¬es, &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")
|
||
}
|
||
}
|