""" Left panel - Flow list with frame type breakdowns """ from typing import List, Optional import curses from ...models import FlowStats class FlowListPanel: """Left panel showing flows and frame type breakdowns""" def __init__(self): self.selected_item = 0 self.scroll_offset = 0 def draw(self, stdscr, x_offset: int, y_offset: int, width: int, height: int, flows_list: List[FlowStats], selected_flow: int): """Draw the flow list panel""" # Draw flows table header with enhanced analysis columns stdscr.addstr(y_offset, x_offset, "FLOWS (Enhanced Analysis):", curses.A_BOLD) headers = f"{'Src:Port':22} {'Dst:Port':22} {'Proto':6} {'Cast':5} {'#Frames':>7} {'Bytes':>7} {'Encoding':12} {'Quality':>7} {'Drift':>8} {'ΔT Avg':>9}" stdscr.addstr(y_offset + 1, x_offset, headers[:width-1], curses.A_UNDERLINE) # Calculate scrolling parameters start_row = y_offset + 2 max_rows = height - 3 # Account for header and title total_items = self._get_total_display_items(flows_list) # Calculate scroll offset to keep selected item visible scroll_offset = self._calculate_scroll_offset(selected_flow, max_rows, total_items) # Draw flows list with frame type breakdowns current_row = start_row display_item = 0 # Track selectable items (flows + frame types) visible_items = 0 # Track items actually drawn for flow_idx, flow in enumerate(flows_list): # Check if main flow line should be displayed if display_item >= scroll_offset and visible_items < max_rows: # Draw main flow line with new column layout src_endpoint = f"{flow.src_ip}:{flow.src_port}" if flow.src_port > 0 else flow.src_ip dst_endpoint = f"{flow.dst_ip}:{flow.dst_port}" if flow.dst_port > 0 else flow.dst_ip # Format bytes with K/M suffix bytes_str = self._format_bytes(flow.total_bytes) # Get encoding information (primary detected protocol) encoding_str = self._get_encoding_display(flow) # Format average time avg_time = f"{flow.avg_inter_arrival:.3f}s" if flow.avg_inter_arrival > 0 else "N/A" # Abbreviate traffic classification cast_abbrev = flow.traffic_classification[:4] if flow.traffic_classification != "Unknown" else "Unk" # Enhanced analysis data quality_str = self._format_quality_score(flow) drift_str = self._format_drift_info(flow) line = f"{src_endpoint:22} {dst_endpoint:22} {flow.transport_protocol:6} {cast_abbrev:5} {flow.frame_count:>7} {bytes_str:>7} {encoding_str:12} {quality_str:>7} {drift_str:>8} {avg_time:>9}" if display_item == selected_flow: stdscr.addstr(current_row, x_offset, line[:width-1], curses.A_REVERSE) else: stdscr.addstr(current_row, x_offset, line[:width-1], curses.A_BOLD) current_row += 1 visible_items += 1 display_item += 1 # Draw frame type breakdowns for this flow if flow.frame_types: sorted_frame_types = sorted(flow.frame_types.items(), key=lambda x: x[1].count, reverse=True) for frame_type, ft_stats in sorted_frame_types: if display_item >= scroll_offset and visible_items < max_rows: # Calculate frame type timing display ft_avg = f"{ft_stats.avg_inter_arrival:.3f}s" if ft_stats.avg_inter_arrival > 0 else "N/A" outlier_count = len(ft_stats.outlier_details) if ft_stats.outlier_details else 0 # Create frame type line aligned with enhanced column layout bytes_str_ft = self._format_bytes(ft_stats.total_bytes) # Enhanced analysis for frame types (inherit from parent flow) quality_str_ft = self._format_quality_score(flow) if frame_type.startswith('CH10') or frame_type == 'TMATS' else "" drift_str_ft = self._format_drift_info(flow) if frame_type.startswith('CH10') or frame_type == 'TMATS' else "" ft_line = f" └─{frame_type:18} {'':22} {'':6} {'':5} {ft_stats.count:>7} {bytes_str_ft:>7} {'':12} {quality_str_ft:>7} {drift_str_ft:>8} {ft_avg:>9}" if display_item == selected_flow: stdscr.addstr(current_row, x_offset, ft_line[:width-1], curses.A_REVERSE) else: stdscr.addstr(current_row, x_offset, ft_line[:width-1]) current_row += 1 visible_items += 1 display_item += 1 def _get_protocol_display(self, flow: FlowStats) -> str: """Get display string for flow protocols""" if flow.detected_protocol_types: # Prioritize specialized protocols specialized = {'CHAPTER10', 'PTP', 'IENA'} found_specialized = flow.detected_protocol_types & specialized if found_specialized: return list(found_specialized)[0] # Use first detected protocol type return list(flow.detected_protocol_types)[0] # Fallback to basic protocols if flow.protocols: return list(flow.protocols)[0] return "Unknown" def _get_total_display_items(self, flows_list: List[FlowStats]) -> int: """Calculate total number of selectable items (flows + frame types)""" total = 0 for flow in flows_list: total += 1 # Flow itself total += len(flow.frame_types) # Frame types under this flow return total def _calculate_scroll_offset(self, selected_item: int, max_visible: int, total_items: int) -> int: """Calculate scroll offset to keep selected item visible""" if total_items <= max_visible: return 0 # No scrolling needed # Keep selected item in the middle third of visible area when possible middle_position = max_visible // 3 # Calculate ideal scroll offset scroll_offset = max(0, selected_item - middle_position) # Ensure we don't scroll past the end max_scroll = max(0, total_items - max_visible) scroll_offset = min(scroll_offset, max_scroll) return scroll_offset def get_total_display_items(self, flows_list: List[FlowStats]) -> int: """Public method to get total display items""" return self._get_total_display_items(flows_list) def _format_bytes(self, bytes_count: int) -> str: """Format byte count with K/M/G suffixes, always include magnitude indicator""" if bytes_count >= 1_000_000_000: return f"{bytes_count / 1_000_000_000:.1f}G" elif bytes_count >= 1_000_000: return f"{bytes_count / 1_000_000:.1f}M" elif bytes_count >= 1_000: return f"{bytes_count / 1_000:.1f}K" else: return f"{bytes_count}B" # Add "B" for plain bytes def _get_encoding_display(self, flow: FlowStats) -> str: """Get the primary encoding/application protocol for display""" # Prioritize specialized protocols (Chapter 10, PTP, IENA) if flow.detected_protocol_types: specialized = {'CH10', 'PTP', 'IENA', 'Chapter10', 'TMATS'} found_specialized = flow.detected_protocol_types.intersection(specialized) if found_specialized: return list(found_specialized)[0] # Use first detected protocol type return list(flow.detected_protocol_types)[0] # Fallback to frame types if available if flow.frame_types: frame_types = list(flow.frame_types.keys()) # Look for interesting frame types first priority_types = ['TMATS', 'CH10-Data', 'PTP-Sync', 'IENA-P', 'IENA-D'] for ptype in priority_types: if ptype in frame_types: return ptype return frame_types[0] # Last resort - check basic protocols if flow.protocols: app_protocols = {'DNS', 'HTTP', 'HTTPS', 'NTP', 'DHCP'} found_app = flow.protocols.intersection(app_protocols) if found_app: return list(found_app)[0] return "Unknown" def _format_quality_score(self, flow: FlowStats) -> str: """Format quality score for display""" enhanced = flow.enhanced_analysis if enhanced.decoder_type == "Standard" or enhanced.avg_frame_quality == 0: return "N/A" # Format quality as percentage quality = enhanced.avg_frame_quality if quality >= 90: return f"{quality:.0f}%" elif quality >= 70: return f"{quality:.0f}%" else: return f"{quality:.0f}%" def _format_drift_info(self, flow: FlowStats) -> str: """Format clock drift information for display""" enhanced = flow.enhanced_analysis if not enhanced.has_internal_timing or enhanced.avg_clock_drift_ppm == 0: return "N/A" # Format drift in PPM drift_ppm = abs(enhanced.avg_clock_drift_ppm) if drift_ppm >= 1000: return f"{drift_ppm/1000:.1f}K" # Show in thousands elif drift_ppm >= 100: return f"{drift_ppm:.0f}ppm" elif drift_ppm >= 10: return f"{drift_ppm:.1f}ppm" else: return f"{drift_ppm:.2f}ppm"