package main import ( "encoding/json" "net/http" "os" "sync" "time" ) // seenStore remembers when each machine's agent last connected and last dropped, in a small file next to the config, so the // control page can say "offline since ..." even after a server restart. type seenStore struct { mu sync.Mutex path string m map[string]*seenRec } type seenRec struct { Up int64 `json:"up"` // unix seconds of the last connect Down int64 `json:"down"` // unix seconds of the last disconnect (0: never dropped since it was recorded) } func newSeenStore(path string) *seenStore { s := &seenStore{path: path, m: map[string]*seenRec{}} if b, err := os.ReadFile(path); err == nil { json.Unmarshal(b, &s.m) } if s.m == nil { s.m = map[string]*seenRec{} } return s } func (s *seenStore) save() { // called with s.mu held if s.path == "" { return } b, _ := json.Marshal(s.m) tmp := s.path + ".tmp" if os.WriteFile(tmp, b, 0600) == nil { os.Rename(tmp, s.path) } } func (s *seenStore) rec(name string) *seenRec { r := s.m[name] if r == nil { r = &seenRec{} s.m[name] = r } return r } func (s *seenStore) up(name string) { if s == nil { return } s.mu.Lock() defer s.mu.Unlock() s.rec(name).Up = time.Now().Unix() s.save() } func (s *seenStore) down(name string) { if s == nil { return } s.mu.Lock() defer s.mu.Unlock() s.rec(name).Down = time.Now().Unix() s.save() } func (s *seenStore) snapshot() map[string]seenRec { out := map[string]seenRec{} if s == nil { return out } s.mu.Lock() defer s.mu.Unlock() for k, v := range s.m { out[k] = *v } return out } // machinesSeen answers the control page: for every machine, when it last connected and last dropped. func (s *Server) machinesSeen(w http.ResponseWriter, r *http.Request) { if !s.authed(r) { http.Error(w, "no", 401) return } w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(s.seen.snapshot()) }