Initial release: Disc Agenda frisbee tournament platform
Some checks failed
Build and Push / build (push) Failing after 8s

Full-stack tournament management app with real-time scoring:
- Go 1.26 backend with REST API and WebSocket live scoring
- React 19 + Vite 8 frontend with mobile-first design
- File-based JSON storage with JSONL audit logs
- Multi-stage Docker build with Gitea CI/CD pipeline
- Post-tournament questionnaire with spirit voting
- Technical documentation and project description

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-15 14:48:15 +01:00
commit a7244406fd
38 changed files with 5749 additions and 0 deletions

View File

@@ -0,0 +1,216 @@
package handlers
import (
"encoding/json"
"log"
"net/http"
"github.com/gorilla/mux"
gorillaWs "github.com/gorilla/websocket"
"github.com/frisbee-tournament/backend/internal/models"
"github.com/frisbee-tournament/backend/internal/storage"
ws "github.com/frisbee-tournament/backend/internal/websocket"
)
type Handler struct {
store *storage.Store
hubMgr *ws.HubManager
}
func New(store *storage.Store, hubMgr *ws.HubManager) *Handler {
return &Handler{store: store, hubMgr: hubMgr}
}
var upgrader = gorillaWs.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true // trust-based, no auth
},
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{"error": msg})
}
// --- Tournament endpoints ---
func (h *Handler) ListTournaments(w http.ResponseWriter, r *http.Request) {
ts, err := h.store.GetTournaments()
if err != nil {
writeError(w, 500, err.Error())
return
}
writeJSON(w, 200, ts)
}
func (h *Handler) GetTournament(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
t, err := h.store.GetTournament(id)
if err != nil {
writeError(w, 404, err.Error())
return
}
writeJSON(w, 200, t)
}
// --- Schedule ---
func (h *Handler) GetSchedule(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
sched, err := h.store.GetSchedule(id)
if err != nil {
writeError(w, 500, err.Error())
return
}
// Merge persisted scores into schedule games
for i := range sched.Games {
g := &sched.Games[i]
state, err := h.store.GetScore(id, g.ID)
if err == nil && state.Status != "" {
g.HomeScore = state.HomeScore
g.AwayScore = state.AwayScore
g.Status = state.Status
}
}
writeJSON(w, 200, sched)
}
// --- Scoring ---
func (h *Handler) GetScore(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
state, err := h.store.GetScore(vars["id"], vars["gid"])
if err != nil {
writeError(w, 500, err.Error())
return
}
writeJSON(w, 200, state)
}
func (h *Handler) UpdateScore(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
var update models.ScoreUpdate
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
writeError(w, 400, "invalid request body")
return
}
hub := h.hubMgr.GetOrCreateHub(vars["id"], vars["gid"])
state, err := hub.HandleScoreUpdate(update)
if err != nil {
writeError(w, 500, err.Error())
return
}
writeJSON(w, 200, state)
}
func (h *Handler) GetAuditLog(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
entries, err := h.store.GetAuditLog(vars["id"], vars["gid"])
if err != nil {
writeError(w, 500, err.Error())
return
}
if entries == nil {
entries = []models.AuditEntry{}
}
writeJSON(w, 200, entries)
}
// --- WebSocket ---
func (h *Handler) WebSocketHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("ws upgrade error: %v", err)
return
}
userID := r.URL.Query().Get("user_id")
if userID == "" {
userID = "anonymous"
}
hub := h.hubMgr.GetOrCreateHub(vars["id"], vars["gid"])
client := hub.RegisterClient(conn, userID)
go client.WritePump()
go client.ReadPump()
}
func (h *Handler) TournamentWebSocketHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("ws upgrade error: %v", err)
return
}
th := h.hubMgr.GetOrCreateTournamentHub(vars["id"])
client := th.RegisterClient(conn)
go client.WritePump()
go client.ReadPumpTournament(th)
}
// --- Questionnaire ---
func (h *Handler) GetQuestionnaire(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
cfg, err := h.store.GetQuestionnaireConfig(id)
if err != nil {
writeError(w, 500, err.Error())
return
}
t, _ := h.store.GetTournament(id)
resp := map[string]any{
"config": cfg,
}
if t != nil {
resp["teams"] = t.Teams
}
writeJSON(w, 200, resp)
}
func (h *Handler) SubmitQuestionnaire(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
var resp models.QuestionnaireResponse
if err := json.NewDecoder(r.Body).Decode(&resp); err != nil {
writeError(w, 400, "invalid request body")
return
}
resp.TourneyID = id
if err := h.store.SaveQuestionnaireResponse(id, &resp); err != nil {
writeError(w, 500, err.Error())
return
}
writeJSON(w, 201, map[string]string{"status": "ok", "id": resp.ID})
}
func (h *Handler) GetQuestionnaireResults(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
resps, err := h.store.GetQuestionnaireResponses(id)
if err != nil {
writeError(w, 500, err.Error())
return
}
if resps == nil {
resps = []models.QuestionnaireResponse{}
}
writeJSON(w, 200, resps)
}
// --- Results ---
func (h *Handler) GetResults(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
res, err := h.store.GetResults(id)
if err != nil {
writeError(w, 500, err.Error())
return
}
writeJSON(w, 200, res)
}