feat: implement enterprise sse global logs and developer mode

This commit is contained in:
SaelixCode
2026-03-09 14:02:10 -04:00
parent 7336ea896e
commit 448a64a10d
5 changed files with 236 additions and 19 deletions
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { RefreshCw, Download, Trash2, Search, Filter } from 'lucide-react';
import { apiFetch } from '@/lib/api';
interface LogEntry {
@@ -20,6 +21,10 @@ export function GlobalObservabilityView() {
const [loading, setLoading] = useState(true);
const [allStacks, setAllStacks] = useState<string[]>([]);
// Settings state
const [devMode, setDevMode] = useState(false);
const [pollRate, setPollRate] = useState(5);
// Filters
const [searchQuery, setSearchQuery] = useState('');
const [selectedStacks, setSelectedStacks] = useState<string[]>([]);
@@ -28,24 +33,24 @@ export function GlobalObservabilityView() {
const bottomRef = useRef<HTMLDivElement>(null);
const [isAutoScrollEnabled, setIsAutoScrollEnabled] = useState(true);
const fetchData = async () => {
setLoading(true);
try {
const logsRes = await fetch('/api/logs/global');
if (logsRes.ok) {
setLogs(await logsRes.json());
}
} catch (error) {
console.error('Failed to fetch global logs:', error);
} finally {
setLoading(false);
}
};
// SSE throttle buffer
const bufferRef = useRef<LogEntry[]>([]);
// Fetch settings on mount
useEffect(() => {
fetchData();
const interval = setInterval(fetchData, 10000); // Poll every 10s
return () => clearInterval(interval);
const fetchSettings = async () => {
try {
const res = await apiFetch('/settings');
if (res.ok) {
const data = await res.json();
setDevMode(data.developer_mode === '1');
setPollRate(parseInt(data.global_logs_refresh || '5', 10));
}
} catch (e) {
console.error('Failed to fetch settings:', e);
}
};
fetchSettings();
}, []);
// Fetch definitive stack list from the filesystem, independent of log data
@@ -64,6 +69,65 @@ export function GlobalObservabilityView() {
fetchStacks();
}, []);
// Data fetching: Polling (standard) vs SSE (dev mode)
useEffect(() => {
if (devMode) {
// SSE mode
const eventSource = new EventSource('/api/logs/global/stream');
eventSource.onmessage = (event) => {
try {
const entry: LogEntry = JSON.parse(event.data);
bufferRef.current.push(entry);
} catch (e) { /* ignore parse errors */ }
};
eventSource.onerror = () => {
// SSE will auto-reconnect, no action needed
};
// 500ms throttle: flush buffer into React state
const flushInterval = setInterval(() => {
if (bufferRef.current.length > 0) {
const batch = bufferRef.current.splice(0);
setLogs(prev => {
const merged = [...prev, ...batch];
// Infinite scroll: no slice limit in dev mode
merged.sort((a, b) => a.timestampMs - b.timestampMs);
return merged;
});
}
}, 500);
setLoading(false);
return () => {
eventSource.close();
clearInterval(flushInterval);
bufferRef.current = [];
};
} else {
// Standard polling mode
const fetchData = async () => {
setLoading(true);
try {
const logsRes = await fetch('/api/logs/global');
if (logsRes.ok) {
setLogs(await logsRes.json());
}
} catch (error) {
console.error('Failed to fetch global logs:', error);
} finally {
setLoading(false);
}
};
fetchData();
const interval = setInterval(fetchData, pollRate * 1000);
return () => clearInterval(interval);
}
}, [devMode, pollRate]);
const handleStackToggle = (stack: string) => {
setSelectedStacks(prev =>
prev.includes(stack) ? prev.filter(s => s !== stack) : [...prev, stack]
@@ -168,6 +232,12 @@ export function GlobalObservabilityView() {
<Button variant="outline" size="sm" onClick={handleDownload} disabled={filteredLogs.length === 0} className="h-8 text-sm px-2">
<Download className="w-3.5 h-3.5" />
</Button>
{devMode && (
<div className="flex items-center px-2 text-xs text-emerald-400 font-mono animate-pulse">
LIVE
</div>
)}
</div>
{loading && logs.length === 0 && (
+63 -3
View File
@@ -11,7 +11,8 @@ import { Label } from '@/components/ui/label';
import { Slider } from '@/components/ui/slider';
import { toast } from 'sonner';
import { apiFetch } from '@/lib/api';
import { Shield, Activity, Bell, Palette, Moon, Sun } from 'lucide-react';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Shield, Activity, Bell, Palette, Moon, Sun, Code } from 'lucide-react';
interface Agent {
type: 'discord' | 'slack' | 'webhook';
@@ -27,7 +28,7 @@ interface SettingsModalProps {
}
export function SettingsModal({ isOpen, onClose, isDarkMode, setIsDarkMode }: SettingsModalProps) {
const [activeSection, setActiveSection] = useState<'account' | 'system' | 'notifications' | 'appearance'>('account');
const [activeSection, setActiveSection] = useState<'account' | 'system' | 'notifications' | 'appearance' | 'developer'>('account');
// Auth State
const [authData, setAuthData] = useState({ oldPassword: '', newPassword: '', confirmPassword: '' });
@@ -45,7 +46,9 @@ export function SettingsModal({ isOpen, onClose, isDarkMode, setIsDarkMode }: Se
host_ram_limit: '90',
host_disk_limit: '90',
global_crash: '1',
docker_janitor_gb: '5'
docker_janitor_gb: '5',
global_logs_refresh: '5',
developer_mode: '0'
});
const [isLoading, setIsLoading] = useState(false);
@@ -255,6 +258,14 @@ export function SettingsModal({ isOpen, onClose, isDarkMode, setIsDarkMode }: Se
<Palette className="w-4 h-4 mr-2" />
Appearance
</Button>
<Button
variant={activeSection === 'developer' ? 'secondary' : 'ghost'}
className="w-full justify-start font-medium"
onClick={() => setActiveSection('developer')}
>
<Code className="w-4 h-4 mr-2" />
Developer
</Button>
</nav>
</div>
@@ -418,6 +429,55 @@ export function SettingsModal({ isOpen, onClose, isDarkMode, setIsDarkMode }: Se
</div>
)}
{activeSection === 'developer' && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold tracking-tight">Developer</h3>
<p className="text-sm text-muted-foreground">Power user settings for real-time observability and extended diagnostics.</p>
</div>
<div className="space-y-6 bg-muted/10 p-4 border border-border rounded-xl">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="developer_mode" className="text-base">Developer Mode</Label>
<p className="text-xs text-muted-foreground">Enable Real-Time Metrics & Extended Logs</p>
</div>
<Switch
id="developer_mode"
checked={settings.developer_mode === '1'}
onCheckedChange={(c) => handleSettingChange('developer_mode', c ? '1' : '0')}
/>
</div>
<div className="space-y-2 pt-4 border-t border-border">
<Label className={`text-base ${settings.developer_mode === '1' ? 'text-muted-foreground' : ''}`}>Standard Log Polling Rate</Label>
<Select
value={settings.global_logs_refresh}
onValueChange={(val) => handleSettingChange('global_logs_refresh', val)}
disabled={settings.developer_mode === '1'}
>
<SelectTrigger className="max-w-[200px]">
<SelectValue placeholder="Select rate" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1 second</SelectItem>
<SelectItem value="3">3 seconds</SelectItem>
<SelectItem value="5">5 seconds</SelectItem>
<SelectItem value="10">10 seconds</SelectItem>
</SelectContent>
</Select>
{settings.developer_mode === '1' && (
<p className="text-xs text-amber-500">SSE streaming is active polling rate is overridden by real-time streaming.</p>
)}
</div>
</div>
<div className="flex justify-end mt-4">
<Button onClick={saveSettings} disabled={isLoading}>Save Developer Settings</Button>
</div>
</div>
)}
</div>
</DialogContent>
</Dialog>