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
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:
1 parent
0ca715ba64
commit
b883315914
36 files changed
+1119
-563
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user