This commit is contained in:
Martino Ferrari
2026-04-26 11:01:55 +02:00
parent 91b42027c9
commit e83e183673
17 changed files with 1009 additions and 182 deletions
+6 -2
View File
@@ -10,6 +10,7 @@ import (
"github.com/uopi/uopi/internal/api"
"github.com/uopi/uopi/internal/broker"
"github.com/uopi/uopi/internal/datasource/synthetic"
"github.com/uopi/uopi/internal/metrics"
"github.com/uopi/uopi/internal/storage"
)
@@ -22,7 +23,7 @@ type Server struct {
// New creates the HTTP server, registers all routes, and returns a ready-to-start Server.
// synth may be nil if the synthetic data source is not enabled.
func New(addr string, webFS fs.FS, brk *broker.Broker, synth *synthetic.Synthetic, store *storage.Store, log *slog.Logger) *Server {
func New(addr string, webFS fs.FS, brk *broker.Broker, synth *synthetic.Synthetic, store *storage.Store, channelFinderURL string, log *slog.Logger) *Server {
mux := http.NewServeMux()
// Health check
@@ -34,8 +35,11 @@ func New(addr string, webFS fs.FS, brk *broker.Broker, synth *synthetic.Syntheti
// WebSocket endpoint
mux.Handle("/ws", &wsHandler{broker: brk, log: log})
// Prometheus-format metrics
mux.HandleFunc("/metrics", metrics.Handler(brk.ActiveSubscriptions))
// REST API
api.New(brk, synth, store, log).Register(mux, apiPrefix)
api.New(brk, synth, store, channelFinderURL, log).Register(mux, apiPrefix)
// Embedded frontend — must be last (catch-all)
mux.Handle("/", http.FileServerFS(webFS))
+18
View File
@@ -13,6 +13,7 @@ import (
"github.com/uopi/uopi/internal/broker"
"github.com/uopi/uopi/internal/datasource"
"github.com/uopi/uopi/internal/metrics"
)
// ── Incoming message types ────────────────────────────────────────────────────
@@ -95,6 +96,8 @@ func (h *wsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.log.Error("ws accept failed", "err", err)
return
}
metrics.IncWsConns()
defer metrics.DecWsConns()
ctx, cancel := context.WithCancel(r.Context())
defer cancel()
@@ -191,6 +194,7 @@ func (c *wsClient) writeLoop(ctx context.Context) {
if err := c.conn.Write(ctx, websocket.MessageText, msg); err != nil {
return
}
metrics.IncMsgOut()
case <-ctx.Done():
return
}
@@ -199,6 +203,7 @@ func (c *wsClient) writeLoop(ctx context.Context) {
// handleMessage parses and routes a single incoming client message.
func (c *wsClient) handleMessage(ctx context.Context, data []byte) {
metrics.IncMsgIn()
var msg inMsg
if err := json.Unmarshal(data, &msg); err != nil {
c.sendError(ctx, "PARSE_ERROR", "invalid JSON: "+err.Error())
@@ -264,6 +269,17 @@ func (c *wsClient) handleWrite(ctx context.Context, msg inMsg) {
return
}
// Enforce write permission before touching the value payload.
meta, err := ds.GetMetadata(ctx, msg.Name)
if err != nil {
c.sendError(ctx, "NOT_FOUND", "unknown signal: "+msg.Name)
return
}
if !meta.Writable {
c.sendError(ctx, "NOT_WRITABLE", "signal is read-only: "+msg.Name)
return
}
// Decode the JSON value as the appropriate Go type.
var value any
if err := json.Unmarshal(msg.Value, &value); err != nil {
@@ -271,12 +287,14 @@ func (c *wsClient) handleWrite(ctx context.Context, msg inMsg) {
return
}
metrics.IncWrites()
if err := ds.Write(ctx, msg.Name, value); err != nil {
c.sendError(ctx, "WRITE_ERROR", err.Error())
}
}
func (c *wsClient) handleHistory(ctx context.Context, msg inMsg) {
metrics.IncHistoryReqs()
ds, ok := c.broker.Source(msg.DS)
if !ok {
c.sendError(ctx, "NOT_FOUND", "unknown data source: "+msg.DS)