mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-08-29 07:58:23 +00:00
fix: #184-#189 — sanitize names, packetsLast24h, ReadMemStats cache, dup name indicator, heatmap warning
#184: Strip non-printable chars (<0x20 except tab/newline) from ADVERT names in Go server decoder, Go ingestor decoder, and Node decoder.js. #185: Add visual (N) badge next to node names when multiple nodes share the same display name (case-insensitive). Shows in list, side pane, and full detail page with 'also known as' links to other keys. #186: Add packetsLast24h field to /api/stats response. #187 #188: Cache runtime.ReadMemStats() with 5s TTL in Go server. #189: Temporarily patch HTMLCanvasElement.prototype.getContext during L.heatLayer().addTo(map) to pass { willReadFrequently: true }, preventing Chrome console warning about canvas readback performance. Tests: 10 new tests for buildDupNameMap + dupNameBadge (143 total frontend). Cache busters bumped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+13
-1
@@ -251,8 +251,8 @@ func decodeAdvert(buf []byte) Payload {
|
||||
}
|
||||
if p.Flags.HasName {
|
||||
name := string(appdata[off:])
|
||||
// Trim trailing null bytes
|
||||
name = strings.TrimRight(name, "\x00")
|
||||
name = sanitizeName(name)
|
||||
p.Name = name
|
||||
}
|
||||
}
|
||||
@@ -605,6 +605,18 @@ func ValidateAdvert(p *Payload) (bool, string) {
|
||||
return true, ""
|
||||
}
|
||||
|
||||
// sanitizeName strips non-printable characters (< 0x20 except tab/newline) and DEL.
|
||||
func sanitizeName(s string) string {
|
||||
var b strings.Builder
|
||||
b.Grow(len(s))
|
||||
for _, c := range s {
|
||||
if c == '\t' || c == '\n' || (c >= 0x20 && c != 0x7f) {
|
||||
b.WriteRune(c)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func advertRole(f *AdvertFlags) string {
|
||||
if f.Repeater {
|
||||
return "repeater"
|
||||
|
||||
@@ -189,6 +189,7 @@ type Stats struct {
|
||||
TotalNodesAllTime int `json:"totalNodesAllTime"`
|
||||
TotalObservers int `json:"totalObservers"`
|
||||
PacketsLastHour int `json:"packetsLastHour"`
|
||||
PacketsLast24h int `json:"packetsLast24h"`
|
||||
}
|
||||
|
||||
// GetStats returns aggregate counts (matches Node.js db.getStats shape).
|
||||
@@ -210,6 +211,9 @@ func (db *DB) GetStats() (*Stats, error) {
|
||||
oneHourAgo := time.Now().Add(-1 * time.Hour).Unix()
|
||||
db.conn.QueryRow("SELECT COUNT(*) FROM observations WHERE timestamp > ?", oneHourAgo).Scan(&s.PacketsLastHour)
|
||||
|
||||
oneDayAgo := time.Now().Add(-24 * time.Hour).Unix()
|
||||
db.conn.QueryRow("SELECT COUNT(*) FROM observations WHERE timestamp > ?", oneDayAgo).Scan(&s.PacketsLast24h)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -229,6 +229,7 @@ func decodeAdvert(buf []byte) Payload {
|
||||
if p.Flags.HasName {
|
||||
name := string(appdata[off:])
|
||||
name = strings.TrimRight(name, "\x00")
|
||||
name = sanitizeName(name)
|
||||
p.Name = name
|
||||
}
|
||||
}
|
||||
@@ -457,6 +458,18 @@ func ValidateAdvert(p *Payload) (bool, string) {
|
||||
return true, ""
|
||||
}
|
||||
|
||||
// sanitizeName strips non-printable characters (< 0x20 except tab/newline) and DEL.
|
||||
func sanitizeName(s string) string {
|
||||
var b strings.Builder
|
||||
b.Grow(len(s))
|
||||
for _, c := range s {
|
||||
if c == '\t' || c == '\n' || (c >= 0x20 && c != 0x7f) {
|
||||
b.WriteRune(c)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func advertRole(f *AdvertFlags) string {
|
||||
if f.Repeater {
|
||||
return "repeater"
|
||||
|
||||
+22
-2
@@ -11,6 +11,7 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -27,6 +28,11 @@ type Server struct {
|
||||
version string
|
||||
commit string
|
||||
buildTime string
|
||||
|
||||
// Cached runtime.MemStats to avoid stop-the-world pauses on every health check
|
||||
memStatsMu sync.Mutex
|
||||
memStatsCache runtime.MemStats
|
||||
memStatsCachedAt time.Time
|
||||
}
|
||||
|
||||
// PerfStats tracks request performance.
|
||||
@@ -66,6 +72,20 @@ func NewServer(db *DB, cfg *Config, hub *Hub) *Server {
|
||||
}
|
||||
}
|
||||
|
||||
const memStatsTTL = 5 * time.Second
|
||||
|
||||
// getMemStats returns cached runtime.MemStats, refreshing at most every 5 seconds.
|
||||
// runtime.ReadMemStats() stops the world; caching prevents per-request GC pauses.
|
||||
func (s *Server) getMemStats() runtime.MemStats {
|
||||
s.memStatsMu.Lock()
|
||||
defer s.memStatsMu.Unlock()
|
||||
if time.Since(s.memStatsCachedAt) > memStatsTTL {
|
||||
runtime.ReadMemStats(&s.memStatsCache)
|
||||
s.memStatsCachedAt = time.Now()
|
||||
}
|
||||
return s.memStatsCache
|
||||
}
|
||||
|
||||
// RegisterRoutes sets up all HTTP routes on the given router.
|
||||
func (s *Server) RegisterRoutes(r *mux.Router) {
|
||||
// Performance instrumentation middleware
|
||||
@@ -274,8 +294,7 @@ func (s *Server) handleConfigMap(w http.ResponseWriter, r *http.Request) {
|
||||
// --- System Handlers ---
|
||||
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
m := s.getMemStats()
|
||||
uptime := time.Since(s.startedAt).Seconds()
|
||||
|
||||
wsClients := 0
|
||||
@@ -381,6 +400,7 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||
TotalNodesAllTime: stats.TotalNodesAllTime,
|
||||
TotalObservers: stats.TotalObservers,
|
||||
PacketsLastHour: stats.PacketsLastHour,
|
||||
PacketsLast24h: stats.PacketsLast24h,
|
||||
Engine: "go",
|
||||
Version: s.version,
|
||||
Commit: s.commit,
|
||||
|
||||
@@ -440,6 +440,9 @@ func (s *PacketStore) GetStoreStats() (*Stats, error) {
|
||||
oneHourAgo := time.Now().Add(-1 * time.Hour).Unix()
|
||||
s.db.conn.QueryRow("SELECT COUNT(*) FROM observations WHERE timestamp > ?", oneHourAgo).Scan(&st.PacketsLastHour)
|
||||
|
||||
oneDayAgo := time.Now().Add(-24 * time.Hour).Unix()
|
||||
s.db.conn.QueryRow("SELECT COUNT(*) FROM observations WHERE timestamp > ?", oneDayAgo).Scan(&st.PacketsLast24h)
|
||||
|
||||
return st, nil
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -1506,6 +1506,9 @@
|
||||
"packetsLastHour": {
|
||||
"type": "number"
|
||||
},
|
||||
"packetsLast24h": {
|
||||
"type": "number"
|
||||
},
|
||||
"counts": {
|
||||
"type": "object",
|
||||
"keys": {
|
||||
|
||||
@@ -62,6 +62,7 @@ type StatsResponse struct {
|
||||
TotalNodesAllTime int `json:"totalNodesAllTime"`
|
||||
TotalObservers int `json:"totalObservers"`
|
||||
PacketsLastHour int `json:"packetsLastHour"`
|
||||
PacketsLast24h int `json:"packetsLast24h"`
|
||||
Engine string `json:"engine"`
|
||||
Version string `json:"version"`
|
||||
Commit string `json:"commit"`
|
||||
|
||||
@@ -595,6 +595,7 @@ function getStats() {
|
||||
totalNodesAllTime: stmts.countNodes.get().count,
|
||||
totalObservers: stmts.countObservers.get().count,
|
||||
packetsLastHour: stmts.countRecentPackets.get(oneHourAgo).count,
|
||||
packetsLast24h: stmts.countRecentPackets.get(new Date(Date.now() - 24 * 3600000).toISOString()).count,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -135,7 +135,10 @@ function decodeAdvert(buf) {
|
||||
off += 8;
|
||||
}
|
||||
if (result.flags.hasName) {
|
||||
result.name = appdata.subarray(off).toString('utf8');
|
||||
let name = appdata.subarray(off).toString('utf8');
|
||||
// Strip non-printable characters (< 0x20 except tab/newline) and DEL
|
||||
name = name.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
|
||||
result.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user