Compatibility evidence
A message arriving does not establish the device’s MQTT version or native MQTT 5 feature support.
| Feature | Status | Evidence |
|---|
Event timeline
Incoming message previews. Pause affects the display, not MQTT processing.
// Package web serves an optional loopback-only, read-only event inspector. package web import ( "context" "embed" "encoding/json" "errors" "fmt" "io/fs" "net" "net/http" "net/url" "strconv" "strings" "sync" "sync/atomic" "time" "robot-market/test-mqtt-5/internal/config" "robot-market/test-mqtt-5/internal/events" ) const maxReplay = 500 //go:embed assets var assets embed.FS type Compatibility struct { Feature string `json:"feature"` Status string `json:"status"` Evidence string `json:"evidence"` } type Snapshot struct { Events []events.Event `json:"events"` Counts map[events.Kind]uint64 `json:"counts"` Gap *events.Notice `json:"gap,omitempty"` Connection string `json:"connection"` LastEventID uint64 `json:"last_event_id"` Compatibility []Compatibility `json:"compatibility"` } type Viewer struct { ready func() bool cfg config.Web store *events.Store shutdown time.Duration handler http.Handler server *http.Server listener net.Listener done chan error slots chan struct{} active atomic.Int32 once sync.Once } func New(cfg config.Web, store *events.Store, shutdown time.Duration) *Viewer { v := &Viewer{cfg: cfg, store: store, shutdown: shutdown, done: make(chan error, 1), slots: make(chan struct{}, cfg.MaxViewers)} mux := http.NewServeMux() mux.HandleFunc("GET /", v.index) sub, _ := fs.Sub(assets, "assets") mux.Handle("GET /assets/", http.StripPrefix("/assets/", http.FileServer(http.FS(sub)))) mux.HandleFunc("GET /api/snapshot", v.snapshot) mux.HandleFunc("GET /api/events", v.stream) mux.HandleFunc("GET /healthz", v.health) v.handler = v.guard(mux) v.server = &http.Server{Handler: v.handler, ReadHeaderTimeout: 3 * time.Second, IdleTimeout: 20 * time.Second, MaxHeaderBytes: 8 << 10} return v } // SetReadiness must be called before Start. Readiness reflects live MQTT state, // independent of connection events rolling out of the bounded viewer history. func (v *Viewer) SetReadiness(ready func() bool) { v.ready = ready } func (v *Viewer) health(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store") if v.ready == nil || !v.ready() { http.Error(w, "MQTT not ready", http.StatusServiceUnavailable) return } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"status":"ready"}`)) } func (v *Viewer) Address() string { if v.listener == nil { return v.cfg.Address } return v.listener.Addr().String() } func (v *Viewer) Handler() http.Handler { return v.handler } func (v *Viewer) Start() error { listener, err := net.Listen("tcp", v.cfg.Address) if err != nil { return errors.New("viewer could not bind configured loopback address") } v.listener = listener go func() { err := v.server.Serve(listener) if errors.Is(err, http.ErrServerClosed) { err = nil } v.done <- err }() return nil } func (v *Viewer) Close(ctx context.Context) error { var result error v.once.Do(func() { if v.listener == nil { return } result = v.server.Shutdown(ctx) if result != nil { _ = v.server.Close() } select { case err := <-v.done: if result == nil { result = err } case <-ctx.Done(): if result == nil { result = errors.New("viewer shutdown deadline exceeded") } } }) return result } func (v *Viewer) guard(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Security-Policy", "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("Referrer-Policy", "no-referrer") w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()") if !allowedHost(r.Host) { http.Error(w, "local viewer only", http.StatusForbidden) return } if origin := r.Header.Get("Origin"); origin != "" { parsed, err := url.Parse(origin) if err != nil || parsed.User != nil || parsed.Path != "" || parsed.RawQuery != "" || parsed.Fragment != "" || parsed.Scheme != "http" || !strings.EqualFold(parsed.Host, r.Host) { http.Error(w, "same-origin viewer only", http.StatusForbidden) return } } next.ServeHTTP(w, r) }) } func allowedHost(hostport string) bool { host, _, err := net.SplitHostPort(hostport) if err != nil { return false } return strings.EqualFold(host, "127.0.0.1") || strings.EqualFold(host, "localhost") } func (v *Viewer) index(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-store") _, _ = w.Write(indexHTML) } var indexHTML = []byte(`
ROBOT MARKET · TEST LAB
Live, read-only view of the Go observer event stream.
A message arriving does not establish the device’s MQTT version or native MQTT 5 feature support.
| Feature | Status | Evidence |
|---|
Incoming message previews. Pause affects the display, not MQTT processing.