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=
This diff is collapsed.
......@@ -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