Release v1.0.0 #major
release / goreleaser (push) Failing after 51s

This commit is contained in:
2026-06-02 23:12:36 -05:00
parent 276981a7df
commit d5e65fbb03
17 changed files with 3447 additions and 0 deletions
+176
View File
@@ -0,0 +1,176 @@
package server
import (
"database/sql"
"strconv"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/websocket/v2"
"log/slog"
"git.destefano.cloud/fdestefano/RocketLeagueBot-Renderer/assets"
"git.destefano.cloud/fdestefano/RocketLeagueBot-Renderer/internal/hub"
"git.destefano.cloud/fdestefano/RocketLeagueBot-Renderer/internal/recording"
"git.destefano.cloud/fdestefano/RocketLeagueBot-Renderer/internal/stats"
)
// Options configures the Fiber app constructed by New.
type Options struct {
Password string
DBPath string
Verbose bool
Compress bool
Retention time.Duration
DB *sql.DB
Hub *hub.Hub
Recorder *recording.Recorder
}
// New constructs the Fiber application with all middleware and routes wired.
func New(opt Options) *fiber.App {
app := fiber.New(fiber.Config{DisableStartupMessage: true})
app.Use(logger.New(logger.Config{
Format: "${time} ${method} ${path} ${status}\n",
Next: func(c *fiber.Ctx) bool {
if opt.Verbose {
return false
}
// Suppress noisy WS access logs by default.
path := c.Path()
return len(path) >= 3 && path[:3] == "/ws"
},
}))
app.Use(AuthMiddleware(opt.Password))
app.Get("/", func(c *fiber.Ctx) error {
data, err := assets.FS.ReadFile("viewer.html")
if err != nil {
return fiber.ErrInternalServerError
}
c.Set("Content-Type", "text/html; charset=utf-8")
return c.Send(data)
})
// Live WebSocket.
app.Use("/ws", upgradeOnly())
app.Get("/ws", websocket.New(func(c *websocket.Conn) {
cl := opt.Hub.Add(c)
slog.Info("client connected", "addr", c.RemoteAddr().String(), "total", opt.Hub.Count())
defer func() {
opt.Hub.Remove(cl)
slog.Info("client disconnected", "addr", c.RemoteAddr().String(), "total", opt.Hub.Count())
}()
for {
if _, _, err := c.ReadMessage(); err != nil {
return
}
}
}))
// Recording API.
app.Get("/api/recordings", func(c *fiber.Ctx) error {
since := c.Query("since", "")
limit, _ := strconv.Atoi(c.Query("limit", "0"))
recs, err := recording.List(opt.DB, since, limit)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(recs)
})
app.Delete("/api/recordings/:id", func(c *fiber.Ctx) error {
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "invalid id"})
}
n, err := recording.Delete(opt.DB, id)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
if n == 0 {
return c.Status(404).JSON(fiber.Map{"error": "not found"})
}
opt.Hub.PublishEvent(map[string]any{
"type": "event", "kind": "recording_deleted",
"recording": map[string]any{"id": id},
})
return c.JSON(fiber.Map{"ok": true})
})
app.Get("/api/stats", statsHandler(opt))
// Playback websocket.
app.Use("/ws/playback", upgradeOnly())
app.Get("/ws/playback", websocket.New(func(c *websocket.Conn) {
idStr := c.Query("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
_ = c.WriteMessage(websocket.TextMessage, []byte(`{"error":"invalid id"}`))
return
}
slog.Info("playback start", "id", id, "addr", c.RemoteAddr().String())
p, err := recording.NewPlayer(opt.DB, c, id)
if err != nil {
_ = c.WriteMessage(websocket.TextMessage, []byte(`{"error":"player init failed"}`))
return
}
p.Run()
slog.Info("playback end", "id", id, "addr", c.RemoteAddr().String())
}))
return app
}
func upgradeOnly() fiber.Handler {
return func(c *fiber.Ctx) error {
if websocket.IsWebSocketUpgrade(c) {
return c.Next()
}
return fiber.ErrUpgradeRequired
}
}
func statsHandler(opt Options) fiber.Handler {
return func(c *fiber.Ctx) error {
dbSize := recording.DBSize(opt.DBPath)
lastUDP := stats.Global.LastUDPPacketUnix.Load()
var lastAgeMs int64 = -1
if lastUDP > 0 {
lastAgeMs = (time.Now().UnixNano() - lastUDP) / int64(time.Millisecond)
}
before := stats.Global.BytesBeforeCompr.Load()
after := stats.Global.BytesAfterCompr.Load()
var compressionRatio float64
if after > 0 {
compressionRatio = float64(before) / float64(after)
}
curID, curName, curFrames, curElapsed := opt.Recorder.CurrentSession()
var current any
if curID != 0 {
current = map[string]any{
"id": curID, "name": curName,
"frames": curFrames, "elapsed_ms": curElapsed,
}
}
return c.JSON(fiber.Map{
"uptime_s": int64(time.Since(stats.Global.StartTime).Seconds()),
"udp_packets_total": stats.Global.UDPPackets.Load(),
"udp_bytes_total": stats.Global.UDPBytes.Load(),
"udp_last_packet_age_ms": lastAgeMs,
"ws_clients": opt.Hub.Count(),
"frames_written_total": stats.Global.FramesWritten.Load(),
"frames_dropped_total": stats.Global.FramesDropped.Load(),
"bytes_before_compr": before,
"bytes_after_compr": after,
"compression_ratio": compressionRatio,
"db_size_bytes": dbSize,
"current_recording": current,
"retention_hours": opt.Retention.Hours(),
"compression_enabled": opt.Compress,
})
}
}