aether-framework/resources/[aether]/aether-loading/html/index.html
Jerrit Fritzsche e4b82dd685 Aether-Framework: Fachmodule, Admin-Panel und Docker-Datenbank
- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark,
  Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI)
- aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource,
  an aether-core gekoppelt (Rechte über aether_users.admin_group), mit
  WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU)
- Prototypen admin/ vervollständigt
- SQL-Schema aether_admin.sql
- Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:55:32 +02:00

381 lines
20 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<title>Aether — Ladebildschirm</title>
<style>
:root{
--bg:#07070c; /* Grundhintergrund (sehr dunkel) */
--fg:#ece9f6; /* Standard-Textfarbe */
--muted:#8b86a6; /* gedämpfter Text (Labels/Sekundär) */
--surface:rgba(24,22,38,0.72); /* Panel-Hintergrund */
--surface2:rgba(38,34,60,0.85); /* hellere Panel-Fläche */
--border:rgba(255,255,255,0.08); /* dezente Rahmen */
--accent:#7c5cf6; /* Aether-Akzent (Violett) */
--accent2:#5b8cff; /* zweiter Akzent (Blau) für Verläufe */
--accent-soft:rgba(124,92,246,0.16); /* Akzent als Fläche */
--good:#34d39a; /* Erfolg/Bereit */
--ink:#160c2e; /* Text auf hellem Akzent */
}
*{box-sizing:border-box;}
html,body{margin:0;height:100%;}
body{
height:100%;background:var(--bg);color:var(--fg);overflow:hidden;
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
}
/* Vollflächiger Partikel-/Aurora-Hintergrund */
#bg{position:fixed;inset:0;z-index:0;}
.aurora{position:fixed;inset:-20% ;z-index:0;filter:blur(60px);opacity:0.55;pointer-events:none;
background:
radial-gradient(40% 40% at 20% 30%,rgba(124,92,246,0.55),transparent 60%),
radial-gradient(35% 45% at 80% 25%,rgba(91,140,255,0.45),transparent 60%),
radial-gradient(45% 40% at 60% 85%,rgba(124,92,246,0.35),transparent 60%);
animation:auroraMove 18s ease-in-out infinite alternate;}
@keyframes auroraMove{from{transform:translate3d(-2%,-1%,0) scale(1);}to{transform:translate3d(3%,2%,0) scale(1.08);}}
/* Dunkler Vignetten-Verlauf für Lesbarkeit über dem Hintergrund */
.vignette{position:fixed;inset:0;z-index:1;pointer-events:none;
background:radial-gradient(120% 90% at 50% 40%,transparent 30%,rgba(4,4,9,0.75) 100%);}
/* Zentrales Layout-Raster: Kopf · Mitte · Fuß */
.screen{position:relative;z-index:2;height:100%;display:grid;
grid-template-rows:auto 1fr auto;padding:34px clamp(24px,5vw,72px);gap:18px;}
/* Kopfzeile: Marke links, Server-Infos rechts */
.top{display:flex;align-items:center;justify-content:space-between;gap:20px;}
.brand{display:flex;align-items:center;gap:14px;}
.brand .mark{width:52px;height:52px;border-radius:15px;display:grid;place-items:center;font-size:26px;
background:linear-gradient(150deg,var(--accent),var(--accent2));box-shadow:0 12px 30px -8px rgba(124,92,246,0.6);}
.brand .name{font-size:24px;font-weight:800;letter-spacing:-0.02em;line-height:1;}
.brand .name span{background:linear-gradient(90deg,var(--accent),var(--accent2));-webkit-background-clip:text;background-clip:text;color:transparent;}
.brand .name small{display:block;font-size:11px;color:var(--muted);font-weight:600;margin-top:5px;letter-spacing:0.14em;text-transform:uppercase;}
.serverinfo{display:flex;gap:10px;}
.stat{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:9px 14px;text-align:right;backdrop-filter:blur(8px);}
.stat .l{font-size:9.5px;color:var(--muted);text-transform:uppercase;letter-spacing:0.08em;font-weight:800;}
.stat .v{font-size:16px;font-weight:800;font-variant-numeric:tabular-nums;}
.stat .v .on{color:var(--good);}
/* Mittelbereich: großer Titel + rotierendes Info-Karussell */
.middle{display:grid;grid-template-columns:1.1fr 0.9fr;align-items:center;gap:40px;}
.hero .kicker{color:var(--accent);font-weight:800;letter-spacing:0.18em;text-transform:uppercase;font-size:12px;margin-bottom:14px;}
.hero h1{font-size:clamp(46px,7vw,86px);margin:0;line-height:0.95;font-weight:900;letter-spacing:-0.03em;
background:linear-gradient(120deg,#fff 20%,var(--accent) 60%,var(--accent2));-webkit-background-clip:text;background-clip:text;color:transparent;}
.hero .sub{color:var(--muted);font-size:16px;margin-top:16px;max-width:460px;line-height:1.5;}
/* Karussell für News / Regeln / Tipps */
.carousel{background:var(--surface);border:1px solid var(--border);border-radius:20px;padding:24px;backdrop-filter:blur(10px);min-height:196px;
box-shadow:0 30px 60px -30px rgba(0,0,0,0.8);display:flex;flex-direction:column;}
.carousel .tag{display:inline-flex;align-items:center;gap:7px;font-size:10.5px;font-weight:800;text-transform:uppercase;letter-spacing:0.06em;
color:var(--accent);background:var(--accent-soft);padding:5px 11px;border-radius:999px;align-self:flex-start;margin-bottom:14px;}
.carousel h3{margin:0 0 8px;font-size:20px;font-weight:800;letter-spacing:-0.01em;}
.carousel p{margin:0;color:var(--muted);font-size:14px;line-height:1.55;}
.carousel .slide{animation:fadeUp .5s ease;flex:1;}
@keyframes fadeUp{from{opacity:0;transform:translateY(10px);}to{opacity:1;transform:none;}}
.dots{display:flex;gap:7px;margin-top:16px;}
.dots i{width:7px;height:7px;border-radius:50%;background:var(--border);transition:.25s;cursor:pointer;}
.dots i.active{background:var(--accent);width:22px;border-radius:4px;}
/* Fußbereich: Ladefortschritt + Musik-Player */
.bottom{display:flex;flex-direction:column;gap:14px;}
.loadrow{display:flex;align-items:center;gap:16px;}
.loadinfo{flex:1;min-width:0;}
.loadhead{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:9px;}
.loadhead .status{font-size:13.5px;font-weight:600;color:var(--fg);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.loadhead .status .file{color:var(--muted);font-weight:500;}
.loadhead .pct{font-size:15px;font-weight:800;font-variant-numeric:tabular-nums;color:var(--accent);}
.bar{height:8px;border-radius:99px;background:rgba(255,255,255,0.07);overflow:hidden;position:relative;}
.bar .fill{height:100%;width:0;border-radius:99px;background:linear-gradient(90deg,var(--accent),var(--accent2));
box-shadow:0 0 16px rgba(124,92,246,0.7);transition:width .35s cubic-bezier(.4,0,.2,1);position:relative;}
/* Wandernder Glanz auf dem Balken */
.bar .fill::after{content:"";position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,0.4),transparent);
transform:translateX(-100%);animation:sheen 1.6s linear infinite;}
@keyframes sheen{to{transform:translateX(100%);}}
/* Musik-Player (WebAudio, ohne externe Datei) */
.player{display:flex;align-items:center;gap:12px;background:var(--surface);border:1px solid var(--border);
border-radius:14px;padding:9px 12px 9px 9px;backdrop-filter:blur(8px);min-width:270px;}
.player .cover{width:40px;height:40px;border-radius:10px;flex:none;display:grid;place-items:center;font-size:19px;
background:linear-gradient(150deg,var(--accent),var(--accent2));position:relative;overflow:hidden;}
.player .cover.playing::before{content:"";position:absolute;inset:0;background:radial-gradient(circle,rgba(255,255,255,0.35),transparent 70%);animation:pulse 1.4s ease-in-out infinite;}
@keyframes pulse{50%{opacity:0.4;}}
.player .meta{min-width:0;flex:1;}
.player .meta .t{font-size:12.5px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.player .meta .a{font-size:10.5px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.pbtn{width:34px;height:34px;flex:none;border-radius:50%;border:1px solid var(--border);background:var(--surface2);color:var(--fg);
cursor:pointer;display:grid;place-items:center;font-size:14px;transition:.12s;}
.pbtn:hover{border-color:var(--accent);color:var(--accent);}
.pbtn.play{background:var(--accent);color:var(--ink);border-color:transparent;}
.vol{width:66px;accent-color:var(--accent);cursor:pointer;}
/* „Bereit"-Zustand, wenn das Laden 100 % erreicht */
.ready-glow{animation:readyPulse 1.6s ease-in-out infinite;}
@keyframes readyPulse{50%{text-shadow:0 0 22px rgba(52,211,153,0.7);}}
/* Kompakteres Layout auf schmalen Fenstern */
@media (max-width:900px){
.middle{grid-template-columns:1fr;gap:22px;}
.serverinfo .stat:nth-child(3){display:none;}
.loadrow{flex-direction:column;align-items:stretch;}
.player{min-width:0;}
}
</style>
</head>
<body>
<canvas id="bg"></canvas>
<div class="aurora"></div>
<div class="vignette"></div>
<div class="screen">
<!-- Kopfzeile: Marke + Server-Statistiken -->
<header class="top">
<div class="brand">
<span class="mark">◆</span>
<div class="name"><span>AETHER</span><small>Los Santos Roleplay</small></div>
</div>
<div class="serverinfo" id="serverinfo"></div>
</header>
<!-- Mittelbereich: Titel + rotierendes Karussell -->
<main class="middle">
<div class="hero">
<div class="kicker">Willkommen zurück</div>
<h1>Deine<br>Geschichte<br>beginnt.</h1>
<p class="sub">Steig ein in das Leben von Los Santos — dein Charakter, deine Entscheidungen, deine Regeln. Bitte warte, während die Welt geladen wird.</p>
</div>
<div class="carousel" id="carousel"></div>
</main>
<!-- Fußbereich: Fortschrittsbalken + Musik-Player -->
<footer class="bottom">
<div class="loadrow">
<div class="loadinfo">
<div class="loadhead">
<div class="status" id="status">Verbinde mit dem Server…</div>
<div class="pct" id="pct">0%</div>
</div>
<div class="bar"><div class="fill" id="fill"></div></div>
</div>
<div class="player" id="player">
<div class="cover" id="cover">♪</div>
<div class="meta">
<div class="t" id="trackTitle">Aether Ambient</div>
<div class="a" id="trackArtist">Lobby · Loop</div>
</div>
<button class="pbtn play" id="playBtn" title="Wiedergabe / Pause">▶</button>
<button class="pbtn" id="nextBtn" title="Nächster Titel">⏭</button>
<input class="vol" id="vol" type="range" min="0" max="100" value="45" title="Lautstärke">
</div>
</div>
</footer>
</div>
<script>
"use strict";
/* Kurzer Zugriffshelfer auf Elemente per ID */
const $=id=>document.getElementById(id);
/* =====================================================================
SERVER-DATEN (im echten Betrieb vom Server geliefert / per NUI gesetzt)
Bewusst vom UI getrennt gehalten.
===================================================================== */
const server={
spielerAktuell:184, // aktuell verbundene Spieler
spielerMax:256, // Slot-Limit des Servers
voiceKanal:"TeamSpeak/PMA-Voice", // genutztes Voice-System
discord:"discord.gg/aether", // Community-Link (nur Anzeige)
};
/* Karussell-Inhalte: Nachrichten, Regeln und Spieltipps im Wechsel */
const karussell=[
{tag:"📰 News", titel:"Neues Immobilien-Update", text:"Ab sofort lassen sich Wohnungen möbeln und Hausschlüssel beim Schlüsseldienst nachmachen. Schau im Markt (AetherImmos) vorbei."},
{tag:"📜 Regel", titel:"Kein RDM / VDM", text:"Töten oder Anfahren ohne Rollenspiel-Grund ist verboten. Jede Gewalt braucht eine nachvollziehbare Vorgeschichte."},
{tag:"💡 Tipp", titel:"Ausweis dabeihaben", text:"Öffne deine Börse und zeig Behörden bei Kontrollen deinen Personalausweis — ohne Papiere wird's teuer."},
{tag:"📜 Regel", titel:"Bleib in der Rolle", text:"Metagaming und Powergaming sind untersagt. Wissen aus Discord oder Stream gehört nicht in die Spielwelt."},
{tag:"💡 Tipp", titel:"Jobs bringen Startgeld", text:"Im Jobcenter findest du 8 legale Jobs. Mit steigendem Level steigt auch dein Lohn — ideal für den Einstieg."},
{tag:"📰 News", titel:"Aether Casino eröffnet", text:"Slots, Blackjack, Roulette, Glücksrad & Münzwurf — teste dein Glück, aber spiel mit Verantwortung."},
];
/* Musik-Titel für den Player (synthetisch erzeugt, ohne externe Datei) */
const playlist=[
{titel:"Aether Ambient", artist:"Lobby · Loop", akkord:[220.00,277.18,329.63]}, // A-Dur-artiges Pad
{titel:"Neon Nights", artist:"Downtown · Loop", akkord:[196.00,246.94,293.66]}, // G-Dur
{titel:"Midnight Drive", artist:"Vinewood · Loop", akkord:[174.61,220.00,261.63]}, // F-Dur
];
/* =====================================================================
SERVER-INFO & KARUSSELL RENDERN
===================================================================== */
/* Baut die Statistik-Kacheln in der Kopfzeile auf */
function renderServerInfo(){
$("serverinfo").innerHTML=
`<div class="stat"><div class="l">Spieler online</div><div class="v"><span class="on">${server.spielerAktuell}</span> / ${server.spielerMax}</div></div>
<div class="stat"><div class="l">Voice</div><div class="v" style="font-size:12.5px">${server.voiceKanal}</div></div>
<div class="stat"><div class="l">Discord</div><div class="v" style="font-size:12.5px">${server.discord}</div></div>`;
}
let slideIndex=0; // aktuell sichtbare Karussell-Folie
let slideTimer=null; // Timer für den automatischen Wechsel
/* Zeigt eine bestimmte Karussell-Folie und baut die Punkt-Navigation */
function zeigeSlide(i){
slideIndex=(i+karussell.length)%karussell.length;
const k=karussell[slideIndex];
$("carousel").innerHTML=
`<div class="slide"><span class="tag">${k.tag}</span><h3>${k.titel}</h3><p>${k.text}</p></div>
<div class="dots">${karussell.map((_,j)=>`<i class="${j===slideIndex?'active':''}" data-dot="${j}"></i>`).join('')}</div>`;
// Klick auf einen Punkt springt zur jeweiligen Folie
$("carousel").querySelectorAll("[data-dot]").forEach(d=>d.addEventListener("click",()=>{starteSlideAuto(+d.dataset.dot);}));
}
/* Startet den automatischen Folienwechsel (alle 5 s) ab einem Index */
function starteSlideAuto(i){
zeigeSlide(i);
clearInterval(slideTimer);
slideTimer=setInterval(()=>zeigeSlide(slideIndex+1),5000);
}
/* =====================================================================
LADEFORTSCHRITT (Simulation; in FiveM von echten Lade-Events getrieben)
===================================================================== */
/* Ressourcen-Namen, die als "wird geladen"-Status durchlaufen */
const ressourcen=["aether-core","aether-phone","aether-inventory","aether-bank","aether-hud",
"aether-character","aether-garage","aether-jobs","aether-police","aether-ems",
"aether-clothing","aether-immobilien","aether-documents","aether-shop","aether-admin","map-lospm"];
let fortschritt=0; // aktueller Fortschritt in Prozent (0–100)
let resIndex=0; // Index der gerade "geladenen" Ressource
/* Erhöht den Fortschritt schrittweise und aktualisiert Balken + Status */
function ladeSchritt(){
// Zufällige, gegen Ende kleinere Sprünge für einen realistischen Verlauf
const rest=100-fortschritt;
const zuwachs=Math.max(0.6,Math.min(rest,(Math.random()*9+2)*(rest/100)+0.6));
fortschritt=Math.min(100,fortschritt+zuwachs);
$("fill").style.width=fortschritt+"%";
$("pct").textContent=Math.round(fortschritt)+"%";
// Status-Text an die aktuelle Ressource koppeln
const zielIndex=Math.min(ressourcen.length-1,Math.floor(fortschritt/100*ressourcen.length));
if(zielIndex!==resIndex){resIndex=zielIndex;
$("status").innerHTML=`Lade Ressource <span class="file">${ressourcen[resIndex]}</span>`;}
if(fortschritt>=100){ladenFertig();return;}
// Nächster Schritt nach zufälliger kurzer Pause
setTimeout(ladeSchritt,Math.random()*260+120);
}
/* Wird aufgerufen, sobald der Fortschritt 100 % erreicht */
function ladenFertig(){
$("status").innerHTML=`<span class="ready-glow" style="color:var(--good);font-weight:800">✓ Bereit — willkommen in Los Santos</span>`;
$("pct").style.color="var(--good)";
$("fill").style.background="linear-gradient(90deg,var(--good),var(--accent2))";
}
/* =====================================================================
MUSIK-PLAYER (WebAudio — sanftes Ambient-Pad, kein externes Audio)
===================================================================== */
let audioCtx=null; // Web-Audio-Kontext (erst bei Nutzer-Interaktion erlaubt)
let masterGain=null; // Haupt-Lautstärke-Regler
let aktiveOsz=[]; // aktuell klingende Oszillatoren des Akkords
let spielt=false; // läuft die Wiedergabe gerade?
let trackIndex=0; // aktueller Titel aus der Playlist
/* Erzeugt für den aktuellen Titel ein weiches Akkord-Pad */
function starteTon(){
if(!audioCtx){
audioCtx=new (window.AudioContext||window.webkitAudioContext)();
masterGain=audioCtx.createGain();
masterGain.gain.value=(+$("vol").value/100)*0.18; // insgesamt dezent halten
masterGain.connect(audioCtx.destination);
}
stoppeOsz();
// Für jede Frequenz des Akkords einen sanften Sinus-Oszillator anlegen
playlist[trackIndex].akkord.forEach((freq,i)=>{
const osz=audioCtx.createOscillator();
const g=audioCtx.createGain();
osz.type="sine";
osz.frequency.value=freq;
// Leichtes Verstimmen für einen volleren, wärmeren Klang
osz.detune.value=(i-1)*4;
g.gain.value=0;
osz.connect(g); g.connect(masterGain);
osz.start();
// Weich einblenden
g.gain.linearRampToValueAtTime(0.33,audioCtx.currentTime+1.2);
// Langsames Schweben der Lautstärke (LFO) für lebendiges Pad
const lfo=audioCtx.createOscillator(), lfoG=audioCtx.createGain();
lfo.frequency.value=0.06+i*0.02; lfoG.gain.value=0.12;
lfo.connect(lfoG); lfoG.connect(g.gain); lfo.start();
aktiveOsz.push(osz,lfo);
});
}
/* Stoppt und verwirft alle laufenden Oszillatoren */
function stoppeOsz(){aktiveOsz.forEach(o=>{try{o.stop();}catch(e){}});aktiveOsz=[];}
/* Schaltet zwischen Wiedergabe und Pause um */
function togglePlay(){
spielt=!spielt;
if(spielt){starteTon();$("playBtn").textContent="⏸";$("cover").classList.add("playing");}
else {stoppeOsz();$("playBtn").textContent="▶";$("cover").classList.remove("playing");}
}
/* Wechselt zum nächsten Titel der Playlist */
function naechsterTitel(){
trackIndex=(trackIndex+1)%playlist.length;
$("trackTitle").textContent=playlist[trackIndex].titel;
$("trackArtist").textContent=playlist[trackIndex].artist;
if(spielt)starteTon(); // bei laufender Wiedergabe direkt neu anspielen
}
/* Player-Bedienelemente verknüpfen */
$("playBtn").addEventListener("click",togglePlay);
$("nextBtn").addEventListener("click",naechsterTitel);
$("vol").addEventListener("input",()=>{if(masterGain)masterGain.gain.value=(+$("vol").value/100)*0.18;});
/* =====================================================================
START
===================================================================== */
renderServerInfo();
starteSlideAuto(0);
// Kurze Verzögerung, damit der leere Balken sichtbar ist, bevor er wächst
setTimeout(ladeSchritt,600);
/* Hintergrund-Partikel: langsam aufsteigende Lichtpunkte auf dem Canvas */
const cv=$("bg"), ctx=cv.getContext("2d");
let partikel=[]; // Liste aller Partikel mit Position/Geschwindigkeit
/* Passt die Canvas-Größe an das Fenster an und erzeugt neue Partikel */
function initCanvas(){
cv.width=innerWidth; cv.height=innerHeight;
partikel=Array.from({length:Math.round(innerWidth/22)},()=>neuerPartikel(true));
}
/* Erzeugt einen einzelnen Partikel (optional an zufälliger Höhe) */
function neuerPartikel(zufallY){
return {
x:Math.random()*cv.width,
y:zufallY?Math.random()*cv.height:cv.height+10,
r:Math.random()*1.8+0.4, // Radius
v:Math.random()*0.4+0.15, // Aufstiegsgeschwindigkeit
a:Math.random()*0.4+0.15, // Deckkraft
drift:(Math.random()-0.5)*0.3, // seitliches Driften
};
}
/* Zeichnet einen Animationsschritt aller Partikel */
function zeichne(){
ctx.clearRect(0,0,cv.width,cv.height);
for(const p of partikel){
p.y-=p.v; p.x+=p.drift;
if(p.y<-10)Object.assign(p,neuerPartikel(false)); // oben raus → unten neu
ctx.beginPath();
ctx.arc(p.x,p.y,p.r,0,Math.PI*2);
ctx.fillStyle=`rgba(150,130,255,${p.a})`;
ctx.fill();
}
requestAnimationFrame(zeichne);
}
addEventListener("resize",initCanvas);
initCanvas(); zeichne();
</script>
</body>
</html>