fix: Ensure Packet Details and Transaction Flow scroll correctly by updating viewports in Update loop

This commit is contained in:
Jose Luis Montañes Ojados
2026-01-19 17:15:49 +01:00
parent f5a2730bc3
commit 23349e2039

View File

@@ -430,6 +430,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
m.detailsViewport.Height = vpHeight m.detailsViewport.Height = vpHeight
m.detailsViewport.Width = leftW - 4 m.detailsViewport.Width = leftW - 4
// Refresh content with new dimensions
if m.subView == SubViewCallDetail {
m.updateCallDetailView()
}
} }
return m, tea.Batch(cmds...) return m, tea.Batch(cmds...)
@@ -491,6 +496,7 @@ func (m *Model) handleViewKeys(msg tea.KeyMsg) tea.Cmd {
} }
case "enter": case "enter":
m.subView = SubViewCallDetail m.subView = SubViewCallDetail
m.updateCallDetailView()
} }
case ViewNetworkMap: 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. // Since View is pure function of state, it's fine.
// But detailsViewport scroll position should reset if packet changes? // But detailsViewport scroll position should reset if packet changes?
m.detailsViewport.GotoTop() m.detailsViewport.GotoTop()
m.updateCallDetailView()
} }
case "down", "j": case "down", "j":
flows := m.callFlowStore.GetRecentFlows(20) 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.viewport.SetYOffset(m.selectedPacketIndex - m.viewport.Height + 1)
} }
m.detailsViewport.GotoTop() m.detailsViewport.GotoTop()
m.updateCallDetailView()
} }
} }
} }
@@ -973,18 +981,6 @@ func (m Model) renderCallDetail() string {
// Title // Title
detailsTitle := m.styles.Title.Render("📦 Packet Details") 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 // Update details viewport
// Calculate available height: Pane Height - Borders(2) - Header(2 approx) // Calculate available height: Pane Height - Borders(2) - Header(2 approx)
// Actually styles handle borders on the container. // 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.Width = leftW - 4 // Margin/Padding
m.detailsViewport.Height = vpHeight m.detailsViewport.Height = vpHeight
m.detailsViewport.SetContent(detailsContent) // Content is set in Update now
// --- Right Pane (Transaction Flow) --- // --- Right Pane (Transaction Flow) ---
// Title // Title
flowTitle := m.styles.Title.Render("🚀 Transaction Flow") 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 // Right Pane Height logic
rvpHeight := (leftTotalH - 2) - 2 // -2 Borders, -2 Title/Margin rvpHeight := (leftTotalH - 2) - 2 // -2 Borders, -2 Title/Margin
if rvpHeight < 0 { if rvpHeight < 0 {
@@ -1401,3 +1334,92 @@ func (m Model) styleForNode(node *config.NetworkNode) lipgloss.Style {
return m.styles.NodeDefault 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())
}