MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m39s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
131 lines
4.5 KiB
Go
131 lines
4.5 KiB
Go
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())
|
||
}
|
||
}
|