// task/store.go – JSON-Persistenz für Tasks (atomisches Schreiben) package task import ( "encoding/json" "fmt" "os" "sync" "time" "my-brain-importer/internal/config" ) // Task repräsentiert eine Aufgabe. type Task struct { ID string `json:"id"` Text string `json:"text"` Done bool `json:"done"` CreatedAt time.Time `json:"created_at"` DoneAt *time.Time `json:"done_at,omitempty"` DueDate *time.Time `json:"due_date,omitempty"` Priority string `json:"priority,omitempty"` // "hoch", "mittel", "niedrig" } // Store verwaltet tasks.json mit atomischen Schreiboperationen. type Store struct { mu sync.Mutex path string } // NewStore erstellt einen Store mit dem Pfad aus der Config. func NewStore() *Store { path := config.Cfg.Tasks.StorePath if path == "" { path = "./tasks.json" } return &Store{path: path} } // Load liest alle Tasks aus der JSON-Datei. func (s *Store) Load() ([]Task, error) { s.mu.Lock() defer s.mu.Unlock() data, err := os.ReadFile(s.path) if os.IsNotExist(err) { return []Task{}, nil } if err != nil { return nil, fmt.Errorf("tasks lesen: %w", err) } var tasks []Task if err := json.Unmarshal(data, &tasks); err != nil { return nil, fmt.Errorf("tasks parsen: %w", err) } return tasks, nil } // save schreibt alle Tasks atomisch. Muss unter mu aufgerufen werden. func (s *Store) save(tasks []Task) error { data, err := json.MarshalIndent(tasks, "", " ") if err != nil { return fmt.Errorf("tasks serialisieren: %w", err) } tmp := s.path + ".tmp" if err := os.WriteFile(tmp, data, 0644); err != nil { return fmt.Errorf("temp-datei schreiben: %w", err) } return os.Rename(tmp, s.path) } // Add fügt einen neuen Task hinzu. func (s *Store) Add(text, priority string, dueDate *time.Time) (Task, error) { s.mu.Lock() defer s.mu.Unlock() tasks, err := s.loadLocked() if err != nil { return Task{}, err } t := Task{ ID: fmt.Sprintf("%d", time.Now().UnixNano()), Text: text, Done: false, CreatedAt: time.Now(), Priority: priority, DueDate: dueDate, } tasks = append(tasks, t) if err := s.save(tasks); err != nil { return Task{}, err } return t, nil } // MarkDone markiert einen Task als erledigt. func (s *Store) MarkDone(id string) error { s.mu.Lock() defer s.mu.Unlock() tasks, err := s.loadLocked() if err != nil { return err } found := false now := time.Now() for i, t := range tasks { if t.ID == id { tasks[i].Done = true tasks[i].DoneAt = &now found = true break } } if !found { return fmt.Errorf("task %q nicht gefunden", id) } return s.save(tasks) } // Delete löscht einen Task. func (s *Store) Delete(id string) error { s.mu.Lock() defer s.mu.Unlock() tasks, err := s.loadLocked() if err != nil { return err } newTasks := tasks[:0] for _, t := range tasks { if t.ID != id { newTasks = append(newTasks, t) } } if len(newTasks) == len(tasks) { return fmt.Errorf("task %q nicht gefunden", id) } return s.save(newTasks) } // OpenTasks gibt alle offenen Tasks zurück. func (s *Store) OpenTasks() ([]Task, error) { tasks, err := s.Load() if err != nil { return nil, err } var open []Task for _, t := range tasks { if !t.Done { open = append(open, t) } } return open, nil } // loadLocked liest ohne eigenes Lock (muss unter s.mu aufgerufen werden). func (s *Store) loadLocked() ([]Task, error) { data, err := os.ReadFile(s.path) if os.IsNotExist(err) { return []Task{}, nil } if err != nil { return nil, fmt.Errorf("tasks lesen: %w", err) } var tasks []Task if err := json.Unmarshal(data, &tasks); err != nil { return nil, fmt.Errorf("tasks parsen: %w", err) } return tasks, nil }