// svxdr — live-data daemon for SvxPortal (nodelist + proxy + astmeta + xlink) // Copyright (C) 2026 SA7AUX (Björn) // SPDX-License-Identifier: AGPL-3.0-or-later // // This program is free software: you can redistribute it and/or modify it under // the terms of the GNU Affero General Public License as published by the Free // Software Foundation, either version 3 of the License, or (at your option) any // later version. See the LICENSE file for details. package nodelist // nodes.json — shared bridge map (see svx-web/nodes.README.md). nodelist takes // the friendly reflector names (System) and the Location format strings from it, // so the Location column ("{system} via {node} / {talker}") is driven by the // same one file that configures astmeta and Asterisk. Merged ON TOP of the // existing lastheard.json, so a box with only lastheard.json is unchanged. import ( "encoding/json" "os" "strconv" "strings" ) type nodesFileNL struct { Lastheard struct { BridgeLocationFormat string `json:"bridge_location_format"` BridgeLocationFormatNoSystem string `json:"bridge_location_format_no_system"` } `json:"lastheard"` Nodes []struct { Allstar int `json:"allstar"` System string `json:"system"` Via string `json:"via"` Callsign string `json:"callsign"` Enabled *bool `json:"enabled"` } `json:"nodes"` } // applyNodesRenderConfig merges nodes.json's reflector names + Location formats // over cfg. A missing/invalid file is a no-op (cfg keeps the lastheard.json / // default values). ReflectorNames is keyed the way bridgeIdentity looks it up: // by the bridge label (via/callsign) and, as a fallback, the far AllStar node. func applyNodesRenderConfig(cfg *lastheardRenderCfg, path string) { b, err := os.ReadFile(path) if err != nil { return } var nf nodesFileNL if json.Unmarshal(b, &nf) != nil { return } if s := strings.TrimSpace(nf.Lastheard.BridgeLocationFormat); s != "" { cfg.BridgeLocationFormat = s } if s := strings.TrimSpace(nf.Lastheard.BridgeLocationFormatNoSystem); s != "" { cfg.BridgeLocationFormatNoSystem = s } if cfg.ReflectorNames == nil { cfg.ReflectorNames = map[string]string{} } for _, e := range nf.Nodes { if (e.Enabled != nil && !*e.Enabled) || e.System == "" { continue } if e.Via != "" { cfg.ReflectorNames[e.Via] = e.System } if e.Callsign != "" { cfg.ReflectorNames[e.Callsign] = e.System } if e.Allstar != 0 { cfg.ReflectorNames[strconv.Itoa(e.Allstar)] = e.System } } } // nodesJSONPath returns the nodes.json path (NODES_JSON env, default /etc/nodes.json). func nodesJSONPath() string { if p := os.Getenv("NODES_JSON"); p != "" { return p } return "/etc/nodes.json" }