Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ func openDB(dsn, logLevel string) (*gorm.DB, error) {
if err := db.Exec("PRAGMA busy_timeout=5000;").Error; err != nil {
slog.Warn("Failed to set busy timeout for SQLite", "error", err)
}
if err := db.Exec("PRAGMA synchronous=NORMAL;").Error; err != nil {
slog.Warn("Failed to set synchronous for SQLite", "error", err)
}
if err := db.Exec("PRAGMA cache_size=-64000;").Error; err != nil { // 64MB cache
slog.Warn("Failed to set cache size for SQLite", "error", err)
}
}
}

Expand Down
1 change: 1 addition & 0 deletions docker-compose.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ services:
- LOG_LEVEL
- ENABLE_USER_REGISTRATION
- GITHUB_TOKEN
- MAX_CONCURRENT_RENDERS=${MAX_CONCURRENT_RENDERS:-5}

volumes:
go_modules:
Expand Down
1 change: 1 addition & 0 deletions docker-compose.https.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ services:
- PRODUCTION
- ENABLE_USER_REGISTRATION
- GITHUB_TOKEN
- MAX_CONCURRENT_RENDERS=${MAX_CONCURRENT_RENDERS:-5}

healthcheck:
test: ["CMD", "/app/tronbyt-server", "health"]
Expand Down
1 change: 1 addition & 0 deletions docker-compose.postgres.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ services:
- ENABLE_USER_REGISTRATION
- SINGLE_USER_AUTO_LOGIN
- GITHUB_TOKEN
- MAX_CONCURRENT_RENDERS=${MAX_CONCURRENT_RENDERS:-5}
- DB_DSN=host=db user=tronbyt password=tronbyt dbname=tronbyt port=5432 sslmode=disable TimeZone=UTC
depends_on:
- db
Expand Down
1 change: 1 addition & 0 deletions docker-compose.redis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ services:
- ENABLE_USER_REGISTRATION
- SINGLE_USER_AUTO_LOGIN
- GITHUB_TOKEN
- MAX_CONCURRENT_RENDERS=${MAX_CONCURRENT_RENDERS:-5}

healthcheck:
test: ["CMD", "/app/tronbyt-server", "health"]
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ services:
- ENABLE_USER_REGISTRATION
- SINGLE_USER_AUTO_LOGIN
- GITHUB_TOKEN
- MAX_CONCURRENT_RENDERS=${MAX_CONCURRENT_RENDERS:-5}

healthcheck:
test: ["CMD", "/app/tronbyt-server", "health"]
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Settings struct {
TrustedProxies string `env:"TRONBYT_TRUSTED_PROXIES" envDefault:"*"`
LogLevel string `env:"LOG_LEVEL" envDefault:"INFO"`
EnableUpdateChecks bool `env:"ENABLE_UPDATE_CHECKS" envDefault:"true"`
MaxConcurrentRenders int `env:"MAX_CONCURRENT_RENDERS" envDefault:"10"`
}

// TemplateConfig holds configuration values needed in templates.
Expand Down
25 changes: 21 additions & 4 deletions internal/server/handlers_device_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,24 @@ import (
"tronbyt-server/internal/data"

"gorm.io/gorm"
"gorm.io/gorm/clause"
)

// handleNextApp is the handler for GET /{id}/next.
func (s *Server) handleNextApp(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")

renderMetrics.RecordRequest()

// Acquire per-device semaphore to prevent queue backup from same device.
// If already processing a request for this device, return cached image immediately.
if !s.acquireDeviceSemaphore(id) {
slog.Debug("Device busy, serving cached image", "device", id)
s.serveCachedImageForDevice(w, r, id)
return
}
// Ensure semaphore is released when this function returns
defer s.releaseDeviceSemaphore(id)

var device *data.Device
if d, err := DeviceFromContext(r.Context()); err == nil {
device = d
Expand Down Expand Up @@ -64,10 +75,10 @@ func (s *Server) handleNextApp(w http.ResponseWriter, r *http.Request) {
}

// Update device info if needed
// We use a transaction with locking to avoid race conditions with other requests
// Note: Removed SELECT FOR UPDATE row locking as it was causing lock contention
// and "locking protocol" errors when requests timed out mid-transaction
err := s.DB.Transaction(func(tx *gorm.DB) error {
// Lock the row to ensure we read the latest state and no one else updates it
freshDevice, err := gorm.G[data.Device](tx, clause.Locking{Strength: "UPDATE"}).Where("id = ?", device.ID).First(r.Context())
freshDevice, err := gorm.G[data.Device](tx).Where("id = ?", device.ID).First(r.Context())
if err != nil {
return err
}
Expand Down Expand Up @@ -107,9 +118,13 @@ func (s *Server) handleNextApp(w http.ResponseWriter, r *http.Request) {
// Send default image if error (or not found)
slog.Error("Failed to get next app image", "device", device.ID, "error", err)
s.sendDefaultImage(w, r, device)
webpMetrics.RecordWebPServed(0)
webpMetrics.RecordUniqueDevice(device.ID)
Comment on lines +121 to +122
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It seems like webpMetrics.RecordWebPServed(0) and webpMetrics.RecordUniqueDevice(device.ID) are called when there is an error. It might be useful to log the error message along with these metrics to provide more context for debugging.

return
}

webpMetrics.RecordUniqueDevice(device.ID)

// For HTTP devices, we assume "Sent" equals "Displaying" (or roughly so).
// We update DisplayingApp here so the Preview uses the explicit field instead of fallback.
if app != nil {
Expand Down Expand Up @@ -142,6 +157,8 @@ func (s *Server) handleNextApp(w http.ResponseWriter, r *http.Request) {
dwell := device.GetEffectiveDwellTime(app)
w.Header().Set("Tronbyt-Dwell-Secs", fmt.Sprintf("%d", dwell))

webpMetrics.RecordWebPServed(len(imgData))

if _, err := w.Write(imgData); err != nil {
slog.Error("Failed to write image data to response", "error", err)
// Log error, but can't change HTTP status after writing headers.
Expand Down
21 changes: 21 additions & 0 deletions internal/server/handlers_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,3 +429,24 @@ func (s *Server) handleRefreshSystemRepo(w http.ResponseWriter, r *http.Request)

http.Redirect(w, r, "/auth/edit", http.StatusSeeOther)
}

func (s *Server) handleAdminDashboard(w http.ResponseWriter, r *http.Request) {
user := GetUser(r)
if !user.IsAdmin {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}

var totalDevices, totalUsers int64
s.DB.Model(&data.Device{}).Count(&totalDevices)
s.DB.Model(&data.User{}).Count(&totalUsers)

stats := GetStatsSnapshot()

s.renderTemplate(w, r, "admin_dashboard", TemplateData{
User: user,
TotalDevices: totalDevices,
TotalUsers: totalUsers,
Stats: stats,
})
}
5 changes: 5 additions & 0 deletions internal/server/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ type TemplateData struct {
AppConfig map[string]any
AppMetadata *apps.AppMetadata

// Admin Dashboard
TotalDevices int64
TotalUsers int64
Stats StatsSnapshot

// Device Update Extras
ColorFilterOptions []ColorFilterOption
ShowFullAnimationOptions []ShowFullAnimationOption
Expand Down
Loading
Loading