Refaktor 4/4: admin kao Blueprint-ovi + auto-meni
Admin stranica app-a živi u folderu app-a: - idriz/apps/superheroji/admin.py (Flask Blueprint BP + NAV), template preseljen u idriz/apps/superheroji/templates/superheroji.html. - admin/app.py: registruje sve iz ADMIN_APPS, lijevi meni se puni iz njihovih NAV (context processor), globalni before_request štiti i Blueprint rute. - Uklonjena inline /superheroji ruta i hardkodiran link u base.html. Dodavanje admin stranice novom app-u = admin.py (BP+NAV) u folderu app-a + upis u ADMIN_APPS. Time je mini-app potpuno samostalan: prompt, tools, config i admin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzKpYaEsuWDevuNnvSjcFW
This commit is contained in:
58
admin/app.py
58
admin/app.py
@@ -17,9 +17,12 @@ from flask import (
|
||||
Flask, render_template, request, redirect, url_for, session, flash
|
||||
)
|
||||
|
||||
# Pristup skladištu igre (bez openai zavisnosti) iz glavnog idriz paketa.
|
||||
# Admin stranice mini-app-ova (lagani moduli iz glavnog paketa, bez openai zavisnosti).
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from idriz.apps.superheroji import store as gamestore # noqa: E402
|
||||
from idriz.apps.superheroji import admin as _superheroji_admin # noqa: E402
|
||||
|
||||
# Svaki app-admin modul ima BP (Blueprint) i NAV (stavka lijevog menija).
|
||||
ADMIN_APPS = [_superheroji_admin]
|
||||
|
||||
# --- Konfiguracija -----------------------------------------------------------
|
||||
|
||||
@@ -52,6 +55,24 @@ app.config.update(
|
||||
PERMANENT_SESSION_LIFETIME=8 * 3600,
|
||||
)
|
||||
|
||||
# Registruj admin stranice mini-app-ova i napuni lijevi meni iz njih.
|
||||
for _m in ADMIN_APPS:
|
||||
app.register_blueprint(_m.BP)
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def _inject_nav():
|
||||
return {"nav_apps": [_m.NAV for _m in ADMIN_APPS]}
|
||||
|
||||
|
||||
@app.before_request
|
||||
def _require_login():
|
||||
"""Sve rute (uključujući app Blueprint-ove) traže prijavu, osim logina i statike."""
|
||||
if request.endpoint in (None, "login", "static"):
|
||||
return
|
||||
if not session.get("user"):
|
||||
return redirect(url_for("login", next=request.path))
|
||||
|
||||
|
||||
def login_required(f):
|
||||
@wraps(f)
|
||||
@@ -201,39 +222,6 @@ def status():
|
||||
)
|
||||
|
||||
|
||||
@app.route("/superheroji", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def superheroji():
|
||||
if request.method == "POST":
|
||||
action = request.form.get("action")
|
||||
if action == "rounds":
|
||||
try:
|
||||
n = int(request.form.get("rounds", ""))
|
||||
saved = gamestore.save_rounds(n)
|
||||
flash(f"Broj rundi postavljen na {saved}.")
|
||||
except (ValueError, TypeError):
|
||||
flash("Neispravan broj rundi.")
|
||||
elif action == "reset":
|
||||
store = gamestore.HeroStore()
|
||||
store.heroes = {}
|
||||
store.aliases = {}
|
||||
store.matchups = {}
|
||||
store.save()
|
||||
flash("Pamćenje jačina i dvoboja je obrisano.")
|
||||
return redirect(url_for("superheroji"))
|
||||
|
||||
store = gamestore.HeroStore()
|
||||
return render_template(
|
||||
"superheroji.html",
|
||||
rounds=gamestore.load_rounds(),
|
||||
min_rounds=gamestore.MIN_ROUNDS,
|
||||
max_rounds=gamestore.MAX_ROUNDS,
|
||||
heroes=store.leaderboard(),
|
||||
matchups=store.matchup_list(),
|
||||
active_page="superheroji",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from waitress import serve
|
||||
print(f"Idriz admin na http://{LISTEN_HOST}:{LISTEN_PORT}")
|
||||
|
||||
@@ -98,9 +98,11 @@
|
||||
<a href="{{ url_for('status') }}" class="{{ 'active' if active_page == 'status' else '' }}">
|
||||
<span class="icon">●</span> Status servisa
|
||||
</a>
|
||||
<a href="{{ url_for('superheroji') }}" class="{{ 'active' if active_page == 'superheroji' else '' }}">
|
||||
<span class="icon">★</span> Superheroji
|
||||
{% for it in nav_apps %}
|
||||
<a href="{{ url_for(it.endpoint) }}" class="{{ 'active' if active_page == it.page else '' }}">
|
||||
<span class="icon">{{ it.icon }}</span> {{ it.title }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
<div class="sidebar-foot">
|
||||
{% if session.user %}<a href="{{ url_for('logout') }}">↩ Odjava</a>{% endif %}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Superheroji — Idriz Admin{% endblock %}
|
||||
{% block extra_css %}
|
||||
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
|
||||
padding: 20px 22px; margin-bottom: 22px; max-width: 640px; }
|
||||
.panel h2 { margin: 0 0 4px; font-size: 16px; }
|
||||
.panel p.hint { color: var(--muted); font-size: 13px; margin: 0 0 16px; }
|
||||
.row { display: flex; align-items: center; gap: 12px; }
|
||||
input[type=number] { width: 90px; padding: 9px 12px; font-size: 15px;
|
||||
border: 1px solid var(--line); border-radius: 8px; }
|
||||
button { padding: 9px 18px; font-size: 14px; font-weight: 600; color: #fff;
|
||||
background: var(--accent); border: none; border-radius: 8px; cursor: pointer; }
|
||||
button:hover { filter: brightness(1.05); }
|
||||
button.danger { background: var(--bad); }
|
||||
table { width: 100%; border-collapse: collapse; background: var(--panel);
|
||||
border: 1px solid var(--line); border-radius: 10px; overflow: hidden; max-width: 640px; }
|
||||
th, td { text-align: left; padding: 10px 14px; font-size: 14px; }
|
||||
th { background: #fafbfc; color: var(--muted); font-weight: 600; border-bottom: 1px solid var(--line); }
|
||||
tr:not(:last-child) td { border-bottom: 1px solid var(--line); }
|
||||
td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.rank { color: var(--muted); width: 30px; }
|
||||
.src { font-size: 11px; color: var(--muted); }
|
||||
.empty { color: var(--muted); font-style: italic; }
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Superheroji</h1>
|
||||
<p class="sub">Podešavanje igre i pamćenje relativnih jačina likova.</p>
|
||||
|
||||
<div class="panel">
|
||||
<h2>Broj rundi po partiji</h2>
|
||||
<p class="hint">Koliko rundi traje jedna partija ({{ min_rounds }}–{{ max_rounds }}). Primjenjuje se na sljedeću partiju.</p>
|
||||
<form method="post" class="row">
|
||||
<input type="hidden" name="action" value="rounds">
|
||||
<input type="number" name="rounds" min="{{ min_rounds }}" max="{{ max_rounds }}" value="{{ rounds }}">
|
||||
<button type="submit">Sačuvaj</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h2 style="font-size:16px; margin: 0 0 4px;">Rang-lista heroja</h2>
|
||||
<p class="sub">Snaga (rejting) raste kad heroj pobjeđuje. Idriz je koristi da borbe budu dosljedne kroz partije.</p>
|
||||
{% if heroes %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th class="rank">#</th><th>Heroj</th><th class="num">Snaga</th><th class="num">Pobjede</th><th class="num">Porazi</th><th class="num">Borbi</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for h in heroes %}
|
||||
<tr>
|
||||
<td class="rank">{{ loop.index }}</td>
|
||||
<td>{{ h.name }} {% if h.source == 'web' %}<span class="src">(web)</span>{% endif %}</td>
|
||||
<td class="num">{{ h.rating }}</td>
|
||||
<td class="num">{{ h.wins }}</td>
|
||||
<td class="num">{{ h.losses }}</td>
|
||||
<td class="num">{{ h.games }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="empty">Još nema odigranih borbi. Rang-lista se puni kako djeca igraju.</p>
|
||||
{% endif %}
|
||||
|
||||
<h2 style="font-size:16px; margin: 26px 0 4px;">Zabilježeni dvoboji</h2>
|
||||
<p class="sub">Konkretni ishodi sa razlogom. Kad se isti dvoboj ponovi, Idriz čita odavde i ne pita LLM ponovo.</p>
|
||||
{% if matchups %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Pobjednik</th><th>Gubitnik</th><th>Razlog</th><th class="num">Puta</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in matchups %}
|
||||
<tr>
|
||||
<td><strong>{{ m.winner }}</strong></td>
|
||||
<td>{{ m.loser }}</td>
|
||||
<td>{{ m.reason }}</td>
|
||||
<td class="num">{{ m.count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="empty">Još nema zabilježenih dvoboja.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if heroes or matchups %}
|
||||
<form method="post" onsubmit="return confirm('Obrisati sve zapamćene jačine i dvoboje? Ovo se ne može poništiti.');" style="margin-top:18px;">
|
||||
<input type="hidden" name="action" value="reset">
|
||||
<button type="submit" class="danger">Obriši pamćenje</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user