AstraOS Ver. 1.29:
All checks were successful
Deploy AstraOS Panel / deploy (push) Successful in 12s

- AyranTracker bearbeitet
This commit is contained in:
elias 2026-06-07 12:17:29 +02:00
parent d9bdb3d527
commit 7aaff575d7
2 changed files with 150 additions and 48 deletions

View file

@ -3,7 +3,7 @@
{% block title %}AstraOS — AyranTracker{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto px-4 py-6">
<div class="max-w-5xl mx-auto px-4 py-6">
<header class="mb-10 text-center">
<h2 class="text-4xl font-black tracking-widest text-transparent bg-clip-text bg-gradient-to-r from-purple-400 via-violet-200 to-white">
AYRAN<span class="text-purple-400">TRACKER</span>
@ -15,12 +15,17 @@
<h3 class="text-lg font-bold text-purple-300 mb-4 text-center">Spieler-Analyse</h3>
<div class="space-y-4">
<div>
<label class="block text-xs font-semibold text-slate-400 mb-2">Riot ID Name</label>
<input type="text" id="riotName" placeholder="Z.B. TenZ" class="w-full bg-slate-950 border border-slate-800 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:border-purple-500 transition text-white">
<label class="block text-xs font-semibold text-slate-400 mb-2">Riot ID (Name#TAG)</label>
<input type="text" id="riotIdInput" placeholder="Z.B. TenZ#NA1" class="w-full bg-slate-950 border border-slate-800 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:border-purple-500 transition text-white">
</div>
<div>
<label class="block text-xs font-semibold text-slate-400 mb-2">Tag</label>
<input type="text" id="riotTag" placeholder="Z.B. NA1" class="w-full bg-slate-950 border border-slate-800 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:border-purple-500 transition text-white">
<label class="block text-xs font-semibold text-slate-400 mb-2">Match Typ</label>
<select id="matchModeSelect" class="w-full bg-slate-950 border border-slate-800 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:border-purple-500 transition text-white">
<option value="competitive" selected>Competitive (Standard)</option>
<option value="swiftplay">Swiftplay</option>
<option value="unrated">Unrated</option>
<option value="spikerush">Spike Rush</option>
</select>
</div>
<button id="searchBtn" class="w-full py-3 rounded-lg font-bold text-sm transition text-white {% if maintenance_active %}bg-yellow-600 hover:bg-yellow-700{% else %}bg-purple-600 hover:bg-purple-700{% endif %}">
Analyse starten
@ -34,7 +39,7 @@
</button>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-black/40 border border-purple-500/10 p-6 rounded-2xl flex flex-col items-center text-center backdrop-blur-md">
<div class="bg-black/40 border border-purple-500/10 p-6 rounded-2xl flex flex-col items-center text-center backdrop-blur-md h-fit">
<img id="playerCard" src="" alt="Player Card" class="w-24 h-24 rounded-xl object-cover mb-4 border border-purple-500/30 shadow-md">
<h4 id="playerNameTag" class="text-xl font-black text-white">Loading...</h4>
<p id="playerLevel" class="text-xs text-slate-400 mt-1 bg-slate-950 px-2 py-0.5 rounded-full">Level: --</p>
@ -46,8 +51,9 @@
</div>
</div>
<div class="bg-black/40 border border-purple-500/10 p-6 rounded-2xl backdrop-blur-md md:col-span-2 space-y-6">
<h4 class="text-sm font-bold text-slate-400 tracking-wider uppercase">Berechnete Performance (Letzte Matches)</h4>
<div class="md:col-span-2 space-y-6">
<div class="bg-black/40 border border-purple-500/10 p-6 rounded-2xl backdrop-blur-md space-y-6">
<h4 class="text-sm font-bold text-slate-400 tracking-wider uppercase">Berechnete Performance</h4>
<div class="grid grid-cols-2 gap-4">
<div class="bg-slate-950/50 border border-slate-900 p-4 rounded-xl">
@ -70,7 +76,13 @@
<div>
<h5 class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-3">Top Agenten</h5>
<div id="agentContainer" class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div id="agentContainer" class="grid grid-cols-1 sm:grid-cols-2 gap-3"></div>
</div>
</div>
<div class="bg-black/40 border border-purple-500/10 p-6 rounded-2xl backdrop-blur-md space-y-4">
<h4 class="text-sm font-bold text-slate-400 tracking-wider uppercase">Match Historie (Letzte 15 Matches)</h4>
<div id="matchHistoryContainer" class="space-y-2 max-h-[500px] overflow-y-auto pr-1">
</div>
</div>
</div>
@ -80,12 +92,22 @@
<script>
document.getElementById('searchBtn').addEventListener('click', async () => {
const name = document.getElementById('riotName').value.trim();
const tag = document.getElementById('riotTag').value.trim();
const riotId = document.getElementById('riotIdInput').value.trim();
const mode = document.getElementById('matchModeSelect').value;
const btn = document.getElementById('searchBtn');
// Splitte Name und Tag anhand des '#'
if (!riotId.includes('#')) {
alert("Bitte gib die Riot ID im Format Name#TAG an (z.B. TenZ#NA1)!");
return;
}
const parts = riotId.split('#');
const name = parts[0].trim();
const tag = parts[1].trim();
if (!name || !tag) {
alert("Bitte fülle Name und Tag aus!");
alert("Name oder Tag darf nicht leer sein!");
return;
}
@ -93,10 +115,11 @@
btn.disabled = true;
try {
const response = await fetch(`/api/v1/profile/${encodeURIComponent(name)}/${encodeURIComponent(tag)}`);
const response = await fetch(`/api/v1/profile/${encodeURIComponent(name)}/${encodeURIComponent(tag)}?mode=${mode}`);
const data = await response.json();
if (response.ok && data.status === "success") {
// Basisdaten füllen
document.getElementById('playerNameTag').innerText = `${data.account.name}#${data.account.tag}`;
document.getElementById('playerLevel').innerText = `Level: ${data.account.level}`;
document.getElementById('playerCard').src = data.account.card_url || 'https://media.valorant-api.com/playercards/9fb348bc-41a4-91ad-d131-1ca1efdc3535/smallart.png';
@ -104,15 +127,17 @@
document.getElementById('playerRank').innerText = data.rank.current_tier_name;
document.getElementById('playerRR').innerText = `${data.rank.ranking_in_tier} RR`;
// Statistiken füllen
document.getElementById('statWinrate').innerText = data.calculated_stats.win_rate;
document.getElementById('statKD').innerText = data.calculated_stats.kd_ratio;
document.getElementById('statADR').innerText = data.calculated_stats.avg_damage_round;
document.getElementById('statMatches').innerText = data.calculated_stats.matches_analyzed;
// Agenten füllen
const agentContainer = document.getElementById('agentContainer');
agentContainer.innerHTML = '';
if (data.top_agents.length === 0) {
agentContainer.innerHTML = '<p class="text-xs text-slate-500 py-2">Keine Agenten-Daten verfügbar.</p>';
agentContainer.innerHTML = '<p class="text-xs text-slate-500 py-2">Keine Daten verfügbar.</p>';
} else {
data.top_agents.forEach(agent => {
agentContainer.insertAdjacentHTML('beforeend', `
@ -124,13 +149,44 @@
});
}
// Match Historie aufbauen
const historyContainer = document.getElementById('matchHistoryContainer');
historyContainer.innerHTML = '';
if (!data.matches || data.matches.length === 0) {
historyContainer.innerHTML = '<p class="text-xs text-slate-500 text-center py-4">Keine Matches in diesem Spielmodus gefunden.</p>';
} else {
data.matches.forEach(match => {
const borderClass = match.has_won ? 'border-emerald-500/30 bg-emerald-950/10' : 'border-rose-500/30 bg-rose-950/10';
const textClass = match.has_won ? 'text-emerald-400' : 'text-rose-400';
const statusText = match.has_won ? 'SIEG' : 'NIEDERLAGE';
historyContainer.insertAdjacentHTML('beforeend', `
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center border p-4 rounded-xl backdrop-blur-sm gap-4 ${borderClass}">
<div class="flex items-center gap-3">
<img src="${match.agent_img}" alt="${match.agent}" class="w-10 h-10 rounded-lg border border-slate-700 bg-slate-900 object-cover" onerror="this.style.display='none'">
<div>
<span class="block text-sm font-black text-white">${match.agent}</span>
<span class="block text-xs text-slate-400">${match.map} • ${match.mode}</span>
</div>
</div>
<div class="flex flex-row sm:flex-col justify-between sm:justify-center w-full sm:w-auto items-center sm:items-end gap-1">
<span class="text-xs font-bold ${textClass}">${statusText} (${match.score})</span>
<span class="text-sm font-semibold text-slate-300">KDA: ${match.kills} / ${match.deaths} / ${match.assists}</span>
</div>
</div>
`);
});
}
// Ansichten umschalten
document.getElementById('searchSection').classList.add('hidden');
document.getElementById('profileSection').classList.remove('hidden');
} else {
alert("Fehler: " + (data.message || "Spieler nicht gefunden"));
alert("Fehler: " + (data.message || "Spieler nicht gefunden. Überprüfe Name, TAG und Spielmodus."));
}
} catch (error) {
alert("Verbindungsfehler zur API.");
alert("Verbindungsfehler oder API antwortet nicht.");
} finally {
btn.innerText = "Analyse starten";
btn.disabled = false;

View file

@ -1521,6 +1521,7 @@ def update_maintenance():
def page_not_found(e):
return render_template('404.html'), 404
VALORANT_API_KEY = os.getenv("VALORANT_API_KEY", "")
VALORANT_API_BASE = "https://api.henrikdev.xyz/valorant"
@ -1536,13 +1537,15 @@ def ayrantracker():
@login_required
@permission_required('view_ayrantracker')
def get_valorant_stats(name, tag):
# Sicherheitsprüfung, falls du vergessen hast, den Key in die .env einzutragen
if not VALORANT_API_KEY:
return jsonify({"status": "error", "message": "API-Key Konfiguration fehlt in der .env-Datei."}), 500
# Filter/Match-Typ aus den Query-Parametern auslesen (Standard: competitive)
match_mode = request.args.get('mode', 'competitive')
headers = {"Authorization": VALORANT_API_KEY}
try:
# 1. Account Basis-Daten abrufen
acc_res = requests.get(f"{VALORANT_API_BASE}/v1/account/{name}/{tag}?force=true", headers=headers, timeout=10)
if acc_res.status_code != 200:
return jsonify(
@ -1551,56 +1554,98 @@ def get_valorant_stats(name, tag):
acc_data = acc_res.json().get('data', {}) or {}
region = acc_data.get("region", "eu")
# 2. Aktueller Rang (MMR v1)
mmr_res = requests.get(f"{VALORANT_API_BASE}/v1/mmr/{region}/{name}/{tag}", headers=headers, timeout=10)
mmr_data = mmr_res.json().get('data', {}) if mmr_res.status_code == 200 else {}
if not mmr_data: mmr_data = {}
matches_res = requests.get(f"{VALORANT_API_BASE}/v3/matches/{region}/{name}/{tag}?size=15&mode=competitive",
# 3. Match Historie abrufen (Größe auf 15 erhöht, dynamischer Modus)
matches_res = requests.get(f"{VALORANT_API_BASE}/v3/matches/{region}/{name}/{tag}?size=15&mode={match_mode}",
headers=headers, timeout=10)
matches_list = matches_res.json().get('data', []) if matches_res.status_code == 200 else []
if not matches_list: matches_list = []
match_count = len(matches_list)
# Statistiken und Match-Details vorbereiten
match_count = 0
wins = 0
total_kills = 0
total_deaths = 0
total_assists = 0
total_damage = 0
total_rounds = 0
agent_counts = {}
processed_matches = []
for match in matches_list:
metadata = match.get('metadata', {})
players = match.get('players', {}).get('all_players', [])
# Eigenen Spieler im Match suchen
me = next((p for p in players if
p.get('name', '').lower() == name.lower() and p.get('tag', '').lower() == tag.lower()), None)
if not me:
continue
match_count += 1
agent_name = me.get('character', 'Unknown')
agent_counts[agent_name] = agent_counts.get(agent_name, 0) + 1
# Stats sammeln
stats = me.get('stats', {})
total_kills += stats.get('kills', 0)
total_deaths += stats.get('deaths', 0)
m_kills = stats.get('kills', 0)
m_deaths = stats.get('deaths', 0)
m_assists = stats.get('assists', 0)
total_kills += m_kills
total_deaths += m_deaths
total_assists += m_assists
# Rundenauswertung für Win/Loss & Schaden
teams = match.get('teams', {})
my_team_color = me.get('team', '').lower()
my_team_data = teams.get(my_team_color, {})
if my_team_data.get('has_won') is True:
has_won = my_team_data.get('has_won') is True
if has_won:
wins += 1
rounds_played = (teams.get('red', {}).get('rounds_won', 0) or 0) + (
teams.get('blue', {}).get('rounds_won', 0) or 0)
# Runden-Zähler extrahieren
red_rounds = teams.get('red', {}).get('rounds_won', 0) or 0
blue_rounds = teams.get('blue', {}).get('rounds_won', 0) or 0
rounds_played = red_rounds + blue_rounds
total_rounds += rounds_played
# Score-Anzeige formatieren (z.B. "13 - 5")
if my_team_color == 'red':
score_str = f"{red_rounds} : {blue_rounds}"
else:
score_str = f"{blue_rounds} : {red_rounds}"
# Schaden aus Runden summieren
match_damage = 0
round_data = match.get('rounds', [])
for r in round_data:
player_stats = r.get('player_stats', [])
p_round_stat = next((ps for ps in player_stats if ps.get('player_name', '').lower() == name.lower()),
None)
if p_round_stat:
total_damage += p_round_stat.get('damage', 0)
match_damage += p_round_stat.get('damage', 0)
total_damage += match_damage
# Einzelnes Match für die Liste aufbereiten
processed_matches.append({
"map": metadata.get('map', 'Unknown'),
"mode": metadata.get('mode', 'Unknown'),
"agent": agent_name,
"agent_img": me.get('assets', {}).get('agent', {}).get('small', ''),
"kills": m_kills,
"deaths": m_deaths,
"assists": m_assists,
"score": score_str,
"has_won": has_won
})
# Globale Schnitte berechnen
kd_calculated = round(total_kills / total_deaths, 2) if total_deaths > 0 else total_kills
winrate_calculated = round((wins / match_count) * 100, 1) if match_count > 0 else 0
adr_calculated = round(total_damage / total_rounds, 1) if total_rounds > 0 else 0
@ -1626,7 +1671,8 @@ def get_valorant_stats(name, tag):
"avg_damage_round": str(adr_calculated),
"matches_analyzed": match_count
},
"top_agents": top_agents
"top_agents": top_agents,
"matches": processed_matches
})
except Exception as e: