feat(phase2): bulk delete + audit log; update roadmap/parity docs

This commit is contained in:
2026-06-30 10:20:57 -04:00
parent d3b7f39b9e
commit d7c84647e5
12 changed files with 660 additions and 77 deletions
+129
View File
@@ -0,0 +1,129 @@
import { useEffect, useState } from 'react';
import { api } from '../lib/api.ts';
import { useServerStore } from '../stores/server.ts';
interface ServerSettingsModalProps {
serverId: string;
onClose: () => void;
}
interface AuditEntry {
id: string;
server_id: string;
user_id: string | null;
username: string | null;
action_type: string;
target_type: string | null;
target_id: string | null;
reason: string | null;
changes: unknown;
created_at: string;
}
const ACTION_LABELS: Record<string, string> = {
KICK: 'kicked member',
BAN: 'banned member',
UNBAN: 'unbanned member',
MUTE: 'muted member',
UNMUTE: 'unmuted member',
ROLE_CREATE: 'created role',
ROLE_UPDATE: 'updated role',
ROLE_DELETE: 'deleted role',
MEMBER_ROLES_UPDATE: 'updated member roles',
CHANNEL_CREATE: 'created channel',
CHANNEL_UPDATE: 'updated channel',
CHANNEL_DELETE: 'deleted channel',
SERVER_UPDATE: 'updated server',
MESSAGE_DELETE: 'deleted message',
BULK_DELETE: 'bulk deleted messages',
};
export function ServerSettingsModal({ serverId, onClose }: ServerSettingsModalProps) {
const [tab, setTab] = useState<'audit'>('audit');
const [entries, setEntries] = useState<AuditEntry[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const servers = useServerStore((s) => s.servers);
const server = servers.find((s) => s.id === serverId);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
onClose();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose]);
useEffect(() => {
if (tab !== 'audit') return;
setLoading(true);
api.get<AuditEntry[]>(`/servers/${serverId}/audit-log`)
.then((data) => setEntries(Array.isArray(data) ? data : []))
.catch((err) => setError(err instanceof Error ? err.message : 'Failed to load audit log'))
.finally(() => setLoading(false));
}, [tab, serverId]);
const formatTime = (iso: string) => {
const d = new Date(iso);
return d.toLocaleString();
};
return (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
onClick={onClose}
>
<div
className="bg-gb-bg border border-gb-bg-t w-[700px] max-h-[80vh] flex flex-col font-mono"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between px-4 py-3 border-b border-gb-bg-t">
<span className="text-sm text-gb-orange">SERVER SETTINGS: {server?.name ?? serverId}</span>
<button onClick={onClose} className="text-xs text-gb-red">[x]</button>
</div>
<div className="flex border-b border-gb-bg-t">
<button
onClick={() => setTab('audit')}
className={`px-4 py-2 text-xs ${tab === 'audit' ? 'text-gb-orange bg-gb-bg-s' : 'text-gb-fg-f hover:text-gb-orange'}`}
>
[AUDIT LOG]
</button>
</div>
<div className="flex-1 overflow-y-auto p-4">
{tab === 'audit' && (
<>
{loading && <p className="text-gb-fg-f text-xs">[loading...]</p>}
{error && <p className="text-gb-red text-xs">ERR: {error}</p>}
{!loading && !error && entries.length === 0 && (
<p className="text-gb-fg-f text-xs">[no audit log entries]</p>
)}
<div className="space-y-2">
{entries.map((entry) => (
<div key={entry.id} className="text-xs border border-gb-bg-t p-2 bg-gb-bg-s">
<div className="flex items-center justify-between text-gb-fg-f">
<span>{formatTime(entry.created_at)}</span>
<span className="text-gb-orange">{entry.action_type}</span>
</div>
<div className="text-gb-fg mt-1">
<span className="text-gb-aqua">{entry.username ?? entry.user_id ?? 'system'}</span>
{' '}<span className="text-gb-fg-s">{ACTION_LABELS[entry.action_type] ?? entry.action_type}</span>
{entry.target_id && (
<span className="text-gb-fg-f"> target:{entry.target_id}</span>
)}
</div>
{entry.reason && (
<div className="text-gb-fg-f mt-1">reason: {entry.reason}</div>
)}
</div>
))}
</div>
</>
)}
</div>
</div>
</div>
);
}