feat: Add colored node labels by type and clean up display format

This commit is contained in:
Jose Luis Montañes Ojados
2026-01-19 14:48:03 +01:00
parent 7375539def
commit 7cb29c45e3
3 changed files with 163 additions and 2 deletions

Binary file not shown.

View File

@@ -56,7 +56,7 @@ func (nm *NetworkMap) FindByIP(ip string) *NetworkNode {
// LabelForIP returns a human-readable label for an IP, or the IP itself if unknown // LabelForIP returns a human-readable label for an IP, or the IP itself if unknown
func (nm *NetworkMap) LabelForIP(ip string) string { func (nm *NetworkMap) LabelForIP(ip string) string {
if node := nm.FindByIP(ip); node != nil { if node := nm.FindByIP(ip); node != nil {
return node.Name + " (" + string(node.Type) + ")" return node.Name
} }
return ip return ip
} }

View File

@@ -99,6 +99,30 @@ type Styles struct {
Box lipgloss.Style Box lipgloss.Style
PacketRow lipgloss.Style PacketRow lipgloss.Style
CallFlow lipgloss.Style CallFlow lipgloss.Style
// SIP Styles
MethodInvite lipgloss.Style
MethodBye lipgloss.Style
MethodRegister lipgloss.Style
MethodOther lipgloss.Style
Status1xx lipgloss.Style
Status2xx lipgloss.Style
Status3xx lipgloss.Style
Status4xx lipgloss.Style
Status5xx lipgloss.Style
Status6xx lipgloss.Style
NodeLabel lipgloss.Style
NodePBX lipgloss.Style
NodeProxy lipgloss.Style
NodeGateway lipgloss.Style
NodeCarrier lipgloss.Style
NodeEndpoint lipgloss.Style
NodeDefault lipgloss.Style
ArrowOut lipgloss.Style
ArrowIn lipgloss.Style
} }
func defaultStyles() Styles { func defaultStyles() Styles {
@@ -135,6 +159,60 @@ func defaultStyles() Styles {
Border(lipgloss.NormalBorder()). Border(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("#44475A")). BorderForeground(lipgloss.Color("#44475A")).
Padding(0, 1), Padding(0, 1),
// SIP Styles Initialization
MethodInvite: lipgloss.NewStyle().Foreground(lipgloss.Color("#8BE9FD")).Bold(true), // Cyan
MethodBye: lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5555")).Bold(true), // Red
MethodRegister: lipgloss.NewStyle().Foreground(lipgloss.Color("#50FA7B")).Bold(true), // Green
MethodOther: lipgloss.NewStyle().Foreground(lipgloss.Color("#BD93F9")).Bold(true), // Purple
Status1xx: lipgloss.NewStyle().Foreground(lipgloss.Color("#F1FA8C")), // Yellow
Status2xx: lipgloss.NewStyle().Foreground(lipgloss.Color("#50FA7B")), // Green
Status3xx: lipgloss.NewStyle().Foreground(lipgloss.Color("#8BE9FD")), // Cyan
Status4xx: lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5555")), // Red
Status5xx: lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5555")).Bold(true), // Red Bold
Status6xx: lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5555")).Bold(true).Underline(true),
NodeLabel: lipgloss.NewStyle().
Foreground(lipgloss.Color("#000000")).
Background(lipgloss.Color("#BD93F9")). // Purple background
Padding(0, 1).
Bold(true),
// Node Styles with different background colors
NodePBX: lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFFFF")).
Background(lipgloss.Color("#FF79C6")). // Pink
Padding(0, 1).
Bold(true),
NodeProxy: lipgloss.NewStyle().
Foreground(lipgloss.Color("#000000")).
Background(lipgloss.Color("#8BE9FD")). // Cyan
Padding(0, 1).
Bold(true),
NodeGateway: lipgloss.NewStyle().
Foreground(lipgloss.Color("#000000")).
Background(lipgloss.Color("#F1FA8C")). // Yellow
Padding(0, 1).
Bold(true),
NodeCarrier: lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFFFF")).
Background(lipgloss.Color("#BD93F9")). // Purple
Padding(0, 1).
Bold(true),
NodeEndpoint: lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFFFF")).
Background(lipgloss.Color("#6272A4")). // Grey/Blue
Padding(0, 1).
Bold(true),
NodeDefault: lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFFFF")).
Background(lipgloss.Color("#44475A")). // Grey
Padding(0, 1).
Bold(true),
ArrowOut: lipgloss.NewStyle().Foreground(lipgloss.Color("#FF79C6")), // Pink
ArrowIn: lipgloss.NewStyle().Foreground(lipgloss.Color("#F1FA8C")), // Yellow
} }
} }
@@ -608,6 +686,8 @@ func (m Model) renderAddNodeForm() string {
b.WriteString("\n") b.WriteString("\n")
} }
b.WriteString("\n") b.WriteString("\n")
b.WriteString(m.styles.Help.Render("Node Types: PBX, Proxy, SBC, Carrier, Handset, Firewall"))
b.WriteString("\n")
b.WriteString(m.styles.Help.Render("Tab navigate • Enter submit • Esc cancel")) b.WriteString(m.styles.Help.Render("Tab navigate • Enter submit • Esc cancel"))
return b.String() return b.String()
} }
@@ -634,8 +714,19 @@ func (m Model) renderCallDetail() string {
// Find first packet to get initial IPs // Find first packet to get initial IPs
if len(flow.Packets) > 0 { if len(flow.Packets) > 0 {
first := flow.Packets[0] first := flow.Packets[0]
srcLabel := m.networkMap.LabelForIP(first.SourceIP) srcLabel := m.networkMap.LabelForIP(first.SourceIP)
if srcLabel != first.SourceIP {
// Find node type to apply style
node := m.networkMap.FindByIP(first.SourceIP)
srcLabel = m.styleForNode(node).Render(srcLabel)
}
dstLabel := m.networkMap.LabelForIP(first.DestIP) dstLabel := m.networkMap.LabelForIP(first.DestIP)
if dstLabel != first.DestIP {
node := m.networkMap.FindByIP(first.DestIP)
dstLabel = m.styleForNode(node).Render(dstLabel)
}
b.WriteString(fmt.Sprintf(" Source: %s (%s:%d)\n", srcLabel, first.SourceIP, first.SourcePort)) b.WriteString(fmt.Sprintf(" Source: %s (%s:%d)\n", srcLabel, first.SourceIP, first.SourcePort))
b.WriteString(fmt.Sprintf(" Destination: %s (%s:%d)\n", dstLabel, first.DestIP, first.DestPort)) b.WriteString(fmt.Sprintf(" Destination: %s (%s:%d)\n", dstLabel, first.DestIP, first.DestPort))
@@ -645,22 +736,63 @@ func (m Model) renderCallDetail() string {
b.WriteString("Transaction Flow:\n") b.WriteString("Transaction Flow:\n")
for i, pkt := range flow.Packets { for i, pkt := range flow.Packets {
arrow := "→" arrow := "→"
arrowStyle := m.styles.ArrowOut
// Simple direction indicator based on whether it matches initial source // Simple direction indicator based on whether it matches initial source
if len(flow.Packets) > 0 && pkt.SourceIP != flow.Packets[0].SourceIP { if len(flow.Packets) > 0 && pkt.SourceIP != flow.Packets[0].SourceIP {
arrow = "←" arrow = "←"
arrowStyle = m.styles.ArrowIn
}
// Style the packet summary (Method or Status)
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
}
} }
// Format timestamp // Format timestamp
ts := pkt.Timestamp.Format("15:04:05.000") ts := pkt.Timestamp.Format("15:04:05.000")
// Clean packet info line (Timestamp + Arrow + Method/Status) // Clean packet info line (Timestamp + Arrow + Method/Status)
b.WriteString(fmt.Sprintf(" %d. [%s] %s %s\n", i+1, ts, arrow, pkt.Summary())) b.WriteString(fmt.Sprintf(" %d. [%s] %s %s\n",
i+1,
ts,
arrowStyle.Render(arrow),
summaryStyle.Render(pkt.Summary())))
// Show SDP info if present // Show SDP info if present
if pkt.SDP != nil { if pkt.SDP != nil {
mediaIP := pkt.SDP.GetSDPMediaIP() mediaIP := pkt.SDP.GetSDPMediaIP()
if mediaIP != "" { if mediaIP != "" {
label := m.networkMap.LabelForIP(mediaIP) label := m.networkMap.LabelForIP(mediaIP)
if label != mediaIP {
node := m.networkMap.FindByIP(mediaIP)
label = m.styleForNode(node).Render(label)
}
b.WriteString(fmt.Sprintf(" SDP Media: %s (%s)\n", mediaIP, label)) b.WriteString(fmt.Sprintf(" SDP Media: %s (%s)\n", mediaIP, label))
} }
} }
@@ -949,3 +1081,32 @@ func extractUser(sipAddr string) string {
} }
return sipAddr return sipAddr
} }
// styleForNode returns the style for a given node type
func (m Model) styleForNode(node *config.NetworkNode) lipgloss.Style {
if node == nil {
return m.styles.NodeDefault
}
switch node.Type {
case config.NodeTypePBX:
return m.styles.NodePBX
case config.NodeTypeProxy:
return m.styles.NodeProxy
case config.NodeTypeGateway:
return m.styles.NodeGateway
case config.NodeTypeMediaServer:
// Reusing Proxy style for MediaServer if no specific one defined
return m.styles.NodeProxy
case config.NodeTypeEndpoint:
return m.styles.NodeEndpoint
case config.NodeTypeUnknown:
return m.styles.NodeDefault
default:
// Attempt to match by name if type is custom or carrier
lowerName := strings.ToLower(node.Name)
if strings.Contains(lowerName, "carrier") || strings.Contains(lowerName, "provider") {
return m.styles.NodeCarrier
}
return m.styles.NodeDefault
}
}