From b2674080c4a28888b5fbe98b085ec53c93f19aac Mon Sep 17 00:00:00 2001 From: SaelixCode Date: Mon, 9 Mar 2026 12:21:12 -0400 Subject: [PATCH] fix: implement smart auto-scroll and definitive stack filtering in global logs --- CHANGELOG.md | 2 + backend/src/index.ts | 4 +- .../components/GlobalObservabilityView.tsx | 46 +++++++++++++------ 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60b695f5..12b7021e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- **Fixed:** Global Logs aggressive auto-scrolling preventing users from reading log history. +- **Fixed:** Quiet stacks missing from the Global Logs filter dropdown by fetching the definitive stack list independently. - **Fixed:** Global logs misclassifying INFO messages as errors due to naive string matching. - **Changed:** Global logs now display chronologically (newest at bottom) with smooth auto-scrolling. - **Changed:** Renamed Observability navigation tab to Logs. diff --git a/backend/src/index.ts b/backend/src/index.ts index 4f6e6f5c..b9019dc6 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -839,7 +839,9 @@ app.get('/api/logs/global', async (req: Request, res: Response) => { payload.split('\n').forEach(line => parseAndPushLog(line, streamType === 2 ? 'STDERR' : 'STDOUT')); } } - } catch (err) { /* ignore */ } + } catch (err) { + console.warn(`[GlobalLogs] Failed to fetch/parse logs for container ${containerName} (${c.Id.substring(0, 12)}):`, (err as Error).message); + } })); // Sort globally by timestamp ascending (newest bottom) and limit to 2000 lines diff --git a/frontend/src/components/GlobalObservabilityView.tsx b/frontend/src/components/GlobalObservabilityView.tsx index ce0756c3..45148011 100644 --- a/frontend/src/components/GlobalObservabilityView.tsx +++ b/frontend/src/components/GlobalObservabilityView.tsx @@ -1,10 +1,10 @@ -import { useEffect, useState, useMemo, useRef } from 'react'; +import { useEffect, useState, useMemo, useRef, useCallback } from 'react'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; 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 { ScrollArea } from '@/components/ui/scroll-area'; + interface LogEntry { stackName: string; @@ -18,6 +18,7 @@ interface LogEntry { export function GlobalObservabilityView() { const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(true); + const [allStacks, setAllStacks] = useState([]); // Filters const [searchQuery, setSearchQuery] = useState(''); @@ -25,6 +26,7 @@ export function GlobalObservabilityView() { const [streamFilter, setStreamFilter] = useState<'ALL' | 'STDOUT' | 'STDERR'>('ALL'); const [clearedAt, setClearedAt] = useState(0); const bottomRef = useRef(null); + const [isAutoScrollEnabled, setIsAutoScrollEnabled] = useState(true); const fetchData = async () => { setLoading(true); @@ -46,11 +48,21 @@ export function GlobalObservabilityView() { return () => clearInterval(interval); }, []); - - const uniqueStacks = useMemo(() => { - const stacks = new Set(logs.map(l => l.stackName)); - return Array.from(stacks).sort(); - }, [logs]); + // Fetch definitive stack list from the filesystem, independent of log data + useEffect(() => { + const fetchStacks = async () => { + try { + const res = await fetch('/api/stacks'); + if (res.ok) { + const stacks: string[] = await res.json(); + setAllStacks(stacks.sort()); + } + } catch (err) { + console.error('Failed to fetch stacks:', err); + } + }; + fetchStacks(); + }, []); const handleStackToggle = (stack: string) => { setSelectedStacks(prev => @@ -78,8 +90,16 @@ export function GlobalObservabilityView() { }, [logs, selectedStacks, streamFilter, searchQuery, clearedAt]); useEffect(() => { - bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [filteredLogs]); + if (isAutoScrollEnabled) { + bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); + } + }, [filteredLogs, isAutoScrollEnabled]); + + const handleScroll = useCallback((e: React.UIEvent) => { + const target = e.currentTarget; + const isAtBottom = target.scrollHeight - target.scrollTop <= target.clientHeight + 50; + setIsAutoScrollEnabled(isAtBottom); + }, []); const handleDownload = () => { if (filteredLogs.length === 0) return; @@ -116,7 +136,7 @@ export function GlobalObservabilityView() { - {uniqueStacks.map(stack => ( + {allStacks.map(stack => ( ))} - {uniqueStacks.length === 0 && ( + {allStacks.length === 0 && (
No stacks found
)}
@@ -156,7 +176,7 @@ export function GlobalObservabilityView() { )} - +
{filteredLogs.length > 0 ? ( <> {filteredLogs.map((log, idx) => ( @@ -174,7 +194,7 @@ export function GlobalObservabilityView() { {logs.length === 0 ? "No active logs found." : "No logs match the current filters."}
)} -
+ ); }