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>
107 lines
4.4 KiB
Go
107 lines
4.4 KiB
Go
package storage
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/sandrodach/mindsift/internal/domain"
|
|
)
|
|
|
|
// Origin of tasks, thoughts and list items: which API key created an entry and which one changed it
|
|
// last, and over which access (REST "api" or "mcp"). Both stay for good, also after the user edits the
|
|
// entry. Key names are resolved when reading, so renaming a key shows everywhere at once.
|
|
|
|
const (
|
|
OriginAPI = "api" // REST: /inbox, /webhooks/ingest and the other endpoints a key may use
|
|
OriginMCP = "mcp"
|
|
)
|
|
|
|
// Tables with origin columns; RecordOrigin only takes these
|
|
const (
|
|
OriginTableThoughts = "thoughts"
|
|
OriginTableListItems = "list_items"
|
|
)
|
|
|
|
// migrateOrigin replaces agent_mark (migration 12, which the user's own edit cleared) with the
|
|
// permanent origin. Old marks and the sources webhook/agent are carried over without a key.
|
|
func (s *Store) migrateOrigin() error {
|
|
for _, table := range []string{OriginTableThoughts, OriginTableListItems} {
|
|
for _, col := range []string{"created_via", "created_key_id", "changed_via", "changed_key_id", "changed_at"} {
|
|
if err := s.addColumnIfMissing(table, col, "TEXT NOT NULL DEFAULT ''"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return s.execInTx(
|
|
stmt(`UPDATE thoughts SET created_via = 'api' WHERE created_via = '' AND source = 'webhook'`),
|
|
stmt(`UPDATE thoughts SET created_via = 'mcp' WHERE created_via = '' AND source = 'agent'`),
|
|
stmt(`UPDATE thoughts SET changed_via = 'mcp' WHERE changed_via = '' AND agent_mark = 'edited'`),
|
|
stmt(`UPDATE list_items SET created_via = 'mcp' WHERE created_via = '' AND agent_mark = 'created'`),
|
|
stmt(`UPDATE list_items SET changed_via = 'mcp' WHERE changed_via = '' AND agent_mark = 'edited'`),
|
|
)
|
|
}
|
|
|
|
// originColumns: the five origin values of a SELECT, key ids resolved to names (prefix e.g. "li." for
|
|
// an aliased table). Scan them with scanOrigin.
|
|
func originColumns(prefix string) string {
|
|
keyName := func(col string) string {
|
|
return `CASE WHEN ` + prefix + col + ` = '' THEN '' ELSE COALESCE((SELECT k.name FROM api_keys k WHERE k.id = ` +
|
|
prefix + col + `), 'Gelöschter Schlüssel') END`
|
|
}
|
|
return prefix + `created_via, ` + keyName("created_key_id") + `, ` + prefix + `changed_via, ` + keyName("changed_key_id") + `, ` + prefix + `changed_at`
|
|
}
|
|
|
|
// originDest holds the scanned origin columns; result is nil for entries no key ever touched.
|
|
type originDest struct{ createdVia, createdKey, changedVia, changedKey, changedAt string }
|
|
|
|
func (o *originDest) targets() []any {
|
|
return []any{&o.createdVia, &o.createdKey, &o.changedVia, &o.changedKey, &o.changedAt}
|
|
}
|
|
|
|
func (o *originDest) result() *domain.Origin {
|
|
if o.createdVia == "" && o.changedVia == "" {
|
|
return nil
|
|
}
|
|
return &domain.Origin{CreatedVia: o.createdVia, CreatedKey: o.createdKey, ChangedVia: o.changedVia, ChangedKey: o.changedKey, ChangedAt: o.changedAt}
|
|
}
|
|
|
|
// RecordOrigin notes that an API key created (created) or changed an entry and returns the entry's
|
|
// origin as it is now. via is OriginAPI or OriginMCP; keyID may be empty (MCP from a login session).
|
|
func (s *Store) RecordOrigin(table, id, via, keyID string, created bool) *domain.Origin {
|
|
if table != OriginTableThoughts && table != OriginTableListItems {
|
|
return nil
|
|
}
|
|
if created {
|
|
_, _ = s.db.Exec(`UPDATE `+table+` SET created_via = ?, created_key_id = ? WHERE id = ?`, via, keyID, id)
|
|
} else {
|
|
_, _ = s.db.Exec(`UPDATE `+table+` SET changed_via = ?, changed_key_id = ?, changed_at = ? WHERE id = ?`, via, keyID, domain.NowISO(), id)
|
|
}
|
|
return s.Origin(table, id)
|
|
}
|
|
|
|
// Origin reads the origin of one entry (nil if no key touched it).
|
|
func (s *Store) Origin(table, id string) *domain.Origin {
|
|
if table != OriginTableThoughts && table != OriginTableListItems {
|
|
return nil
|
|
}
|
|
var o originDest
|
|
if err := s.db.QueryRow(`SELECT `+originColumns("")+` FROM `+table+` WHERE id = ?`, id).Scan(o.targets()...); err != nil {
|
|
return nil
|
|
}
|
|
return o.result()
|
|
}
|
|
|
|
// copyOriginToListItem: a thought turned into a list item in the triage keeps where it came from.
|
|
func (s *Store) copyOriginToListItem(thoughtID, itemID string) {
|
|
cols := []string{"created_via", "created_key_id", "changed_via", "changed_key_id", "changed_at"}
|
|
set := make([]string, len(cols))
|
|
for i, c := range cols {
|
|
set[i] = c + ` = (SELECT ` + c + ` FROM thoughts WHERE id = ?)`
|
|
}
|
|
args := []any{}
|
|
for range cols {
|
|
args = append(args, thoughtID)
|
|
}
|
|
args = append(args, itemID)
|
|
_, _ = s.db.Exec(`UPDATE list_items SET `+strings.Join(set, ", ")+` WHERE id = ?`, args...)
|
|
}
|