2025-07-25 15:52:16 -04:00
|
|
|
"""
|
|
|
|
|
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"""
|
|
|
|
|
|
2025-07-26 22:46:49 -04:00
|
|
|
# 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}"
|
2025-07-25 15:52:16 -04:00
|
|
|
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:
|
2025-07-26 16:51:37 -04:00
|
|
|
# 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
|
2025-07-25 15:52:16 -04:00
|
|
|
avg_time = f"{flow.avg_inter_arrival:.3f}s" if flow.avg_inter_arrival > 0 else "N/A"
|
|
|
|
|
|
2025-07-26 16:51:37 -04:00
|
|
|
# Abbreviate traffic classification
|
|
|
|
|
cast_abbrev = flow.traffic_classification[:4] if flow.traffic_classification != "Unknown" else "Unk"
|
|
|
|
|
|
2025-07-26 22:46:49 -04:00
|
|
|
# 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}"
|
2025-07-25 15:52:16 -04:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2025-07-26 22:46:49 -04:00
|
|
|
# Create frame type line aligned with enhanced column layout
|
2025-07-26 16:51:37 -04:00
|
|
|
bytes_str_ft = self._format_bytes(ft_stats.total_bytes)
|
2025-07-26 22:46:49 -04:00
|
|
|
|
|
|
|
|
# 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}"
|
2025-07-25 15:52:16 -04:00
|
|
|
|
|
|
|
|
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"""
|
2025-07-26 16:51:37 -04:00
|
|
|
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]
|
|
|
|
|
|
2025-07-26 22:46:49 -04:00
|
|
|
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"
|