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

+18 -18
View File
@@ -1,7 +1,6 @@
package api
import (
"context"
"encoding/json"
"errors"
"fmt"
@@ -70,14 +69,15 @@ func (s *Server) handleWebhookIngest(w http.ResponseWriter, r *http.Request) {
}
// Same checks as every other request (revoked/outdated sessions, inactive accounts)
user, keyScopes := s.authenticateToken(token)
user, key := s.authenticateToken(token)
if user == nil {
writeError(w, http.StatusUnauthorized, "Ungültiger Webhook-Token oder API-Key")
return
}
if keyScopes != nil {
r = r.WithContext(context.WithValue(r.Context(), apiKeyScopesKey{}, keyScopes))
if key != nil {
r = r.WithContext(withAPIKey(r.Context(), key, storage.OriginAPI))
}
actor := actorFrom(r.Context())
var req struct {
Title string `json:"title"`
@@ -163,11 +163,11 @@ func (s *Server) handleWebhookIngest(w http.ResponseWriter, r *http.Request) {
}
if target == "list_item" {
s.ingestListItem(w, user.ID, req.ListID, bodyText, req.Category, req.Quantity)
s.ingestListItem(w, user.ID, req.ListID, bodyText, req.Category, req.Quantity, actor)
return
}
if target == "task" {
s.ingestTask(w, user.ID, bodyText, req.Priority, source, dueDate, req.TaskListID)
s.ingestTask(w, user.ID, bodyText, req.Priority, source, dueDate, req.TaskListID, actor)
return
}
@@ -179,6 +179,7 @@ func (s *Server) handleWebhookIngest(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
s.recordThoughtOrigin(thought, actor, true)
s.wsHub.BroadcastToUsers(s.taskAudience(thought), "TASK_CREATED", thought)
@@ -188,9 +189,9 @@ func (s *Server) handleWebhookIngest(w http.ResponseWriter, r *http.Request) {
})
}
// createTask creates a task directly (webhook scope webhook:task, MCP tool create_task), optionally in
// a task list the user may write to (otherwise storage.ErrTaskListReadOnly).
func (s *Server) createTask(userID, content, priority, source, dueDate, taskListID, notes string) (*domain.Thought, error) {
// createTask creates a task directly (POST /webhooks/ingest, MCP tool create_task), optionally in a
// task list the user may write to (otherwise storage.ErrTaskListReadOnly). actor: the API key, if any.
func (s *Server) createTask(userID, content, priority, source, dueDate, taskListID, notes string, actor *apiActor) (*domain.Thought, error) {
if priority == "" {
priority = "normal"
}
@@ -205,12 +206,13 @@ func (s *Server) createTask(userID, content, priority, source, dueDate, taskList
if err != nil {
return nil, err
}
s.recordThoughtOrigin(task, actor, true)
s.wsHub.BroadcastToUsers(s.taskAudience(task), "TASK_CREATED", task)
return task, nil
}
func (s *Server) ingestTask(w http.ResponseWriter, userID, content, priority, source, dueDate, taskListID string) {
task, err := s.createTask(userID, content, priority, source, dueDate, taskListID, "")
func (s *Server) ingestTask(w http.ResponseWriter, userID, content, priority, source, dueDate, taskListID string, actor *apiActor) {
task, err := s.createTask(userID, content, priority, source, dueDate, taskListID, "", actor)
if errors.Is(err, storage.ErrTaskListReadOnly) {
writeError(w, http.StatusForbidden, err.Error())
return
@@ -226,7 +228,7 @@ var errListIDRequired = errors.New("Feld 'listId' ist für Listeneinträge erfor
// addListItem appends an item to a list the user may write to; the error is storage.ErrListReadOnly for
// viewers and "not found" for lists the user cannot see.
func (s *Server) addListItem(userID, listID, content, category, quantity string, byAgent bool) (*domain.ListItem, error) {
func (s *Server) addListItem(userID, listID, content, category, quantity string, actor *apiActor) (*domain.ListItem, error) {
if strings.TrimSpace(listID) == "" {
return nil, errListIDRequired
}
@@ -237,16 +239,14 @@ func (s *Server) addListItem(userID, listID, content, category, quantity string,
if err != nil {
return nil, err
}
if byAgent {
item.AgentMark = s.store.MarkListItemAgentCreated(item.ID)
}
s.recordListItemOrigin(item, actor, true)
s.wsHub.BroadcastListEvent(listID, "ITEM_CREATED", item)
return item, nil
}
// ingestListItem adds an item to a list (scope webhook:list) the owner of the key may write to.
func (s *Server) ingestListItem(w http.ResponseWriter, userID, listID, content, category, quantity string) {
item, err := s.addListItem(userID, listID, content, category, quantity, false)
// ingestListItem adds an item to a list the owner of the key may write to.
func (s *Server) ingestListItem(w http.ResponseWriter, userID, listID, content, category, quantity string, actor *apiActor) {
item, err := s.addListItem(userID, listID, content, category, quantity, actor)
if err != nil {
status := http.StatusNotFound
switch {