284 lines
11 KiB
TypeScript
284 lines
11 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
||
import {
|
||
Key,
|
||
Plus,
|
||
Trash2,
|
||
Copy,
|
||
Check,
|
||
Webhook,
|
||
Download,
|
||
ShieldCheck,
|
||
Zap,
|
||
Terminal,
|
||
} from 'lucide-react';
|
||
import { APIKey, PriorityLevel } from '../../lib/types';
|
||
import { api, getToken, triggerHaptic } from '../../lib/api';
|
||
|
||
interface EcosystemViewProps {
|
||
onIngested: () => void;
|
||
}
|
||
|
||
export const EcosystemView: React.FC<EcosystemViewProps> = ({ onIngested }) => {
|
||
const [keys, setKeys] = useState<APIKey[]>([]);
|
||
const [newKeyName, setNewKeyName] = useState('');
|
||
const [newlyCreatedKey, setNewlyCreatedKey] = useState<string | null>(null);
|
||
const [copiedKey, setCopiedKey] = useState(false);
|
||
|
||
// Live Webhook Simulator State
|
||
const [simTitle, setSimTitle] = useState('Waschmaschine im Keller ist fertig!');
|
||
const [simPriority, setSimPriority] = useState<PriorityLevel>('high');
|
||
const [simStatus, setSimStatus] = useState<string | null>(null);
|
||
|
||
const loadKeys = async () => {
|
||
const k = await api.getAPIKeys();
|
||
setKeys(k);
|
||
};
|
||
|
||
useEffect(() => {
|
||
loadKeys();
|
||
}, []);
|
||
|
||
const handleCreateKey = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
triggerHaptic(15);
|
||
const created = await api.createAPIKey(newKeyName.trim() || 'Home Assistant');
|
||
if (created.rawKey) {
|
||
setNewlyCreatedKey(created.rawKey);
|
||
}
|
||
setNewKeyName('');
|
||
await loadKeys();
|
||
};
|
||
|
||
const handleDeleteKey = async (id: string) => {
|
||
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);
|
||
const tokenToUse = newlyCreatedKey || getToken() || '';
|
||
try {
|
||
await api.testWebhookIngest(tokenToUse, simTitle, simPriority, ['smarthome', 'automation']);
|
||
setSimStatus('✅ Webhook erfolgreich ausgelöst! Neue Karte liegt in deiner Inbox.');
|
||
onIngested();
|
||
setTimeout(() => setSimStatus(null), 4000);
|
||
} catch (err: any) {
|
||
setSimStatus(`❌ Fehler: ${err.message}`);
|
||
}
|
||
};
|
||
|
||
const handleFullExport = async () => {
|
||
triggerHaptic(15);
|
||
const data = await api.exportAll();
|
||
const blob = new Blob([JSON.stringify(data, null, 2)], {
|
||
type: 'application/json;charset=utf-8',
|
||
});
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `mindsift-backup-${new Date().toISOString().slice(0, 10)}.json`;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
};
|
||
|
||
const exampleToken = newlyCreatedKey || 'mindsift_sec_dein_api_key';
|
||
const baseUrl = window.location.origin;
|
||
|
||
return (
|
||
<div className="max-w-4xl mx-auto space-y-6">
|
||
{/* Section 1: API Keys for Smarthome & Automation */}
|
||
<div className="p-6 sm:p-8 rounded-2xl bg-[#1e1f20] border border-white/[0.08]">
|
||
<div className="flex items-center gap-3 mb-2">
|
||
<div className="w-10 h-10 rounded-full bg-[#a8c7fa]/15 flex items-center justify-center">
|
||
<Key className="w-5 h-5 text-[#a8c7fa]" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-lg font-medium text-[#e3e3e3]">
|
||
Webhooks & Ingestion API-Schlüssel
|
||
</h3>
|
||
<p className="text-xs sm:text-sm text-[#8e918f]">
|
||
Verbinde Home Assistant, Zigbee2MQTT, n8n oder eigene Cronjobs direkt mit deiner MindSift Inbox.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<form onSubmit={handleCreateKey} className="mt-5 flex flex-col sm:flex-row gap-2.5">
|
||
<input
|
||
type="text"
|
||
value={newKeyName}
|
||
onChange={(e) => setNewKeyName(e.target.value)}
|
||
placeholder="Bezeichnung (z. B. Home Assistant, Proxmox Backup, Rauchmelder)…"
|
||
className="flex-1 h-11 px-4 rounded-full bg-[#131314] text-sm text-[#e3e3e3] placeholder-[#8e918f] border border-white/[0.08] focus:outline-none focus:border-[#a8c7fa] transition-colors"
|
||
/>
|
||
<button
|
||
type="submit"
|
||
className="h-11 px-6 rounded-full bg-[#a8c7fa] text-[#041e49] font-medium text-sm flex items-center justify-center gap-1.5 hover:bg-[#c2e7ff] transition-colors"
|
||
>
|
||
<Plus className="w-4 h-4" />
|
||
<span>API-Key erzeugen</span>
|
||
</button>
|
||
</form>
|
||
|
||
{newlyCreatedKey && (
|
||
<div className="mt-4 p-4 rounded-2xl bg-[#a8dab5]/10 border border-[#a8dab5]/30 flex flex-wrap items-center justify-between gap-3">
|
||
<div className="min-w-0">
|
||
<p className="text-xs font-medium text-[#a8dab5] mb-1">
|
||
Neuer API-Schlüssel erstellt (bitte jetzt kopieren):
|
||
</p>
|
||
<code className="text-xs sm:text-sm font-mono text-[#e3e3e3] break-all">
|
||
{newlyCreatedKey}
|
||
</code>
|
||
</div>
|
||
<button
|
||
onClick={() => handleCopy(newlyCreatedKey)}
|
||
className="h-9 px-4 rounded-full bg-[#a8dab5] text-[#041e49] font-medium text-xs flex items-center gap-1.5 hover:bg-[#c2e7ff] transition-colors"
|
||
>
|
||
{copiedKey ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
|
||
<span>{copiedKey ? 'Kopiert!' : 'Key kopieren'}</span>
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* Active Keys List */}
|
||
<div className="mt-5 space-y-2">
|
||
{keys.length === 0 ? (
|
||
<p className="text-xs text-[#8e918f] py-2">
|
||
Noch keine dedizierten API-Schlüssel hinterlegt.
|
||
</p>
|
||
) : (
|
||
keys.map((k) => (
|
||
<div
|
||
key={k.id}
|
||
className="p-3.5 rounded-2xl bg-[#131314] border border-white/[0.08] flex items-center justify-between gap-3"
|
||
>
|
||
<div>
|
||
<span className="text-sm font-medium text-[#e3e3e3]">{k.name}</span>
|
||
<span className="ml-3 text-xs font-mono text-[#a8c7fa]">{k.keyPrefix}</span>
|
||
{k.lastUsedAt && (
|
||
<span className="ml-3 text-xs text-[#8e918f]">
|
||
Zuletzt genutzt: {new Date(k.lastUsedAt).toLocaleDateString('de-DE')}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<button
|
||
onClick={() => handleDeleteKey(k.id)}
|
||
className="w-8 h-8 rounded-full hover:bg-[#f28b82]/20 text-[#8e918f] hover:text-[#f28b82] flex items-center justify-center transition-colors"
|
||
>
|
||
<Trash2 className="w-3.5 h-3.5" />
|
||
</button>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Section 2: Interactive Smarthome Webhook Simulator */}
|
||
<div className="p-6 sm:p-8 rounded-2xl bg-[#1e1f20] border border-white/[0.08]">
|
||
<div className="flex items-center gap-3 mb-4">
|
||
<div className="w-10 h-10 rounded-full bg-[#a8c7fa]/15 flex items-center justify-center">
|
||
<Webhook className="w-5 h-5 text-[#a8c7fa]" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-lg font-medium text-[#e3e3e3]">
|
||
Live Smarthome Webhook-Simulator
|
||
</h3>
|
||
<p className="text-xs sm:text-sm text-[#8e918f]">
|
||
Teste den Webhook-Endpunkt direkt hier – das Event landet sofort als Karte im Triage-Deck.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-wrap gap-2 mb-4">
|
||
{[
|
||
{ 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) => (
|
||
<button
|
||
key={preset.label}
|
||
type="button"
|
||
onClick={() => {
|
||
setSimTitle(preset.text);
|
||
setSimPriority(preset.prio);
|
||
}}
|
||
className="px-3.5 py-1.5 rounded-full bg-[#131314] hover:bg-[#282a2c] border border-white/[0.08] text-xs text-[#c4c7c5] hover:text-[#e3e3e3] transition-colors"
|
||
>
|
||
{preset.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div className="flex flex-col sm:flex-row gap-2.5">
|
||
<input
|
||
type="text"
|
||
value={simTitle}
|
||
onChange={(e) => setSimTitle(e.target.value)}
|
||
className="flex-1 h-11 px-4 rounded-full bg-[#131314] text-sm text-[#e3e3e3] border border-white/[0.08] focus:outline-none focus:border-[#a8c7fa] transition-colors"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={handleSimulateWebhook}
|
||
className="h-11 px-6 rounded-full bg-[#a8c7fa] text-[#041e49] font-medium text-sm flex items-center justify-center gap-1.5 hover:bg-[#c2e7ff] transition-colors"
|
||
>
|
||
<Zap className="w-4 h-4" />
|
||
<span>Webhook jetzt abfeuern</span>
|
||
</button>
|
||
</div>
|
||
|
||
{simStatus && (
|
||
<p className="mt-3 text-xs font-medium text-[#a8dab5]">{simStatus}</p>
|
||
)}
|
||
|
||
{/* Code Example Snippets */}
|
||
<div className="mt-6 space-y-3">
|
||
<div className="flex items-center gap-2 text-xs font-medium text-[#8e918f]">
|
||
<Terminal className="w-3.5 h-3.5 text-[#a8c7fa]" />
|
||
<span>cURL / Home Assistant REST Command:</span>
|
||
</div>
|
||
<pre className="p-4 rounded-2xl bg-[#131314] border border-white/[0.08] font-mono text-xs text-[#a8dab5] overflow-x-auto">
|
||
{`curl -X POST "${baseUrl}/api/v1/webhooks/ingest?token=${exampleToken}" \\
|
||
-H "Content-Type: application/json" \\
|
||
-d '{
|
||
"title": "Kellerfeuchtigkeit über 70%",
|
||
"priority": "urgent",
|
||
"tags": ["smarthome", "keller"]
|
||
}'`}
|
||
</pre>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Section 3: Data Sovereignty & Export */}
|
||
<div className="p-6 sm:p-8 rounded-2xl bg-[#1e1f20] border border-white/[0.08] flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 rounded-full bg-[#a8dab5]/15 flex items-center justify-center flex-shrink-0">
|
||
<ShieldCheck className="w-5 h-5 text-[#a8dab5]" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-base font-medium text-[#e3e3e3]">
|
||
100 % Datensouveränität & Komplett-Export
|
||
</h3>
|
||
<p className="text-xs sm:text-sm text-[#8e918f]">
|
||
Exportiere jederzeit alle Gedanken, Aufgaben, Dokumente und geteilten Listen als portables JSON-Archiv.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
onClick={handleFullExport}
|
||
className="h-11 px-6 rounded-full bg-[#a8dab5] text-[#041e49] font-medium text-sm flex items-center gap-2 hover:bg-[#c2e7ff] transition-colors flex-shrink-0"
|
||
>
|
||
<Download className="w-4 h-4" />
|
||
<span>Komplett-Backup (.json)</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|