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 { SCOPES, SCOPE_GROUPS, scopeChip } from '../../lib/apiScopes'; import { api, triggerHaptic } from '../../lib/api'; import { ToggleSwitch } from '../../components/ToggleSwitch'; interface ApiKeysPanelProps { onIngested?: () => void; } type SimTarget = 'triage' | 'task' | 'list_item'; type McpClient = 'connector' | '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('connector'); // Address for claude.ai / ChatGPT connectors; empty while the server has no OAuth (no BASE_URL) const [connectorUrl, setConnectorUrl] = useState(''); const [copiedConnector, setCopiedConnector] = useState(false); const [access, setAccess] = useState('rest'); // The operator can switch MCP off; then only the curl / smart home access is shown const [mcpEnabled, setMcpEnabled] = useState(true); // 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(() => {}); api .getAuthStatus() .then((st) => { setConnectorUrl(st.mcpConnectorUrl || ''); setMcpEnabled(st.mcpEnabled !== false); }) .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, notes and inbox (same layout for a new and an existing key) const renderScopeSwitches = (selected: ApiKeyScope[], toggle: (id: ApiKeyScope) => void, label: string) => (
{SCOPE_GROUPS.filter(([group]) => mcpEnabled || group !== 'notes').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 (k: APIKey) => { if (!window.confirm(k.kind === 'oauth' ? `Die Verbindung „${k.name}“ wirklich trennen?` : 'Diesen API-Schlüssel wirklich widerrufen?')) return; triggerHaptic(20); await api.deleteAPIKey(k.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, nur Notizen gibt es ausschließlich über MCP. Einstellungen bleiben für Schlüssel gesperrt. Was ein Schlüssel bei Aufgaben und Listen 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}`)}

{k.kind === 'oauth' ? 'Die Verbindung bleibt bestehen; die neuen Rechte gelten ab sofort.' : '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.kind === 'oauth' ? ( OAuth ) : ( {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 */}
{mcpEnabled && (
{( [ ['rest', 'curl / Smart Home', Terminal], ['mcp', 'KI-Agenten (MCP)', Bot], ] as const ).map(([value, label, Icon]) => ( ))}
)} {(access === 'rest' || !mcpEnabled) && (
{/* 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' && mcpEnabled && (

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.ai und ChatGPT verbinden sich ohne Schlüssel: MindSift fragt beim Verbinden, was der Agent darf.

{( [ ['connector', 'claude.ai / ChatGPT'], ['claude', 'Claude Code'], ['codex', 'OpenAI Codex'], ['json', 'Claude Desktop / Cursor'], ] as const ).map(([value, label]) => ( ))}
{mcpClient === 'connector' ? ( connectorUrl ? (
{connectorUrl}

In claude.ai unter Einstellungen → Connectors → Eigenen Connector hinzufügen{' '} diese Adresse eintragen (in ChatGPT entsprechend als Connector bzw. App). Beim Verbinden öffnet sich MindSift: anmelden, Rechte wählen, erlauben. Die Verbindung erscheint oben in der Liste und lässt sich dort ändern oder trennen; unbenutzt endet sie nach einiger Zeit von selbst.

) : (

Verbinden über claude.ai und ChatGPT braucht OAuth, und das setzt BASE_URL in der Server-Konfiguration voraus. Bitte die Person, die MindSift betreibt, sie zu setzen.

) ) : (
{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,{' '} list_notes, get_note,{' '} create_note, search,{' '} capture_to_inbox. Der Agent sieht nur die Werkzeuge, für die der Schlüssel bzw. die Verbindung berechtigt ist; Freigaben gelten wie für dich selbst.

)}
); };