feat: complete triage, multi-task lists, kanban, workspace wiki, and admin management
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 16s

This commit is contained in:
sandrodach committed 2026-10-24 22:08:43 +02:00
1 parent 5c4e130862
commit 4ef71b61cd
12 files changed
+3940 -664

No files matched your search

+453 -27
View File
@@ -1,11 +1,13 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"io/fs"
"net/http"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
@@ -25,12 +27,14 @@ type Server struct {
}
func NewServer(cfg *config.Config, store *storage.Store, wsHub *ws.Hub, staticFS fs.FS) *Server {
return &Server{
srv := &Server{
cfg: cfg,
store: store,
wsHub: wsHub,
staticFS: staticFS,
}
go srv.startDiscordAlarmTicker()
return srv
}
func (s *Server) Router() http.Handler {
@@ -60,6 +64,7 @@ func (s *Server) Router() http.Handler {
r.Use(s.authMiddleware)
r.Get("/api/v1/auth/me", s.handleMe)
r.Patch("/api/v1/auth/settings", s.handleUpdateUserSettings)
r.Get("/api/v1/stats", s.handleStats)
r.Get("/api/v1/search", s.handleSearch)
r.Get("/api/v1/export", s.handleExport)
@@ -76,6 +81,12 @@ func (s *Server) Router() http.Handler {
r.Patch("/api/v1/triage/{id}", s.handleTriageAction)
r.Get("/api/v1/tasks", s.handleGetTasks)
// Task Lists
r.Get("/api/v1/task-lists", s.handleGetTaskLists)
r.Post("/api/v1/task-lists", s.handleCreateTaskList)
r.Put("/api/v1/task-lists/{id}", s.handleUpdateTaskList)
r.Delete("/api/v1/task-lists/{id}", s.handleDeleteTaskList)
// Workspace Documents
r.Get("/api/v1/documents", s.handleGetDocuments)
r.Post("/api/v1/documents", s.handleCreateDocument)
@@ -97,10 +108,31 @@ func (s *Server) Router() http.Handler {
r.Delete("/api/v1/lists/{id}/items/{itemId}", s.handleDeleteListItem)
r.Post("/api/v1/lists/{id}/clear-completed", s.handleClearCompletedItems)
// Organizations & Teams
r.Get("/api/v1/organizations", s.handleGetOrganizations)
r.Post("/api/v1/organizations", s.handleCreateOrganization)
r.Get("/api/v1/organizations/{id}/teams", s.handleGetOrgTeams)
r.Post("/api/v1/organizations/{id}/teams", s.handleCreateOrgTeam)
r.Get("/api/v1/teams", s.handleGetUserTeams)
r.Post("/api/v1/teams/{id}/members", s.handleAddTeamMember)
r.Delete("/api/v1/teams/{id}/members/{userId}", s.handleRemoveTeamMember)
// API Keys Management
r.Get("/api/v1/keys", s.handleGetAPIKeys)
r.Post("/api/v1/keys", s.handleCreateAPIKey)
r.Delete("/api/v1/keys/{id}", s.handleDeleteAPIKey)
// Admin Panel Routes
r.Group(func(r chi.Router) {
r.Use(s.adminMiddleware)
r.Get("/api/v1/admin/users", s.handleAdminGetUsers)
r.Post("/api/v1/admin/users", s.handleAdminCreateUser)
r.Patch("/api/v1/admin/users/{id}", s.handleAdminUpdateUser)
r.Put("/api/v1/admin/users/{id}/password", s.handleAdminResetPassword)
r.Delete("/api/v1/admin/users/{id}", s.handleAdminDeleteUser)
r.Get("/api/v1/admin/settings", s.handleAdminGetSettings)
r.Put("/api/v1/admin/settings", s.handleAdminUpdateSettings)
})
})
// Embedded Frontend SPA Handler
@@ -186,6 +218,22 @@ func (s *Server) authMiddleware(next http.Handler) http.Handler {
})
}
func (s *Server) adminMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
if userID == "" {
writeError(w, http.StatusUnauthorized, "Nicht autorisiert")
return
}
user, err := s.store.GetUserByID(userID)
if err != nil || user == nil || user.Role != "admin" {
writeError(w, http.StatusForbidden, "Administrator-Rechte erforderlich")
return
}
next.ServeHTTP(w, r)
})
}
// --- HANDLERS ---
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
@@ -197,7 +245,8 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
if !s.cfg.AllowRegistration {
sysSettings, _ := s.store.GetSystemSettings()
if !s.cfg.AllowRegistration || (sysSettings != nil && !sysSettings.AllowRegistration) {
count, _ := s.store.CountUsers()
if count > 0 {
writeError(w, http.StatusForbidden, "Registrierung ist auf dieser Instanz deaktiviert")
@@ -228,7 +277,13 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
return
}
user, err := s.store.CreateUser(req.Email, hash, req.DisplayName)
count, _ := s.store.CountUsers()
role := "member"
if count == 0 {
role = "admin"
}
user, err := s.store.CreateUser(req.Email, hash, req.DisplayName, role)
if err != nil {
writeError(w, http.StatusConflict, "E-Mail-Adresse existiert bereits")
return
@@ -263,6 +318,10 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusUnauthorized, "E-Mail oder Passwort ungültig")
return
}
if !user.IsActive {
writeError(w, http.StatusForbidden, "Dieses Benutzerkonto ist deaktiviert")
return
}
token, err := auth.GenerateToken(s.cfg.JWTSecret, user.ID, user.Email, user.DisplayName)
if err != nil {
@@ -326,10 +385,12 @@ func (s *Server) handleCaptureInbox(w http.ResponseWriter, r *http.Request) {
Priority string `json:"priority"`
Source string `json:"source"`
DueDate string `json:"dueDate"`
DueDate2 string `json:"due_date"` // Compatibility with snake_case README example
Target string `json:"target"` // inbox | task | list_item | document
DueDate2 string `json:"due_date"`
Target string `json:"target"` // inbox | task | list_item | document
TargetListID string `json:"targetListId"`
TargetList2 string `json:"target_list_id"`
TaskListID string `json:"taskListId"`
TaskListID2 string `json:"task_list_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
@@ -343,8 +404,12 @@ func (s *Server) handleCaptureInbox(w http.ResponseWriter, r *http.Request) {
if targetListID == "" {
targetListID = req.TargetList2
}
taskListID := req.TaskListID
if taskListID == "" {
taskListID = req.TaskListID2
}
thought, err := s.store.CreateThought(userID, req.Content, req.Priority, req.Source, dueDate)
thought, err := s.store.CreateThoughtWithDetails(userID, req.Content, req.Priority, req.Source, dueDate, taskListID, "todo", "")
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
@@ -352,7 +417,7 @@ func (s *Server) handleCaptureInbox(w http.ResponseWriter, r *http.Request) {
target := strings.TrimSpace(req.Target)
if target == "task" || target == "document" || (target == "list_item" && targetListID != "") {
if converted, convErr := s.store.UpdateThoughtTriage(userID, thought.ID, "keep", target, targetListID, dueDate); convErr == nil && converted != nil {
if converted, convErr := s.store.UpdateThoughtTriage(userID, thought.ID, "keep", target, targetListID, dueDate, taskListID); convErr == nil && converted != nil {
thought = converted
if target == "list_item" && targetListID != "" {
if lst, lErr := s.store.GetListByID(targetListID); lErr == nil && lst != nil {
@@ -396,6 +461,8 @@ func (s *Server) handleTriageAction(w http.ResponseWriter, r *http.Request) {
ConvertTo2 string `json:"convert_to"`
TargetListID string `json:"targetListId"`
TargetList2 string `json:"target_list_id"`
TaskListID string `json:"taskListId"`
TaskListID2 string `json:"task_list_id"`
DueDate string `json:"dueDate"`
DueDate2 string `json:"due_date"`
}
@@ -412,12 +479,16 @@ func (s *Server) handleTriageAction(w http.ResponseWriter, r *http.Request) {
if targetListID == "" {
targetListID = req.TargetList2
}
taskListID := req.TaskListID
if taskListID == "" {
taskListID = req.TaskListID2
}
dueDate := req.DueDate
if dueDate == "" {
dueDate = req.DueDate2
}
updated, err := s.store.UpdateThoughtTriage(userID, thoughtID, req.Action, convertTo, targetListID, dueDate)
updated, err := s.store.UpdateThoughtTriage(userID, thoughtID, req.Action, convertTo, targetListID, dueDate, taskListID)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
@@ -435,7 +506,8 @@ func (s *Server) handleTriageAction(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleGetTasks(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
tasks, err := s.store.GetTasks(userID)
taskListID := r.URL.Query().Get("taskListId")
tasks, err := s.store.GetTasks(userID, taskListID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
@@ -448,17 +520,20 @@ func (s *Server) handleUpdateThought(w http.ResponseWriter, r *http.Request) {
thoughtID := chi.URLParam(r, "id")
var req struct {
Content *string `json:"content"`
IsCompleted *bool `json:"isCompleted"`
DueDate *string `json:"dueDate"`
Priority *string `json:"priority"`
Content *string `json:"content"`
IsCompleted *bool `json:"isCompleted"`
DueDate *string `json:"dueDate"`
Priority *string `json:"priority"`
TaskListID *string `json:"taskListId"`
KanbanStatus *string `json:"kanbanStatus"`
Notes *string `json:"notes"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
return
}
updated, err := s.store.UpdateThoughtDetails(userID, thoughtID, req.Content, req.IsCompleted, req.DueDate, req.Priority)
updated, err := s.store.UpdateThoughtDetails(userID, thoughtID, req.Content, req.IsCompleted, req.DueDate, req.Priority, req.TaskListID, req.KanbanStatus, req.Notes)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
@@ -501,19 +576,20 @@ func (s *Server) handleGetDocuments(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleCreateDocument(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
var req struct {
SpaceID string `json:"spaceId"`
Title string `json:"title"`
Icon string `json:"icon"`
Blocks []domain.DocumentBlock `json:"blocks"`
Tags []string `json:"tags"`
IsPinned bool `json:"isPinned"`
SpaceID string `json:"spaceId"`
Title string `json:"title"`
Icon string `json:"icon"`
FolderPath string `json:"folderPath"`
Blocks []domain.DocumentBlock `json:"blocks"`
Tags []string `json:"tags"`
IsPinned bool `json:"isPinned"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
return
}
doc, err := s.store.CreateDocument(userID, req.SpaceID, req.Title, req.Icon, req.Blocks, req.Tags, req.IsPinned)
doc, err := s.store.CreateDocument(userID, req.SpaceID, req.Title, req.Icon, req.FolderPath, req.Blocks, req.Tags, req.IsPinned)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
@@ -536,18 +612,19 @@ func (s *Server) handleUpdateDocument(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
docID := chi.URLParam(r, "id")
var req struct {
Title *string `json:"title"`
Icon *string `json:"icon"`
Blocks *[]domain.DocumentBlock `json:"blocks"`
Tags *[]string `json:"tags"`
IsPinned *bool `json:"isPinned"`
Title *string `json:"title"`
Icon *string `json:"icon"`
FolderPath *string `json:"folderPath"`
Blocks *[]domain.DocumentBlock `json:"blocks"`
Tags *[]string `json:"tags"`
IsPinned *bool `json:"isPinned"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
return
}
doc, err := s.store.UpdateDocument(userID, docID, req.Title, req.Icon, req.Blocks, req.Tags, req.IsPinned)
doc, err := s.store.UpdateDocument(userID, docID, req.Title, req.Icon, req.FolderPath, req.Blocks, req.Tags, req.IsPinned)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
@@ -906,3 +983,352 @@ func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Disposition", "attachment; filename=mindsift-export.json")
writeJSON(w, http.StatusOK, exportData)
}
// --- USER SETTINGS ---
func (s *Server) handleUpdateUserSettings(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
var req struct {
DisplayName *string `json:"displayName"`
Email *string `json:"email"`
Password *string `json:"password"`
DiscordWebhookURL *string `json:"discordWebhookUrl"`
NotifyBeforeMinutes *int `json:"notifyBeforeMinutes"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
return
}
if req.Password != nil && strings.TrimSpace(*req.Password) != "" {
hash, err := auth.HashPassword(*req.Password)
if err != nil {
writeError(w, http.StatusInternalServerError, "Fehler beim Hashen des Passworts")
return
}
_ = s.store.UpdateUserPassword(userID, hash)
}
u, err := s.store.UpdateUser(userID, req.DisplayName, req.Email, nil, nil, req.DiscordWebhookURL, req.NotifyBeforeMinutes)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, u)
}
// --- TASK LISTS ---
func (s *Server) handleGetTaskLists(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
lists, err := s.store.GetTaskLists(userID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, lists)
}
func (s *Server) handleCreateTaskList(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
var req struct {
SpaceID string `json:"spaceId"`
Title string `json:"title"`
Icon string `json:"icon"`
Color string `json:"color"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
return
}
tl, err := s.store.CreateTaskList(userID, req.SpaceID, req.Title, req.Icon, req.Color)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusCreated, tl)
}
func (s *Server) handleUpdateTaskList(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
listID := chi.URLParam(r, "id")
var req struct {
Title *string `json:"title"`
Icon *string `json:"icon"`
Color *string `json:"color"`
SpaceID *string `json:"spaceId"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
return
}
tl, err := s.store.UpdateTaskList(userID, listID, req.Title, req.Icon, req.Color, req.SpaceID)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, tl)
}
func (s *Server) handleDeleteTaskList(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
listID := chi.URLParam(r, "id")
if err := s.store.DeleteTaskList(userID, listID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
}
// --- ORGANIZATIONS & TEAMS ---
func (s *Server) handleGetOrganizations(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
orgs, err := s.store.GetOrganizationsForUser(userID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, orgs)
}
func (s *Server) handleCreateOrganization(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
var req struct {
Name string `json:"name"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
return
}
org, err := s.store.CreateOrganization(userID, req.Name)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusCreated, org)
}
func (s *Server) handleGetOrgTeams(w http.ResponseWriter, r *http.Request) {
orgID := chi.URLParam(r, "id")
teams, err := s.store.GetTeamsForOrg(orgID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, teams)
}
func (s *Server) handleCreateOrgTeam(w http.ResponseWriter, r *http.Request) {
orgID := chi.URLParam(r, "id")
var req struct {
Name string `json:"name"`
Description string `json:"description"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
return
}
team, err := s.store.CreateTeam(orgID, req.Name, req.Description)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusCreated, team)
}
func (s *Server) handleGetUserTeams(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
teams, err := s.store.GetTeamsForUser(userID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, teams)
}
func (s *Server) handleAddTeamMember(w http.ResponseWriter, r *http.Request) {
teamID := chi.URLParam(r, "id")
var req struct {
UserID string `json:"userId"`
Role string `json:"role"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
return
}
if err := s.store.AddTeamMember(teamID, req.UserID, req.Role); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]bool{"added": true})
}
func (s *Server) handleRemoveTeamMember(w http.ResponseWriter, r *http.Request) {
teamID := chi.URLParam(r, "id")
memberUserID := chi.URLParam(r, "userId")
if err := s.store.RemoveTeamMember(teamID, memberUserID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]bool{"removed": true})
}
// --- ADMIN PANEL ---
func (s *Server) handleAdminGetUsers(w http.ResponseWriter, r *http.Request) {
users, err := s.store.GetAllUsers()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, users)
}
func (s *Server) handleAdminCreateUser(w http.ResponseWriter, r *http.Request) {
var req struct {
Email string `json:"email"`
Password string `json:"password"`
DisplayName string `json:"displayName"`
Role string `json:"role"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
return
}
if req.Email == "" || req.Password == "" {
writeError(w, http.StatusBadRequest, "E-Mail und Passwort erforderlich")
return
}
hash, err := auth.HashPassword(req.Password)
if err != nil {
writeError(w, http.StatusInternalServerError, "Passwort-Hash fehlgeschlagen")
return
}
u, err := s.store.CreateUser(req.Email, hash, req.DisplayName, req.Role)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusCreated, u)
}
func (s *Server) handleAdminUpdateUser(w http.ResponseWriter, r *http.Request) {
targetUserID := chi.URLParam(r, "id")
var req struct {
DisplayName *string `json:"displayName"`
Email *string `json:"email"`
Role *string `json:"role"`
IsActive *bool `json:"isActive"`
DiscordWebhookURL *string `json:"discordWebhookUrl"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
return
}
u, err := s.store.UpdateUser(targetUserID, req.DisplayName, req.Email, req.Role, req.IsActive, req.DiscordWebhookURL, nil)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, u)
}
func (s *Server) handleAdminResetPassword(w http.ResponseWriter, r *http.Request) {
targetUserID := chi.URLParam(r, "id")
var req struct {
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.Password) == "" {
writeError(w, http.StatusBadRequest, "Neues Passwort erforderlich")
return
}
hash, err := auth.HashPassword(req.Password)
if err != nil {
writeError(w, http.StatusInternalServerError, "Passwort-Hash fehlgeschlagen")
return
}
if err := s.store.UpdateUserPassword(targetUserID, hash); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]bool{"success": true})
}
func (s *Server) handleAdminDeleteUser(w http.ResponseWriter, r *http.Request) {
targetUserID := chi.URLParam(r, "id")
currUserID := auth.GetUserID(r.Context())
if targetUserID == currUserID {
writeError(w, http.StatusBadRequest, "Sie können Ihren eigenen Benutzer nicht löschen")
return
}
if err := s.store.DeleteUser(targetUserID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
}
func (s *Server) handleAdminGetSettings(w http.ResponseWriter, r *http.Request) {
settings, err := s.store.GetSystemSettings()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, settings)
}
func (s *Server) handleAdminUpdateSettings(w http.ResponseWriter, r *http.Request) {
var settings domain.SystemSettings
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
return
}
if err := s.store.UpdateSystemSettings(&settings); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, settings)
}
// --- DISCORD ALARMS BACKGROUND SCHEDULER ---
func (s *Server) startDiscordAlarmTicker() {
ticker := time.NewTicker(60 * time.Second)
defer ticker.Stop()
httpClient := &http.Client{Timeout: 10 * time.Second}
for range ticker.C {
alarms, err := s.store.GetDueTasksForAlarm()
if err != nil || len(alarms) == 0 {
continue
}
today := time.Now().Format("2006-01-02")
for _, a := range alarms {
if a.DueDate <= today && a.DiscordWebhookURL != "" {
payload := map[string]interface{}{
"content": fmt.Sprintf("⏰ **MindSift Aufgaben-Erinnerung**"),
"embeds": []map[string]interface{}{
{
"title": a.Title,
"description": fmt.Sprintf("Diese Aufgabe ist fällig am **%s** (Priorität: %s)", a.DueDate, a.Priority),
"color": 3066993,
"footer": map[string]string{
"text": "MindSift Reminder",
},
},
},
}
body, mErr := json.Marshal(payload)
if mErr == nil {
resp, pErr := httpClient.Post(a.DiscordWebhookURL, "application/json", bytes.NewReader(body))
if pErr == nil {
_ = resp.Body.Close()
_ = s.store.MarkTaskAlarmSent(a.TaskID)
}
}
}
}
}
}