""" Right panel - Flow details with frame type table """ from typing import List, Optional, Tuple, Dict import curses from ...models import FlowStats, FrameTypeStats class DetailPanel: """Right panel showing detailed flow information with tabs""" def __init__(self): self.active_tab = 0 # 0 = Info, 1 = Decode self.tabs = ["Info", "Decode"] 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: # Draw tab bar tab_y = y_offset self._draw_tab_bar(stdscr, x_offset, tab_y, width) y_offset += 2 # Space for tab bar max_lines -= 2 # Draw content based on active tab if self.active_tab == 0: self._draw_info_tab(stdscr, x_offset, y_offset, width, max_lines, flow, selected_frame_type) elif self.active_tab == 1: self._draw_decode_tab(stdscr, x_offset, y_offset, width, max_lines, flow, selected_frame_type) except curses.error: # Ignore curses errors from writing outside screen bounds pass def _draw_tab_bar(self, stdscr, x_offset: int, y_offset: int, width: int): """Draw the tab bar at the top of the panel""" tab_line = "" for i, tab_name in enumerate(self.tabs): if i == self.active_tab: tab_line += f"[{tab_name}]" else: tab_line += f" {tab_name} " if i < len(self.tabs) - 1: tab_line += " " # Add tab navigation hint tab_line += f" {' ' * (width - len(tab_line) - 15)}[Tab] to switch" stdscr.addstr(y_offset, x_offset, tab_line[:width-1], curses.A_BOLD) stdscr.addstr(y_offset + 1, x_offset, "─" * min(width-1, 50)) def _draw_info_tab(self, stdscr, x_offset: int, y_offset: int, width: int, max_lines: int, flow: FlowStats, selected_frame_type: Optional[str]): """Draw the Info tab content (original detail panel content)""" 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 # Enhanced analysis information if flow.enhanced_analysis.decoder_type != "Standard": y_offset += 1 self._draw_enhanced_analysis(stdscr, x_offset, y_offset, width, flow) y_offset += self._get_enhanced_analysis_lines(flow) # 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 def _draw_enhanced_analysis(self, stdscr, x_offset: int, y_offset: int, width: int, flow: FlowStats): """Draw enhanced analysis information""" enhanced = flow.enhanced_analysis try: # Enhanced analysis header stdscr.addstr(y_offset, x_offset, f"Enhanced Analysis ({enhanced.decoder_type}):", curses.A_BOLD) y_offset += 1 # Timing analysis for CH10 if enhanced.has_internal_timing: stdscr.addstr(y_offset, x_offset + 2, f"Clock Drift: {enhanced.avg_clock_drift_ppm:.2f} PPM (max: {enhanced.max_clock_drift_ppm:.2f})") y_offset += 1 stdscr.addstr(y_offset, x_offset + 2, f"Timing Quality: {enhanced.timing_quality.title()} | Stability: {enhanced.timing_stability.title()}") y_offset += 1 if enhanced.anomaly_rate > 0: stdscr.addstr(y_offset, x_offset + 2, f"Anomaly Rate: {enhanced.anomaly_rate:.1%} | Confidence: {enhanced.avg_confidence_score:.2f}") y_offset += 1 # Frame quality metrics if enhanced.avg_frame_quality > 0: stdscr.addstr(y_offset, x_offset + 2, f"Frame Quality: {enhanced.avg_frame_quality:.1f}%") y_offset += 1 # Error counts errors = [] if enhanced.sequence_gaps > 0: errors.append(f"Seq: {enhanced.sequence_gaps}") if enhanced.rtc_sync_errors > 0: errors.append(f"RTC: {enhanced.rtc_sync_errors}") if enhanced.format_errors > 0: errors.append(f"Fmt: {enhanced.format_errors}") if enhanced.overflow_errors > 0: errors.append(f"Ovf: {enhanced.overflow_errors}") if errors: stdscr.addstr(y_offset, x_offset + 2, f"Errors: {' | '.join(errors)}") y_offset += 1 # Data analysis if enhanced.channel_count > 0: channel_info = f"Channels: {enhanced.channel_count}" if enhanced.analog_channels > 0: channel_info += f" (Analog: {enhanced.analog_channels})" if enhanced.pcm_channels > 0: channel_info += f" (PCM: {enhanced.pcm_channels})" if enhanced.tmats_frames > 0: channel_info += f" (TMATS: {enhanced.tmats_frames})" stdscr.addstr(y_offset, x_offset + 2, channel_info) y_offset += 1 # Primary data type if enhanced.primary_data_type != "Unknown": stdscr.addstr(y_offset, x_offset + 2, f"Primary Type: {enhanced.primary_data_type}") y_offset += 1 except curses.error: pass def _get_enhanced_analysis_lines(self, flow: FlowStats) -> int: """Calculate how many lines the enhanced analysis will take""" enhanced = flow.enhanced_analysis lines = 1 # Header line if enhanced.has_internal_timing: lines += 2 # Clock drift + timing quality if enhanced.anomaly_rate > 0: lines += 1 # Anomaly rate if enhanced.avg_frame_quality > 0: lines += 1 # Frame quality # Check if we have errors to display if any([enhanced.sequence_gaps, enhanced.rtc_sync_errors, enhanced.format_errors, enhanced.overflow_errors]): lines += 1 if enhanced.channel_count > 0: lines += 1 # Channel info if enhanced.primary_data_type != "Unknown": lines += 1 # Primary data type return lines def _draw_decode_tab(self, stdscr, x_offset: int, y_offset: int, width: int, max_lines: int, flow: FlowStats, selected_frame_type: Optional[str]): """Draw the Decode tab content showing decoded frame information""" if flow.enhanced_analysis.decoder_type == "Standard": stdscr.addstr(y_offset, x_offset, "No enhanced decoder available for this flow", curses.A_DIM) return # Header for decode information stdscr.addstr(y_offset, x_offset, f"DECODED DATA ({flow.enhanced_analysis.decoder_type}):", curses.A_BOLD) y_offset += 2 # Tree view of decoded information if flow.enhanced_analysis.decoder_type == "Chapter10_Enhanced": self._draw_ch10_decode_tree(stdscr, x_offset, y_offset, width, max_lines, flow) else: stdscr.addstr(y_offset, x_offset, f"Decoder type '{flow.enhanced_analysis.decoder_type}' display not implemented") def _draw_ch10_decode_tree(self, stdscr, x_offset: int, y_offset: int, width: int, max_lines: int, flow: FlowStats): """Draw Chapter 10 decoded information in tree format""" enhanced = flow.enhanced_analysis tree_items = [ ("📋 Header Information", None, 0), (" Decoder Type", enhanced.decoder_type, 1), (" Primary Data Type", enhanced.primary_data_type, 1), (" Channel Count", str(enhanced.channel_count) if enhanced.channel_count > 0 else "Unknown", 1), ("", None, 0), # Spacer ("⏱️ Timing Analysis", None, 0), (" Has Internal Timing", "Yes" if enhanced.has_internal_timing else "No", 1), (" Timing Quality", enhanced.timing_quality.title(), 1), (" Clock Drift (avg)", f"{enhanced.avg_clock_drift_ppm:.2f} PPM" if enhanced.avg_clock_drift_ppm != 0 else "N/A", 1), (" Clock Drift (max)", f"{enhanced.max_clock_drift_ppm:.2f} PPM" if enhanced.max_clock_drift_ppm != 0 else "N/A", 1), (" Timing Stability", enhanced.timing_stability.title(), 1), (" Anomaly Rate", f"{enhanced.anomaly_rate:.1%}" if enhanced.anomaly_rate > 0 else "0%", 1), (" Confidence Score", f"{enhanced.avg_confidence_score:.2f}" if enhanced.avg_confidence_score > 0 else "N/A", 1), ("", None, 0), # Spacer ("📊 Quality Metrics", None, 0), (" Frame Quality (avg)", f"{enhanced.avg_frame_quality:.1f}%" if enhanced.avg_frame_quality > 0 else "N/A", 1), (" Sequence Gaps", str(enhanced.sequence_gaps), 1), (" RTC Sync Errors", str(enhanced.rtc_sync_errors), 1), (" Format Errors", str(enhanced.format_errors), 1), (" Overflow Errors", str(enhanced.overflow_errors), 1), ("", None, 0), # Spacer ("📡 Channel Information", None, 0), (" Total Channels", str(enhanced.channel_count), 1), (" Analog Channels", str(enhanced.analog_channels), 1), (" PCM Channels", str(enhanced.pcm_channels), 1), (" TMATS Frames", str(enhanced.tmats_frames), 1), ] # Add decoded frame samples if available if enhanced.sample_decoded_fields: tree_items.extend([ ("", None, 0), # Spacer ("🔍 Decoded Frame Samples", None, 0), ]) for frame_key, frame_data in enhanced.sample_decoded_fields.items(): tree_items.append((f" {frame_key.replace('_', ' ').title()}", None, 1)) # Show important fields first priority_fields = ['packet_timestamp', 'internal_timestamp', 'data_type_name', 'channel_id', 'sequence_number', 'frame_quality_score', 'rtc_sync_error'] # Add priority fields for field_name in priority_fields: if field_name in frame_data: display_value = self._format_field_value(field_name, frame_data[field_name]) tree_items.append((f" {field_name.replace('_', ' ').title()}", display_value, 2)) # Add other fields (up to 5 more) other_fields = [k for k in frame_data.keys() if k not in priority_fields] for i, field_name in enumerate(other_fields[:5]): display_value = self._format_field_value(field_name, frame_data[field_name]) tree_items.append((f" {field_name.replace('_', ' ').title()}", display_value, 2)) if len(other_fields) > 5: tree_items.append((f" ... and {len(other_fields) - 5} more fields", "", 2)) # Add available fields summary if enhanced.available_field_names: tree_items.extend([ ("", None, 0), # Spacer ("📝 Available Decoder Fields", None, 0), (" Total Fields Available", str(len(enhanced.available_field_names)), 1), (" Field Categories", self._categorize_fields(enhanced.available_field_names), 1), ]) # Render tree items current_y = y_offset for label, value, indent_level in tree_items: if current_y >= max_lines: break if not label: # Spacer line current_y += 1 continue indent = " " * indent_level if value is None: # Category header line = f"{indent}{label}" stdscr.addstr(current_y, x_offset, line[:width-1], curses.A_BOLD) else: # Key-value pair line = f"{indent}{label}: {value}" stdscr.addstr(current_y, x_offset, line[:width-1]) current_y += 1 # Add scrolling hint if content is cut off if current_y >= max_lines and len(tree_items) > (max_lines - y_offset): if max_lines > 0: stdscr.addstr(max_lines - 1, x_offset, "... (content truncated)", curses.A_DIM) def switch_tab(self): """Switch to the next tab""" self.active_tab = (self.active_tab + 1) % len(self.tabs) def _format_field_value(self, field_name: str, value) -> str: """Format field value for display""" if value is None: return "N/A" # Special formatting for different field types if "timestamp" in field_name.lower(): try: return f"{float(value):.6f}s" except: return str(value) elif "error" in field_name.lower() or field_name.endswith("_error"): return "Yes" if value else "No" elif "quality" in field_name.lower() and isinstance(value, (int, float)): return f"{value:.1f}%" elif isinstance(value, float): return f"{value:.3f}" elif isinstance(value, bool): return "Yes" if value else "No" else: return str(value) def _categorize_fields(self, field_names: List[str]) -> str: """Categorize available fields into groups""" categories = { "Timing": 0, "Quality": 0, "Data": 0, "Header": 0, "Other": 0 } for field_name in field_names: lower_name = field_name.lower() if any(keyword in lower_name for keyword in ["time", "timestamp", "rtc", "drift"]): categories["Timing"] += 1 elif any(keyword in lower_name for keyword in ["quality", "error", "sync", "confidence"]): categories["Quality"] += 1 elif any(keyword in lower_name for keyword in ["data", "analog", "pcm", "channel", "sample"]): categories["Data"] += 1 elif any(keyword in lower_name for keyword in ["header", "type", "id", "sequence", "length"]): categories["Header"] += 1 else: categories["Other"] += 1 # Format as compact string active_categories = [f"{k}:{v}" for k, v in categories.items() if v > 0] return ", ".join(active_categories)