Files
canon/internal/server/admin.go
T
cdnutterandClaude Opus 5.5 e9a75a968a Initial TopDeck: self-hosted ranked favorites
A single Go binary with SQLite that serves a public page of the owner's
ranked lists (movies, TV, albums, songs, video games, theater) and a
login-gated editor.

- Lists of 1–65,535 entries (100 recommended), several per category
- Search-to-add via TMDB, TheTVDB, IGDB, Deezer/MusicBrainz, Wikipedia;
  manual entry as a fallback
- Theater entries carry a production (Broadway, tour, …), venue and cast
- Artwork cached locally; URL override and uploads
- Drag, arrow and type-a-rank reordering; paginated for long lists
- 12 theme presets plus custom CSS
- First-run setup code or env-provided admin; bcrypt, rate-limited login,
  CSRF-guarded JSON API
- Dockerfile and docker-compose.yml

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 16:54:19 -07:00

84 lines
2.0 KiB
Go

package server
import (
"encoding/json"
"html/template"
"net/http"
"github.com/cdnutter/topdeck/internal/store"
)
// adminPage carries a JSON blob the page script boots from.
type adminPage struct {
Boot template.JS
List *store.List
}
func bootJSON(v any) template.JS {
b, _ := json.Marshal(v)
return template.JS(b)
}
type sizeLimits struct {
Max int `json:"max"`
Recommended int `json:"recommended"`
Warn int `json:"warn"`
}
var limits = sizeLimits{store.MaxListSize, store.RecommendedListSize, store.SoftListSizeWarning}
func (s *Server) handleAdminLists(w http.ResponseWriter, r *http.Request) {
lists, err := s.store.Lists(r.Context(), true)
if err != nil {
s.serverError(w, r, err)
return
}
if lists == nil {
lists = []store.List{}
}
covers, err := s.store.ListCovers(r.Context(), 3)
if err != nil {
s.serverError(w, r, err)
return
}
s.render(w, r, http.StatusOK, "admin_lists", "Your lists", adminPage{Boot: bootJSON(map[string]any{
"lists": lists,
"covers": covers,
"categories": store.Categories,
"limits": limits,
})})
}
func (s *Server) handleAdminList(w http.ResponseWriter, r *http.Request) {
id, err := pathID(r)
if err != nil {
s.notFound(w, r)
return
}
l, err := s.store.ListByID(r.Context(), id)
if err != nil {
s.notFound(w, r)
return
}
cat, _ := store.CategoryByKey(l.Category)
provider := ""
if p, err := s.providers.For(r.Context(), l.Category); err == nil {
provider = p.Label()
}
s.render(w, r, http.StatusOK, "admin_list", l.Title, adminPage{List: &l, Boot: bootJSON(map[string]any{
"list": l,
"category": cat,
"categories": store.Categories,
"limits": limits,
"provider": provider,
"productions": store.TheaterProductions,
"pageSize": listPageSize,
})})
}
func (s *Server) handleAdminSettings(w http.ResponseWriter, r *http.Request) {
s.render(w, r, http.StatusOK, "admin_settings", "Settings", adminPage{Boot: bootJSON(map[string]any{
"categories": store.Categories,
})})
}