A machine is titled by its own hostname, and follows a rename

The name a machine was added under stays its identity — the URL, its accent, what the hub's
API calls it — and is what the Machines page shows as 'Added as'. The hostname is read live
rather than at start-up, so renaming a machine reaches the console without a restart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-21 17:04:17 -07:00
co-authored by Claude Opus 5
parent ec94cb8bff
commit dbfe163a75
8 changed files with 66 additions and 31 deletions
+3 -3
View File
@@ -210,7 +210,7 @@ func (e *alertEngine) evaluate() {
}
}
qb, _ := json.Marshal(query)
machines := append([]string{e.s.host}, e.s.nodes.Names()...)
machines := append([]string{e.s.hostName()}, e.s.nodes.Names()...)
results := map[string]map[string]any{} // machine → ruleID → value | {id: value}
reach := map[string]bool{}
var wg sync.WaitGroup
@@ -221,7 +221,7 @@ func (e *alertEngine) evaluate() {
defer wg.Done()
var res map[string]any
ok := false
if m == e.s.host {
if m == e.s.hostName() {
if out, err := sensors.Shape(e.s.sensors.All(), qb, false); err == nil {
res, ok = out.(map[string]any)
}
@@ -515,6 +515,6 @@ func (s *Server) alertsTest(w http.ResponseWriter, _ *http.Request) {
jsonErr(w, 400, "no channels to test")
return
}
e.notify(channels, AlertEvent{T: time.Now().UnixMilli(), Kind: "firing", Rule: "Test alert", Machine: s.host, Sensor: "hamlab.test", Value: "this is only a test", Severity: "info"})
e.notify(channels, AlertEvent{T: time.Now().UnixMilli(), Kind: "firing", Rule: "Test alert", Machine: s.hostName(), Sensor: "hamlab.test", Value: "this is only a test", Severity: "info"})
w.WriteHeader(204)
}
+29 -8
View File
@@ -73,15 +73,17 @@ type Server struct {
version string
started time.Time
user string
host string
host string // the machine's hostname as last read; use hostName(), it can change under us
hostMu sync.Mutex
hostAt time.Time
ui fs.FS
mux *http.ServeMux
}
func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *metrics.Collector, st *store.Store, svc *services.Manager, cr *cron.Manager, tm *tasks.Manager, up *updates.Manager, sto *storage.Manager, fsLocal, fsSudo files.FS, nm *nodes.Manager, fingerprint string, ui fs.FS, version string) *Server {
host, _ := os.Hostname()
s := &Server{cfg: cfg, sessions: sessions, mgr: mgr, metrics: col, sensors: sensors.NewRegistry(col), store: st, svc: svc, cron: cr, tasks: tm, updates: up, storage: sto, fsLocal: fsLocal, fsSudo: fsSudo, nodes: nm, fingerprint: fingerprint, ui: ui, version: version,
started: time.Now(), host: host, user: os.Getenv("USER")}
started: time.Now(), user: os.Getenv("USER")}
s.hostName() // read it once now so the first request doesn't have to
if u, err := user.Current(); err == nil && u.Username != "" {
s.user = u.Username
}
@@ -572,7 +574,7 @@ func (s *Server) revokeToken(w http.ResponseWriter, r *http.Request) {
// ---- nodes: the machines this hub manages ----
func (s *Server) listNodes(w http.ResponseWriter, _ *http.Request) {
jsonOK(w, 200, map[string]any{"nodes": s.nodes.List(), "host": s.host, "fingerprint": s.fingerprint})
jsonOK(w, 200, map[string]any{"nodes": s.nodes.List(), "host": s.hostName(), "fingerprint": s.fingerprint})
}
func (s *Server) addNode(w http.ResponseWriter, r *http.Request) {
@@ -797,6 +799,25 @@ func clientIP(r *http.Request) string {
// ---- me ----
// hostName is this machine's hostname now, not when the daemon started: rename a machine
// (hostnamectl, or a DHCP lease) and the console says so within a few seconds — in its header,
// in the machine menu, in alerts and in the fleet's sensor queries — without a restart. Read
// through a short cache because it is asked for on most requests and on every alert pass.
func (s *Server) hostName() string {
s.hostMu.Lock()
defer s.hostMu.Unlock()
if time.Since(s.hostAt) > 5*time.Second {
if h, err := os.Hostname(); err == nil && h != "" {
if s.host != "" && h != s.host {
log.Printf("hostname: %s is now %s", s.host, h)
}
s.host = h
}
s.hostAt = time.Now()
}
return s.host
}
func (s *Server) me(w http.ResponseWriter, _ *http.Request) {
load := ""
if b, err := os.ReadFile("/proc/loadavg"); err == nil {
@@ -820,7 +841,7 @@ func (s *Server) me(w http.ResponseWriter, _ *http.Request) {
}
})
jsonOK(w, 200, map[string]any{
"host": s.host, "user": s.user, "version": s.version,
"host": s.hostName(), "user": s.user, "version": s.version,
"load": load, "uptime": uptime, "service_started": s.started,
"reboot_needed": rebootErr == nil, "fingerprint": s.fingerprint, "arch": runtime.GOARCH, "demo_url": demoURL, "addr": addr,
})
@@ -1086,7 +1107,7 @@ func (s *Server) fleetQuery(w http.ResponseWriter, r *http.Request) {
jsonErr(w, 400, err.Error())
return
}
names := append([]string{s.host}, s.nodes.Names()...)
names := append([]string{s.hostName()}, s.nodes.Names()...)
if len(want) > 0 {
names = names[:0]
for _, n := range want {
@@ -1107,7 +1128,7 @@ func (s *Server) fleetQuery(w http.ResponseWriter, r *http.Request) {
var mu sync.Mutex
var wg sync.WaitGroup
for _, name := range names {
if name == s.host {
if name == s.hostName() {
out[name] = local
continue
}
@@ -1478,7 +1499,7 @@ func (s *Server) power(w http.ResponseWriter, r *http.Request) {
Confirm string `json:"confirm"`
}
_ = json.NewDecoder(io.LimitReader(r.Body, 4096)).Decode(&body)
if body.Confirm != s.host {
if body.Confirm != s.hostName() {
jsonErr(w, 400, "confirmation must be the hostname")
return
}
+1 -1
View File
@@ -363,7 +363,7 @@ func (s *Server) hubStacks(ctx context.Context, node string) ([]stack, error) {
st.Note = note
continue
}
st.Remote, st.HeldBy = remote, s.host
st.Remote, st.HeldBy = remote, s.hostName()
}
var held []stack
for _, st := range res.Stacks {