package live import ( "sort" "sync" "time" ) // RosterEntry is one observed Sendspin client. Populated from // client/hello messages — independent of whether the connection was // then accepted or rejected. This is what gives the UI the ability to // show the *actual* `client_id` of every speaker on the network, even // when ClientFilter rejected its current attempt. type RosterEntry struct { ClientID string `json:"client_id"` Name string `json:"name"` LastSeen time.Time `json:"last_seen"` } // Roster caches every (client_id, name) pair the sendspin server has // observed via client/hello. Records are kept indefinitely — speakers // usually keep the same client_id across reboots, so a stale entry // remains useful when the user is configuring presets offline. type Roster struct { mu sync.RWMutex entries map[string]RosterEntry } func NewRoster() *Roster { return &Roster{entries: make(map[string]RosterEntry)} } // Record is intended to be plugged into ServerConfig.OnClientHello. // Updates the entry's LastSeen on every call so the UI can show // "active now" vs. "last seen 5 minutes ago". func (r *Roster) Record(clientID, name string) { if clientID == "" { return } r.mu.Lock() defer r.mu.Unlock() r.entries[clientID] = RosterEntry{ ClientID: clientID, Name: name, LastSeen: time.Now(), } } func (r *Roster) List() []RosterEntry { r.mu.RLock() defer r.mu.RUnlock() out := make([]RosterEntry, 0, len(r.entries)) for _, e := range r.entries { out = append(out, e) } sort.Slice(out, func(i, j int) bool { return out[i].ClientID < out[j].ClientID }) return out }