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

- Dashboard verändert
- AyranTracker hinzugefügt
This commit is contained in:
elias 2026-06-07 11:29:58 +02:00
parent 5455ec17d0
commit 8432279bdf
3 changed files with 506 additions and 97 deletions

View file

@ -0,0 +1,287 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ayran Tracker - Professional Valorant Analytics | AstraOS</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.bg-cyberpunk {
background: radial-gradient(circle at 50% 20%, #1e1b4b 0%, #090514 100%);
}
</style>
</head>
<body class="bg-cyberpunk text-slate-100 font-sans min-h-screen flex flex-col justify-between">
{% include 'sidebar.html' %}
<div class="flex-1 ml-0 md:ml-64">
<header class="border-b border-purple-900/40 bg-slate-950/70 backdrop-blur-md sticky top-0 z-40">
<div class="px-4 py-4 flex justify-between items-center">
<div class="flex items-center space-x-3">
<span class="text-2xl">🥛</span>
<span class="text-xl 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>
</span>
</div>
</div>
</header>
<main class="flex-grow flex flex-col items-center justify-center px-4 py-12">
<div id="searchSection" class="w-full flex flex-col items-center justify-center text-center my-auto">
<div class="mb-10">
<span class="px-3 py-1 bg-purple-500/10 text-purple-400 border border-purple-500/20 rounded-full text-xs font-semibold uppercase tracking-wider">
Live Match Analytics
</span>
<h1 class="text-4xl md:text-5xl font-black mt-4 mb-4 tracking-tight uppercase">
Track Your <span class="text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-violet-300">Performance</span>
</h1>
</div>
<div class="w-full max-w-xl bg-slate-950/60 p-5 rounded-xl border border-purple-900/30 shadow-2xl backdrop-blur-md">
<form id="searchForm" class="flex flex-col sm:flex-row gap-2">
<div class="flex-grow">
<input type="text" id="riotInput" placeholder="Riot ID #TAG (z.B. Drax#SWEAT)" required class="w-full bg-slate-900/90 border border-purple-900/40 rounded-lg px-4 py-3.5 text-sm text-white focus:outline-none focus:border-purple-500 text-center sm:text-left">
</div>
<button type="submit" class="bg-purple-600 hover:bg-purple-700 text-white text-sm font-bold px-6 py-3.5 rounded-lg uppercase tracking-wider transition whitespace-nowrap">
Analyse
</button>
</form>
</div>
</div>
<div id="profileSection" class="w-full max-w-6xl hidden">
<div class="bg-slate-950/60 border border-purple-900/30 rounded-2xl p-6 flex flex-col md:flex-row justify-between items-center gap-6 mb-6 backdrop-blur-md">
<div class="flex items-center space-x-4 text-center md:text-left flex-col md:flex-row">
<div id="playerCard" class="w-20 h-20 bg-purple-950 border border-purple-500/30 rounded-xl flex items-center justify-center text-2xl font-bold text-purple-300">VAL</div>
<div>
<div class="flex items-center justify-center md:justify-start space-x-2">
<h2 id="displayPlayerName" class="text-2xl font-black tracking-wide">Player</h2>
<span id="displayPlayerTag" class="text-slate-400 font-bold bg-slate-900 px-2 py-0.5 rounded text-xs">#TAG</span>
</div>
<p class="text-xs text-slate-400 mt-1">Account Level: <span id="displayLevel" class="text-purple-400 font-bold">--</span></p>
</div>
</div>
<div class="bg-slate-900/80 border border-purple-900/40 p-4 rounded-xl flex items-center space-x-4 min-w-[240px]">
<div class="w-10 h-10 bg-purple-900/50 rounded-lg flex items-center justify-center text-xl">🏆</div>
<div>
<p class="text-[10px] text-slate-400 uppercase tracking-widest font-bold">Current Rank</p>
<p id="displayRank" class="text-lg font-black text-purple-300">Loading...</p>
<p id="displayRR" class="text-xs text-slate-500">-- RR</p>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div class="bg-slate-950/40 border border-purple-950 p-4 rounded-xl">
<p class="text-xs text-slate-400 font-semibold uppercase tracking-wider mb-1" id="winrateLabel">Win Rate (Competitive)</p>
<p id="calcWinrate" class="text-2xl font-black text-emerald-400">--</p>
</div>
<div class="bg-slate-950/40 border border-purple-950 p-4 rounded-xl">
<p class="text-xs text-slate-400 font-semibold uppercase tracking-wider mb-1">Average K/D Ratio</p>
<p id="calcKD" class="text-2xl font-black text-slate-100">--</p>
</div>
<div class="bg-slate-950/40 border border-purple-950 p-4 rounded-xl">
<p class="text-xs text-slate-400 font-semibold uppercase tracking-wider mb-1">Avg. Damage / Round (ADR)</p>
<p id="calcADR" class="text-2xl font-black text-purple-400">--</p>
</div>
</div>
<div class="mb-6 flex justify-between items-center">
<h3 class="text-sm font-bold uppercase tracking-wider text-slate-300">Match Filter</h3>
<select id="modeFilter" class="bg-slate-900/80 border border-purple-900/40 rounded-lg px-4 py-2 text-sm text-white focus:outline-none focus:border-purple-500">
<option value="competitive">Competitive</option>
<option value="unrated">Unrated</option>
<option value="swift">Swift</option>
<option value="team deathmatch">Team Deathmatch</option>
<option value="custom">Custom</option>
</select>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-2 bg-slate-950/40 border border-purple-900/20 rounded-2xl p-5">
<h3 class="text-sm font-bold uppercase tracking-wider mb-4 text-slate-300" id="matchesTitle">Recent Matches (Competitive)</h3>
<div id="matchHistoryContainer" class="space-y-2.5">
</div>
</div>
<div class="bg-slate-950/40 border border-purple-900/20 rounded-2xl p-5">
<h3 class="text-sm font-bold uppercase tracking-wider mb-4 text-slate-300">Most Played Agents</h3>
<div id="agentContainer" class="space-y-3">
</div>
</div>
</div>
<button onclick="window.location.href='/dashboard'" class="mt-8 text-xs text-purple-400 hover:text-purple-300 uppercase font-bold transition flex items-center space-x-2 mx-auto">
<span>← Zurück zum Dashboard</span>
</button>
</div>
</main>
</div>
<script>
let currentPlayer = { name: '', tag: '' };
let allMatches = [];
const modeNames = {
'competitive': 'Competitive',
'unrated': 'Unrated',
'swift': 'Swift',
'team deathmatch': 'Team Deathmatch',
'custom': 'Custom'
};
document.getElementById('modeFilter').addEventListener('change', function() {
if (currentPlayer.name && currentPlayer.tag) {
fetchAndDisplayMatches(currentPlayer.name, currentPlayer.tag, this.value);
}
});
async function fetchAndDisplayMatches(name, tag, mode = 'competitive') {
const response = await fetch(`/api/v1/profile/${encodeURIComponent(name)}/${encodeURIComponent(tag)}?mode=${mode}`);
const data = await response.json();
if (data.status === "success") {
updateMatchDisplay(data, mode);
} else {
alert("Fehler: " + data.message);
}
}
function updateMatchDisplay(data, mode) {
document.getElementById('calcWinrate').innerText = data.calculated_stats.win_rate;
document.getElementById('calcKD').innerText = data.calculated_stats.kd_ratio;
document.getElementById('calcADR').innerText = data.calculated_stats.avg_damage_round;
const modeName = modeNames[mode] || mode.charAt(0).toUpperCase() + mode.slice(1);
document.getElementById('winrateLabel').innerText = `Win Rate (${modeName})`;
document.getElementById('matchesTitle').innerText = `Recent Matches (${modeName} - Last ${data.calculated_stats.matches_analyzed})`;
const historyContainer = document.getElementById('matchHistoryContainer');
historyContainer.innerHTML = '';
if (data.matches.length === 0) {
historyContainer.innerHTML = `<p class="text-xs text-slate-500 p-4 text-center">Keine ${modeName} Matches gefunden.</p>`;
} else {
data.matches.forEach(match => {
const isWin = match.result === "WIN";
const borderColor = isWin ? 'border-emerald-500' : 'border-rose-500';
const textColor = isWin ? 'text-emerald-400' : 'text-rose-400';
let rrDisplay = "";
let rrClass = "";
if (match.rr_change > 0) {
rrDisplay = `+${match.rr_change} RR`;
rrClass = "text-emerald-400 bg-emerald-950/40 border border-emerald-900/30";
} else if (match.rr_change < 0) {
rrDisplay = `${match.rr_change} RR`;
rrClass = "text-rose-400 bg-rose-950/40 border border-rose-900/30";
} else {
rrDisplay = "0 RR";
rrClass = "text-slate-400 bg-slate-900 border border-slate-800";
}
const matchHTML = `
<div class="bg-slate-900/60 border-l-4 ${borderColor} rounded-r-xl p-3.5 flex justify-between items-center text-xs sm:text-sm transition hover:bg-slate-900/90">
<div>
<span class="font-black ${textColor} tracking-wider mr-2">${match.result}</span>
<span class="font-bold text-slate-200">${match.rounds_won}:${match.rounds_lost}</span>
<span class="text-slate-400 text-xs block mt-0.5">${match.map} • <span class="text-purple-300">${match.agent}</span></span>
</div>
<div class="text-center">
<p class="text-[10px] text-slate-500 uppercase tracking-wider">K / D / A</p>
<p class="font-bold text-slate-200">${match.kills} / <span class="text-rose-400/80">${match.deaths}</span> / ${match.assists}</p>
<p class="text-[10px] text-purple-400 font-mono mt-0.5">Score: ${match.score}</p>
</div>
<div class="text-right flex flex-col items-end justify-center">
<span class="font-semibold text-xs px-2.5 py-1 rounded-md ${rrClass}">
${rrDisplay}
</span>
<p class="text-[9px] text-slate-500 block mt-1.5 font-medium">${match.date}</p>
</div>
</div>
`;
historyContainer.insertAdjacentHTML('beforeend', matchHTML);
});
}
const agentContainer = document.getElementById('agentContainer');
agentContainer.innerHTML = '';
if (data.top_agents.length === 0) {
agentContainer.innerHTML = '<p class="text-xs text-slate-500 text-center py-2">Keine Agenten-Daten.</p>';
} else {
data.top_agents.forEach(agent => {
agentContainer.insertAdjacentHTML('beforeend', `
<div class="flex justify-between items-center bg-slate-900/50 p-3 rounded-lg border border-purple-950/40">
<span class="text-xs font-bold text-purple-300">${agent.name}</span>
<span class="text-xs font-semibold text-slate-400 bg-slate-950 px-2 py-0.5 rounded">${agent.count}x gespielt</span>
</div>
`);
});
}
}
document.getElementById('searchForm').addEventListener('submit', async function(e) {
e.preventDefault();
const rawInput = document.getElementById('riotInput').value.trim();
if (!rawInput.includes('#')) {
alert("Bitte gib deine ID im Format 'Name#Tag' an (z.B. Drax#SWEAT).");
return;
}
const parts = rawInput.split('#');
const name = parts[0].trim();
const tag = parts[1].trim();
if (!name || !tag) {
alert("Name oder Tag unvollständig. Beispiel: Drax#SWEAT");
return;
}
currentPlayer = { name, tag };
const btn = this.querySelector('button[type="submit"]');
btn.innerText = "Analyzing...";
btn.disabled = true;
try {
const response = await fetch(`/api/v1/profile/${encodeURIComponent(name)}/${encodeURIComponent(tag)}?mode=competitive`);
const data = await response.json();
if (data.status === "success") {
document.getElementById('displayPlayerName').innerText = data.account.name;
document.getElementById('displayPlayerTag').innerText = `#${data.account.tag.toUpperCase()}`;
document.getElementById('displayLevel').innerText = data.account.level;
document.getElementById('displayRank').innerText = data.rank.current_tier_name;
document.getElementById('displayRR').innerText = `${data.rank.ranking_in_tier} RR`;
if (data.account.card_url) {
document.getElementById('playerCard').style.backgroundImage = `url('${data.account.card_url}')`;
document.getElementById('playerCard').style.backgroundSize = 'cover';
document.getElementById('playerCard').innerText = '';
}
document.getElementById('modeFilter').value = 'competitive';
updateMatchDisplay(data, 'competitive');
document.getElementById('searchSection').classList.add('hidden');
document.getElementById('profileSection').classList.remove('hidden');
} else {
alert("Fehler: " + data.message);
}
} catch (error) {
alert("API-Fehler oder Verbindungsabbruch: " + error.message);
} finally {
btn.innerText = "Analyse";
btn.disabled = false;
}
});
</script>
</body>
</html>

View file

@ -12,18 +12,18 @@
box-shadow: 0 0 20px rgba(192,132,252,0.3);
transform: translateY(-4px);
}
#system-status-text.small {
font-size: 2rem;
text-align: center;
margin: 0 auto;
display: block;
.quick-link {
padding: 0.75rem 1rem;
border-radius: 0.5rem;
background: rgba(139, 92, 246, 0.1);
border: 1px solid rgba(139, 92, 246, 0.3);
transition: all 0.2s ease;
text-decoration: none;
color: #e6e6e6;
}
#system-status-desc .wartung-desc {
font-size: 0.75rem;
display: block;
text-align: center;
margin: 0 auto;
font-weight: 500;
.quick-link:hover {
background: rgba(139, 92, 246, 0.2);
border-color: rgba(139, 92, 246, 0.6);
}
</style>
{% endblock %}
@ -36,28 +36,14 @@
<div class="grid grid-cols-1 gap-8">
<div class="glow-card opacity-0 translate-y-8 lg:col-span-2 bg-black/40 p-6 rounded-xl backdrop-blur
{% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<h3 class="text-2xl font-semibold mb-4">🚀 Willkommen bei AstraOS!</h3>
<h3 class="text-2xl font-semibold mb-4">🚀 Willkommen bei AstraOS</h3>
<p class="text-gray-300">
AstraOS ist da! Wir haben alles von Grund auf neu gebaut, damit du deine Systeme noch intuitiver und effizienter verwalten kannst. Wir hoffen, dir gefallen das neue Design und die vielen praktischen Funktionen.
Schön, dass du da bist. Deine Systeme sind einsatzbereit.
</p>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="glow-card opacity-0 translate-y-8 bg-black/40 rounded-xl p-6 backdrop-blur
{% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<h3 class="text-xl font-semibold mb-2 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Aktive Benutzer</h3>
<p class="text-4xl font-bold" id="active-users">{{ stats.active_users.value }}</p>
<p class="text-green-400 text-sm mt-1" id="active-users-percent">
{% set pct = stats.active_users.percent %}
{% if pct is not none %}
{{ '+' if pct>=0 else '' }}{{ pct }}% seit letzter Woche
{% else %}
-
{% endif %}
</p>
</div>
<div id="server-load-card" class="glow-card opacity-0 translate-y-8 bg-black/40 border border-red-500/40 rounded-xl p-6 backdrop-blur">
<h3 class="text-xl font-semibold mb-2 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Server-Auslastung</h3>
<p class="text-4xl font-bold" id="server-load">{{ stats.server_load.percent }}%</p>
@ -66,28 +52,14 @@
<div class="glow-card opacity-0 translate-y-8 bg-black/40 rounded-xl p-6 backdrop-blur
{% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<h3 class="text-xl font-semibold mb-2 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Offene Tickets</h3>
<p class="text-4xl font-bold" id="open-tickets">{{ stats.open_tickets.count }}</p>
<p class="text-gray-400 text-sm mt-1">Keine offenen Anfragen</p>
</div>
<div id="system-status-card" class="glow-card opacity-0 translate-y-8 bg-black/40 border border-green-500/40 rounded-xl p-6 backdrop-blur">
<h3 class="text-xl font-semibold mb-2 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Systemstatus</h3>
{% set status = stats.system_status.status %}
<p id="system-status-text"
class="text-4xl font-bold
{% if 'online' in status.lower() %}text-green-400
{% elif 'wartung' in status.lower() or 'systeme im wartungsmodus' in status.lower() %}text-yellow-400 small
{% else %}text-red-400{% endif %}">
{{ status }}
</p>
<p class="text-gray-400 text-sm mt-1" id="system-status-desc">
{% if 'wartung' in status.lower() or 'systeme im wartungsmodus' in status.lower() %}
<span class="wartung-desc">Systeme im Wartungsmodus</span>
{% elif 'online' in status.lower() %}
Alle Systeme funktionsfähig
<h3 class="text-xl font-semibold mb-2 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Aktive Benutzer</h3>
<p class="text-4xl font-bold" id="active-users">{{ stats.active_users.value }}</p>
<p class="text-green-400 text-sm mt-1">
{% set pct = stats.active_users.percent %}
{% if pct is not none %}
{{ '+' if pct>=0 else '' }}{{ pct }}% vs. letzte Woche
{% else %}
Teilweise oder komplett offline
-
{% endif %}
</p>
</div>
@ -101,21 +73,31 @@
<p class="text-yellow-400 text-sm mt-1">{{ maint.status if maint.status else 'Wartungsarbeiten laufen' }}</p>
{% elif maint and maint.time_str %}
<p class="text-2xl font-bold">{{ maint.time_str }}</p>
<p class="text-yellow-400 text-sm mt-1">{{ maint.status if maint and maint.status else 'Geplant' }} {% if maint and maint.duration_minutes %}— Dauer: {{ maint.duration_minutes }} Min.{% endif %}</p>
<p class="text-yellow-400 text-sm mt-1">{{ maint.status if maint and maint.status else 'Geplant' }} {% if maint and maint.duration_minutes %}— {{ maint.duration_minutes }} Min.{% endif %}</p>
{% else %}
<p class="text-2xl font-bold">Keine geplant</p>
<p class="text-gray-400 text-sm mt-1">Alle Systeme sind betriebsbereit.</p>
<p class="text-gray-400 text-sm mt-1">Alles läuft normal.</p>
{% endif %}
</div>
</div>
<div class="grid grid-cols-1 gap-8 mt-8">
<div class="glow-card opacity-0 translate-y-8 bg-black/40 rounded-xl p-6 backdrop-blur
{% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<h3 class="text-xl font-semibold mb-2 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">Letzte Benutzererstellung</h3>
{% set last = stats.last_user_created %}
<p class="text-2xl font-bold" id="last-user-time">{{ last.time_str if last and last.time_str else 'Keine Daten' }}</p>
<p class="text-gray-400 text-sm mt-1" id="last-user-by">Erstellter Benutzer: {{ last.by if last and last.by else 'N/A' }}</p>
<h3 class="text-xl font-semibold mb-4 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">⚡ Quick Access</h3>
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3">
{% for tool in available_tools %}
<a href="{{ url_for(tool.url) }}" class="quick-link text-center">
<div class="text-sm font-medium">{{ tool.label }}</div>
</a>
{% endfor %}
</div>
{% if not available_tools %}
<div class="text-gray-500 text-sm italic">Keine verfügbaren Tools für dich.</div>
{% endif %}
</div>
</div>
<script>
gsap.registerPlugin(ScrollTrigger);
@ -126,7 +108,6 @@ setTimeout(() => {
tl.to(".glow-card", { opacity: 1, y: 0, stagger: 0.08 });
// Header Parallax
gsap.to("header", {
y: 8,
ease: "none",
@ -157,22 +138,18 @@ function updateServerLoad() {
document.getElementById('server-load').textContent = data.percent + '%';
loadStatus.textContent = data.status;
// Klassen für Farben entfernen
loadCard.classList.remove('border-red-500/40', 'border-green-500/40', 'border-yellow-500/40');
loadStatus.classList.remove('text-red-400', 'text-green-400', 'text-yellow-400');
if (data.status === 'Online') {
if (data.percent > 75) {
// Gelb bei hoher Auslastung
loadCard.classList.add('border-yellow-500/40');
loadStatus.classList.add('text-yellow-400');
} else {
// Grün bei normaler Auslastung
loadCard.classList.add('border-green-500/40');
loadStatus.classList.add('text-green-400');
}
} else {
// Rot bei Fehler/Offline
loadCard.classList.add('border-red-500/40');
loadStatus.classList.add('text-red-400');
}
@ -180,24 +157,5 @@ function updateServerLoad() {
}
setInterval(updateServerLoad, 5000);
updateServerLoad();
function updateSystemStatusCard() {
const card = document.getElementById('system-status-card');
const statusText = document.getElementById('system-status-text');
if (!card || !statusText) return;
card.classList.remove('border-green-500/40', 'border-yellow-500/40', 'border-red-500/40');
statusText.classList.remove('small');
const status = statusText.textContent.toLowerCase();
if (status.includes('wartung') || status.includes('systeme im wartungsmodus')) {
card.classList.add('border-yellow-500/40');
statusText.classList.add('small');
} else if (status.includes('online')) {
card.classList.add('border-green-500/40');
} else {
card.classList.add('border-red-500/40');
}
}
setTimeout(updateSystemStatusCard, 100);
setInterval(updateSystemStatusCard, 2000);
</script>
{% endblock %}

View file

@ -91,7 +91,8 @@ try:
"instranet",
"view_ip_analyzer",
"view_age_verification",
"manage_age_verification"
"manage_age_verification",
"view_ayran_tracker"
]
if roles_collection.count_documents({}) == 0:
print("WARNUNG: Keine Rollen in der Datenbank gefunden. Erstelle Standard-Rollen...")
@ -616,21 +617,34 @@ def dashboard():
return v.strftime("%d. %b %Y, %H:%M")
except Exception:
return str(v)
maint = scheduled_maintenance or {"time": None, "status": "Keine", "duration_minutes": None}
maint["time_str"] = _dt_to_str(maint.get("time"))
stats = {
"active_users": {"value": user_count, "percent": 0},
"server_load": server_load,
"open_tickets": {"count": open_tickets},
"system_status": system_status,
"scheduled_maintenance": maint,
"last_user_created": {
"time": last_user_time,
"by": last_user_by,
"time_str": _dt_to_str(last_user_time)
}
}
return render_template('dashboard.html', stats=stats, active_page='dashboard')
maint = scheduled_maintenance or {"time": None, "status": "Keine", "duration_minutes": None}
maint["time_str"] = _dt_to_str(maint.get("time"))
user_perms = session.get('permissions', [])
all_tools = [
{"label": "👤 User Mgmt", "url": "user_management", "perm": "view_user_management"},
{"label": "🤖 Bots", "url": "discord_bots", "perm": "manage_discord_bots"},
{"label": "🚚 ETS2 DLCs", "url": "ets2_dlcs", "perm": "view_ets2_dlcs"},
{"label": "🌐 IP Analyzer", "url": "ip_analyzer", "perm": "view_ip_analyzer"},
{"label": "📜 Logs", "url": "logs", "perm": "view_logs"},
{"label": "🎂 Age Verify", "url": "age_verification", "perm": "view_age_verification"},
{"label": "🥛 Ayran Tracker", "url": "ayran_tracker", "perm": "view_ayran_tracker"},
]
available_tools = [t for t in all_tools if t["perm"] in user_perms]
stats = {
"active_users": {"value": user_count, "percent": 0},
"server_load": server_load,
"open_tickets": {"count": open_tickets},
"system_status": system_status,
"scheduled_maintenance": maint,
"last_user_created": {
"time": last_user_time,
"by": last_user_by,
"time_str": _dt_to_str(last_user_time)
}
}
return render_template('dashboard.html', stats=stats, active_page='dashboard', available_tools=available_tools)
@app.route('/logout')
@login_required
def logout():
@ -1513,9 +1527,159 @@ def update_maintenance():
{"$set": update_data},
upsert=True
)
add_log("Wartungseinstellungen aktualisiert", f"Aktiviert: {is_enabled}, Nachricht: {status_message}")
flash("Wartungseinstellungen wurden aktualisiert.", "success")
return redirect(url_for('maintenance_management'))
add_log("Wartungseinstellungen aktualisiert", f"Aktiviert: {is_enabled}, Nachricht: {status_message}")
flash("Wartungseinstellungen wurden aktualisiert.", "success")
return redirect(url_for('maintenance_management'))
@app.route('/ayran_tracker')
@login_required
@permission_required('view_ayran_tracker')
def ayran_tracker():
return render_template('ayran_tracker.html', active_page='ayran_tracker')
@app.route('/api/v1/profile/<name>/<tag>', methods=['GET'])
@login_required
@permission_required('view_ayran_tracker')
def get_valorant_stats(name, tag):
VALORANT_API_KEY = os.environ.get('VALORANT_API_KEY')
VALORANT_API_BASE = "https://api.henrikdev.xyz/valorant"
mode = request.args.get('mode', 'competitive').lower()
if not VALORANT_API_KEY:
return jsonify({"status": "error", "message": "API-Schlüssel nicht konfiguriert"}), 500
headers = {"Authorization": VALORANT_API_KEY}
try:
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({"status": "error", "message": "Spieler nicht gefunden oder API-Limit erreicht."}), acc_res.status_code
acc_data = acc_res.json().get('data', {}) or {}
region = acc_data.get("region", "eu")
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={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 = []
mmr_hist_res = requests.get(f"{VALORANT_API_BASE}/v1/mmr-history/{region}/{name}/{tag}", headers=headers, timeout=10)
mmr_history = mmr_hist_res.json().get('data', []) if mmr_hist_res.status_code == 200 else []
if not mmr_history: mmr_history = []
mmr_history_dict = {}
for entry in mmr_history:
m_id = entry.get("match_id")
if m_id:
mmr_history_dict[m_id] = entry
total_kills = 0
total_deaths = 0
wins = 0
total_damage = 0
total_rounds = 0
agent_counts = {}
processed_matches = []
for match in matches_list:
if not match: continue
metadata = match.get('metadata', {}) or {}
match_id = metadata.get('matchid')
players = match.get('players', {}).get('all_players', []) or []
teams = match.get('teams', {}) or {}
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
stats = me.get('stats', {}) or {}
kills = stats.get('kills', 0) or 0
deaths = stats.get('deaths', 0) or 0
assists = stats.get('assists', 0) or 0
score = stats.get('score', 0) or 0
damage = stats.get('damage', 0) or 0
total_kills += kills
total_deaths += deaths
total_damage += damage
agent_name = me.get('character', 'Unknown') or 'Unknown'
agent_counts[agent_name] = agent_counts.get(agent_name, 0) + 1
my_team = (me.get('team', '') or '').lower()
rounds_won = teams.get(my_team, {}).get('rounds_won', 0) if teams.get(my_team) else 0
rounds_won = rounds_won or 0
enemy_team = 'red' if my_team == 'blue' else 'blue'
rounds_lost = teams.get(enemy_team, {}).get('rounds_won', 0) if teams.get(enemy_team) else 0
rounds_lost = rounds_lost or 0
total_rounds += (rounds_won + rounds_lost)
is_win = rounds_won > rounds_lost
if is_win:
wins += 1
start_time = metadata.get('game_start', 0) or 0
date_str = datetime.fromtimestamp(start_time).strftime('%d.%m.%Y') if start_time else "Unbekannt"
rr_change = 0
if match_id and match_id in mmr_history_dict:
rr_change = mmr_history_dict[match_id].get('mmr_change_to_last_game', 0) or 0
processed_matches.append({
"map": metadata.get('map', 'Unknown') or 'Unknown',
"mode": metadata.get('mode', 'Competitive') or 'Competitive',
"date": date_str,
"result": "WIN" if is_win else "LOSS",
"rounds_won": rounds_won,
"rounds_lost": rounds_lost,
"kills": kills,
"deaths": deaths,
"assists": assists,
"score": score,
"agent": agent_name,
"rr_change": rr_change
})
match_count = len(processed_matches)
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
sorted_agents = sorted(agent_counts.items(), key=lambda x: x[1], reverse=True)
top_agents = [{"name": a[0], "count": a[1]} for a in sorted_agents[:2]]
add_log("Ayran Tracker verwendet", f"Spieler: {name}#{tag}, Mode: {mode}")
return jsonify({
"status": "success",
"account": {
"name": acc_data.get('name'),
"tag": acc_data.get('tag'),
"level": acc_data.get('account_level'),
"card_url": acc_data.get('card', {}).get('small') if acc_data.get('card') else None
},
"rank": {
"current_tier_name": mmr_data.get('currenttierpatched', 'Unranked') or 'Unranked',
"ranking_in_tier": mmr_data.get('ranking_in_tier', 0) or 0,
},
"calculated_stats": {
"win_rate": f"{winrate_calculated}%",
"kd_ratio": str(kd_calculated),
"avg_damage_round": str(adr_calculated),
"matches_analyzed": match_count
},
"top_agents": top_agents,
"matches": processed_matches
})
except Exception as e:
return jsonify({"status": "error", "message": f"Fehler bei der Berechnung: {str(e)}"}), 500
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404