Show connected clients and their last human action in Status

This commit is contained in:
Ian Gulliver
2026-10-17 23:24:26 -07:00
parent 28c4549151
commit 8d9ba10849
8 changed files with 332 additions and 246 deletions
+29 -7
View File
@@ -3,17 +3,36 @@ package server
import (
"sort"
"sync"
"time"
"g.fc.run/misc/byre/internal/api"
)
type clientKey struct{ login, host, commit string }
type hub struct {
mu sync.Mutex
subs map[chan api.Event]api.ConnectedClient
mu sync.Mutex
subs map[chan api.Event]api.ConnectedClient
lastAction map[clientKey]time.Time
lastNotified map[clientKey]time.Time
}
func newHub() *hub {
return &hub{subs: map[chan api.Event]api.ConnectedClient{}}
return &hub{subs: map[chan api.Event]api.ConnectedClient{}, lastAction: map[clientKey]time.Time{}, lastNotified: map[clientKey]time.Time{}}
}
func (h *hub) acted(login, host, commit string) bool {
h.mu.Lock()
defer h.mu.Unlock()
k := clientKey{login, host, commit}
now := time.Now()
h.lastAction[k] = now
prev, ok := h.lastNotified[k]
if ok && now.Sub(prev) < time.Second {
return false
}
h.lastNotified[k] = now
return true
}
func (h *hub) subscribe(client api.ConnectedClient) chan api.Event {
@@ -57,10 +76,9 @@ func (h *hub) count() int {
func (h *hub) clients() []api.ConnectedClient {
h.mu.Lock()
defer h.mu.Unlock()
type key struct{ login, host, commit string }
oldest := map[key]api.ConnectedClient{}
oldest := map[clientKey]api.ConnectedClient{}
for _, c := range h.subs {
k := key{c.Login, c.Host, c.Commit}
k := clientKey{c.Login, c.Host, c.Commit}
prev, ok := oldest[k]
if ok && !c.ConnectedAt.Before(prev.ConnectedAt) {
continue
@@ -68,7 +86,11 @@ func (h *hub) clients() []api.ConnectedClient {
oldest[k] = c
}
out := []api.ConnectedClient{}
for _, c := range oldest {
for k, c := range oldest {
at, ok := h.lastAction[k]
if ok {
c.LastActionAt = &at
}
out = append(out, c)
}
sort.Slice(out, func(i, j int) bool { return out[i].ConnectedAt.Before(out[j].ConnectedAt) })
+24
View File
@@ -134,10 +134,34 @@ func (s *Server) identify(next http.Handler) http.Handler {
return
}
if (r.Method != http.MethodGet && !automaticRequests[r.Pattern]) || humanGets[r.Pattern] {
s.acted(login, r)
}
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxKey{}, login)))
})
}
func (s *Server) acted(login string, r *http.Request) {
if s.hub.acted(login, r.Header.Get("Byre-Host"), r.Header.Get("Byre-Commit")) {
s.clientsChanged()
}
}
func (s *Server) clientsChanged() {
s.store.Broadcast(api.Event{Kind: "admin.clients", Actor: "byre", Payload: map[string]any{}})
}
var automaticRequests = map[string]bool{
"PUT /api/me/pane-focus": true,
"PUT /api/tasks/{taskId}/summary-read": true,
"POST /api/inbox/{itemId}/runs": true,
}
var humanGets = map[string]bool{
"GET /api/inbox/{itemId}/bundle": true,
}
func (s *Server) ensureUser(ctx context.Context, login, name string) error {
s.usersMu.Lock()
defer s.usersMu.Unlock()
+2
View File
@@ -61,7 +61,9 @@ func (s *Server) StreamEvents(w http.ResponseWriter, r *http.Request, params api
Commit: r.Header.Get("Byre-Commit"),
ConnectedAt: time.Now(),
})
defer s.clientsChanged()
defer s.hub.unsubscribe(live)
s.clientsChanged()
for {
events, err := backfill(last)
+3 -2
View File
@@ -232,7 +232,7 @@ func (s *Server) GetTaskTerminal(w http.ResponseWriter, r *http.Request, taskID
defer cancel()
go func() {
defer cancel()
terminalInput(ctx, conn, term)
terminalInput(ctx, conn, term, func() { s.acted(actor(r), r) })
}()
if len(backlog) > 0 {
@@ -264,13 +264,14 @@ func (s *Server) GetTaskTerminal(w http.ResponseWriter, r *http.Request, taskID
}
}
func terminalInput(ctx context.Context, conn *websocket.Conn, term *terminal) {
func terminalInput(ctx context.Context, conn *websocket.Conn, term *terminal, acted func()) {
for {
typ, b, err := conn.Read(ctx)
if err != nil {
return
}
if typ == websocket.MessageBinary {
acted()
_, err = term.ptmx.Write(b)
if err != nil {
slog.Debug("terminal input", "err", err)