Commit 2ffe26b9 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Created web interface

parent b2118460
......@@ -9,6 +9,7 @@ import (
"github.com/go-chi/chi/v5"
"stockfish-api/internal/admin"
"stockfish-api/internal/api"
"stockfish-api/internal/engine"
)
......@@ -21,18 +22,28 @@ func main() {
engine.InitPool(sfPath, poolSize, time.Duration(idleTimeout)*time.Second)
admin.SetSettings(admin.ServerSettings{
Port: port,
PoolSize: poolSize,
IdleTimeout: idleTimeout,
})
r := chi.NewRouter()
r.Use(api.CORSMiddleware)
r.Use(api.LoggingMiddleware)
r.Use(api.RateLimitMiddleware)
// API routes
r.Post("/api/chess/move", api.HandleGetMove)
r.Post("/api/chess/analyze", api.HandleAnalyze)
r.Get("/api/chess/bots", api.HandleListBots)
r.Get("/api/chess/stats", api.HandleStats)
r.Get("/health", api.HandleHealth)
// Admin panel
admin.RegisterRoutes(r)
log.Printf("Stockfish API starting on :%s (pool_size=%d, idle_timeout=%ds)", port, poolSize, idleTimeout)
if err := http.ListenAndServe(":"+port, r); err != nil {
log.Fatal(err)
......
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r1HyIntPvPfITZECERMunWMTRw=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
package admin
import (
"crypto/rand"
"encoding/hex"
"fmt"
"html/template"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/go-chi/chi/v5"
"stockfish-api/internal/bots"
"stockfish-api/internal/engine"
"stockfish-api/web"
)
// Template functions
var funcMap = template.FuncMap{
"mul": func(a, b interface{}) float64 {
return toFloat64(a) * toFloat64(b)
},
}
func toFloat64(v interface{}) float64 {
switch n := v.(type) {
case float64:
return n
case float32:
return float64(n)
case int:
return float64(n)
case int64:
return float64(n)
default:
return 0
}
}
// pageTemplates holds pre-parsed templates for each page (layout + page content).
var pageTemplates map[string]*template.Template
// loginTemplate is the standalone login page template.
var loginTemplate *template.Template
func init() {
layoutBytes, _ := web.TemplateFS.ReadFile("templates/layout.html")
layoutStr := string(layoutBytes)
pages := []string{
"templates/dashboard.html",
"templates/bots.html",
"templates/bot_edit.html",
"templates/bot_create.html",
"templates/pool.html",
"templates/settings.html",
"templates/logs.html",
"templates/test_move.html",
}
pageTemplates = make(map[string]*template.Template, len(pages))
for _, page := range pages {
pageBytes, _ := web.TemplateFS.ReadFile(page)
t := template.Must(
template.New("").Funcs(funcMap).Parse(layoutStr + "\n" + string(pageBytes)),
)
pageTemplates[page] = t
}
loginBytes, _ := web.TemplateFS.ReadFile("templates/login.html")
loginTemplate = template.Must(
template.New("").Funcs(funcMap).Parse(string(loginBytes)),
)
}
// --- Authentication ---
const (
adminUsername = "admin"
adminPassword = "Alarcade123#"
cookieName = "admin_session"
sessionMaxAge = 24 * time.Hour
)
type session struct {
username string
createdAt time.Time
}
var (
sessions = make(map[string]*session)
sessionsMu sync.RWMutex
)
func generateToken() string {
b := make([]byte, 32)
rand.Read(b)
return hex.EncodeToString(b)
}
func createSession(username string) string {
token := generateToken()
sessionsMu.Lock()
sessions[token] = &session{
username: username,
createdAt: time.Now(),
}
sessionsMu.Unlock()
return token
}
func getSession(token string) *session {
sessionsMu.RLock()
defer sessionsMu.RUnlock()
s, ok := sessions[token]
if !ok {
return nil
}
if time.Since(s.createdAt) > sessionMaxAge {
return nil
}
return s
}
func deleteSession(token string) {
sessionsMu.Lock()
delete(sessions, token)
sessionsMu.Unlock()
}
// RequireAuth is a middleware that checks for a valid session cookie.
func RequireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(cookieName)
if err != nil || getSession(cookie.Value) == nil {
http.Redirect(w, r, "/admin/login", http.StatusFound)
return
}
next.ServeHTTP(w, r)
})
}
// --- Log Ring Buffer ---
type LogEntry struct {
Timestamp string
Method string
Path string
Status int
Duration string
IP string
}
const maxLogs = 100
var (
logBuffer [maxLogs]LogEntry
logIndex int
logCount int
logMu sync.Mutex
)
// AddLog records a request log entry into the ring buffer.
func AddLog(method, path string, status int, duration time.Duration, ip string) {
logMu.Lock()
defer logMu.Unlock()
logBuffer[logIndex] = LogEntry{
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
Method: method,
Path: path,
Status: status,
Duration: duration.String(),
IP: ip,
}
logIndex = (logIndex + 1) % maxLogs
if logCount < maxLogs {
logCount++
}
}
func getRecentLogs() []LogEntry {
logMu.Lock()
defer logMu.Unlock()
logs := make([]LogEntry, 0, logCount)
// Read from newest to oldest
for i := 0; i < logCount; i++ {
idx := (logIndex - 1 - i + maxLogs) % maxLogs
logs = append(logs, logBuffer[idx])
}
return logs
}
// --- Server Settings (in-memory) ---
type ServerSettings struct {
Port string
PoolSize int
IdleTimeout int
}
var (
settings = ServerSettings{
Port: "8082",
PoolSize: 12,
IdleTimeout: 300,
}
settingsMu sync.RWMutex
)
// GetSettings returns current server settings.
func GetSettings() ServerSettings {
settingsMu.RLock()
defer settingsMu.RUnlock()
return settings
}
// SetSettings updates server settings in memory.
func SetSettings(s ServerSettings) {
settingsMu.Lock()
settings = s
settingsMu.Unlock()
}
// --- Handlers ---
func HandleLogin(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
renderTemplate(w, "login", map[string]interface{}{
"Error": "",
})
return
}
username := r.FormValue("username")
password := r.FormValue("password")
if username == adminUsername && password == adminPassword {
token := createSession(username)
http.SetCookie(w, &http.Cookie{
Name: cookieName,
Value: token,
Path: "/",
HttpOnly: true,
MaxAge: int(sessionMaxAge.Seconds()),
})
http.Redirect(w, r, "/admin/dashboard", http.StatusFound)
return
}
renderTemplate(w, "login", map[string]interface{}{
"Error": "Invalid username or password",
})
}
func HandleLogout(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(cookieName)
if err == nil {
deleteSession(cookie.Value)
}
http.SetCookie(w, &http.Cookie{
Name: cookieName,
Value: "",
Path: "/",
HttpOnly: true,
MaxAge: -1,
})
http.Redirect(w, r, "/admin/login", http.StatusFound)
}
func HandleDashboard(w http.ResponseWriter, r *http.Request) {
alive, idle := engine.PoolStats()
logMu.Lock()
reqCount := logCount
logMu.Unlock()
renderPage(w, "dashboard.html", map[string]interface{}{
"Title": "Dashboard",
"PoolAlive": alive,
"PoolIdle": idle,
"BotCount": len(bots.Personalities),
"RequestCount": reqCount,
})
}
func HandleBots(w http.ResponseWriter, r *http.Request) {
renderPage(w, "bots.html", map[string]interface{}{
"Title": "Bots",
"Bots": bots.ListBots(),
})
}
func HandleBotEdit(w http.ResponseWriter, r *http.Request) {
botID := chi.URLParam(r, "id")
bot := bots.GetBot(botID)
if bot == nil {
http.Error(w, "Bot not found", http.StatusNotFound)
return
}
if r.Method == http.MethodGet {
renderPage(w, "bot_edit.html", map[string]interface{}{
"Title": "Edit Bot",
"Bot": bot,
"Success": false,
"Error": "",
})
return
}
// POST - update bot parameters
r.ParseForm()
if v, err := strconv.Atoi(r.FormValue("skill_level")); err == nil {
bot.SkillLevel = v
}
if v, err := strconv.Atoi(r.FormValue("depth")); err == nil {
bot.Depth = v
}
if v, err := strconv.Atoi(r.FormValue("contempt")); err == nil {
bot.Contempt = v
}
if v, err := strconv.Atoi(r.FormValue("blunder_chance")); err == nil {
bot.BlunderChance = float64(v) / 100.0
}
if v, err := strconv.Atoi(r.FormValue("think_time_min")); err == nil {
bot.ThinkTimeMin = v
}
if v, err := strconv.Atoi(r.FormValue("think_time_max")); err == nil {
bot.ThinkTimeMax = v
}
if v, err := strconv.Atoi(r.FormValue("elo_min")); err == nil {
bot.ELOMin = v
}
if v, err := strconv.Atoi(r.FormValue("elo_max")); err == nil {
bot.ELOMax = v
}
renderPage(w, "bot_edit.html", map[string]interface{}{
"Title": "Edit Bot",
"Bot": bot,
"Success": true,
"Error": "",
})
}
func HandleBotCreate(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
renderPage(w, "bot_create.html", map[string]interface{}{
"Title": "Create Bot",
"Error": "",
})
return
}
r.ParseForm()
id := strings.TrimSpace(r.FormValue("id"))
name := strings.TrimSpace(r.FormValue("name"))
style := strings.TrimSpace(r.FormValue("style"))
if id == "" || name == "" || style == "" {
renderPage(w, "bot_create.html", map[string]interface{}{
"Title": "Create Bot",
"Error": "ID, Name, and Style are required",
})
return
}
if bots.GetBot(id) != nil {
renderPage(w, "bot_create.html", map[string]interface{}{
"Title": "Create Bot",
"Error": fmt.Sprintf("Bot with ID '%s' already exists", id),
})
return
}
bot := &bots.BotPersonality{
ID: id,
Name: name,
Style: style,
}
if v, err := strconv.Atoi(r.FormValue("skill_level")); err == nil {
bot.SkillLevel = v
}
if v, err := strconv.Atoi(r.FormValue("depth")); err == nil {
bot.Depth = v
}
if v, err := strconv.Atoi(r.FormValue("contempt")); err == nil {
bot.Contempt = v
}
if v, err := strconv.Atoi(r.FormValue("blunder_chance")); err == nil {
bot.BlunderChance = float64(v) / 100.0
}
if v, err := strconv.Atoi(r.FormValue("think_time_min")); err == nil {
bot.ThinkTimeMin = v
}
if v, err := strconv.Atoi(r.FormValue("think_time_max")); err == nil {
bot.ThinkTimeMax = v
}
if v, err := strconv.Atoi(r.FormValue("elo_min")); err == nil {
bot.ELOMin = v
}
if v, err := strconv.Atoi(r.FormValue("elo_max")); err == nil {
bot.ELOMax = v
}
bots.Personalities[id] = bot
http.Redirect(w, r, "/admin/bots", http.StatusFound)
}
func HandleBotDelete(w http.ResponseWriter, r *http.Request) {
botID := chi.URLParam(r, "id")
delete(bots.Personalities, botID)
http.Redirect(w, r, "/admin/bots", http.StatusFound)
}
func HandlePool(w http.ResponseWriter, r *http.Request) {
alive, idle := engine.PoolStats()
s := GetSettings()
if r.Method == http.MethodPost {
r.ParseForm()
if v, err := strconv.Atoi(r.FormValue("pool_size")); err == nil {
s.PoolSize = v
SetSettings(s)
}
renderPage(w, "pool.html", map[string]interface{}{
"Title": "Pool",
"PoolAlive": alive,
"PoolIdle": idle,
"PoolSize": s.PoolSize,
"Success": true,
})
return
}
renderPage(w, "pool.html", map[string]interface{}{
"Title": "Pool",
"PoolAlive": alive,
"PoolIdle": idle,
"PoolSize": s.PoolSize,
"Success": false,
})
}
func HandleSettings(w http.ResponseWriter, r *http.Request) {
s := GetSettings()
if r.Method == http.MethodPost {
r.ParseForm()
if v := r.FormValue("port"); v != "" {
s.Port = v
}
if v, err := strconv.Atoi(r.FormValue("pool_size")); err == nil {
s.PoolSize = v
}
if v, err := strconv.Atoi(r.FormValue("idle_timeout")); err == nil {
s.IdleTimeout = v
}
SetSettings(s)
renderPage(w, "settings.html", map[string]interface{}{
"Title": "Settings",
"Port": s.Port,
"PoolSize": s.PoolSize,
"IdleTimeout": s.IdleTimeout,
"Success": true,
})
return
}
renderPage(w, "settings.html", map[string]interface{}{
"Title": "Settings",
"Port": s.Port,
"PoolSize": s.PoolSize,
"IdleTimeout": s.IdleTimeout,
"Success": false,
})
}
func HandleLogs(w http.ResponseWriter, r *http.Request) {
logs := getRecentLogs()
renderPage(w, "logs.html", map[string]interface{}{
"Title": "Logs",
"Logs": logs,
"Count": len(logs),
})
}
func HandleTestMove(w http.ResponseWriter, r *http.Request) {
botList := bots.ListBots()
defaultFEN := "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
if r.Method == http.MethodGet {
renderPage(w, "test_move.html", map[string]interface{}{
"Title": "Test Move",
"Bots": botList,
"FEN": defaultFEN,
"SelectedBot": "",
"Result": nil,
"MoveError": "",
})
return
}
r.ParseForm()
fen := r.FormValue("fen")
botID := r.FormValue("bot_id")
if fen == "" {
fen = defaultFEN
}
bot := bots.GetBot(botID)
if bot == nil {
renderPage(w, "test_move.html", map[string]interface{}{
"Title": "Test Move",
"Bots": botList,
"FEN": fen,
"SelectedBot": botID,
"Result": nil,
"MoveError": "Bot not found",
})
return
}
req := engine.MoveRequest{
FEN: fen,
Depth: bot.Depth,
SkillLevel: bot.SkillLevel,
Contempt: bot.Contempt,
MultiPV: 1,
}
resp, err := engine.GetMove(r.Context(), req)
if err != nil {
renderPage(w, "test_move.html", map[string]interface{}{
"Title": "Test Move",
"Bots": botList,
"FEN": fen,
"SelectedBot": botID,
"Result": nil,
"MoveError": "Engine error: " + err.Error(),
})
return
}
renderPage(w, "test_move.html", map[string]interface{}{
"Title": "Test Move",
"Bots": botList,
"FEN": fen,
"SelectedBot": botID,
"Result": resp,
"MoveError": "",
})
}
// --- Route Registration ---
// RegisterRoutes mounts all admin routes under the provided router.
func RegisterRoutes(r chi.Router) {
r.Get("/admin/login", HandleLogin)
r.Post("/admin/login", HandleLogin)
r.Group(func(protected chi.Router) {
protected.Use(RequireAuth)
protected.Get("/admin/logout", HandleLogout)
protected.Get("/admin/dashboard", HandleDashboard)
protected.Get("/admin/bots", HandleBots)
protected.Get("/admin/bots/edit/{id}", HandleBotEdit)
protected.Post("/admin/bots/edit/{id}", HandleBotEdit)
protected.Get("/admin/bots/create", HandleBotCreate)
protected.Post("/admin/bots/create", HandleBotCreate)
protected.Post("/admin/bots/delete/{id}", HandleBotDelete)
protected.Get("/admin/pool", HandlePool)
protected.Post("/admin/pool", HandlePool)
protected.Get("/admin/settings", HandleSettings)
protected.Post("/admin/settings", HandleSettings)
protected.Get("/admin/logs", HandleLogs)
protected.Get("/admin/test-move", HandleTestMove)
protected.Post("/admin/test-move", HandleTestMove)
})
// Redirect /admin to dashboard
r.Get("/admin", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/admin/dashboard", http.StatusFound)
})
}
// --- Template Rendering ---
func renderTemplate(w http.ResponseWriter, name string, data interface{}) {
err := loginTemplate.ExecuteTemplate(w, name, data)
if err != nil {
http.Error(w, "Template error: "+err.Error(), http.StatusInternalServerError)
}
}
func renderPage(w http.ResponseWriter, page string, data interface{}) {
t, ok := pageTemplates["templates/"+page]
if !ok {
http.Error(w, "Template not found: "+page, http.StatusInternalServerError)
return
}
err := t.ExecuteTemplate(w, "layout", data)
if err != nil {
http.Error(w, "Template error: "+err.Error(), http.StatusInternalServerError)
}
}
......@@ -5,6 +5,8 @@ import (
"net/http"
"sync"
"time"
"stockfish-api/internal/admin"
)
func CORSMiddleware(next http.Handler) http.Handler {
......@@ -23,11 +25,30 @@ func CORSMiddleware(next http.Handler) http.Handler {
})
}
type statusWriter struct {
http.ResponseWriter
status int
}
func (sw *statusWriter) WriteHeader(code int) {
sw.status = code
sw.ResponseWriter.WriteHeader(code)
}
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
sw := &statusWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(sw, r)
duration := time.Since(start)
ip := r.RemoteAddr
if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
ip = fwd
}
admin.AddLog(r.Method, r.URL.Path, sw.status, duration, ip)
log.Printf("%s %s %d %s", r.Method, r.URL.Path, sw.status, duration)
})
}
......
package web
import "embed"
//go:embed templates/*.html
var TemplateFS embed.FS
{{define "content"}}
<div class="card">
<h2>Create New Bot</h2>
{{if .Error}}<div class="alert alert-error">{{.Error}}</div>{{end}}
<form method="POST" action="/admin/bots/create">
<div class="form-group">
<label>ID (lowercase, no spaces)</label>
<input type="text" name="id" required pattern="[a-z0-9_]+">
</div>
<div class="form-group">
<label>Name</label>
<input type="text" name="name" required>
</div>
<div class="form-group">
<label>Style</label>
<input type="text" name="style" required>
</div>
<div class="form-group">
<label>Skill Level (0-20)</label>
<input type="number" name="skill_level" value="10" min="0" max="20">
</div>
<div class="form-group">
<label>Depth</label>
<input type="number" name="depth" value="10" min="1" max="30">
</div>
<div class="form-group">
<label>Contempt</label>
<input type="number" name="contempt" value="0" min="-100" max="100">
</div>
<div class="form-group">
<label>Blunder Chance (0-100%)</label>
<input type="number" name="blunder_chance" value="5" min="0" max="100">
</div>
<div class="form-group">
<label>Think Time Min (ms)</label>
<input type="number" name="think_time_min" value="1000" min="0">
</div>
<div class="form-group">
<label>Think Time Max (ms)</label>
<input type="number" name="think_time_max" value="3000" min="0">
</div>
<div class="form-group">
<label>ELO Min</label>
<input type="number" name="elo_min" value="1000" min="0">
</div>
<div class="form-group">
<label>ELO Max</label>
<input type="number" name="elo_max" value="1200" min="0">
</div>
<button type="submit" class="btn btn-success">Create Bot</button>
<a href="/admin/bots" class="btn" style="margin-left:1rem;">Cancel</a>
</form>
</div>
{{end}}
{{define "content"}}
<div class="card">
<h2>Edit Bot: {{.Bot.Name}}</h2>
{{if .Success}}<div class="alert alert-success">Bot updated successfully.</div>{{end}}
{{if .Error}}<div class="alert alert-error">{{.Error}}</div>{{end}}
<form method="POST" action="/admin/bots/edit/{{.Bot.ID}}">
<div class="form-group">
<label>Skill Level (0-20)</label>
<input type="number" name="skill_level" value="{{.Bot.SkillLevel}}" min="0" max="20">
</div>
<div class="form-group">
<label>Depth</label>
<input type="number" name="depth" value="{{.Bot.Depth}}" min="1" max="30">
</div>
<div class="form-group">
<label>Contempt</label>
<input type="number" name="contempt" value="{{.Bot.Contempt}}" min="-100" max="100">
</div>
<div class="form-group">
<label>Blunder Chance (0-100%)</label>
<input type="number" name="blunder_chance" value="{{printf "%.0f" (mul .Bot.BlunderChance 100)}}" min="0" max="100">
</div>
<div class="form-group">
<label>Think Time Min (ms)</label>
<input type="number" name="think_time_min" value="{{.Bot.ThinkTimeMin}}" min="0">
</div>
<div class="form-group">
<label>Think Time Max (ms)</label>
<input type="number" name="think_time_max" value="{{.Bot.ThinkTimeMax}}" min="0">
</div>
<div class="form-group">
<label>ELO Min</label>
<input type="number" name="elo_min" value="{{.Bot.ELOMin}}" min="0">
</div>
<div class="form-group">
<label>ELO Max</label>
<input type="number" name="elo_max" value="{{.Bot.ELOMax}}" min="0">
</div>
<button type="submit" class="btn btn-primary">Save Changes</button>
<a href="/admin/bots" class="btn" style="margin-left:1rem;">Cancel</a>
</form>
</div>
{{end}}
{{define "content"}}
<div class="card">
<h2>Bots</h2>
<p style="margin-bottom: 1rem;"><a href="/admin/bots/create" class="btn btn-success">+ Create Bot</a></p>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Style</th>
<th>ELO</th>
<th>Skill</th>
<th>Depth</th>
<th>Blunder %</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{range .Bots}}
<tr>
<td>{{.ID}}</td>
<td>{{.Name}}</td>
<td>{{.Style}}</td>
<td>{{.ELOMin}}-{{.ELOMax}}</td>
<td>{{.SkillLevel}}</td>
<td>{{.Depth}}</td>
<td>{{printf "%.0f" (mul .BlunderChance 100)}}%</td>
<td>
<a href="/admin/bots/edit/{{.ID}}" class="btn btn-primary">Edit</a>
<form method="POST" action="/admin/bots/delete/{{.ID}}" style="display:inline;" onsubmit="return confirm('Delete this bot?')">
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
{{define "content"}}
<h1 style="margin-bottom: 1.5rem;">Dashboard</h1>
<div class="stats-grid">
<div class="stat-card">
<div class="value">{{.PoolAlive}}</div>
<div class="label">Pool Alive</div>
</div>
<div class="stat-card">
<div class="value">{{.PoolIdle}}</div>
<div class="label">Pool Idle</div>
</div>
<div class="stat-card">
<div class="value">{{.BotCount}}</div>
<div class="label">Bots</div>
</div>
<div class="stat-card">
<div class="value">{{.RequestCount}}</div>
<div class="label">Requests (recent)</div>
</div>
</div>
{{end}}
{{define "layout"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}} - Stockfish Admin</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; }
.nav { background: #1a1a2e; padding: 1rem 2rem; display: flex; align-items: center; gap: 2rem; }
.nav a { color: #eee; text-decoration: none; padding: 0.5rem 1rem; border-radius: 4px; }
.nav a:hover { background: #16213e; }
.nav .brand { font-weight: bold; font-size: 1.2rem; color: #fff; }
.container { max-width: 1200px; margin: 2rem auto; padding: 0 2rem; }
.card { background: #fff; border-radius: 8px; padding: 1.5rem; margin-bottom: 1.5rem; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.card h2 { margin-bottom: 1rem; color: #1a1a2e; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 0.75rem; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; }
.btn { display: inline-block; padding: 0.5rem 1rem; border-radius: 4px; text-decoration: none; border: none; cursor: pointer; font-size: 0.9rem; }
.btn-primary { background: #4361ee; color: #fff; }
.btn-danger { background: #e63946; color: #fff; }
.btn-success { background: #2a9d8f; color: #fff; }
.btn:hover { opacity: 0.9; }
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; margin-bottom: 0.3rem; font-weight: 500; }
.form-group input, .form-group textarea, .form-group select { width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; font-size: 1rem; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; }
.stat-card { background: #fff; border-radius: 8px; padding: 1.5rem; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.stat-card .value { font-size: 2rem; font-weight: bold; color: #4361ee; }
.stat-card .label { color: #666; margin-top: 0.5rem; }
.alert { padding: 1rem; border-radius: 4px; margin-bottom: 1rem; }
.alert-success { background: #d4edda; color: #155724; }
.alert-error { background: #f8d7da; color: #721c24; }
.log-entry { font-family: monospace; font-size: 0.85rem; padding: 0.3rem 0; border-bottom: 1px solid #f0f0f0; }
</style>
</head>
<body>
<nav class="nav">
<span class="brand">Stockfish Admin</span>
<a href="/admin/dashboard">Dashboard</a>
<a href="/admin/bots">Bots</a>
<a href="/admin/pool">Pool</a>
<a href="/admin/settings">Settings</a>
<a href="/admin/logs">Logs</a>
<a href="/admin/test-move">Test Move</a>
<a href="/admin/logout">Logout</a>
</nav>
<div class="container">
{{template "content" .}}
</div>
</body>
</html>
{{end}}
{{define "login"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Stockfish Admin</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.login-box { background: #fff; padding: 2rem; border-radius: 8px; width: 100%; max-width: 400px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
.login-box h1 { text-align: center; margin-bottom: 1.5rem; color: #1a1a2e; }
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; margin-bottom: 0.3rem; font-weight: 500; }
.form-group input { width: 100%; padding: 0.75rem; border: 1px solid #ddd; border-radius: 4px; font-size: 1rem; }
.btn { width: 100%; padding: 0.75rem; background: #4361ee; color: #fff; border: none; border-radius: 4px; font-size: 1rem; cursor: pointer; }
.btn:hover { opacity: 0.9; }
.error { color: #e63946; text-align: center; margin-bottom: 1rem; }
</style>
</head>
<body>
<div class="login-box">
<h1>Stockfish Admin</h1>
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
<form method="POST" action="/admin/login">
<div class="form-group">
<label>Username</label>
<input type="text" name="username" required autofocus>
</div>
<div class="form-group">
<label>Password</label>
<input type="password" name="password" required>
</div>
<button type="submit" class="btn">Login</button>
</form>
</div>
</body>
</html>
{{end}}
{{define "content"}}
<div class="card">
<h2>Recent Request Logs</h2>
<p style="margin-bottom: 1rem; color: #666;">Last {{.Count}} requests</p>
{{if .Logs}}
<table>
<thead>
<tr>
<th>Time</th>
<th>Method</th>
<th>Path</th>
<th>Status</th>
<th>Duration</th>
<th>IP</th>
</tr>
</thead>
<tbody>
{{range .Logs}}
<tr>
<td>{{.Timestamp}}</td>
<td>{{.Method}}</td>
<td>{{.Path}}</td>
<td>{{.Status}}</td>
<td>{{.Duration}}</td>
<td>{{.IP}}</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<p>No logs yet.</p>
{{end}}
</div>
{{end}}
{{define "content"}}
<div class="card">
<h2>Engine Pool</h2>
{{if .Success}}<div class="alert alert-success">Pool settings updated.</div>{{end}}
<div class="stats-grid" style="margin-bottom: 1.5rem;">
<div class="stat-card">
<div class="value">{{.PoolAlive}}</div>
<div class="label">Alive</div>
</div>
<div class="stat-card">
<div class="value">{{.PoolIdle}}</div>
<div class="label">Idle</div>
</div>
<div class="stat-card">
<div class="value">{{.PoolSize}}</div>
<div class="label">Max Size</div>
</div>
</div>
<form method="POST" action="/admin/pool">
<div class="form-group">
<label>Pool Size (requires restart to take effect)</label>
<input type="number" name="pool_size" value="{{.PoolSize}}" min="1" max="64">
</div>
<button type="submit" class="btn btn-primary">Update</button>
</form>
</div>
{{end}}
{{define "content"}}
<div class="card">
<h2>Server Settings</h2>
{{if .Success}}<div class="alert alert-success">Settings updated (restart required for changes to take effect).</div>{{end}}
<form method="POST" action="/admin/settings">
<div class="form-group">
<label>Port</label>
<input type="text" name="port" value="{{.Port}}">
</div>
<div class="form-group">
<label>Pool Size</label>
<input type="number" name="pool_size" value="{{.PoolSize}}" min="1" max="64">
</div>
<div class="form-group">
<label>Idle Timeout (seconds)</label>
<input type="number" name="idle_timeout" value="{{.IdleTimeout}}" min="30">
</div>
<button type="submit" class="btn btn-primary">Save Settings</button>
</form>
</div>
{{end}}
{{define "content"}}
<div class="card">
<h2>Test Move</h2>
<form method="POST" action="/admin/test-move">
<div class="form-group">
<label>Bot</label>
<select name="bot_id">
{{range .Bots}}
<option value="{{.ID}}" {{if eq .ID $.SelectedBot}}selected{{end}}>{{.Name}} ({{.Style}}, ELO {{.ELOMin}}-{{.ELOMax}})</option>
{{end}}
</select>
</div>
<div class="form-group">
<label>FEN Position</label>
<input type="text" name="fen" value="{{.FEN}}" placeholder="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1">
</div>
<button type="submit" class="btn btn-primary">Get Move</button>
</form>
{{if .Result}}
<div style="margin-top: 1.5rem; padding: 1rem; background: #f8f9fa; border-radius: 4px;">
<h3>Result</h3>
<p><strong>Best Move:</strong> {{.Result.BestMove}}</p>
<p><strong>Evaluation:</strong> {{printf "%.2f" .Result.Evaluation}}</p>
<p><strong>Depth:</strong> {{.Result.Depth}}</p>
<p><strong>Think Time:</strong> {{.Result.ThinkTimeMs}}ms</p>
{{if .Result.PV}}<p><strong>PV:</strong> {{.Result.PV}}</p>{{end}}
</div>
{{end}}
{{if .MoveError}}
<div class="alert alert-error" style="margin-top: 1rem;">{{.MoveError}}</div>
{{end}}
</div>
{{end}}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment