good layout

This commit is contained in:
2025-07-28 11:06:10 -04:00
parent a3c50fd845
commit 2ab3f1fe9e
7 changed files with 482 additions and 20 deletions

View File

@@ -20,6 +20,7 @@ from .widgets.sparkline import SparklineWidget
from .widgets.metric_card import MetricCard
from .widgets.flow_table_v2 import EnhancedFlowTable
from .widgets.split_flow_details import FlowMainDetailsPanel, SubFlowDetailsPanel
from .widgets.debug_panel import DebugPanel
if TYPE_CHECKING:
from ...analysis.core import EthernetAnalyzer
@@ -59,6 +60,7 @@ class StreamLensAppV2(App):
bytes_per_sec = reactive(0.0)
enhanced_flows = reactive(0)
outlier_count = reactive(0)
debug_visible = reactive(False) # Hide debug panel for now
# Update timers
metric_timer: Timer = None
@@ -90,7 +92,7 @@ class StreamLensAppV2(App):
yield MetricCard("Enhanced", f"{self.enhanced_flows}", color="success", id="enhanced-metric")
yield MetricCard("Outliers", f"{self.outlier_count}", color="warning" if self.outlier_count > 0 else "normal", id="outliers-metric")
# Main content area with 3 clean panels
# Main content area with conditional debug panel
with Horizontal(id="content-area"):
# Left - Enhanced flow table
yield EnhancedFlowTable(
@@ -99,15 +101,32 @@ class StreamLensAppV2(App):
classes="panel-wide"
)
# Right top - Main flow details
with Vertical(id="right-panels"):
# Middle - Flow details
with Vertical(id="flow-panels"):
yield FlowMainDetailsPanel(id="main-flow-details")
yield SubFlowDetailsPanel(id="sub-flow-details")
# Right - Debug panel (conditionally visible)
if self.debug_visible:
yield DebugPanel(id="debug-panel")
yield Footer()
def on_mount(self) -> None:
"""Initialize the application with TipTop-style updates"""
try:
debug_panel = self.query_one("#debug-panel", DebugPanel)
debug_panel.add_debug_message("APP: Application mounted, checking panels...")
try:
main_panel = self.query_one("#main-flow-details", FlowMainDetailsPanel)
sub_panel = self.query_one("#sub-flow-details", SubFlowDetailsPanel)
debug_panel.add_debug_message("APP: Both panels found successfully")
except Exception as e:
debug_panel.add_debug_message(f"APP: Panel query failed: {e}")
except:
pass # Debug panel not visible
self.update_metrics()
# Set up update intervals like TipTop
@@ -239,6 +258,13 @@ class StreamLensAppV2(App):
def on_enhanced_flow_table_flow_selected(self, event: EnhancedFlowTable.FlowSelected) -> None:
"""Handle flow selection events"""
try:
debug_panel = self.query_one("#debug-panel", DebugPanel)
flow_info = f"{event.flow.src_ip}:{event.flow.src_port}" if event.flow else "None"
debug_panel.add_debug_message(f"APP: Flow selected - {flow_info}, subflow={event.subflow_type}")
except:
pass # Debug panel not visible
if event.flow:
# Update main flow details panel
main_panel = self.query_one("#main-flow-details", FlowMainDetailsPanel)

View File

@@ -60,9 +60,9 @@ MetricCard {
padding: 0;
}
/* Right Panels - Details (compact) */
#right-panels {
width: 25%;
/* Flow Panels - Details (30% width) */
#flow-panels {
width: 30%;
background: #1a1a1a;
padding: 0;
}
@@ -79,6 +79,12 @@ SubFlowDetailsPanel {
border: solid #ff8800;
}
/* Debug Panel - Fixed width when visible */
#debug-panel {
width: 25%;
background: #1a1a1a;
}
/* Sparkline Charts */
SparklineWidget {
height: 5;

View File

@@ -0,0 +1,69 @@
"""
Debug Panel - Real-time debugging information in TUI
"""
from textual.widget import Widget
from textual.containers import Vertical
from textual.widgets import Static
from rich.text import Text
from rich.console import RenderableType
from typing import Optional, List
from datetime import datetime
class DebugPanel(Vertical):
"""Debug panel showing real-time flow selection and logic information"""
DEFAULT_CSS = """
DebugPanel {
height: 1fr;
padding: 1;
background: #1a1a1a;
border: solid #ff0080;
}
DebugPanel Static {
margin-bottom: 0;
}
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.debug_messages = []
self.max_messages = 20
def compose(self):
"""Create the debug panel layout"""
yield Static("DEBUG PANEL", classes="panel-header")
yield Static(
"Waiting for flow selection...",
id="debug-content"
)
def add_debug_message(self, message: str) -> None:
"""Add a debug message with timestamp"""
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
full_message = f"[{timestamp}] {message}"
self.debug_messages.append(full_message)
if len(self.debug_messages) > self.max_messages:
self.debug_messages.pop(0)
self._update_display()
def _update_display(self) -> None:
"""Update the debug display with recent messages"""
content_widget = self.query_one("#debug-content", Static)
if not self.debug_messages:
content_widget.update("No debug messages yet...")
return
# Show recent messages, newest at bottom
display_text = "\n".join(self.debug_messages[-15:]) # Show last 15 messages
content_widget.update(Text(display_text, style="white"))
def clear_messages(self) -> None:
"""Clear all debug messages"""
self.debug_messages.clear()
self._update_display()

View File

@@ -328,7 +328,7 @@ class EnhancedFlowTable(Vertical):
subrows = []
combinations = self._get_protocol_frame_combinations(flow)
for extended_proto, frame_type, count, percentage in combinations[:3]: # Max 3 subrows
for extended_proto, frame_type, count, percentage in combinations: # Show all subrows
# Calculate timing for this frame type if available
frame_delta_t = ""
frame_sigma = ""
@@ -436,6 +436,18 @@ class EnhancedFlowTable(Vertical):
"""Handle row highlight to update selection"""
selected_flow = self.get_selected_flow()
subflow_type = self.get_selected_subflow_type()
# Debug through app's debug panel
flow_info = f"{selected_flow.src_ip}:{selected_flow.src_port}" if selected_flow else "None"
table = self.query_one("#flows-data-table", DataTable)
current_row = table.cursor_row if table.cursor_row is not None else -1
try:
debug_panel = self.app.query_one("#debug-panel")
debug_panel.add_debug_message(f"TABLE: Row {current_row} - {flow_info}, subflow:{subflow_type}")
except:
pass # Debug panel might not be available yet
self.post_message(self.FlowSelected(selected_flow, subflow_type))
# Helper methods from original implementation

View File

@@ -44,6 +44,13 @@ class FlowMainDetailsPanel(Vertical):
def update_flow(self, flow: Optional['FlowStats']) -> None:
"""Update panel with main flow details"""
flow_info = f"{flow.src_ip}:{flow.src_port}" if flow else "None"
try:
debug_panel = self.app.query_one("#debug-panel")
debug_panel.add_debug_message(f"MAIN_PANEL: Update called - {flow_info}")
except:
pass
self.current_flow = flow
content_widget = self.query_one("#main-details-content", Static)
@@ -52,6 +59,14 @@ class FlowMainDetailsPanel(Vertical):
return
details = self._create_main_flow_details(flow)
# Debug what content we're actually setting
try:
debug_panel = self.app.query_one("#debug-panel")
debug_panel.add_debug_message(f"CONTENT: Setting panel content for {flow.src_ip}:{flow.src_port}")
except:
pass
content_widget.update(details)
def _create_main_flow_details(self, flow: 'FlowStats') -> RenderableType:
@@ -91,21 +106,57 @@ class FlowMainDetailsPanel(Vertical):
sections.append(Text("Enhanced Analysis", style="bold green"))
sections.append(enhanced_table)
# Timing analysis with new columns
timing_table = Table(show_header=False, box=None, padding=0)
timing_table.add_column(style="dim", width=12)
timing_table.add_column()
# Timing analysis - only show if no sub-flows exist
# Match the same logic as _should_show_subrows in flow_table_v2.py
has_subflows = (len(flow.frame_types) > 1 or
flow.enhanced_analysis.decoder_type != "Standard")
timing_table.add_row("Duration:", f"{flow.duration:.2f}s")
timing_table.add_row("Avg ΔT:", f"{flow.avg_inter_arrival * 1000:.1f}ms")
timing_table.add_row("Std σ:", f"{flow.std_inter_arrival * 1000:.1f}ms")
timing_table.add_row("Outliers:", f"{len(flow.outlier_frames)}")
timing_table.add_row("Jitter:", f"{flow.jitter * 1000:.2f}ms")
timing_table.add_row("First Seen:", self._format_timestamp(flow.first_seen))
timing_table.add_row("Last Seen:", self._format_timestamp(flow.last_seen))
# Debug output
try:
debug_panel = self.app.query_one("#debug-panel")
debug_panel.add_debug_message(f"TIMING_LOGIC: {flow.src_ip}:{flow.src_port} - types={len(flow.frame_types)}, decoder={flow.enhanced_analysis.decoder_type}, has_subflows={has_subflows}")
except:
pass
sections.append(Text("Timing Analysis", style="bold cyan"))
sections.append(timing_table)
if not has_subflows:
try:
debug_panel = self.app.query_one("#debug-panel")
debug_panel.add_debug_message(f"BRANCH: Taking FULL timing branch for {flow.src_ip}:{flow.src_port}")
except:
pass
timing_table = Table(show_header=False, box=None, padding=0)
timing_table.add_column(style="dim", width=12)
timing_table.add_column()
timing_table.add_row("Duration:", f"{flow.duration:.2f}s")
timing_table.add_row("Avg ΔT:", f"{flow.avg_inter_arrival * 1000:.1f}ms")
timing_table.add_row("Std σ:", f"{flow.std_inter_arrival * 1000:.1f}ms")
timing_table.add_row("Outliers:", f"{len(flow.outlier_frames)}")
timing_table.add_row("Jitter:", f"{flow.jitter * 1000:.2f}ms")
timing_table.add_row("First Seen:", self._format_timestamp(flow.first_seen))
timing_table.add_row("Last Seen:", self._format_timestamp(flow.last_seen))
sections.append(Text("Timing Analysis", style="bold cyan"))
sections.append(timing_table)
else:
try:
debug_panel = self.app.query_one("#debug-panel")
debug_panel.add_debug_message(f"BRANCH: Taking BASIC timeline branch for {flow.src_ip}:{flow.src_port}")
except:
pass
# Just show duration and timestamps for flows with sub-flows
basic_timing_table = Table(show_header=False, box=None, padding=0)
basic_timing_table.add_column(style="dim", width=12)
basic_timing_table.add_column()
basic_timing_table.add_row("Duration:", f"{flow.duration:.2f}s")
basic_timing_table.add_row("First Seen:", self._format_timestamp(flow.first_seen))
basic_timing_table.add_row("Last Seen:", self._format_timestamp(flow.last_seen))
sections.append(Text("Flow Timeline", style="bold cyan"))
sections.append(basic_timing_table)
return Group(*sections)