diff --git a/internal/tui/model.go b/internal/tui/model.go index a921b01..ecc6fe7 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -430,6 +430,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.detailsViewport.Height = vpHeight m.detailsViewport.Width = leftW - 4 + + // Refresh content with new dimensions + if m.subView == SubViewCallDetail { + m.updateCallDetailView() + } } return m, tea.Batch(cmds...) @@ -491,6 +496,7 @@ func (m *Model) handleViewKeys(msg tea.KeyMsg) tea.Cmd { } case "enter": m.subView = SubViewCallDetail + m.updateCallDetailView() } case ViewNetworkMap: @@ -642,6 +648,7 @@ func (m *Model) updateSubView(msg tea.Msg) (tea.Model, tea.Cmd) { // Since View is pure function of state, it's fine. // But detailsViewport scroll position should reset if packet changes? m.detailsViewport.GotoTop() + m.updateCallDetailView() } case "down", "j": flows := m.callFlowStore.GetRecentFlows(20) @@ -654,6 +661,7 @@ func (m *Model) updateSubView(msg tea.Msg) (tea.Model, tea.Cmd) { m.viewport.SetYOffset(m.selectedPacketIndex - m.viewport.Height + 1) } m.detailsViewport.GotoTop() + m.updateCallDetailView() } } } @@ -973,18 +981,6 @@ func (m Model) renderCallDetail() string { // Title detailsTitle := m.styles.Title.Render("📦 Packet Details") - // Content - var detailsContent string - if m.selectedPacketIndex < len(flow.Packets) { - pkt := flow.Packets[m.selectedPacketIndex] - detailsContent = fmt.Sprintf("Time: %s\nIP: %s -> %s\n\n%s", - pkt.Timestamp.Format("15:04:05.000"), - pkt.SourceIP, pkt.DestIP, - pkt.Raw) - } else { - detailsContent = "No packet selected" - } - // Update details viewport // Calculate available height: Pane Height - Borders(2) - Header(2 approx) // Actually styles handle borders on the container. @@ -999,75 +995,12 @@ func (m Model) renderCallDetail() string { m.detailsViewport.Width = leftW - 4 // Margin/Padding m.detailsViewport.Height = vpHeight - m.detailsViewport.SetContent(detailsContent) + // Content is set in Update now // --- Right Pane (Transaction Flow) --- // Title flowTitle := m.styles.Title.Render("🚀 Transaction Flow") - var right strings.Builder - for i, pkt := range flow.Packets { - arrow := "→" - arrowStyle := m.styles.ArrowOut - if len(flow.Packets) > 0 && pkt.SourceIP != flow.Packets[0].SourceIP { - arrow = "←" - arrowStyle = m.styles.ArrowIn - } - - var summaryStyle lipgloss.Style - if pkt.IsRequest { - switch pkt.Method { - case sip.MethodINVITE: - summaryStyle = m.styles.MethodInvite - case sip.MethodBYE, sip.MethodCANCEL: - summaryStyle = m.styles.MethodBye - case sip.MethodREGISTER: - summaryStyle = m.styles.MethodRegister - default: - summaryStyle = m.styles.MethodOther - } - } else { - switch { - case pkt.StatusCode >= 100 && pkt.StatusCode < 200: - summaryStyle = m.styles.Status1xx - case pkt.StatusCode >= 200 && pkt.StatusCode < 300: - summaryStyle = m.styles.Status2xx - case pkt.StatusCode >= 300 && pkt.StatusCode < 400: - summaryStyle = m.styles.Status3xx - case pkt.StatusCode >= 400 && pkt.StatusCode < 500: - summaryStyle = m.styles.Status4xx - case pkt.StatusCode >= 500 && pkt.StatusCode < 600: - summaryStyle = m.styles.Status5xx - default: - summaryStyle = m.styles.Status6xx - } - } - - ts := pkt.Timestamp.Format("15:04:05.000") - lineStr := fmt.Sprintf("%d. [%s] %s %s", i+1, ts, arrowStyle.Render(arrow), summaryStyle.Render(pkt.Summary())) - - if pkt.SDP != nil { - mediaIP := pkt.SDP.GetSDPMediaIP() - if mediaIP != "" { - label := m.networkMap.LabelForIP(mediaIP) - if label != mediaIP { - node := m.networkMap.FindByIP(mediaIP) - label = m.styleForNode(node).Render(label) - } - lineStr += fmt.Sprintf(" (SDP: %s %s)", mediaIP, label) - } - } - - if i == m.selectedPacketIndex { - lineStr = m.styles.Active.Render("> " + lineStr) - } else { - lineStr = " " + lineStr - } - right.WriteString(lineStr + "\n") - } - - m.viewport.SetContent(right.String()) - // Right Pane Height logic rvpHeight := (leftTotalH - 2) - 2 // -2 Borders, -2 Title/Margin if rvpHeight < 0 { @@ -1401,3 +1334,92 @@ func (m Model) styleForNode(node *config.NetworkNode) lipgloss.Style { return m.styles.NodeDefault } } + +func (m *Model) updateCallDetailView() { + flows := m.callFlowStore.GetRecentFlows(20) + if m.selectedFlow >= len(flows) || len(flows) == 0 { + m.detailsViewport.SetContent("No call selected") + m.viewport.SetContent("") + return + } + + flow := flows[m.selectedFlow] + + // --- Selected Packet Details --- + var detailsContent string + if m.selectedPacketIndex < len(flow.Packets) { + pkt := flow.Packets[m.selectedPacketIndex] + detailsContent = fmt.Sprintf("Time: %s\nIP: %s -> %s\n\n%s", + pkt.Timestamp.Format("15:04:05.000"), + pkt.SourceIP, pkt.DestIP, + pkt.Raw) + } else { + detailsContent = "No packet selected" + } + + m.detailsViewport.SetContent(detailsContent) + + // --- Transaction Flow --- + var right strings.Builder + for i, pkt := range flow.Packets { + arrow := "→" + arrowStyle := m.styles.ArrowOut + if len(flow.Packets) > 0 && pkt.SourceIP != flow.Packets[0].SourceIP { + arrow = "←" + arrowStyle = m.styles.ArrowIn + } + + var summaryStyle lipgloss.Style + if pkt.IsRequest { + switch pkt.Method { + case sip.MethodINVITE: + summaryStyle = m.styles.MethodInvite + case sip.MethodBYE, sip.MethodCANCEL: + summaryStyle = m.styles.MethodBye + case sip.MethodREGISTER: + summaryStyle = m.styles.MethodRegister + default: + summaryStyle = m.styles.MethodOther + } + } else { + switch { + case pkt.StatusCode >= 100 && pkt.StatusCode < 200: + summaryStyle = m.styles.Status1xx + case pkt.StatusCode >= 200 && pkt.StatusCode < 300: + summaryStyle = m.styles.Status2xx + case pkt.StatusCode >= 300 && pkt.StatusCode < 400: + summaryStyle = m.styles.Status3xx + case pkt.StatusCode >= 400 && pkt.StatusCode < 500: + summaryStyle = m.styles.Status4xx + case pkt.StatusCode >= 500 && pkt.StatusCode < 600: + summaryStyle = m.styles.Status5xx + default: + summaryStyle = m.styles.Status6xx + } + } + + ts := pkt.Timestamp.Format("15:04:05.000") + lineStr := fmt.Sprintf("%d. [%s] %s %s", i+1, ts, arrowStyle.Render(arrow), summaryStyle.Render(pkt.Summary())) + + if pkt.SDP != nil { + mediaIP := pkt.SDP.GetSDPMediaIP() + if mediaIP != "" { + label := m.networkMap.LabelForIP(mediaIP) + if label != mediaIP { + node := m.networkMap.FindByIP(mediaIP) + label = m.styleForNode(node).Render(label) + } + lineStr += fmt.Sprintf(" (SDP: %s %s)", mediaIP, label) + } + } + + if i == m.selectedPacketIndex { + lineStr = m.styles.Active.Render("> " + lineStr) + } else { + lineStr = " " + lineStr + } + right.WriteString(lineStr + "\n") + } + + m.viewport.SetContent(right.String()) +}