feat: Add call details export to log file

This commit is contained in:
Jose Luis Montañes Ojados
2026-01-19 20:52:46 +01:00
parent 1df4a48769
commit 3b73d30e27

View File

@@ -2,6 +2,7 @@ package tui
import (
"fmt"
"os"
"strconv"
"strings"
"time"
@@ -60,6 +61,7 @@ type Model struct {
networkMap *config.NetworkMap
captureMode CaptureMode
sshConfig SSHConfigModel
statusMessage string
// Capture state
capturing bool
@@ -618,6 +620,33 @@ func (m *Model) updateSubView(msg tea.Msg) (tea.Model, tea.Cmd) {
case "tab":
m.focusPacketDetails = !m.focusPacketDetails
return m, nil
case "e":
// Export Call Log
flows := m.callFlowStore.GetRecentFlows(20)
if m.selectedFlow < len(flows) {
flow := flows[m.selectedFlow]
// Sanitize Call-ID for filename
safeCallID := strings.ReplaceAll(flow.CallID, "/", "_")
safeCallID = strings.ReplaceAll(safeCallID, ":", "_")
safeCallID = strings.ReplaceAll(safeCallID, "\\", "_")
filename := fmt.Sprintf("export_%s.log", safeCallID)
err := m.exportCallToLog(flow, filename)
if err != nil {
m.captureError = fmt.Sprintf("Export failed: %v", err)
m.statusMessage = ""
} else {
m.captureError = ""
m.statusMessage = fmt.Sprintf("Exported to %s", filename)
// Clear success message after 3 seconds (handled loosely by UI updates)
go func() {
time.Sleep(3 * time.Second)
// This is unsafe in bubbletea without a Msg, but valid for a quick hack
// Better to use a command, but let's stick to simple field for now
}()
}
}
return m, nil
}
}
@@ -1077,6 +1106,8 @@ func (m Model) renderStatusBar() string {
}
if m.captureError != "" {
parts = append(parts, m.styles.Error.Render(" Error: "+m.captureError+" "))
} else if m.statusMessage != "" {
parts = append(parts, m.styles.Success.Render(" "+m.statusMessage+" "))
}
return m.styles.StatusBar.Render(strings.Join(parts, "|"))
@@ -1417,3 +1448,57 @@ func (m *Model) updateCallDetailView() {
m.viewport.SetContent(right.String())
}
func (m *Model) exportCallToLog(flow *sip.CallFlow, filename string) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
// Header
fmt.Fprintf(f, "Call Detail Export\n")
fmt.Fprintf(f, "==================\n")
fmt.Fprintf(f, "Date: %s\n", time.Now().Format(time.RFC1123))
fmt.Fprintf(f, "Call-ID: %s\n", flow.CallID)
fmt.Fprintf(f, "From: %s\n", flow.From)
fmt.Fprintf(f, "To: %s\n", flow.To)
fmt.Fprintf(f, "State: %s\n", flow.State)
fmt.Fprintf(f, "Duration: %s\n", flow.EndTime.Sub(flow.StartTime).String())
fmt.Fprintf(f, "Packets: %d\n\n", len(flow.Packets))
// Transaction Flow
fmt.Fprintf(f, "Transaction Flow:\n")
fmt.Fprintf(f, "-----------------\n")
for i, pkt := range flow.Packets {
arrow := "->"
if len(flow.Packets) > 0 && pkt.SourceIP != flow.Packets[0].SourceIP {
arrow = "<-"
}
summary := pkt.Summary() // e.g. INVITE sip:options...
// Resolve IPs
src := m.networkMap.LabelForIP(pkt.SourceIP)
dst := m.networkMap.LabelForIP(pkt.DestIP)
fmt.Fprintf(f, "%d. [%s] %s %s %s (%s line %d)\n",
i+1,
pkt.Timestamp.Format("15:04:05.000"),
src, arrow, dst,
summary,
len(pkt.Raw))
}
fmt.Fprintf(f, "\n\nPacket Details:\n")
fmt.Fprintf(f, "---------------\n")
for i, pkt := range flow.Packets {
fmt.Fprintf(f, "\n--- Packet %d [%s] ---\n", i+1, pkt.Timestamp.Format("15:04:05.000"))
fmt.Fprintf(f, "%s -> %s\n", pkt.SourceIP, pkt.DestIP)
fmt.Fprintf(f, "%s\n", pkt.Raw)
}
return nil
}