# Capitolo 6: Interfaccia Grafica (GUI Real-Time)

[← Modalità Esecuzione](05_Modalita_Esecuzione.md) | [Torna all'Indice](00_INDEX.md) | [Prossimo: Output e Report →](07_Output_Report.md)

---

## 6.1 Overview GUI

L'interfaccia grafica fornisce **monitoraggio real-time** dell'esecuzione test, mostrando progresso, statistiche e risultati mentre il test è in corso.

### 6.1.1 Caratteristiche Principali

| Feature | Descrizione | Beneficio |
|---------|-------------|-----------|
| **Real-Time Updates** | Aggiornamenti live durante esecuzione | Visibilità immediata stato test |
| **Thread-Safe** | Queue-based communication | Nessun deadlock o race condition |
| **Per-Run Table** | Tabella dettagliata ogni run | Tracking granulare risultati |
| **Color Coding** | Verde=PASS, Rosso=FAIL | Identificazione rapida problemi |
| **Tooltip Informativi** | Hover su intestazioni per spiegazioni | UX migliorata per nuovi utenti |
| **Event Log** | Log scrollabile eventi test | Debug e troubleshooting |
| **Non-Blocking** | GUI su thread separato | Test continua anche se GUI chiusa |

### 6.1.2 Screenshot Concettuale Layout

```
┌─────────────────────────────────────────────────────────────────┐
│  GRIFO M-PBIT Test Monitor                       ● RUNNING      │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Test Progress                                                   │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │  Current Run:  8 / 10                                    │  │
│  │  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░  80%               │  │
│  │  PBIT Time:    17.3s                                     │  │
│  │  Estimated Remaining: 2m 15s (20%)                       │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                  │
│  Current Scenario                                                │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │  Scenario: Random Component Failures                     │  │
│  │  Description: Random selection of failures (30% proc...) │  │
│  │  Expected Failures:                                      │  │
│  │    ✗ processor_status                                    │  │
│  │    ✗ transmitter_status                                  │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                  │
│  Runs Overview                                                   │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │ Run  Status  PBIT   B6 Status  B8 Checks  Serial   Targ..│  │
│  │            Time    (P/F/K)    (Pass/Fail/Known) Msgs     et    │  │
│  │                                           (E/F/R)         │  │
│  ├──────────────────────────────────────────────────────────┤  │
│  │ 1/10 PASS   2.2s   11/0/1     173/0/0    0/0/0    2000m  │  │
│  │ 2/10 PASS   0.7s   11/0/1     173/0/0    0/0/0    -      │  │
│  │ 3/10 FAIL   0.1s   9/2/1      -          0/3/3    -      │  │
│  │ 4/10 FAIL   0.1s   9/2/1      173/0/0    0/3/0    -      │  │
│  │ 5/10 PASS   2.0s   9/2/1      173/0/0    0/0/0    -      │  │
│  │ 6/10 PASS   0.3s   11/0/1     173/0/0    0/2/2    -      │  │
│  │ 7/10 PASS   0.0s   11/0/1     173/0/0    0/2/2    2000m  │  │
│  │ 8/10 PASS   ....   ......     ......     ......   ....   │  │◄ Running
│  │ 9/10                                                      │  │
│  │10/10                                                      │  │
│  └──────────────────────────────────────────────────────────┘  │
│    ▲ Verde = PASS  │  Rosso = FAIL  │  Double-click = Details │
│                                                                  │
│  Event Log                                                       │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │ [15:24:52] Starting run 8                                │  │
│  │ [15:24:54] Power ON - setup starting immediately        │  │
│  │ [15:24:54] SILENCE, STANDBAY, and scan params set       │  │
│  │ [15:24:54] Tellback verification skipped (fast mode)    │  │
│  │ [15:24:54] Starting PBIT test...                         │  │
│  │ [15:24:54] BIT completed in 2.9s                         │  │
│  │ [15:24:54] BIT available after 0.0s                      │  │
│  │ [15:24:54] B6: ALL checks passed                         │  │
│  │ [15:24:54] STANDBY removed                               │  │
│  │ [15:24:54] Run 8: Target detection test starting...     │  │
│  │ [15:31:25] Target HIT #1: range=2000m @ iteration 3     │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
```

## 6.2 Architettura GUI

### 6.2.1 Pattern Threading

**Problema**: Tkinter **non è thread-safe**. Widgets devono essere aggiornati dal **main thread** (GUI thread).

**Soluzione**: **Queue-based Producer-Consumer Pattern**

```
┌──────────────────────────────────────────────────────────┐
│                   Main Thread                             │
│                  (GUI Thread)                             │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Tkinter Event Loop                                │  │
│  │  - root.mainloop()                                 │  │
│  │  - Handles UI events (clicks, resize, etc.)       │  │
│  │  - Calls _poll_updates() every 100ms               │  │
│  └──────────────┬─────────────────────────────────────┘  │
│                 │                                         │
│                 │ Reads from                              │
│                 ↓                                         │
│  ┌────────────────────────────────────────────────────┐  │
│  │  update_queue (queue.Queue)                        │  │
│  │  Thread-safe FIFO queue                            │  │
│  └──────────────▲─────────────────────────────────────┘  │
│                 │                                         │
└─────────────────┼─────────────────────────────────────────┘
                  │
                  │ Writes to
                  │
┌─────────────────┼─────────────────────────────────────────┐
│                 │          Worker Thread                   │
│                 │         (Test Thread)                    │
│  ┌──────────────┴─────────────────────────────────────┐  │
│  │  Test Execution                                    │  │
│  │  - main_test()                                     │  │
│  │  - Power cycling                                   │  │
│  │  - PBIT execution                                  │  │
│  │  - B6/B8 checks                                    │  │
│  │  - Calls gui_monitor.update_*() methods           │  │
│  └────────────────────────────────────────────────────┘  │
│                                                            │
└────────────────────────────────────────────────────────────┘
```

### 6.2.2 Classe TestMonitorGUI

**File**: `GRIFO_M_PBIT_gui.py` (~1100 righe)

**Struttura Classe**:
```python
class TestMonitorGUI:
    """
    Real-time GUI monitor for GRIFO PBIT test execution.
    Thread-safe via queue-based updates.
    """
    
    def __init__(self):
        """Initialize (does not show window yet)"""
        self.root = None              # Tkinter root window
        self.thread = None            # GUI thread
        self.running = False          # Control flag
        self.update_queue = queue.Queue()  # Thread-safe queue
        
        # Data storage (read by GUI thread)
        self.data = {
            'run_current': 0,
            'run_total': 0,
            'scenario_name': 'Initializing...',
            'pbit_time': 0.0,
            'b6_pass': 0,
            'b6_fail': 0,
            # ... altri campi
        }
        
        # Widget references
        self.widgets = {}
        
        # Per-run data for detail dialog
        self.runs_data = {}  # {run_id: {metadata}}
    
    # ===== PUBLIC API (chiamate da test thread) =====
    
    def start(self):
        """Start GUI on separate thread"""
        self.thread = threading.Thread(target=self._run_gui, daemon=True)
        self.thread.start()
        time.sleep(0.5)  # Wait for window creation
    
    def stop(self):
        """Stop GUI gracefully"""
        self.running = False
        if self.thread:
            self.thread.join(timeout=2.0)
    
    def update_status(self, run_current=None, run_total=None, ...):
        """Update test progress (thread-safe)"""
        data = {}
        if run_current is not None:
            data['run_current'] = run_current
        # ... altri campi
        if data:
            self.update_queue.put(('status', data))
    
    def update_run(self, run: int, **kwargs):
        """Update single run row (thread-safe)"""
        data = {'run': run}
        data.update(kwargs)
        self.update_queue.put(('run_row', data))
    
    def log_event(self, severity: str, message: str):
        """Add log entry (thread-safe)"""
        self.update_queue.put(('log', {
            'severity': severity,
            'message': message
        }))
    
    # ===== PRIVATE GUI THREAD METHODS =====
    
    def _run_gui(self):
        """GUI thread main function"""
        try:
            self.root = tk.Tk()
            self.root.title("GRIFO M-PBIT Test Monitor")
            self.running = True
            
            self._build_ui()       # Create all widgets
            self._poll_updates()   # Start queue polling
            
            self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
            self.root.mainloop()   # Blocking call
        
        except Exception as e:
            logging.error(f"GUI thread error: {e}")
        finally:
            self.running = False
    
    def _poll_updates(self):
        """Poll update queue and apply changes (called every 100ms)"""
        if not self.running:
            return
        
        try:
            # Process all queued updates
            while not self.update_queue.empty():
                update_type, data = self.update_queue.get_nowait()
                
                if update_type == 'status':
                    self._update_status(data)
                elif update_type == 'run_row':
                    self._update_run_row(data)
                elif update_type == 'log':
                    self._add_log_entry(data)
                # ... altri tipi
        
        except queue.Empty:
            pass
        except Exception as e:
            logging.error(f"GUI update error: {e}")
        
        # Re-schedule next poll
        self.root.after(100, self._poll_updates)  # 100ms = 10 Hz
    
    def _build_ui(self):
        """Create all widgets"""
        # ... (vedi sezione 6.3)
    
    def _update_status(self, data: Dict):
        """Apply status update to widgets"""
        # ... (aggiorna progress bar, label, ecc.)
    
    def _update_run_row(self, data: Dict):
        """Update table row for specific run"""
        # ... (vedi sezione 6.4)
```

### 6.2.3 Lifecycle GUI

```
1. INITIALIZATION (main thread)
   ├─ gui_monitor = TestMonitorGUI()
   ├─ gui_monitor.start()  → Spawns GUI thread
   └─ Wait 0.5s for window creation

2. GUI THREAD START
   ├─ Create Tkinter root window
   ├─ Build all widgets (_build_ui)
   ├─ Start queue polling (_poll_updates every 100ms)
   └─ Enter mainloop() [BLOCKING]

3. TEST EXECUTION (worker thread)
   ├─ Run test loop
   ├─ Call gui_monitor.update_status(...)
   ├─ Call gui_monitor.update_run(...)
   └─ Call gui_monitor.log_event(...)
        ↓
   Updates enqueued to update_queue

4. GUI POLLING (GUI thread, every 100ms)
   ├─ Dequeue all pending updates
   ├─ Apply updates to widgets
   └─ Schedule next poll (root.after(100, ...))

5. COMPLETION (worker thread)
   ├─ Test finishes
   ├─ Call gui_monitor.show_results(output_folder)
   └─ Call gui_monitor.stop() [optional]

6. CLEANUP
   ├─ User closes window OR stop() called
   ├─ Exit mainloop()
   ├─ running = False
   └─ Thread terminates
```

## 6.3 Costruzione UI (_build_ui)

### 6.3.1 Layout Grid

La GUI usa **Tkinter Grid Layout** con 5 righe principali:

```python
def _build_ui(self):
    """Create all UI widgets"""
    main_frame = ttk.Frame(self.root, padding="10")
    main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
    
    # Configure grid weights for resizing
    self.root.columnconfigure(0, weight=1)
    self.root.rowconfigure(0, weight=1)
    main_frame.columnconfigure(0, weight=1)
    main_frame.columnconfigure(1, weight=1)
    
    # ROW 0: Test Progress
    self._create_progress_section(main_frame, row=0)
    
    # ROW 1: Current Scenario
    self._create_scenario_section(main_frame, row=1)
    
    # ROW 2: Runs Overview Table
    self._create_runs_table(main_frame, row=2)
    
    # ROW 3: Event Log
    self._create_event_log(main_frame, row=3)
    
    # Set window size and position
    self.root.geometry("900x700")
    self._center_window()
```

### 6.3.2 Progress Section (Row 0)

```python
def _create_progress_section(self, parent, row):
    """Create test progress widgets"""
    progress_frame = ttk.LabelFrame(parent, text="Test Progress", padding="10")
    progress_frame.grid(row=row, column=0, columnspan=2, 
                       sticky=(tk.W, tk.E), pady=(0, 10))
    progress_frame.columnconfigure(1, weight=1)
    
    # Current run label
    ttk.Label(progress_frame, text="Current Run:").grid(
        row=0, column=0, sticky=tk.W)
    self.widgets['run_label'] = ttk.Label(progress_frame, text="0 / 0", 
                                          font=('Arial', 10, 'bold'))
    self.widgets['run_label'].grid(row=0, column=1, sticky=tk.W, padx=(10, 0))
    
    # Progress bar
    self.widgets['progress_bar'] = ttk.Progressbar(
        progress_frame, mode='determinate', length=400)
    self.widgets['progress_bar'].grid(row=1, column=0, columnspan=2, 
                                     sticky=(tk.W, tk.E), pady=(5, 0))
    
    # PBIT time
    ttk.Label(progress_frame, text="PBIT Time:").grid(
        row=2, column=0, sticky=tk.W, pady=(5, 0))
    self.widgets['pbit_label'] = ttk.Label(progress_frame, text="0.0s", 
                                           font=('Arial', 10))
    self.widgets['pbit_label'].grid(row=2, column=1, sticky=tk.W, 
                                    padx=(10, 0), pady=(5, 0))
    
    # Estimated remaining time (ETA)
    ttk.Label(progress_frame, text="Estimated Remaining:").grid(
        row=2, column=2, sticky=tk.W, pady=(5, 0))
    self.widgets['eta_label'] = ttk.Label(progress_frame, text="--:--", 
                                          font=('Arial', 10))
    self.widgets['eta_label'].grid(row=2, column=3, sticky=tk.E, 
                                   padx=(10, 0), pady=(5, 0))
```

### 6.3.3 Runs Table con Header Custom e Tooltip

```python
def _create_runs_table(self, parent, row):
    """Create per-run results table with custom headers and tooltips"""
    runs_frame = ttk.LabelFrame(parent, text="Runs Overview", padding="5")
    runs_frame.grid(row=row, column=0, columnspan=2, 
                   sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
    runs_frame.columnconfigure(0, weight=1)
    runs_frame.rowconfigure(1, weight=1)
    
    # ===== CUSTOM HEADER FRAME WITH TOOLTIPS =====
    header_frame = ttk.Frame(runs_frame)
    header_frame.grid(row=0, column=0, sticky=(tk.W, tk.E))
    
    # Define headers with tooltips
    headers_with_tooltips = [
        ('Run', None, 60),
        ('Status', None, 80),
        ('PBIT Time', None, 80),
        ('B6 Status (P/F/K)', 'P = Pass\nF = Fail\nK = Known Failures', 120),
        ('B8 Checks (Pass/Fail/Known)', 'Number of B8 checks\nFormat: Pass/Fail/Known', 140),
        ('Serial Messages (E/F/R)', 'E = Errors\nF = Fatal\nR = Recycles', 120),
        ('Target', 'Target distance and cycle\nFormat: XXXXm @ cycle Y', 140),
        ('Target Test', None, 80)
    ]
    
    # Create header labels with tooltips
    for idx, (title, tooltip, width) in enumerate(headers_with_tooltips):
        label = ttk.Label(header_frame, text=title, 
                        font=('Arial', 9, 'bold'), 
                        anchor=tk.CENTER, relief=tk.RAISED, borderwidth=1)
        label.grid(row=0, column=idx, sticky=(tk.W, tk.E), ipadx=2, ipady=3)
        header_frame.columnconfigure(idx, weight=1 if idx > 0 else 0, 
                                    minsize=width)
        
        # Add tooltip if specified
        if tooltip:
            ToolTip(label, tooltip)
    
    # ===== TREEVIEW TABLE (hide built-in headers) =====
    cols = ('run', 'status', 'pbit_time', 'b6_status', 'b8_checks', 
           'serial_msgs', 'target', 'target_test')
    
    self.widgets['runs_table'] = ttk.Treeview(
        runs_frame, columns=cols, show='tree', height=12)
    self.widgets['runs_table'].grid(row=1, column=0, 
                                    sticky=(tk.W, tk.E, tk.N, tk.S))
    self.widgets['runs_table'].column('#0', width=0, stretch=tk.NO)
    
    # Set column widths
    column_widths = [60, 80, 80, 120, 120, 120, 140, 80]
    for c, width in zip(cols, column_widths):
        self.widgets['runs_table'].column(c, width=width, anchor=tk.CENTER)
    
    # Scrollbar
    runs_scroll = ttk.Scrollbar(runs_frame, orient=tk.VERTICAL, 
                               command=self.widgets['runs_table'].yview)
    runs_scroll.grid(row=1, column=1, sticky=(tk.N, tk.S))
    self.widgets['runs_table'].configure(yscrollcommand=runs_scroll.set)
    
    # Style tags for color coding
    self.widgets['runs_table'].tag_configure('pass', background='#d4edda')
    self.widgets['runs_table'].tag_configure('fail', background='#f8d7da')
    
    # Double-click for details
    self.widgets['runs_table'].bind('<Double-Button-1>', 
                                   self._on_run_double_click)
```

### 6.3.4 ToolTip Class

```python
class ToolTip:
    """
    Creates a tooltip for a given widget.
    Shows tooltip on mouse enter, hides on leave.
    """
    def __init__(self, widget, text):
        self.widget = widget
        self.text = text
        self.tooltip_window = None
        self.widget.bind("<Enter>", self.show_tooltip)
        self.widget.bind("<Leave>", self.hide_tooltip)
    
    def show_tooltip(self, event=None):
        """Display tooltip below widget"""
        if self.tooltip_window or not self.text:
            return
        
        # Get widget position
        x = self.widget.winfo_rootx() + 20
        y = self.widget.winfo_rooty() + self.widget.winfo_height() + 5
        
        # Create tooltip window (no decorations)
        self.tooltip_window = tw = tk.Toplevel(self.widget)
        tw.wm_overrideredirect(True)
        tw.wm_geometry(f"+{x}+{y}")
        
        # Create label
        label = tk.Label(tw, text=self.text, justify=tk.LEFT,
                        background="#ffffe0", relief=tk.SOLID, 
                        borderwidth=1, font=("Arial", 9))
        label.pack(ipadx=5, ipady=3)
    
    def hide_tooltip(self, event=None):
        """Hide tooltip"""
        if self.tooltip_window:
            self.tooltip_window.destroy()
            self.tooltip_window = None
```

## 6.4 Aggiornamenti Real-Time

### 6.4.1 Update Status

```python
def _update_status(self, data: Dict):
    """Apply status updates to progress widgets"""
    # Update run counter
    if 'run_current' in data:
        self.data['run_current'] = data['run_current']
    if 'run_total' in data:
        self.data['run_total'] = data['run_total']
    
    # Update label
    run_text = f"{self.data['run_current']} / {self.data['run_total']}"
    self.widgets['run_label'].config(text=run_text)
    
    # Update progress bar
    if self.data['run_total'] > 0:
        progress = (self.data['run_current'] / self.data['run_total']) * 100
        self.widgets['progress_bar']['value'] = progress
    
    # Update PBIT time
    if 'pbit_time' in data:
        self.data['pbit_time'] = data['pbit_time']
        pbit_text = f"{self.data['pbit_time']:.1f}s"
        self.widgets['pbit_label'].config(text=pbit_text)
    
    # Update ETA
    try:
        runs_remaining = self.data['run_total'] - self.data['run_current']
        if runs_remaining > 0 and self.data['run_current'] > 0:
            # Estimate based on average time per run so far
            avg_time_per_run = self.data.get('elapsed_total', 0) / self.data['run_current']
            eta_seconds = avg_time_per_run * runs_remaining
            eta_text = self._format_time(eta_seconds)
            percentage = (runs_remaining / self.data['run_total']) * 100
            self.widgets['eta_label'].config(
                text=f"{eta_text} ({percentage:.0f}%)")
    except Exception:
        pass
```

### 6.4.2 Update Run Row

```python
def _update_run_row(self, data: Dict):
    """Update single row in runs table"""
    table = self.widgets.get('runs_table')
    if not table:
        return
    
    run_num = str(data.get('run'))
    if not table.exists(run_num):
        # Insert if missing
        total = self.data.get('run_total', 0)
        table.insert('', 'end', iid=run_num, 
                    values=(f'{run_num}/{total}', '', '', '', '', '', '', ''))
    
    # Read old values
    old = table.item(run_num).get('values', [])
    if not old:
        old = [''] * 8
    
    # Build new values
    new = list(old)
    
    # Column 0: Run number
    new[0] = f"{data.get('run')}/{self.data.get('run_total', '')}"
    
    # Column 1: Status (PASS/FAIL)
    if 'result' in data:
        new[1] = data['result']
    
    # Column 2: PBIT Time
    if 'pbit' in data:
        pbit = data['pbit']
        new[2] = f"{pbit:.1f}s" if isinstance(pbit, (int, float)) else str(pbit)
    
    # Column 3: B6 Status (P/F/K format)
    if 'b6_pass' in data or 'b6_fail' in data or 'b6_known_fail' in data:
        b6_pass = data.get('b6_pass', 0)
        b6_fail = data.get('b6_fail', 0)
        b6_known = data.get('b6_known_fail', 0)
        new[3] = f"{b6_pass}/{b6_fail}/{b6_known}"
    
    # Column 4: B8 Checks (Pass/Fail format)
    if 'b8_pass' in data or 'b8_checked' in data:
        b8_pass = data.get('b8_pass', 0)
        b8_fail = data.get('b8_fail', 0)
        b8_checked = data.get('b8_checked', 0)
        if b8_checked > 0:
            new[4] = f"{b8_pass}/{b8_fail}"
        else:
            new[4] = "-"
    
    # Column 5: Serial Messages (E/F/R format)
    if 'serial_errors' in data or 'serial_fatal' in data:
        serial_e = data.get('serial_errors', 0)
        serial_f = data.get('serial_fatal', 0)
        serial_r = data.get('serial_recycles', 0)
        new[5] = f"{serial_e}/{serial_f}/{serial_r}"
    
    # Column 6: Target (distance @ cycle format)
    if 'target_simulated' in data:
        tgt = data['target_simulated']
        if tgt and isinstance(tgt, dict):
            try:
                distance = tgt.get('distance', '?')
                cycle = tgt.get('appeared_after_cycles', 
                              tgt.get('found_at_iter', '?'))
                new[6] = f"{distance}m @ cycle {cycle}"
            except Exception:
                new[6] = "-"
        else:
            new[6] = "-"
    
    # Column 7: Target Test (PASS/FAIL/-)
    if 'target_detected' in data:
        detected = data['target_detected']
        if detected is None:
            new[7] = "-"
        elif detected:
            new[7] = "PASS"
        else:
            new[7] = "FAIL"
    
    # Update table
    table.item(run_num, values=new)
    
    # Apply color tag
    try:
        res = new[1].strip() if len(new) > 1 and isinstance(new[1], str) else ''
        if res.upper() == 'PASS':
            table.item(run_num, tags=('pass',))
        elif res.upper() == 'FAIL':
            table.item(run_num, tags=('fail',))
        else:
            table.item(run_num, tags=())
    except Exception:
        pass
    
    # Store metadata for detail dialog
    try:
        meta = self.runs_data.get(run_num, {})
        for k, v in data.items():
            meta[k] = v
        meta['display'] = {
            'status': new[1], 'pbit_time': new[2], 'b6_status': new[3],
            'b8_checks': new[4], 'serial_msgs': new[5], 
            'target': new[6], 'target_test': new[7]
        }
        self.runs_data[run_num] = meta
    except Exception:
        pass
    
    # Scroll to updated row
    try:
        table.see(run_num)
    except Exception:
        pass
```

## 6.5 Dialog Dettagli Run

### 6.5.1 Double-Click Handler

```python
def _on_run_double_click(self, event=None):
    """Handle double-click on run row: open detail dialog"""
    try:
        table = self.widgets.get('runs_table')
        if not table:
            return
        
        sel = table.focus()
        if not sel:
            sel_items = table.selection()
            if not sel_items:
                return
            sel = sel_items[0]
        
        run_id = str(sel)
        meta = self.runs_data.get(run_id, {})
        
        # Build detail dialog
        self._show_run_detail_dialog(run_id, meta)
    
    except Exception as e:
        logging.error(f"Failed to open run detail: {e}")
```

### 6.5.2 Detail Dialog

```python
def _show_run_detail_dialog(self, run_id: str, meta: Dict):
    """Show detailed run information in popup dialog"""
    dlg = tk.Toplevel(self.root)
    dlg.title(f"Run {run_id} Details")
    dlg.transient(self.root)
    
    frm = ttk.Frame(dlg, padding=12)
    frm.pack(fill=tk.BOTH, expand=True)
    
    # Header
    hdr = ttk.Label(frm, text=f"Run {run_id}", font=('Arial', 12, 'bold'))
    hdr.pack(anchor=tk.W)
    
    # Key/value grid
    grid = ttk.Frame(frm)
    grid.pack(fill=tk.X, pady=(6, 6))
    
    def _kv(row, key, val):
        """Helper to add key-value pair"""
        ttk.Label(grid, text=key+":", font=('Arial', 9, 'bold')).grid(
            row=row, column=0, sticky=tk.W, padx=(0,6))
        ttk.Label(grid, text=str(val), font=('Arial', 9)).grid(
            row=row, column=1, sticky=tk.W)
    
    display = meta.get('display', {})
    _kv(0, 'Status', display.get('status', ''))
    _kv(1, 'PBIT Time', display.get('pbit_time', ''))
    _kv(2, 'B6 Status (P/F/K)', display.get('b6_status', ''))
    _kv(3, 'B8 Checks', display.get('b8_checks', ''))
    _kv(4, 'Serial Msgs (E/F/R)', display.get('serial_msgs', ''))
    _kv(5, 'Target', display.get('target', ''))
    _kv(6, 'Target Test', display.get('target_test', ''))
    
    # Timing details
    start = meta.get('start_time')
    end = meta.get('end_time')
    duration = meta.get('run_duration')
    _kv(7, 'Start Time', start)
    _kv(8, 'End Time', end)
    _kv(9, 'Run Duration', f"{duration:.2f}s" if duration else "N/A")
    
    # Target details
    tgt_detected = meta.get('target_detected', None)
    tgt_status = 'YES' if tgt_detected is True else ('NO' if tgt_detected is False else 'N/A')
    _kv(10, 'Target Detected', tgt_status)
    
    # Failures detail (multi-line text box)
    ttk.Label(frm, text='Failures (detailed):', 
             font=('Arial', 9, 'bold')).pack(anchor=tk.W, pady=(6,0))
    failures_text = tk.Text(frm, height=6, width=80, 
                           font=('Courier', 9), wrap=tk.WORD)
    failures = meta.get('failures') or ''
    if isinstance(failures, (list, tuple)):
        body = '\n'.join([str(x) for x in failures])
    else:
        body = str(failures)
    failures_text.insert('1.0', body)
    failures_text.config(state='disabled')
    failures_text.pack(fill=tk.BOTH, expand=False, pady=(2,6))
    
    # Close button
    btn_frame = ttk.Frame(frm)
    btn_frame.pack(fill=tk.X, pady=(6,0))
    close_btn = ttk.Button(btn_frame, text='Close', 
                          command=lambda: dlg.destroy())
    close_btn.pack(side=tk.RIGHT)
    
    # Center dialog
    dlg.update_idletasks()
    w = dlg.winfo_reqwidth()
    h = dlg.winfo_reqheight()
    px = self.root.winfo_rootx()
    py = self.root.winfo_rooty()
    pw = self.root.winfo_width()
    ph = self.root.winfo_height()
    x = px + max(0, (pw - w)//2)
    y = py + max(0, (ph - h)//2)
    dlg.geometry(f"{w}x{h}+{x}+{y}")
```

## 6.6 Best Practices GUI

### 6.6.1 Thread Safety Checklist

✅ **DO**:
- Usa `update_queue.put()` per comunicare da worker thread a GUI thread
- Chiama widget methods solo da GUI thread (in `_poll_updates` o callbacks)
- Usa `daemon=True` per GUI thread (non blocca exit)

❌ **DON'T**:
- Non chiamare widget methods direttamente da worker thread
- Non usare variabili shared senza lock (usa queue invece)
- Non fare operazioni blocking in GUI thread (blocca UI)

### 6.6.2 Performance Tips

- **Polling Frequency**: 100ms (10 Hz) è un buon bilanciamento responsività/CPU
- **Batch Updates**: `_poll_updates` processa tutti i messaggi in queue prima di re-schedule
- **Lazy Rendering**: Tabella usa virtualizzazione (solo righe visibili renderizzate)

### 6.6.3 Error Handling

```python
def _poll_updates(self):
    """Poll with error isolation"""
    try:
        while not self.update_queue.empty():
            try:
                update_type, data = self.update_queue.get_nowait()
                # Process update...
            except Exception as e:
                # Log but continue processing other updates
                logging.error(f"Failed to process update {update_type}: {e}")
    except Exception as e:
        # Critical error in polling loop
        logging.error(f"GUI polling error: {e}")
    finally:
        # Always re-schedule (unless stopping)
        if self.running:
            self.root.after(100, self._poll_updates)
```

---

[← Modalità Esecuzione](05_Modalita_Esecuzione.md) | [Torna all'Indice](00_INDEX.md) | [Prossimo: Output e Report →](07_Output_Report.md)
