""" Right panel - Flow details with frame type table """ from typing import List, Optional, Tuple import curses from ...models import FlowStats, FrameTypeStats class DetailPanel: """Right panel showing detailed flow information""" def draw(self, stdscr, x_offset: int, y_offset: int, width: int, flows_list: List[FlowStats], selected_flow: int, max_height: Optional[int] = None): """Draw detailed information panel for selected flow or frame type""" if not flows_list: stdscr.addstr(y_offset, x_offset, "No flows available") return # Get the selected flow and frame type flow, selected_frame_type = self._get_selected_flow_and_frame_type(flows_list, selected_flow) if not flow: stdscr.addstr(y_offset, x_offset, "No flow selected") return if max_height is None: height, _ = stdscr.getmaxyx() max_lines = height - y_offset - 2 else: max_lines = y_offset + max_height try: # ALWAYS show flow details first stdscr.addstr(y_offset, x_offset, f"FLOW DETAILS: {flow.src_ip} -> {flow.dst_ip}", curses.A_BOLD) y_offset += 2 stdscr.addstr(y_offset, x_offset, f"Packets: {flow.frame_count} | Bytes: {flow.total_bytes:,}") y_offset += 1 # Frame types table if flow.frame_types and y_offset < max_lines: y_offset += 1 stdscr.addstr(y_offset, x_offset, "Frame Types:", curses.A_BOLD) y_offset += 1 # Table header header = f"{'Type':<12} {'#Pkts':<6} {'Bytes':<8} {'Avg ΔT':<8} {'2σ Out':<6}" stdscr.addstr(y_offset, x_offset, header, curses.A_UNDERLINE) y_offset += 1 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 y_offset >= max_lines: break avg_str = f"{ft_stats.avg_inter_arrival:.3f}s" if ft_stats.avg_inter_arrival > 0 else "N/A" bytes_str = f"{ft_stats.total_bytes:,}" if ft_stats.total_bytes < 10000 else f"{ft_stats.total_bytes/1000:.1f}K" outliers_count = len(ft_stats.outlier_details) if ft_stats.outlier_details else 0 # Truncate frame type name if too long type_name = frame_type[:11] if len(frame_type) > 11 else frame_type ft_line = f"{type_name:<12} {ft_stats.count:<6} {bytes_str:<8} {avg_str:<8} {outliers_count:<6}" stdscr.addstr(y_offset, x_offset, ft_line) y_offset += 1 # Timing statistics if y_offset < max_lines: y_offset += 1 stdscr.addstr(y_offset, x_offset, "Timing:", curses.A_BOLD) y_offset += 1 if flow.avg_inter_arrival > 0: stdscr.addstr(y_offset, x_offset + 2, f"Avg: {flow.avg_inter_arrival:.6f}s") y_offset += 1 if y_offset < max_lines: stdscr.addstr(y_offset, x_offset + 2, f"Std: {flow.std_inter_arrival:.6f}s") y_offset += 1 else: stdscr.addstr(y_offset, x_offset + 2, "No timing data") y_offset += 1 # Display outlier frame details for each frame type if flow.frame_types and y_offset < max_lines: outlier_frame_types = [(frame_type, ft_stats) for frame_type, ft_stats in flow.frame_types.items() if ft_stats.outlier_details] if outlier_frame_types: y_offset += 1 if y_offset < max_lines: stdscr.addstr(y_offset, x_offset, "Outlier Frames:", curses.A_BOLD) y_offset += 1 for frame_type, ft_stats in outlier_frame_types: if y_offset >= max_lines: break # Display frame type header if y_offset < max_lines: stdscr.addstr(y_offset, x_offset + 2, f"{frame_type}:", curses.A_UNDERLINE) y_offset += 1 # Display outlier details as individual table rows in format "frame# | deltaT" for frame_num, frame_inter_arrival_time in ft_stats.outlier_details: if y_offset >= max_lines: break outlier_line = f"{frame_num} | {frame_inter_arrival_time:.3f}s" stdscr.addstr(y_offset, x_offset + 4, outlier_line) y_offset += 1 # If a frame type is selected, show additional frame type specific details if selected_frame_type and selected_frame_type in flow.frame_types and y_offset < max_lines: ft_stats = flow.frame_types[selected_frame_type] # Add separator y_offset += 2 if y_offset < max_lines: stdscr.addstr(y_offset, x_offset, "─" * min(width-2, 40)) y_offset += 1 # Frame type specific header if y_offset < max_lines: stdscr.addstr(y_offset, x_offset, f"FRAME TYPE: {selected_frame_type}", curses.A_BOLD) y_offset += 2 # Frame type specific info if y_offset < max_lines: stdscr.addstr(y_offset, x_offset, f"Count: {ft_stats.count}") y_offset += 1 if y_offset < max_lines: stdscr.addstr(y_offset, x_offset, f"Bytes: {ft_stats.total_bytes:,}") y_offset += 1 # Frame type timing if y_offset < max_lines: y_offset += 1 stdscr.addstr(y_offset, x_offset, "Timing:", curses.A_BOLD) y_offset += 1 if ft_stats.avg_inter_arrival > 0: if y_offset < max_lines: stdscr.addstr(y_offset, x_offset + 2, f"Avg: {ft_stats.avg_inter_arrival:.6f}s") y_offset += 1 if y_offset < max_lines: stdscr.addstr(y_offset, x_offset + 2, f"Std: {ft_stats.std_inter_arrival:.6f}s") y_offset += 1 else: if y_offset < max_lines: stdscr.addstr(y_offset, x_offset + 2, "No timing data") y_offset += 1 except curses.error: # Ignore curses errors from writing outside screen bounds pass def _get_selected_flow_and_frame_type(self, flows_list: List[FlowStats], selected_flow: int) -> Tuple[Optional[FlowStats], Optional[str]]: """Get the currently selected flow and frame type based on selection index""" current_item = 0 for flow in flows_list: if current_item == selected_flow: return flow, None # Selected the main flow current_item += 1 # Check frame types 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 current_item == selected_flow: return flow, frame_type # Selected a frame type current_item += 1 # Fallback to first flow if selection is out of bounds return flows_list[0] if flows_list else None, None