Add realtime gateway and simulator bridge
This commit is contained in:
228
realtime-gateway/internal/gateway/admin_ui.go
Normal file
228
realtime-gateway/internal/gateway/admin_ui.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"realtime-gateway/internal/model"
|
||||
)
|
||||
|
||||
//go:embed adminui/*
|
||||
var adminUIFiles embed.FS
|
||||
|
||||
type adminOverview struct {
|
||||
Status string `json:"status"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
Now time.Time `json:"now"`
|
||||
UptimeSeconds int64 `json:"uptimeSeconds"`
|
||||
HTTPListen string `json:"httpListen"`
|
||||
Anonymous bool `json:"anonymousConsumers"`
|
||||
Metrics map[string]any `json:"metrics"`
|
||||
Auth map[string]any `json:"auth"`
|
||||
Endpoints map[string]any `json:"endpoints"`
|
||||
}
|
||||
|
||||
func (s *Server) registerAdminRoutes(mux *http.ServeMux) error {
|
||||
sub, err := fs.Sub(adminUIFiles, "adminui")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileServer := http.FileServer(http.FS(sub))
|
||||
mux.Handle("/assets/", http.StripPrefix("/assets/", noStoreHandler(fileServer)))
|
||||
mux.HandleFunc("/", s.handleAdminIndex)
|
||||
mux.HandleFunc("/admin", s.handleAdminIndex)
|
||||
mux.HandleFunc("/api/admin/overview", s.handleAdminOverview)
|
||||
mux.HandleFunc("/api/admin/sessions", s.handleAdminSessions)
|
||||
mux.HandleFunc("/api/admin/latest", s.handleAdminLatest)
|
||||
mux.HandleFunc("/api/admin/traffic", s.handleAdminTraffic)
|
||||
mux.HandleFunc("/api/admin/live", s.handleAdminLive)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" && r.URL.Path != "/admin" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := adminUIFiles.ReadFile("adminui/index.html")
|
||||
if err != nil {
|
||||
http.Error(w, "admin ui unavailable", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
func noStoreHandler(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminOverview(w http.ResponseWriter, _ *http.Request) {
|
||||
subscriberCount, latestStateCount := s.hub.Stats()
|
||||
traffic := s.hub.TrafficSnapshot()
|
||||
now := time.Now()
|
||||
writeJSON(w, http.StatusOK, adminOverview{
|
||||
Status: "ok",
|
||||
StartedAt: s.startedAt,
|
||||
Now: now,
|
||||
UptimeSeconds: int64(now.Sub(s.startedAt).Seconds()),
|
||||
HTTPListen: s.cfg.Server.HTTPListen,
|
||||
Anonymous: s.cfg.Auth.AllowAnonymousConsumers,
|
||||
Metrics: map[string]any{
|
||||
"sessions": s.sessions.Count(),
|
||||
"subscribers": subscriberCount,
|
||||
"latestState": latestStateCount,
|
||||
"channels": len(s.channels.List()),
|
||||
"pluginHandlers": s.plugins.HandlerCount(),
|
||||
"published": traffic.Published,
|
||||
"dropped": traffic.Dropped,
|
||||
"fanout": traffic.Fanout,
|
||||
},
|
||||
Auth: map[string]any{
|
||||
"producerTokens": len(s.cfg.Auth.ProducerTokens),
|
||||
"consumerTokens": len(s.cfg.Auth.ConsumerTokens),
|
||||
"controllerTokens": len(s.cfg.Auth.ControllerTokens),
|
||||
},
|
||||
Endpoints: map[string]any{
|
||||
"websocket": "/ws",
|
||||
"health": "/healthz",
|
||||
"metrics": "/metrics",
|
||||
"createChannel": "/api/channel/create",
|
||||
"channels": "/api/admin/channels",
|
||||
"traffic": "/api/admin/traffic",
|
||||
"admin": "/admin",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminSessions(w http.ResponseWriter, _ *http.Request) {
|
||||
snapshots := s.sessions.List()
|
||||
sort.Slice(snapshots, func(i int, j int) bool {
|
||||
return snapshots[i].CreatedAt.After(snapshots[j].CreatedAt)
|
||||
})
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"items": snapshots,
|
||||
"count": len(snapshots),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminLatest(w http.ResponseWriter, r *http.Request) {
|
||||
envelopes := s.hub.LatestStates()
|
||||
sort.Slice(envelopes, func(i int, j int) bool {
|
||||
return envelopes[i].Timestamp > envelopes[j].Timestamp
|
||||
})
|
||||
|
||||
query := strings.TrimSpace(r.URL.Query().Get("topic"))
|
||||
if query != "" {
|
||||
filtered := make([]any, 0, len(envelopes))
|
||||
for _, envelope := range envelopes {
|
||||
if envelope.Topic != query {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, adminLatestItem(envelope))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"items": filtered,
|
||||
"count": len(filtered),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]any, 0, len(envelopes))
|
||||
for _, envelope := range envelopes {
|
||||
items = append(items, adminLatestItem(envelope))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"items": items,
|
||||
"count": len(items),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminLive(w http.ResponseWriter, r *http.Request) {
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
topic := strings.TrimSpace(r.URL.Query().Get("topic"))
|
||||
channelID := strings.TrimSpace(r.URL.Query().Get("channelId"))
|
||||
deviceID := strings.TrimSpace(r.URL.Query().Get("deviceId"))
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
|
||||
id, stream := s.hub.SubscribeLive(64)
|
||||
defer s.hub.UnsubscribeLive(id)
|
||||
|
||||
fmt.Fprint(w, ": live stream ready\n\n")
|
||||
flusher.Flush()
|
||||
|
||||
ping := time.NewTicker(15 * time.Second)
|
||||
defer ping.Stop()
|
||||
|
||||
ctx := r.Context()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ping.C:
|
||||
fmt.Fprint(w, ": ping\n\n")
|
||||
flusher.Flush()
|
||||
case envelope, ok := <-stream:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if topic != "" && envelope.Topic != topic {
|
||||
continue
|
||||
}
|
||||
if channelID != "" && envelope.Target.ChannelID != channelID {
|
||||
continue
|
||||
}
|
||||
if deviceID != "" && envelope.Target.DeviceID != deviceID {
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := json.Marshal(adminLatestItem(envelope))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, "event: envelope\ndata: %s\n\n", data)
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminTraffic(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, http.StatusOK, s.hub.TrafficSnapshot())
|
||||
}
|
||||
|
||||
func adminLatestItem(envelope model.Envelope) map[string]any {
|
||||
payload := map[string]any{}
|
||||
_ = json.Unmarshal(envelope.Payload, &payload)
|
||||
|
||||
return map[string]any{
|
||||
"timestamp": envelope.Timestamp,
|
||||
"topic": envelope.Topic,
|
||||
"channelId": envelope.Target.ChannelID,
|
||||
"deviceId": envelope.Target.DeviceID,
|
||||
"groupId": envelope.Target.GroupID,
|
||||
"sourceId": envelope.Source.ID,
|
||||
"mode": envelope.Source.Mode,
|
||||
"payload": payload,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user