diff --git a/static/css/components/signal-timeline.css b/static/css/components/signal-timeline.css index 60ff7c0..aee831a 100644 --- a/static/css/components/signal-timeline.css +++ b/static/css/components/signal-timeline.css @@ -11,17 +11,63 @@ background: var(--bg-card, #1a1a1a); border: 1px solid var(--border-color, #333); border-radius: 6px; - padding: 12px; font-family: 'JetBrains Mono', monospace; } +.signal-timeline.collapsed .signal-timeline-body { + display: none; +} + +.signal-timeline.collapsed .signal-timeline-header { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + .signal-timeline-header { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 12px; - padding-bottom: 10px; - border-bottom: 1px solid var(--border-color, #333); + padding: 10px 12px; + cursor: pointer; + user-select: none; +} + +.signal-timeline-header:hover { + background: rgba(255, 255, 255, 0.02); +} + +.signal-timeline-body { + padding: 0 12px 12px 12px; + border-top: 1px solid var(--border-color, #333); +} + +.signal-timeline-collapse-icon { + margin-right: 8px; + font-size: 10px; + transition: transform 0.2s ease; +} + +.signal-timeline.collapsed .signal-timeline-collapse-icon { + transform: rotate(-90deg); +} + +.signal-timeline-header-stats { + display: flex; + gap: 12px; + font-size: 10px; + color: var(--text-dim, #666); +} + +.signal-timeline-header-stat { + display: flex; + align-items: center; + gap: 4px; +} + +.signal-timeline-header-stat .stat-value { + color: var(--text-primary, #fff); + font-weight: 500; } .signal-timeline-title { @@ -113,8 +159,27 @@ display: flex; flex-direction: column; gap: 2px; - max-height: 300px; + max-height: 180px; overflow-y: auto; + margin-top: 8px; +} + +.signal-timeline-lanes::-webkit-scrollbar { + width: 6px; +} + +.signal-timeline-lanes::-webkit-scrollbar-track { + background: var(--bg-secondary, #252525); + border-radius: 3px; +} + +.signal-timeline-lanes::-webkit-scrollbar-thumb { + background: var(--border-color, #444); + border-radius: 3px; +} + +.signal-timeline-lanes::-webkit-scrollbar-thumb:hover { + background: var(--text-dim, #666); } .signal-timeline-lane { @@ -315,6 +380,8 @@ margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border-color, #333); + max-height: 80px; + overflow-y: auto; } .signal-timeline-annotation { diff --git a/static/js/components/signal-timeline.js b/static/js/components/signal-timeline.js index 2f779ec..c4bca50 100644 --- a/static/js/components/signal-timeline.js +++ b/static/js/components/signal-timeline.js @@ -17,7 +17,8 @@ const SignalTimeline = (function() { '2h': 2 * 60 * 60 * 1000 }, defaultWindow: '30m', - maxSignals: 50, + maxSignals: 100, // max signals to track in memory + maxDisplayedLanes: 15, // max lanes to show at once (scroll for more) burstThreshold: 5, // messages in burst window = burst burstWindow: 60 * 1000, // 1 minute updateInterval: 5000, // refresh every 5 seconds @@ -108,9 +109,35 @@ const SignalTimeline = (function() { const windowMs = config.timeWindows['2h']; signal.events = signal.events.filter(e => now - e.timestamp < windowMs); + // Prune old signals if we exceed max + if (state.signals.size > config.maxSignals) { + pruneOldSignals(); + } + return signal; } + /** + * Remove oldest/least active signals to stay under limit + */ + function pruneOldSignals() { + const signals = Array.from(state.signals.entries()); + // Sort by last seen (oldest first), but keep flagged signals + signals.sort((a, b) => { + if (a[1].flagged && !b[1].flagged) return 1; + if (!a[1].flagged && b[1].flagged) return -1; + return a[1].lastSeen - b[1].lastSeen; + }); + + // Remove oldest signals until under limit + const toRemove = signals.length - config.maxSignals; + for (let i = 0; i < toRemove; i++) { + if (!signals[i][1].flagged) { + state.signals.delete(signals[i][0]); + } + } + } + /** * Update signal status based on activity */ @@ -229,18 +256,39 @@ const SignalTimeline = (function() { /** * Create the timeline DOM element */ - function createTimeline(containerId) { + function createTimeline(containerId, options = {}) { const container = document.getElementById(containerId); if (!container) return null; + const startCollapsed = options.collapsed !== false; + const timeline = document.createElement('div'); - timeline.className = 'signal-timeline'; + timeline.className = 'signal-timeline' + (startCollapsed ? ' collapsed' : ''); timeline.id = 'signalTimeline'; timeline.innerHTML = ` -