feat: complete triage, multi-task lists, kanban, workspace wiki, and admin management
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 16s

This commit is contained in:
sandrodach committed 2026-10-24 22:08:43 +02:00
1 parent 5c4e130862
commit 4ef71b61cd
12 files changed
+3940 -664

No files matched your search

@@ -0,0 +1,953 @@
import React, { useEffect, useState } from 'react';
import {
X,
User as UserIcon,
Shield,
Bell,
Key,
Users,
Settings as SettingsIcon,
Plus,
Trash2,
Check,
Copy,
RefreshCw,
LogOut,
Terminal,
Download,
AlertTriangle,
Lock,
Share2,
} from 'lucide-react';
import { APIKey, Organization, SystemSettings, Team, User } from '../../lib/types';
import { api, clearToken, getToken, triggerHaptic } from '../../lib/api';
interface SettingsModalProps {
currentUser: User | null;
onClose: () => void;
onUserUpdated: (u: User) => void;
onLoggedOut: () => void;
}
type SettingsTab = 'profile' | 'alarms' | 'webhooks' | 'teams' | 'admin';
export const SettingsModal: React.FC<SettingsModalProps> = ({
currentUser,
onClose,
onUserUpdated,
onLoggedOut,
}) => {
const [activeTab, setActiveTab] = useState<SettingsTab>('profile');
// --- Profile State ---
const [displayName, setDisplayName] = useState(currentUser?.displayName || '');
const [email, setEmail] = useState(currentUser?.email || '');
const [newPassword, setNewPassword] = useState('');
const [profileMsg, setProfileMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null);
// --- Alarms / Discord State ---
const [discordWebhookUrl, setDiscordWebhookUrl] = useState(currentUser?.discordWebhookUrl || '');
const [notifyBeforeMinutes, setNotifyBeforeMinutes] = useState(currentUser?.notifyBeforeMinutes || 30);
const [alarmMsg, setAlarmMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null);
const [testingDiscord, setTestingDiscord] = useState(false);
// --- Webhooks / API Keys State ---
const [apiKeys, setApiKeys] = useState<APIKey[]>([]);
const [newKeyName, setNewKeyName] = useState('');
const [newlyCreatedKey, setNewlyCreatedKey] = useState<string | null>(null);
const [copiedKey, setCopiedKey] = useState(false);
// --- Organizations & Teams State ---
const [orgs, setOrgs] = useState<Organization[]>([]);
const [teams, setTeams] = useState<Team[]>([]);
const [newOrgName, setNewOrgName] = useState('');
const [selectedOrgId, setSelectedOrgId] = useState('');
const [newTeamName, setNewTeamName] = useState('');
const [newTeamDesc, setNewTeamDesc] = useState('');
// --- Admin State ---
const [adminUsers, setAdminUsers] = useState<User[]>([]);
const [systemSettings, setSystemSettings] = useState<SystemSettings>({
sharingMode: 'teams_only',
allowRegistration: true,
instanceName: 'MindSift',
});
const [newAdminUserEmail, setNewAdminUserEmail] = useState('');
const [newAdminUserPassword, setNewAdminUserPassword] = useState('');
const [newAdminUserDisplayName, setNewAdminUserDisplayName] = useState('');
const [newAdminUserRole, setNewAdminUserRole] = useState<'member' | 'admin'>('member');
const [resetPwdUserId, setResetPwdUserId] = useState<string | null>(null);
const [resetPwdVal, setResetPwdVal] = useState('');
useEffect(() => {
loadKeys();
loadOrgsAndTeams();
if (currentUser?.role === 'admin') {
loadAdminData();
}
}, []);
const loadKeys = async () => {
try {
const keys = await api.getAPIKeys();
setApiKeys(keys);
} catch {
// ignore
}
};
const loadOrgsAndTeams = async () => {
try {
const [o, t] = await Promise.all([api.getOrganizations(), api.getUserTeams()]);
setOrgs(o);
setTeams(t);
if (o.length > 0 && !selectedOrgId) {
setSelectedOrgId(o[0].id);
}
} catch {
// ignore
}
};
const loadAdminData = async () => {
try {
const [u, s] = await Promise.all([api.adminGetUsers(), api.adminGetSettings()]);
setAdminUsers(u);
setSystemSettings(s);
} catch {
// ignore
}
};
// --- Profile Handlers ---
const handleSaveProfile = async (e: React.FormEvent) => {
e.preventDefault();
setProfileMsg(null);
try {
const patch: any = { displayName, email };
if (newPassword.trim()) {
patch.password = newPassword.trim();
}
const updated = await api.updateSettings(patch);
onUserUpdated(updated);
setNewPassword('');
setProfileMsg({ type: 'ok', text: 'Profil erfolgreich gespeichert.' });
} catch (err: any) {
setProfileMsg({ type: 'err', text: err.message || 'Fehler beim Speichern' });
}
};
// --- Alarms Handlers ---
const handleSaveAlarms = async (e: React.FormEvent) => {
e.preventDefault();
setAlarmMsg(null);
try {
const updated = await api.updateSettings({
discordWebhookUrl: discordWebhookUrl.trim(),
notifyBeforeMinutes: Number(notifyBeforeMinutes),
});
onUserUpdated(updated);
setAlarmMsg({ type: 'ok', text: 'Discord Alarm-Einstellungen gespeichert.' });
} catch (err: any) {
setAlarmMsg({ type: 'err', text: err.message || 'Fehler beim Speichern' });
}
};
const handleTestDiscordWebhook = async () => {
if (!discordWebhookUrl.trim()) {
setAlarmMsg({ type: 'err', text: 'Bitte zuerst eine Discord Webhook URL eingeben.' });
return;
}
setTestingDiscord(true);
setAlarmMsg(null);
try {
const res = await fetch(discordWebhookUrl.trim(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: '🔔 **MindSift Test-Alarm**: Verbindung zu Discord erfolgreich hergestellt!',
embeds: [
{
title: 'MindSift Aufgaben-Alarm eingerichtet',
description: 'Du erhältst hier Benachrichtigungen, wenn Aufgaben fällig werden.',
color: 3066993,
footer: { text: 'MindSift System Test' },
},
],
}),
});
if (!res.ok) {
throw new Error(`Discord antwortete mit Status ${res.status}`);
}
setAlarmMsg({ type: 'ok', text: '✅ Test-Nachricht erfolgreich an Discord gesendet!' });
} catch (err: any) {
setAlarmMsg({ type: 'err', text: `Fehler: ${err.message}` });
} finally {
setTestingDiscord(false);
}
};
// --- Webhook / Key Handlers ---
const handleCreateAPIKey = async (e: React.FormEvent) => {
e.preventDefault();
triggerHaptic(15);
try {
const created = await api.createAPIKey(newKeyName.trim() || 'Home Assistant / Script');
if (created.rawKey) {
setNewlyCreatedKey(created.rawKey);
}
setNewKeyName('');
loadKeys();
} catch (err: any) {
alert(err.message);
}
};
const handleDeleteAPIKey = async (id: string) => {
if (!window.confirm('Schlüssel widerrufen?')) return;
await api.deleteAPIKey(id);
loadKeys();
};
const handleCopy = (text: string) => {
navigator.clipboard.writeText(text);
setCopiedKey(true);
setTimeout(() => setCopiedKey(false), 2000);
};
const handleExportData = async () => {
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);
};
// --- Orgs & Teams Handlers ---
const handleCreateOrg = async (e: React.FormEvent) => {
e.preventDefault();
if (!newOrgName.trim()) return;
try {
const created = await api.createOrganization(newOrgName.trim());
setOrgs((prev) => [...prev, created]);
setSelectedOrgId(created.id);
setNewOrgName('');
} catch (err: any) {
alert(err.message);
}
};
const handleCreateTeam = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedOrgId || !newTeamName.trim()) return;
try {
const created = await api.createOrgTeam(selectedOrgId, newTeamName.trim(), newTeamDesc.trim());
setTeams((prev) => [...prev, created]);
setNewTeamName('');
setNewTeamDesc('');
} catch (err: any) {
alert(err.message);
}
};
// --- Admin Handlers ---
const handleUpdateSystemSettings = async (patch: Partial<SystemSettings>) => {
const next = { ...systemSettings, ...patch };
setSystemSettings(next);
try {
await api.adminUpdateSettings(next);
} catch (err: any) {
alert(err.message);
}
};
const handleAdminCreateUser = async (e: React.FormEvent) => {
e.preventDefault();
try {
const created = await api.adminCreateUser({
email: newAdminUserEmail,
password: newAdminUserPassword,
displayName: newAdminUserDisplayName || newAdminUserEmail.split('@')[0],
role: newAdminUserRole,
});
setAdminUsers((prev) => [...prev, created]);
setNewAdminUserEmail('');
setNewAdminUserPassword('');
setNewAdminUserDisplayName('');
} catch (err: any) {
alert(err.message);
}
};
const handleToggleUserActive = async (u: User) => {
try {
const updated = await api.adminUpdateUser(u.id, { isActive: !u.isActive });
setAdminUsers((prev) => prev.map((item) => (item.id === u.id ? updated : item)));
} catch (err: any) {
alert(err.message);
}
};
const handleToggleUserRole = async (u: User) => {
const nextRole = u.role === 'admin' ? 'member' : 'admin';
try {
const updated = await api.adminUpdateUser(u.id, { role: nextRole });
setAdminUsers((prev) => prev.map((item) => (item.id === u.id ? updated : item)));
} catch (err: any) {
alert(err.message);
}
};
const handleConfirmResetPassword = async (userId: string) => {
if (!resetPwdVal.trim()) return;
try {
await api.adminResetPassword(userId, resetPwdVal.trim());
alert('Passwort erfolgreich zurückgesetzt.');
setResetPwdUserId(null);
setResetPwdVal('');
} catch (err: any) {
alert(err.message);
}
};
const handleAdminDeleteUser = async (userId: string) => {
if (!window.confirm('Benutzer unwiderruflich löschen? Alle Daten dieses Nutzers werden gelöscht.')) return;
try {
await api.adminDeleteUser(userId);
setAdminUsers((prev) => prev.filter((u) => u.id !== userId));
} catch (err: any) {
alert(err.message);
}
};
const baseUrl = window.location.origin;
const curlExample = `curl -X POST ${baseUrl}/api/v1/inbox \\
-H "Authorization: Bearer ${newlyCreatedKey || apiKeys[0]?.keyPrefix || 'DEIN_API_KEY'}" \\
-H "Content-Type: application/json" \\
-d '{"content": "Server-Backup abgeschlossen #homelab #backup", "priority": "high", "target": "task"}'`;
return (
<div className="fixed inset-0 z-50 bg-black/75 backdrop-blur-sm flex items-center justify-center p-2 sm:p-4">
<div className="w-full max-w-3xl bg-panel border border-border shadow-2xl flex flex-col max-h-[90vh] overflow-hidden">
{/* Header */}
<div className="p-3 bg-canvas border-b border-border flex items-center justify-between">
<div className="flex items-center gap-2">
<SettingsIcon className="w-4 h-4 text-accent" />
<h2 className="text-sm font-bold text-white tracking-tight">
Konto-Einstellungen & Administration
</h2>
</div>
<button
onClick={onClose}
className="p-1 text-zinc-400 hover:text-white transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Modal Navigation Tabs */}
<div className="flex items-stretch border-b border-border bg-canvas overflow-x-auto no-scrollbar text-xs">
<button
onClick={() => setActiveTab('profile')}
className={`h-9 px-3.5 flex items-center gap-1.5 font-medium border-r border-border transition-colors ${
activeTab === 'profile' ? 'bg-panel text-white border-b-2 border-b-accent' : 'text-zinc-400 hover:text-white'
}`}
>
<UserIcon className="w-3.5 h-3.5" />
<span>Profil & Sicherheit</span>
</button>
<button
onClick={() => setActiveTab('alarms')}
className={`h-9 px-3.5 flex items-center gap-1.5 font-medium border-r border-border transition-colors ${
activeTab === 'alarms' ? 'bg-panel text-white border-b-2 border-b-accent' : 'text-zinc-400 hover:text-white'
}`}
>
<Bell className="w-3.5 h-3.5" />
<span>Alarme & Discord</span>
</button>
<button
onClick={() => setActiveTab('webhooks')}
className={`h-9 px-3.5 flex items-center gap-1.5 font-medium border-r border-border transition-colors ${
activeTab === 'webhooks' ? 'bg-panel text-white border-b-2 border-b-accent' : 'text-zinc-400 hover:text-white'
}`}
>
<Key className="w-3.5 h-3.5" />
<span>Webhooks & Export</span>
</button>
<button
onClick={() => setActiveTab('teams')}
className={`h-9 px-3.5 flex items-center gap-1.5 font-medium border-r border-border transition-colors ${
activeTab === 'teams' ? 'bg-panel text-white border-b-2 border-b-accent' : 'text-zinc-400 hover:text-white'
}`}
>
<Users className="w-3.5 h-3.5" />
<span>Organisationen & Teams</span>
</button>
{currentUser?.role === 'admin' && (
<button
onClick={() => setActiveTab('admin')}
className={`h-9 px-3.5 flex items-center gap-1.5 font-medium border-r border-border transition-colors ${
activeTab === 'admin' ? 'bg-panel text-accent font-semibold border-b-2 border-b-accent' : 'text-zinc-400 hover:text-white'
}`}
>
<Shield className="w-3.5 h-3.5 text-accent" />
<span>Administration</span>
</button>
)}
</div>
{/* Modal Body */}
<div className="p-4 overflow-y-auto flex-1 space-y-4 text-xs">
{/* TAB 1: PROFIL & SICHERHEIT */}
{activeTab === 'profile' && (
<div className="space-y-4 max-w-lg">
<div>
<h3 className="text-sm font-semibold text-white mb-1">Benutzerprofil bearbeiten</h3>
<p className="text-zinc-400 text-xs">
Passe deinen Anzeigenamen und dein Anmeldekennwort an.
</p>
</div>
{profileMsg && (
<div
className={`p-2 border text-xs ${
profileMsg.type === 'ok'
? 'bg-blue-900/20 border-accent text-blue-200'
: 'bg-red-900/20 border-red-500 text-red-200'
}`}
>
{profileMsg.text}
</div>
)}
<form onSubmit={handleSaveProfile} className="space-y-3">
<div>
<label className="block text-zinc-400 mb-1">Anzeigename</label>
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
required
className="w-full h-8 px-2.5 input-field text-white"
/>
</div>
<div>
<label className="block text-zinc-400 mb-1">E-Mail-Adresse</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full h-8 px-2.5 input-field text-white"
/>
</div>
<div>
<label className="block text-zinc-400 mb-1">Neues Passwort (leer lassen für keine Änderung)</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="Mindestens 4 Zeichen"
className="w-full h-8 px-2.5 input-field text-white"
/>
</div>
<div className="flex items-center justify-between pt-2 border-t border-border">
<button
type="button"
onClick={onLoggedOut}
className="text-red-400 hover:underline flex items-center gap-1.5 py-1"
>
<LogOut className="w-3.5 h-3.5" />
<span>Abmelden</span>
</button>
<button type="submit" className="btn-primary h-8 px-4 text-xs">
Änderungen speichern
</button>
</div>
</form>
</div>
)}
{/* TAB 2: ALARME & DISCORD */}
{activeTab === 'alarms' && (
<div className="space-y-4 max-w-lg">
<div>
<h3 className="text-sm font-semibold text-white mb-1">Aufgaben-Alarme & Discord Benachrichtigungen</h3>
<p className="text-zinc-400 text-xs leading-relaxed">
Trage deine Discord Webhook URL ein, damit der MindSift Server dich automatisch erinnert, sobald Aufgaben fällig werden.
</p>
</div>
{alarmMsg && (
<div
className={`p-2 border text-xs ${
alarmMsg.type === 'ok'
? 'bg-blue-900/20 border-accent text-blue-200'
: 'bg-red-900/20 border-red-500 text-red-200'
}`}
>
{alarmMsg.text}
</div>
)}
<form onSubmit={handleSaveAlarms} className="space-y-3">
<div>
<label className="block text-zinc-400 mb-1">Discord Webhook URL</label>
<input
type="url"
value={discordWebhookUrl}
onChange={(e) => setDiscordWebhookUrl(e.target.value)}
placeholder="https://discord.com/api/webhooks/123456789/token..."
className="w-full h-8 px-2.5 input-field text-white font-mono text-xs"
/>
</div>
<div>
<label className="block text-zinc-400 mb-1">Vorwarnzeit vor Fälligkeit</label>
<select
value={notifyBeforeMinutes}
onChange={(e) => setNotifyBeforeMinutes(Number(e.target.value))}
className="w-full h-8 px-2 input-field text-white bg-canvas"
>
<option value={0}>Direkt bei Fälligkeit</option>
<option value={15}>15 Minuten vorher</option>
<option value={30}>30 Minuten vorher</option>
<option value={60}>1 Stunde vorher</option>
<option value={1440}>1 Tag vorher</option>
</select>
</div>
<div className="flex items-center justify-between pt-2 border-t border-border">
<button
type="button"
onClick={handleTestDiscordWebhook}
disabled={testingDiscord || !discordWebhookUrl.trim()}
className="btn-action h-8 px-3 text-xs disabled:opacity-40"
>
<span>{testingDiscord ? 'Sende Test…' : 'Webhook testen'}</span>
</button>
<button type="submit" className="btn-primary h-8 px-4 text-xs">
Alarm-Einstellungen sichern
</button>
</div>
</form>
</div>
)}
{/* TAB 3: WEBHOOKS & EXPORT */}
{activeTab === 'webhooks' && (
<div className="space-y-4">
<div>
<h3 className="text-sm font-semibold text-white mb-1">Private Webhook Keys & Daten-Export</h3>
<p className="text-zinc-400 text-xs">
Erstelle API-Keys für eigene Bash-Skripte, Home Assistant oder Cronjobs, um Gedanken direkt per Curl zu erfassen.
</p>
</div>
{/* API Key Creator */}
<form onSubmit={handleCreateAPIKey} className="flex gap-2">
<input
type="text"
value={newKeyName}
onChange={(e) => setNewKeyName(e.target.value)}
placeholder="Key Bezeichnung (z.B. Smart Home, Backup Skript)…"
className="flex-1 h-8 px-2.5 input-field text-xs text-white"
/>
<button type="submit" className="btn-primary h-8 px-3 text-xs flex items-center gap-1">
<Plus className="w-3.5 h-3.5" />
<span>Key erstellen</span>
</button>
</form>
{newlyCreatedKey && (
<div className="p-2.5 bg-accent/15 border border-accent/40 flex items-center justify-between gap-2">
<div className="min-w-0">
<p className="text-[10px] text-accent font-semibold">Neuer Key (jetzt kopieren):</p>
<code className="text-xs font-mono text-white break-all">{newlyCreatedKey}</code>
</div>
<button onClick={() => handleCopy(newlyCreatedKey)} className="btn-primary h-7 px-2.5 text-xs">
{copiedKey ? 'Kopiert!' : 'Kopieren'}
</button>
</div>
)}
{/* Keys list */}
<div className="border border-border divide-y divide-border bg-canvas">
{apiKeys.length === 0 ? (
<div className="p-3 text-center text-zinc-500 text-xs">Noch keine API-Schlüssel angelegt.</div>
) : (
apiKeys.map((k) => (
<div key={k.id} className="p-2 flex items-center justify-between">
<div>
<p className="font-semibold text-white">{k.name}</p>
<p className="text-[10px] text-zinc-500 font-mono">Präfix: {k.keyPrefix}••••••••</p>
</div>
<button
onClick={() => handleDeleteAPIKey(k.id)}
className="btn-icon btn-icon-danger"
title="Key löschen"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
))
)}
</div>
{/* Interactive Curl Example */}
<div>
<div className="flex items-center justify-between mb-1">
<span className="font-semibold text-zinc-300 flex items-center gap-1">
<Terminal className="w-3 h-3 text-accent" /> Curl Schnellbefehl für eigene Skripte:
</span>
<button onClick={() => handleCopy(curlExample)} className="text-zinc-400 hover:text-white text-[10px]">
{copiedKey ? 'Kopiert!' : 'Kopieren'}
</button>
</div>
<pre className="p-2.5 bg-black/60 border border-border text-[11px] font-mono text-zinc-300 overflow-x-auto whitespace-pre">
{curlExample}
</pre>
</div>
{/* Data Export Button */}
<div className="pt-2 border-t border-border flex items-center justify-between">
<div>
<h4 className="font-semibold text-white">Vollständiger Daten-Export</h4>
<p className="text-zinc-500 text-[11px]">Sichere alle Notizen, Aufgaben, Listen und Metadaten als JSON.</p>
</div>
<button onClick={handleExportData} className="btn-action h-8 px-3 text-xs flex items-center gap-1.5">
<Download className="w-3.5 h-3.5 text-accent" />
<span>Backup herunterladen</span>
</button>
</div>
</div>
)}
{/* TAB 4: ORGANISATIONEN & TEAMS */}
{activeTab === 'teams' && (
<div className="space-y-4">
<div>
<h3 className="text-sm font-semibold text-white mb-1">Organisationen & Teams gründen</h3>
<p className="text-zinc-400 text-xs">
Erstelle Organisationen und unterteile sie in Teams, um Aufgabenlisten und Workspace-Dokumente gemeinsam zu bearbeiten.
</p>
</div>
{/* Org Creator */}
<div className="p-3 bg-canvas border border-border space-y-2">
<h4 className="font-semibold text-white">Neue Organisation</h4>
<form onSubmit={handleCreateOrg} className="flex gap-2">
<input
type="text"
value={newOrgName}
onChange={(e) => setNewOrgName(e.target.value)}
placeholder="Name der Organisation (z.B. Familie, Startup, Homelab Crew)…"
className="flex-1 h-8 px-2.5 input-field text-xs text-white"
/>
<button type="submit" className="btn-primary h-8 px-3 text-xs">
Organisation anlegen
</button>
</form>
</div>
{/* Team Creator inside Org */}
{orgs.length > 0 && (
<div className="p-3 bg-canvas border border-border space-y-2">
<h4 className="font-semibold text-white">Neues Team in Organisation</h4>
<form onSubmit={handleCreateTeam} className="space-y-2">
<div className="flex gap-2">
<select
value={selectedOrgId}
onChange={(e) => setSelectedOrgId(e.target.value)}
className="h-8 px-2 input-field text-white bg-panel"
>
{orgs.map((o) => (
<option key={o.id} value={o.id}>
{o.name}
</option>
))}
</select>
<input
type="text"
value={newTeamName}
onChange={(e) => setNewTeamName(e.target.value)}
placeholder="Team Name (z.B. DevOps, Einkauf)…"
required
className="flex-1 h-8 px-2.5 input-field text-xs text-white"
/>
</div>
<div className="flex gap-2">
<input
type="text"
value={newTeamDesc}
onChange={(e) => setNewTeamDesc(e.target.value)}
placeholder="Optionale Teambeschreibung…"
className="flex-1 h-8 px-2.5 input-field text-xs text-white"
/>
<button type="submit" className="btn-primary h-8 px-3 text-xs">
Team erstellen
</button>
</div>
</form>
</div>
)}
{/* Active Teams List */}
<div className="space-y-2">
<h4 className="font-semibold text-zinc-300">Deine Teams ({teams.length}):</h4>
<div className="border border-border divide-y divide-border bg-canvas">
{teams.length === 0 ? (
<div className="p-3 text-center text-zinc-500">Du bist noch keinem Team beigetreten.</div>
) : (
teams.map((t) => (
<div key={t.id} className="p-2.5 flex items-center justify-between">
<div>
<p className="font-bold text-white flex items-center gap-1.5">
<Users className="w-3.5 h-3.5 text-accent" />
<span>{t.name}</span>
</p>
{t.description && <p className="text-zinc-400 text-[11px]">{t.description}</p>}
</div>
<span className="text-[10px] text-zinc-500 font-mono">ID: {t.id}</span>
</div>
))
)}
</div>
</div>
</div>
)}
{/* TAB 5: ADMIN BEREICH */}
{activeTab === 'admin' && currentUser?.role === 'admin' && (
<div className="space-y-4">
<div>
<h3 className="text-sm font-semibold text-white mb-1">Server-Administration & Benutzersteuerung</h3>
<p className="text-zinc-400 text-xs">
Steuere die Instanz-Einstellungen, das Sharing-Verhalten und verwalte registrierte Benutzer.
</p>
</div>
{/* Server Sharing Isolation Setting */}
<div className="p-3 bg-canvas border border-border space-y-3">
<h4 className="font-semibold text-white flex items-center gap-1.5">
<Share2 className="w-3.5 h-3.5 text-accent" />
<span>Sharing-Isolationsmodus</span>
</h4>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<button
type="button"
onClick={() => handleUpdateSystemSettings({ sharingMode: 'open' })}
className={`p-2.5 text-left border transition-colors ${
systemSettings.sharingMode === 'open'
? 'bg-panel border-accent text-white'
: 'border-border text-zinc-400 hover:text-white'
}`}
>
<p className="font-bold text-xs">Offen (Open)</p>
<p className="text-[10px] text-zinc-400 mt-0.5">Alle Benutzer können Spaces finden und beitreten.</p>
</button>
<button
type="button"
onClick={() => handleUpdateSystemSettings({ sharingMode: 'teams_only' })}
className={`p-2.5 text-left border transition-colors ${
systemSettings.sharingMode === 'teams_only'
? 'bg-panel border-accent text-white'
: 'border-border text-zinc-400 hover:text-white'
}`}
>
<p className="font-bold text-xs">Teams isoliert</p>
<p className="text-[10px] text-zinc-400 mt-0.5">Sharing ausschließlich innerhalb zugewiesener Teams.</p>
</button>
<button
type="button"
onClick={() => handleUpdateSystemSettings({ sharingMode: 'strict_private' })}
className={`p-2.5 text-left border transition-colors ${
systemSettings.sharingMode === 'strict_private'
? 'bg-panel border-accent text-white'
: 'border-border text-zinc-400 hover:text-white'
}`}
>
<p className="font-bold text-xs">Strikt Privat</p>
<p className="text-[10px] text-zinc-400 mt-0.5">Vollständige Benutzer-Isolation; kein Teilen möglich.</p>
</button>
</div>
<div className="flex items-center justify-between pt-2 border-t border-border">
<span className="text-zinc-300">Öffentliche Registrierung erlauben:</span>
<button
type="button"
onClick={() =>
handleUpdateSystemSettings({ allowRegistration: !systemSettings.allowRegistration })
}
className={`px-3 py-1 text-xs border ${
systemSettings.allowRegistration
? 'bg-accent/20 border-accent text-accent'
: 'bg-zinc-800 border-zinc-700 text-zinc-400'
}`}
>
{systemSettings.allowRegistration ? 'Aktiviert' : 'Deaktiviert'}
</button>
</div>
</div>
{/* User Creation by Admin */}
<div className="p-3 bg-canvas border border-border space-y-2">
<h4 className="font-semibold text-white">Neuen Benutzer anlegen</h4>
<form onSubmit={handleAdminCreateUser} className="space-y-2">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<input
type="email"
value={newAdminUserEmail}
onChange={(e) => setNewAdminUserEmail(e.target.value)}
placeholder="E-Mail"
required
className="h-8 px-2.5 input-field text-white"
/>
<input
type="password"
value={newAdminUserPassword}
onChange={(e) => setNewAdminUserPassword(e.target.value)}
placeholder="Initial-Passwort"
required
className="h-8 px-2.5 input-field text-white"
/>
<div className="flex gap-2">
<input
type="text"
value={newAdminUserDisplayName}
onChange={(e) => setNewAdminUserDisplayName(e.target.value)}
placeholder="Anzeigename"
className="flex-1 h-8 px-2.5 input-field text-white"
/>
<select
value={newAdminUserRole}
onChange={(e) => setNewAdminUserRole(e.target.value as any)}
className="h-8 px-2 input-field text-white bg-panel"
>
<option value="member">Member</option>
<option value="admin">Admin</option>
</select>
</div>
</div>
<div className="flex justify-end">
<button type="submit" className="btn-primary h-7 px-3 text-xs">
Benutzer erstellen
</button>
</div>
</form>
</div>
{/* Users Table */}
<div className="space-y-2">
<h4 className="font-semibold text-zinc-300">Registrierte Benutzer ({adminUsers.length}):</h4>
<div className="border border-border divide-y divide-border bg-canvas overflow-x-auto">
{adminUsers.map((u) => (
<div key={u.id} className="p-2.5 flex items-center justify-between gap-3 text-xs">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="font-bold text-white">{u.displayName}</span>
<span
className={`px-1.5 py-0.2 text-[10px] uppercase font-bold border ${
u.role === 'admin'
? 'bg-accent/20 border-accent/40 text-accent'
: 'bg-zinc-800 border-zinc-700 text-zinc-400'
}`}
>
{u.role}
</span>
{!u.isActive && (
<span className="px-1.5 py-0.2 text-[10px] bg-red-900/30 border border-red-500/40 text-red-300">
Deaktiviert
</span>
)}
</div>
<p className="text-zinc-500 text-[11px] font-mono">{u.email}</p>
{/* Reset password inline form */}
{resetPwdUserId === u.id && (
<div className="mt-2 flex items-center gap-1.5 bg-panel p-1.5 border border-border">
<input
type="password"
value={resetPwdVal}
onChange={(e) => setResetPwdVal(e.target.value)}
placeholder="Neues Passwort…"
className="h-6 px-2 input-field text-xs text-white"
/>
<button
onClick={() => handleConfirmResetPassword(u.id)}
className="btn-primary h-6 px-2 text-[10px]"
>
Speichern
</button>
<button
onClick={() => setResetPwdUserId(null)}
className="btn-action h-6 px-1.5 text-[10px]"
>
Abbrechen
</button>
</div>
)}
</div>
<div className="flex items-center gap-1.5 flex-shrink-0">
<button
onClick={() => handleToggleUserActive(u)}
className="btn-action h-7 px-2 text-[11px]"
title="Aktiv / Deaktivieren"
>
{u.isActive ? 'Deaktivieren' : 'Aktivieren'}
</button>
<button
onClick={() => handleToggleUserRole(u)}
className="btn-action h-7 px-2 text-[11px]"
title="Rolle wechseln"
>
{u.role === 'admin' ? 'Zu Member' : 'Zu Admin'}
</button>
<button
onClick={() => {
setResetPwdUserId(u.id);
setResetPwdVal('');
}}
className="btn-action h-7 px-2 text-[11px]"
title="Passwort zurücksetzen"
>
<Lock className="w-3 h-3 text-zinc-400" />
</button>
{u.id !== currentUser.id && (
<button
onClick={() => handleAdminDeleteUser(u.id)}
className="btn-icon btn-icon-danger"
title="Benutzer löschen"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
)}
</div>
</div>
))}
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
};