feat(api): one API for curl and MCP, REST reads with keys, origin badge with key name
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m16s

API keys reach tasks and lists over plain REST too (GET /task-lists, GET /tasks with
status/due filters, PATCH/DELETE /thoughts/{id}, GET /lists, list items), guarded by a route
table; notes and settings stay closed. Rights hold for every access alike (webhook:task also
allows create_task over MCP, tasks:write also task delivery).

Tasks, thoughts and list items record which key created them and which one changed them last
(migration 13 replaces agent_mark; origin stays after the user's own edits and survives the
triage into a list item). OriginBadge shows the key name with a webhook or robot icon, details
on hover and tap, and replaces the Webhook and KI badges everywhere, now also on list items.

Settings: category "API" with rights grouped by tasks, lists and inbox and two tabs
(curl / smart home with a Home Assistant sensor, AI agents over MCP). Docs, help, README and
changelog updated; v0.30.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-11-01 10:26:44 +01:00
1 parent 0ca715ba64
commit b883315914
36 files changed
+1119 -563

No files matched your search

+55 -5
View File
@@ -2,8 +2,10 @@ package api
import (
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/sandrodach/mindsift/internal/auth"
@@ -69,6 +71,8 @@ func (s *Server) handleCaptureInbox(w http.ResponseWriter, r *http.Request) {
writeStoreError(w, http.StatusBadRequest, err)
return
}
// Before a conversion, so a list item made from it takes the origin along
s.recordThoughtOrigin(thought, actorFrom(r.Context()), true)
target := strings.TrimSpace(req.Target)
if target == "task" || target == "document" || (target == "list_item" && targetListID != "") {
@@ -175,15 +179,60 @@ 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())
taskListID := r.URL.Query().Get("taskListId")
tasks, err := s.store.GetTasks(userID, taskListID)
q := r.URL.Query()
tasks, err := s.store.GetTasks(userID, q.Get("taskListId"))
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
tasks, err = filterTasks(tasks, q.Get("status"), q.Get("due"), time.Now())
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, tasks)
}
// filterTasks narrows GET /tasks for scripts and smart homes; without parameters nothing changes.
// status: open | done | all (default). due: today | overdue | YYYY-MM-DD (due on or before that day).
// Days count in the server's time zone like due dates themselves (now is passed in for tests).
func filterTasks(tasks []domain.Thought, status, due string, now time.Time) ([]domain.Thought, error) {
switch status {
case "", "all", "open", "done":
default:
return nil, errors.New("status: open, done oder all")
}
today := now.In(time.Local).Format("2006-01-02")
var match func(day string) bool
switch {
case due == "":
case due == "today":
match = func(day string) bool { return day == today }
case due == "overdue":
match = func(day string) bool { return day < today }
default:
if _, err := time.Parse("2006-01-02", due); err != nil {
return nil, errors.New("due: today, overdue oder ein Datum JJJJ-MM-TT")
}
match = func(day string) bool { return day <= due }
}
out := make([]domain.Thought, 0, len(tasks))
for _, t := range tasks {
if (status == "open" && t.IsCompleted) || (status == "done" && !t.IsCompleted) {
continue
}
// Overdue only makes sense for open tasks
if due == "overdue" && t.IsCompleted {
continue
}
if match != nil && (t.DueDate == "" || !match(t.DueDate[:min(10, len(t.DueDate))])) {
continue
}
out = append(out, t)
}
return out, nil
}
func (s *Server) handleReorderTasks(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
taskListID := chi.URLParam(r, "id")
@@ -245,8 +294,8 @@ type taskPatch struct {
Tags *[]string `json:"tags"`
Assignees *[]string `json:"assignees"`
Recurrence *string `json:"recurrence"`
// byAgent: the change comes from an AI agent (MCP) and marks the task; otherwise the mark goes
byAgent bool
// actor: the API key making the change (recorded as the task's origin); nil for the app
actor *apiActor
}
// updateTask applies a patch, keeps a linked note block in step, tells everyone who sees the task
@@ -263,7 +312,7 @@ func (s *Server) updateTask(userID, thoughtID string, req taskPatch) (*domain.Th
if err != nil {
return nil, err
}
updated.AgentMark = s.store.SetThoughtAgentMark(thoughtID, updated.AgentMark, req.byAgent)
s.recordThoughtOrigin(updated, req.actor, false)
if req.Assignees != nil {
assignees, err := s.store.SetTaskAssignees(userID, thoughtID, *req.Assignees)
if err != nil {
@@ -290,6 +339,7 @@ func (s *Server) handleUpdateThought(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
return
}
req.actor = actorFrom(r.Context())
updated, err := s.updateTask(userID, thoughtID, req)
if err != nil {
writeStoreError(w, http.StatusBadRequest, err)