import React, { useEffect, useState } from 'react'; import { Key, Plus, Trash2, Pencil, Copy, Check, Webhook, Zap, Terminal, Bot, } from 'lucide-react'; import { APIKey, ApiKeyScope, PriorityLevel, SharedList, TaskList } from '../../lib/types'; import { api, triggerHaptic } from '../../lib/api'; import { ToggleSwitch } from '../../components/ToggleSwitch'; interface ApiKeysPanelProps { onIngested?: () => void; } // What a key may do. The rights hold for every access alike: curl/REST and MCP for AI agents. The ids of // the delivering rights (webhook:…) stay from the time keys could only deliver data. type ScopeGroup = 'tasks' | 'lists' | 'inbox'; const SCOPES: { id: ApiKeyScope; label: string; chip: string; hint: string; group: ScopeGroup }[] = [ { id: 'tasks:read', label: 'Lesen', chip: 'Aufgaben lesen', hint: 'Aufgabenlisten und Aufgaben lesen, suchen', group: 'tasks' }, { id: 'webhook:task', label: 'Anlegen', chip: 'Aufgaben anlegen', hint: 'Nur neue Aufgaben anlegen', group: 'tasks' }, { id: 'tasks:write', label: 'Ändern', chip: 'Aufgaben ändern', hint: 'Anlegen, ändern, abhaken und löschen', group: 'tasks' }, { id: 'lists:read', label: 'Lesen', chip: 'Listen lesen', hint: 'Listen und ihre Einträge lesen', group: 'lists' }, { id: 'webhook:list', label: 'Einträge hinzufügen', chip: 'Listeneinträge hinzufügen', hint: 'Nur neue Einträge hinzufügen', group: 'lists' }, { id: 'lists:write', label: 'Ändern', chip: 'Listen ändern', hint: 'Einträge hinzufügen, ändern und abhaken', group: 'lists' }, { id: 'webhook:triage', label: 'Inbox füllen', chip: 'Inbox', hint: 'Gedanken in die Inbox einliefern', group: 'inbox' }, ]; const SCOPE_GROUPS: [ScopeGroup, string][] = [ ['tasks', '📋 Aufgaben'], ['lists', '🛒 Listen'], ['inbox', '📥 Inbox'], ]; const scopeChip = (id: string) => SCOPES.find((s) => s.id === id)?.chip || id; type SimTarget = 'triage' | 'task' | 'list_item'; type McpClient = 'claude' | 'codex' | 'json'; type Access = 'rest' | 'mcp'; // Personal API keys with rights (switches) and how to connect: curl / smart home (read examples, // delivery simulator) or AI agents over MCP (settings → API) export const ApiKeysPanel: React.FC = ({ onIngested }) => { const [keys, setKeys] = useState([]); const [newKeyName, setNewKeyName] = useState(''); const [newKeyScopes, setNewKeyScopes] = useState(['webhook:triage']); const [newlyCreatedKey, setNewlyCreatedKey] = useState(null); const [copiedKey, setCopiedKey] = useState(false); const [mcpClient, setMcpClient] = useState('claude'); const [access, setAccess] = useState('rest'); // Live Webhook Simulator State const [simTitle, setSimTitle] = useState('Waschmaschine im Keller ist fertig!'); const [simPriority, setSimPriority] = useState('high'); const [simTarget, setSimTarget] = useState('triage'); const [simListId, setSimListId] = useState(''); const [simTaskListId, setSimTaskListId] = useState(''); const [simStatus, setSimStatus] = useState(null); const [lists, setLists] = useState([]); const [taskLists, setTaskLists] = useState([]); const loadKeys = async () => { const k = await api.getAPIKeys(); setKeys(k); }; useEffect(() => { loadKeys(); api.getAllLists().then(setLists).catch(() => {}); api.getTaskLists().then(setTaskLists).catch(() => {}); }, []); // Editing an existing key: name and rights (the secret stays the same) const [editKeyId, setEditKeyId] = useState(null); const [editName, setEditName] = useState(''); const [editScopes, setEditScopes] = useState([]); const [editError, setEditError] = useState(null); const startEdit = (k: APIKey) => { setEditKeyId(k.id); setEditName(k.name); setEditScopes(k.scopes.split(',').filter(Boolean) as ApiKeyScope[]); setEditError(null); }; const toggleEditScope = (id: ApiKeyScope) => { setEditScopes((prev) => { const next = prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id]; return next.length > 0 ? next : prev; // at least one }); }; const handleSaveEdit = async () => { if (!editKeyId) return; try { await api.updateAPIKey(editKeyId, { name: editName.trim(), scopes: editScopes }); triggerHaptic(15); setEditKeyId(null); await loadKeys(); } catch (err: any) { setEditError(err?.message || 'Speichern fehlgeschlagen'); } }; // Switches grouped by tasks, lists and inbox (same layout for a new and an existing key) const renderScopeSwitches = (selected: ApiKeyScope[], toggle: (id: ApiKeyScope) => void, label: string) => (
{SCOPE_GROUPS.map(([group, heading]) => (

{heading}

{SCOPES.filter((sc) => sc.group === group).map((sc) => (
{sc.label} {sc.hint}
toggle(sc.id)} title={sc.chip} />
))}
))}
); const toggleScope = (id: ApiKeyScope) => { setNewKeyScopes((prev) => { const next = prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id]; return next.length > 0 ? next : prev; // at least one }); }; const handleCreateKey = async (e: React.FormEvent) => { e.preventDefault(); triggerHaptic(15); const created = await api.createAPIKey(newKeyName.trim() || 'Home Assistant', newKeyScopes); if (created.rawKey) { setNewlyCreatedKey(created.rawKey); } setNewKeyName(''); setNewKeyScopes(['webhook:triage']); await loadKeys(); }; const handleDeleteKey = async (id: string) => { if (!window.confirm('Diesen API-Schlüssel wirklich widerrufen?')) return; triggerHaptic(20); await api.deleteAPIKey(id); await loadKeys(); }; const handleCopy = (text: string) => { navigator.clipboard.writeText(text); setCopiedKey(true); setTimeout(() => setCopiedKey(false), 2000); }; const handleSimulateWebhook = async () => { triggerHaptic(20); if (simTarget === 'list_item' && !simListId) { setSimStatus('❌ Bitte eine Liste wählen.'); return; } try { // With a freshly created key the simulator tests exactly that key (and its scopes) await api.testWebhookIngest(newlyCreatedKey, { title: simTitle, priority: simPriority, tags: simTarget === 'list_item' ? [] : ['smarthome', 'automation'], target: simTarget, taskListId: simTarget === 'task' ? simTaskListId || undefined : undefined, listId: simTarget === 'list_item' ? simListId : undefined, }); setSimStatus( simTarget === 'task' ? '✅ Webhook erfolgreich ausgelöst! Die Aufgabe ist angelegt.' : simTarget === 'list_item' ? '✅ Webhook erfolgreich ausgelöst! Der Eintrag steht in der Liste.' : '✅ Webhook erfolgreich ausgelöst! Neue Karte liegt in deiner Inbox.' ); onIngested?.(); setTimeout(() => setSimStatus(null), 4000); } catch (err: any) { setSimStatus(`❌ Fehler: ${err.message}`); } }; const exampleToken = newlyCreatedKey || 'mindsift_sec_dein_api_key'; const baseUrl = window.location.origin; const examplePayload = simTarget === 'task' ? `{ "target": "task", "title": "Filter der Lüftungsanlage wechseln", "priority": "high", "dueDate": "2026-11-15T09:00"${simTaskListId ? `,\n "taskListId": "${simTaskListId}"` : ''} }` : simTarget === 'list_item' ? `{ "target": "list_item", "listId": "${simListId || 'ID_DER_LISTE'}", "content": "Hafermilch", "quantity": "2" }` : `{ "title": "Kellerfeuchtigkeit über 70%", "priority": "urgent", "tags": ["smarthome", "keller"] }`; const mcpUrl = `${baseUrl}/api/v1/mcp`; const mcpSnippet = mcpClient === 'claude' ? `claude mcp add --transport http mindsift ${mcpUrl} \\ --header "X-API-Key: ${exampleToken}"` : mcpClient === 'codex' ? `# ~/.codex/config.toml [mcp_servers.mindsift] url = "${mcpUrl}" http_headers = { "X-API-Key" = "${exampleToken}" }` : `{ "mcpServers": { "mindsift": { "type": "http", "url": "${mcpUrl}", "headers": { "X-API-Key": "${exampleToken}" } } } }`; return (
{/* API keys */}

API-Schlüssel

Ein Schlüssel verbindet Home Assistant, n8n, eigene Skripte oder KI-Agenten (Claude Code, Codex …) mit MindSift. Was er darf, legst du mit den Schaltern fest; das gilt per curl und per MCP gleich. Notizen und Einstellungen bleiben für Schlüssel gesperrt. Was ein Schlüssel anlegt oder ändert, trägt seinen Namen als Badge.

setNewKeyName(e.target.value)} placeholder="Bezeichnung (z. B. Home Assistant, Proxmox Backup, Rauchmelder)…" className="w-full sm:flex-1 min-w-0 h-11 sm:h-9 px-3 input-field text-base sm:text-sm" />
{renderScopeSwitches(newKeyScopes, toggleScope, 'Berechtigungen des Schlüssels')}
{newlyCreatedKey && (

Neuer API-Schlüssel erstellt (bitte jetzt kopieren):

{newlyCreatedKey}
)} {/* Active Keys List */}
{keys.length === 0 ? (

Noch keine dedizierten API-Schlüssel hinterlegt.

) : ( keys.map((k) => editKeyId === k.id ? (
setEditName(e.target.value)} aria-label="Name des Schlüssels" className="w-full sm:flex-1 min-w-0 h-11 sm:h-9 px-3 input-field text-base sm:text-sm" />
{renderScopeSwitches(editScopes, toggleEditScope, `Berechtigungen von ${k.name}`)}

Der Schlüssel selbst bleibt gleich; Geräte und Agenten, die ihn nutzen, müssen nichts ändern. Die neuen Rechte gelten ab sofort.

{editError &&

{editError}

}
) : (
{k.name} {k.keyPrefix} {k.scopes .split(',') .filter(Boolean) .map((sc) => ( {scopeChip(sc)} ))} {k.lastUsedAt && ( Zuletzt genutzt: {new Date(k.lastUsedAt).toLocaleDateString('de-DE')} )}
) ) )}
{/* How to connect: one API, two accesses */}
{( [ ['rest', 'curl / Smart Home', Terminal], ['mcp', 'KI-Agenten (MCP)', Bot], ] as const ).map(([value, label, Icon]) => ( ))}
{access === 'rest' && (
{/* Reading: e.g. a smart home sensor for what is due today */}
Lesen, z. B. was heute fällig ist (Recht „Aufgaben lesen“):
{`curl "${baseUrl}/api/v1/tasks?due=today&status=open" \\
  -H "X-API-Key: ${exampleToken}"`}
              

Filter: due=today, due=overdue oder ein Datum (JJJJ-MM-TT, fällig bis einschließlich), status=open|done,{' '} taskListId. Listen: GET /api/v1/lists und{' '} /lists/ID. Mit „Ändern“ auch PATCH /api/v1/thoughts/ID{' '} (z. B. {'{"isCompleted": true}'}).

Home Assistant: Sensor „Heute fällig“ (configuration.yaml)
{`rest:
  - resource: "${baseUrl}/api/v1/tasks?due=today&status=open"
    headers:
      X-API-Key: "${exampleToken}"
    scan_interval: 900
    sensor:
      - name: "MindSift heute fällig"
        value_template: "{{ value_json | length }}"`}
              

Einliefern testen (Webhook)

Teste das Einliefern per POST /api/v1/webhooks/ingest direkt hier {newlyCreatedKey ? ' – mit dem eben erzeugten Key und seinen Berechtigungen.' : ' – mit deiner Anmeldung.'}

{( [ ['triage', '📥 Inbox'], ['task', '📋 Aufgabe'], ['list_item', '📝 Listeneintrag'], ] as const ).map(([value, label]) => ( ))} {simTarget === 'list_item' && ( )} {simTarget === 'task' && ( )}
{simTarget === 'triage' && (
{[ { label: '🧺 Waschmaschine fertig', text: 'Waschmaschine im Keller ist fertig – Wäsche aufhängen!', prio: 'high' as PriorityLevel }, { label: '🚨 Rauchmelder Batterie', text: 'Rauchmelder Dachgeschoss meldet niedrige Batterie (12%)', prio: 'urgent' as PriorityLevel }, { label: '💧 Luftfeuchtigkeit Keller', text: 'Kellerfeuchtigkeit über 70% – Lüften oder Entfeuchter starten', prio: 'urgent' as PriorityLevel }, ].map((preset) => ( ))}
)}
setSimTitle(e.target.value)} className="flex-1 h-11 sm:h-9 px-3 input-field text-base sm:text-sm" />
{simStatus && (

{simStatus}

)} {/* Code Example Snippets */}
cURL / Home Assistant REST Command:
{`curl -X POST "${baseUrl}/api/v1/webhooks/ingest" \\
  -H "X-API-Key: ${exampleToken}" \\
  -H "Content-Type: application/json" \\
  -d '${examplePayload}'`}
                

Der Key gehört in den Header X-API-Key, nicht in die URL, damit er nicht in Proxy-Logs landet. Fehlt dem Key die Berechtigung für das Ziel, antwortet der Server mit 403.

)} {access === 'mcp' && (

KI-Agenten anbinden (MCP)

MindSift ist ein MCP-Server: Claude Code, Codex, Claude Desktop, Cursor und eigene Agenten nutzen dieselben Schlüssel und Rechte wie curl, nur in der Form, die Agenten erwarten.

{( [ ['claude', 'Claude Code'], ['codex', 'OpenAI Codex'], ['json', 'Claude Desktop / Cursor'], ] as const ).map(([value, label]) => ( ))}
{mcpSnippet}

Werkzeuge: list_task_lists, create_task_list,{' '} list_tasks, get_task,{' '} create_task, update_task,{' '} complete_task, delete_task,{' '} list_lists, create_list,{' '} get_list, add_list_item,{' '} check_list_item, update_list_item,{' '} search, capture_to_inbox. Der Agent sieht nur die Werkzeuge, für die der Schlüssel berechtigt ist; Freigaben gelten wie für dich selbst. ChatGPT im Browser und claude.ai brauchen OAuth und werden noch nicht unterstützt.

)}
); };