Files
MindSift/internal/api/done_cleanup.go
T
sandrodachandClaude Opus 5.5 5a9c5bac37
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m39s
feat: task trash, triage trash and retention settings for every area; admin can forbid keeping things for good
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 15:44:34 +01:00

131 lines
4.5 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 api
import (
"encoding/json"
"errors"
"log/slog"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/sandrodach/mindsift/internal/auth"
"github.com/sandrodach/mindsift/internal/storage"
)
// startDoneCleanupTicker runs the retention delays (storage/retention.go): completed tasks into the
// task trash, the trashes and ticked-off list items emptied; once shortly after the start and then
// every hour.
func (s *Server) startDoneCleanupTicker() {
time.Sleep(2 * time.Minute)
s.runDoneCleanup(time.Now())
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
for now := range ticker.C {
s.runDoneCleanup(now)
}
}
// allowNeverRetention: RETENTION_ALLOW_NEVER if set, else the admin's switch in the system settings.
func (s *Server) allowNeverRetention() bool {
if s.cfg.RetentionAllowNever != nil {
return *s.cfg.RetentionAllowNever
}
return s.store.RetentionAllowNeverSetting()
}
func (s *Server) runDoneCleanup(now time.Time) {
res, err := s.store.PurgeExpired(now, s.allowNeverRetention())
if err != nil {
slog.Error("Aufräumen nach Aufbewahrungsfristen fehlgeschlagen", "error", err)
}
if n := len(res.TasksToTrash) + len(res.TrashedTasks) + res.Discarded + len(res.Notes) + len(res.ListItems); n > 0 {
slog.Info("Aufbewahrungsfristen angewendet", "erledigte_in_papierkorb", len(res.TasksToTrash), "papierkorb_aufgaben", len(res.TrashedTasks),
"triage", res.Discarded, "notizen", len(res.Notes), "listeneintraege", len(res.ListItems))
}
s.broadcastDeletedTasks(res.TasksToTrash)
for _, it := range res.ListItems {
s.wsHub.BroadcastListEvent(it.ListID, "ITEM_DELETED", map[string]string{"itemId": it.ID, "listId": it.ListID})
}
}
// broadcastDeletedTasks tells everyone who saw the tasks; there are no watcher notifications for this.
func (s *Server) broadcastDeletedTasks(deleted []storage.DeletedTask) {
for _, d := range deleted {
audience := append(s.store.GetTaskListUserIDs(d.TaskListID), d.UserID)
s.wsHub.BroadcastToUsers(audience, "TASK_DELETED", map[string]string{"id": d.ID})
}
}
// handleClearDoneTasks: "Erledigte leeren" (into the task trash) for one task list (taskListId) or all
// the user can edit.
func (s *Server) handleClearDoneTasks(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
var req struct {
TaskListID string `json:"taskListId"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
deleted, err := s.store.ClearDoneTasks(userID, req.TaskListID)
if err != nil {
writeStoreError(w, http.StatusBadRequest, err)
return
}
s.broadcastDeletedTasks(deleted)
writeJSON(w, http.StatusOK, map[string]int{"deleted": len(deleted)})
}
// --- Task trash (storage/task_trash.go) ---
// handleGetTaskTrash: GET /tasks/trash?taskListId= – the trashed tasks the user can see.
func (s *Server) handleGetTaskTrash(w http.ResponseWriter, r *http.Request) {
items, err := s.store.GetTaskTrash(auth.GetUserID(r.Context()), r.URL.Query().Get("taskListId"))
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, items)
}
// handleRestoreTrashedTask puts a task back and tells everyone who sees it.
func (s *Server) handleRestoreTrashedTask(w http.ResponseWriter, r *http.Request) {
task, err := s.store.RestoreTrashedTask(auth.GetUserID(r.Context()), chi.URLParam(r, "id"))
if err != nil {
writeTaskTrashError(w, err)
return
}
s.wsHub.BroadcastToUsers(s.taskAudience(task), "TASK_CREATED", task)
writeJSON(w, http.StatusOK, task)
}
func (s *Server) handlePurgeTrashedTask(w http.ResponseWriter, r *http.Request) {
if err := s.store.PurgeTrashedTask(auth.GetUserID(r.Context()), chi.URLParam(r, "id")); err != nil {
writeTaskTrashError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
}
// handleEmptyTaskTrash: POST /tasks/trash/empty {taskListId} – one list or all the user may edit.
func (s *Server) handleEmptyTaskTrash(w http.ResponseWriter, r *http.Request) {
var req struct {
TaskListID string `json:"taskListId"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
ids, err := s.store.EmptyTaskTrash(auth.GetUserID(r.Context()), req.TaskListID)
if err != nil {
writeTaskTrashError(w, err)
return
}
writeJSON(w, http.StatusOK, map[string]int{"deleted": len(ids)})
}
func writeTaskTrashError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, storage.ErrNotInTaskTrash):
writeError(w, http.StatusNotFound, err.Error())
case errors.Is(err, storage.ErrTaskListReadOnly):
writeError(w, http.StatusForbidden, err.Error())
default:
writeError(w, http.StatusInternalServerError, err.Error())
}
}