Commit 37a67945 authored by Mahmoud Aglan's avatar Mahmoud Aglan

PUSHEEDDD

parent 2ffe26b9
......@@ -45,6 +45,8 @@ COPY --from=stockfish-builder /stockfish/src/*.nnue /app/
COPY --from=go-builder /app/stockfish-api /app/stockfish-api
RUN mkdir -p /app/portraits
ENV PORT=80
ENV STOCKFISH_PATH=/usr/local/bin/stockfish
ENV POOL_SIZE=12
......
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAwNRFitb/O8fS9TaUoCZ/VJZUEehvdyb1EgCjPrQbTeT6TlZh
rvkzHYcGIKsgarI6wuP9aK4+rLW8SL9VP6Ey3G4CgY/Hx9ZxhoSN5N2ffZkJE1Ji
hvgkDXzSN+l4P3e422ICxuVQqozba8/o8pZo46EzgRry760i9RcR8Q8JsXysjeQ1
Q68F8JhUYt1GNQlc5/A1EHEHyv1XIMDkYQ0eart1iUf9uvU/tp6pFTNUq/UtL/BT
RaJdnShbstS7bsfZwkyRtzXUlu15z/xdCsoXbbz+GC4oV7thzZQ+eRS8sZBGTsHF
6AaNqvd3QQnbFEpUSDzK3xupVEvLw3BbwYFvxwIDAQABAoIBAB4Gr9F/yvynD/1p
A1mwxPEJ+4tSU1ENeunTuZfA+eN2PVfHcayKV2BIrzaVDxYuLKI+WC5du5qvLeNy
D7c5xa63XqKIHgbLKKBWsbWqoPQwyU397SOxLgP/pMhaDYRsgxd+Oop4GMiF6IDw
PgjQTQLtDhUTejLCFghuEDgmLE87oi4oV3m8y36Yl1gHSHLzHivk8tiJoFdd9Jw3
FdM9wPS2FcafGaT7CDhbmo8XtHgynxbjCAX6D8tOpsbhVuClseRLXMfhkai0UuO4
JhgJ4tvDoxW3G/3qZkSvL/jUr5gybUCjVAcBfE7HIvfcYKQxU+iX9R8Q7cWzFO/d
RjooSWECgYEA9DDSYxBXuOkVHq9KDWnRBWUslLk2i9nvPEL+JVPqzR6Q+uRnZzl/
j54XBd25lAcCkTDmjZTHKFroX5uvgBzHGfGZFGtoZuObfVkzK9eicupPqTe7rcN6
fTJWeJnNJsYbkGzv0jrqvBJOG+/9zGVsn7UQZvWHiS9lgKYrDz2Vx5cCgYEAyieS
3xFK+lytLgJ6pqNx6RuvEAKgouZi3sgYIxwyMSA9Yap/5po6Osj8w9X18Bvm6YYF
gok+Zx63pEB7296RrmGDxkOw5Hl/gH07Yx2hvM4et3RyvK3udOXCdcXgWN0ue/Uf
H75UZ4CLAmNALEUa9lOcB2uydVHOhXCmgPveH1ECgYAtShzLKM3MStaS8VnfsP+G
a6RgFRXrzEjVuWsfizfiQUgMcG5JM93Xyi9k9CGmNcKhIRuxqKVjc7DjgqGDNlMr
GacVpXIgmxhMoE2gVQcZHyIVNXQGn1nJfJuTFJt7FIUqPTohmLHOneqEvfcpgKor
2M4o+mLf6718pdUYp4hvEwKBgBSJhLBIz3cz5xwfgFphjHcEKvrTaYJjKXQ8m8cl
XCwFfHbpnWjODlBejt9OY1frXcAnr3Odgct0IW/8ZRjnOaGfooWH5vavKTbigiAF
qKLHxfMZT3a/rNQPa3wPiEU+4zQQqQLOkUCanIS3lJNqydxwjg9q74xfrT19Pk0o
SV6hAoGBAKfidUGqWGH3FugbgG2cm7rK54nh978brZLKglekR1RlRWKG7QpRP33v
D13y3BD1rRM3vguD2aABhwqbYVt1hjHA+mv+yDzJps08FtZIasiTRpm2mFanOD84
yKf+0/HMD2G45HzoMYdG6BdZ5HP1y4WFNfRoxjwCTnwyrDMNJhOl
-----END RSA PRIVATE KEY-----
This diff is collapsed.
This diff is collapsed.
......@@ -41,6 +41,9 @@ func main() {
r.Get("/api/chess/stats", api.HandleStats)
r.Get("/health", api.HandleHealth)
// Management API
api.RegisterManagementRoutes(r)
// Admin panel
admin.RegisterRoutes(r)
......
......@@ -5,7 +5,10 @@ import (
"encoding/hex"
"fmt"
"html/template"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
......@@ -18,6 +21,8 @@ import (
"stockfish-api/web"
)
const PortraitDir = "/app/portraits"
// Template functions
var funcMap = template.FuncMap{
"mul": func(a, b interface{}) float64 {
......@@ -181,7 +186,7 @@ func AddLog(method, path string, status int, duration time.Duration, ip string)
}
}
func getRecentLogs() []LogEntry {
func GetRecentLogs() []LogEntry {
logMu.Lock()
defer logMu.Unlock()
......@@ -314,6 +319,24 @@ func HandleBotEdit(w http.ResponseWriter, r *http.Request) {
// POST - update bot parameters
r.ParseForm()
if v := strings.TrimSpace(r.FormValue("name")); v != "" {
bot.Name = v
}
if v := r.FormValue("name_ar"); v != "" {
bot.NameAr = v
}
if v := strings.TrimSpace(r.FormValue("style")); v != "" {
bot.Style = v
}
if v := r.FormValue("style_ar"); v != "" {
bot.StyleAr = v
}
if v := r.FormValue("bio"); v != "" {
bot.Bio = v
}
if v := r.FormValue("bio_ar"); v != "" {
bot.BioAr = v
}
if v, err := strconv.Atoi(r.FormValue("skill_level")); err == nil {
bot.SkillLevel = v
}
......@@ -378,9 +401,15 @@ func HandleBotCreate(w http.ResponseWriter, r *http.Request) {
}
bot := &bots.BotPersonality{
ID: id,
Name: name,
Style: style,
ID: id,
Name: name,
NameAr: strings.TrimSpace(r.FormValue("name_ar")),
Style: style,
StyleAr: strings.TrimSpace(r.FormValue("style_ar")),
Bio: strings.TrimSpace(r.FormValue("bio")),
BioAr: strings.TrimSpace(r.FormValue("bio_ar")),
AvatarID: strings.TrimSpace(r.FormValue("avatar_id")),
PortraitURL: "/portraits/" + id + ".png",
}
if v, err := strconv.Atoi(r.FormValue("skill_level")); err == nil {
......@@ -483,7 +512,7 @@ func HandleSettings(w http.ResponseWriter, r *http.Request) {
}
func HandleLogs(w http.ResponseWriter, r *http.Request) {
logs := getRecentLogs()
logs := GetRecentLogs()
renderPage(w, "logs.html", map[string]interface{}{
"Title": "Logs",
"Logs": logs,
......@@ -559,6 +588,59 @@ func HandleTestMove(w http.ResponseWriter, r *http.Request) {
})
}
func HandlePortraitUpload(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
}
r.ParseMultipartForm(10 << 20) // 10MB max
file, header, err := r.FormFile("portrait")
if err != nil {
http.Error(w, "No file uploaded", http.StatusBadRequest)
return
}
defer file.Close()
ext := strings.ToLower(filepath.Ext(header.Filename))
if ext != ".png" && ext != ".jpg" && ext != ".jpeg" && ext != ".webp" {
http.Error(w, "Only PNG, JPG, and WebP files are allowed", http.StatusBadRequest)
return
}
os.MkdirAll(PortraitDir, 0755)
filename := botID + ext
destPath := filepath.Join(PortraitDir, filename)
dst, err := os.Create(destPath)
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
defer dst.Close()
io.Copy(dst, file)
bot.PortraitURL = "/portraits/" + filename
http.Redirect(w, r, "/admin/bots/edit/"+botID, http.StatusFound)
}
func HandlePortraitServe(w http.ResponseWriter, r *http.Request) {
filename := chi.URLParam(r, "*")
filePath := filepath.Join(PortraitDir, filepath.Base(filename))
if _, err := os.Stat(filePath); os.IsNotExist(err) {
http.Error(w, "Not found", http.StatusNotFound)
return
}
http.ServeFile(w, r, filePath)
}
// --- Route Registration ---
// RegisterRoutes mounts all admin routes under the provided router.
......@@ -566,6 +648,9 @@ func RegisterRoutes(r chi.Router) {
r.Get("/admin/login", HandleLogin)
r.Post("/admin/login", HandleLogin)
// Serve portraits publicly (no auth needed for API consumers)
r.Get("/portraits/*", HandlePortraitServe)
r.Group(func(protected chi.Router) {
protected.Use(RequireAuth)
......@@ -574,6 +659,7 @@ func RegisterRoutes(r chi.Router) {
protected.Get("/admin/bots", HandleBots)
protected.Get("/admin/bots/edit/{id}", HandleBotEdit)
protected.Post("/admin/bots/edit/{id}", HandleBotEdit)
protected.Post("/admin/bots/portrait/{id}", HandlePortraitUpload)
protected.Get("/admin/bots/create", HandleBotCreate)
protected.Post("/admin/bots/create", HandleBotCreate)
protected.Post("/admin/bots/delete/{id}", HandleBotDelete)
......
This diff is collapsed.
......@@ -7,6 +7,7 @@ type BotPersonality struct {
Name string `json:"name"`
NameAr string `json:"name_ar"`
Style string `json:"style"`
StyleAr string `json:"style_ar"`
Bio string `json:"bio"`
BioAr string `json:"bio_ar"`
ELOMin int `json:"elo_min"`
......@@ -19,6 +20,7 @@ type BotPersonality struct {
ThinkTimeMax int `json:"think_time_max_ms"`
OpeningBook []string `json:"opening_book"`
AvatarID string `json:"avatar_id"`
PortraitURL string `json:"portrait_url"`
}
var Personalities = map[string]*BotPersonality{
......@@ -27,6 +29,7 @@ var Personalities = map[string]*BotPersonality{
Name: "Amina",
NameAr: "أمينة المبتدئة",
Style: "beginner",
StyleAr: "مبتدئة",
Bio: "Just learning chess! Makes mistakes but tries her best.",
BioAr: "لسه بتتعلم شطرنج! بتغلط كتير بس بتحاول.",
ELOMin: 400,
......@@ -39,12 +42,14 @@ var Personalities = map[string]*BotPersonality{
ThinkTimeMax: 2000,
OpeningBook: []string{},
AvatarID: "bot-amina",
PortraitURL: "/portraits/amina.png",
},
"tarek": {
ID: "tarek",
Name: "Tarek",
NameAr: "طارق المتحفظ",
Style: "defensive",
StyleAr: "دفاعي",
Bio: "Plays it safe. Castles early, protects everything, rarely attacks.",
BioAr: "بيلعب أمان. بيتبيّت بدري و بيحمي كل حاجة.",
ELOMin: 800,
......@@ -57,12 +62,14 @@ var Personalities = map[string]*BotPersonality{
ThinkTimeMax: 3000,
OpeningBook: []string{"italian", "queens_gambit_declined", "caro_kann"},
AvatarID: "bot-tarek",
PortraitURL: "/portraits/tarek.png",
},
"nour": {
ID: "nour",
Name: "Nour",
NameAr: "نور المهاجمة",
Style: "aggressive",
StyleAr: "هجومية",
Bio: "Loves to attack! Sacrifices pieces for a shot at your king.",
BioAr: "بتحب الهجوم! بتضحي بقطع عشان توصل للملك.",
ELOMin: 1000,
......@@ -75,12 +82,14 @@ var Personalities = map[string]*BotPersonality{
ThinkTimeMax: 3000,
OpeningBook: []string{"kings_gambit", "sicilian_dragon", "evans_gambit"},
AvatarID: "bot-nour",
PortraitURL: "/portraits/nour.png",
},
"omar": {
ID: "omar",
Name: "Omar",
NameAr: "عمر الاستراتيجي",
Style: "positional",
StyleAr: "استراتيجي",
Bio: "Controls the center, improves pieces slowly, dominates the endgame.",
BioAr: "بيسيطر على المركز، بيحسّن القطع ببطء، و بيكسب النهايات.",
ELOMin: 1200,
......@@ -93,12 +102,14 @@ var Personalities = map[string]*BotPersonality{
ThinkTimeMax: 4000,
OpeningBook: []string{"queens_gambit", "english", "reti"},
AvatarID: "bot-omar",
PortraitURL: "/portraits/omar.png",
},
"layla": {
ID: "layla",
Name: "Layla",
NameAr: "ليلى المبدعة",
Style: "creative",
StyleAr: "إبداعية",
Bio: "Looks for tactics, pins, forks, and sacrifices. Unpredictable.",
BioAr: "بتدور على التكتيكات والتضحيات. مش هتتوقع خطوتها.",
ELOMin: 1400,
......@@ -111,12 +122,14 @@ var Personalities = map[string]*BotPersonality{
ThinkTimeMax: 5000,
OpeningBook: []string{"sicilian_najdorf", "kings_indian", "grunfeld"},
AvatarID: "bot-layla",
PortraitURL: "/portraits/layla.png",
},
"ziad": {
ID: "ziad",
Name: "Ziad",
NameAr: "زياد الصلب",
Style: "solid",
StyleAr: "صلب",
Bio: "No weaknesses. Plays principled chess. Hard to beat, rarely blunders.",
BioAr: "مفيش نقط ضعف. بيلعب شطرنج محترم. صعب تكسبه.",
ELOMin: 1600,
......@@ -129,12 +142,14 @@ var Personalities = map[string]*BotPersonality{
ThinkTimeMax: 6000,
OpeningBook: []string{"ruy_lopez", "queens_gambit", "nimzo_indian"},
AvatarID: "bot-ziad",
PortraitURL: "/portraits/ziad.png",
},
"grandmaster": {
ID: "grandmaster",
Name: "Grandmaster Bot",
NameAr: "الجراند ماستر",
Style: "near_perfect",
StyleAr: "شبه مثالي",
Bio: "Full Stockfish strength. Punishes every mistake. Good luck.",
BioAr: "قوة ستوكفيش الكاملة. بيعاقب كل غلطة. حظ سعيد.",
ELOMin: 2000,
......@@ -147,6 +162,7 @@ var Personalities = map[string]*BotPersonality{
ThinkTimeMax: 8000,
OpeningBook: []string{},
AvatarID: "bot-grandmaster",
PortraitURL: "/portraits/grandmaster.png",
},
}
......
......@@ -3,52 +3,77 @@
<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 style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<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>Avatar ID</label>
<input type="text" name="avatar_id" placeholder="bot-name">
</div>
<div class="form-group">
<label>Name</label>
<input type="text" name="name" required>
</div>
<div class="form-group">
<label>Name (Arabic)</label>
<input type="text" name="name_ar" dir="rtl">
</div>
<div class="form-group">
<label>Style</label>
<input type="text" name="style" required placeholder="e.g. aggressive, defensive, positional">
</div>
<div class="form-group">
<label>Style (Arabic)</label>
<input type="text" name="style_ar" dir="rtl" placeholder="مثلا: هجومي، دفاعي">
</div>
<div class="form-group" style="grid-column: 1 / -1;">
<label>Bio</label>
<input type="text" name="bio" placeholder="Short description of the bot's personality">
</div>
<div class="form-group" style="grid-column: 1 / -1;">
<label>Bio (Arabic)</label>
<input type="text" name="bio_ar" dir="rtl" placeholder="وصف قصير لشخصية البوت">
</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 (1-30)</label>
<input type="number" name="depth" value="10" min="1" max="30">
</div>
<div class="form-group">
<label>Contempt (-100 to 100)</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>
</div>
<div class="form-group">
<label>Name</label>
<input type="text" name="name" required>
<p style="margin-top: 0.5rem; color: #666; font-size: 0.85rem;">Portrait can be uploaded after creation from the edit page.</p>
<div style="margin-top: 1rem;">
<button type="submit" class="btn btn-success">Create Bot</button>
<a href="/admin/bots" class="btn" style="margin-left:1rem;">Cancel</a>
</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}}
......@@ -3,41 +3,89 @@
<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">
<!-- Portrait Section -->
<div style="margin-bottom: 1.5rem; padding: 1rem; background: #f8f9fa; border-radius: 8px; display: flex; align-items: center; gap: 1.5rem;">
<div style="width: 128px; height: 128px; border-radius: 8px; overflow: hidden; background: #ddd; flex-shrink: 0;">
{{if .Bot.PortraitURL}}
<img src="{{.Bot.PortraitURL}}" alt="{{.Bot.Name}}" style="width: 100%; height: 100%; object-fit: cover;">
{{else}}
<div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: #999; font-size: 2rem;">&#9820;</div>
{{end}}
</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>
<p style="margin-bottom: 0.5rem; font-weight: 600;">Portrait (512x512 px)</p>
<form method="POST" action="/admin/bots/portrait/{{.Bot.ID}}" enctype="multipart/form-data" style="display: flex; gap: 0.5rem; align-items: center;">
<input type="file" name="portrait" accept=".png,.jpg,.jpeg,.webp" required style="font-size: 0.85rem;">
<button type="submit" class="btn btn-primary">Upload</button>
</form>
<p style="margin-top: 0.3rem; color: #666; font-size: 0.8rem;">Accepts PNG, JPG, WebP. Will be served at {{.Bot.PortraitURL}}</p>
</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>
<form method="POST" action="/admin/bots/edit/{{.Bot.ID}}">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<div class="form-group">
<label>Name</label>
<input type="text" name="name" value="{{.Bot.Name}}">
</div>
<div class="form-group">
<label>Name (Arabic)</label>
<input type="text" name="name_ar" value="{{.Bot.NameAr}}" dir="rtl">
</div>
<div class="form-group">
<label>Style</label>
<input type="text" name="style" value="{{.Bot.Style}}">
</div>
<div class="form-group">
<label>Style (Arabic)</label>
<input type="text" name="style_ar" value="{{.Bot.StyleAr}}" dir="rtl">
</div>
<div class="form-group" style="grid-column: 1 / -1;">
<label>Bio</label>
<input type="text" name="bio" value="{{.Bot.Bio}}">
</div>
<div class="form-group" style="grid-column: 1 / -1;">
<label>Bio (Arabic)</label>
<input type="text" name="bio_ar" value="{{.Bot.BioAr}}" dir="rtl">
</div>
<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 (1-30)</label>
<input type="number" name="depth" value="{{.Bot.Depth}}" min="1" max="30">
</div>
<div class="form-group">
<label>Contempt (-100 to 100)</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>
</div>
<div class="form-group">
<label>ELO Max</label>
<input type="number" name="elo_max" value="{{.Bot.ELOMax}}" min="0">
<div style="margin-top: 1rem;">
<button type="submit" class="btn btn-primary">Save Changes</button>
<a href="/admin/bots" class="btn" style="margin-left:1rem;">Cancel</a>
</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}}
......@@ -5,7 +5,7 @@
<table>
<thead>
<tr>
<th>ID</th>
<th></th>
<th>Name</th>
<th>Style</th>
<th>ELO</th>
......@@ -18,9 +18,23 @@
<tbody>
{{range .Bots}}
<tr>
<td>{{.ID}}</td>
<td>{{.Name}}</td>
<td>{{.Style}}</td>
<td>
<div style="width: 40px; height: 40px; border-radius: 50%; overflow: hidden; background: #eee;">
{{if .PortraitURL}}
<img src="{{.PortraitURL}}" alt="{{.Name}}" style="width: 100%; height: 100%; object-fit: cover;">
{{else}}
<div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: #999;">&#9820;</div>
{{end}}
</div>
</td>
<td>
<strong>{{.Name}}</strong><br>
<span style="color: #666; font-size: 0.8rem;" dir="rtl">{{.NameAr}}</span>
</td>
<td>
{{.Style}}<br>
<span style="color: #666; font-size: 0.8rem;" dir="rtl">{{.StyleAr}}</span>
</td>
<td>{{.ELOMin}}-{{.ELOMax}}</td>
<td>{{.SkillLevel}}</td>
<td>{{.Depth}}</td>
......
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