diff --git a/internal/cluster/cluster.go b/internal/cluster/cluster.go index 2616a64..f28cce9 100644 --- a/internal/cluster/cluster.go +++ b/internal/cluster/cluster.go @@ -18,6 +18,8 @@ import ( "strings" "sync" "time" + + "hamlog/internal/applog" ) // ServerConfig is the persisted shape of one cluster node. Mirrors the @@ -106,6 +108,7 @@ type session struct { doneCh chan struct{} stopped bool // guards against double-stop on the same session spotsCnt int + dbgN int // diagnostic: how many raw lines logged this connection } // Manager owns N sessions, one per enabled server. Safe for concurrent @@ -392,10 +395,31 @@ func (s *session) runOnce() (time.Time, error) { return connectedAt, fmt.Errorf("read: %w", err) } line = strings.TrimRight(line, "\r\n") + // Strip terminal BELLs (\a) and other control bytes that some nodes + // (e.g. F5LEN) append to spot lines — "DX de …0935Z KN04\a\a" — which + // would otherwise break the parser's end-of-line anchor. Tabs → spaces. + line = strings.Map(func(r rune) rune { + if r == '\t' { + return ' ' + } + if r < 0x20 || r == 0x7f { + return -1 + } + return r + }, line) if line == "" { continue } + // Safety net: warn (rate-limited) about DX lines the parser still can't + // read, so a future format drift is visible in the log. + if strings.Contains(line, "DX de") && s.dbgN < 10 { + if _, ok := parseSpot(line); !ok { + s.dbgN++ + applog.Printf("cluster[%s] UNPARSED spot: %q", s.cfg.Name, line) + } + } + // Login on explicit prompt. if !loginSent && s.login != "" && isLoginPrompt(line) { _, _ = conn.Write([]byte(s.login + "\r\n"))