522 lines
15 KiB
JavaScript
522 lines
15 KiB
JavaScript
window.addEventListener("load", () => {
|
|
const params = new URLSearchParams(window.location.search);
|
|
if (params.get("lofi") === "true") {
|
|
zeigeYouTubeVideo();
|
|
}
|
|
});
|
|
|
|
const stripTags = (html) => (typeof html === "string" ? html.replace(/<[^>]*>/g, "") : "");
|
|
const formatBid = (value) => {
|
|
const num = Number(value);
|
|
if (Number.isNaN(num)) return "n/a";
|
|
const abs = Math.abs(num);
|
|
if (abs >= 100) return num.toFixed(2);
|
|
if (abs >= 10) return num.toFixed(3);
|
|
return num.toFixed(4);
|
|
};
|
|
const instrumentEmoji = (inst) => {
|
|
const map = {
|
|
"USD/JPY": "$/¥",
|
|
"OIL/USD": "🛢️",
|
|
"XAU/USD": "⛏️",
|
|
"USD/EUR": "€/$",
|
|
"USD/CHF": "$/CHF",
|
|
"USD/GBP": "£/$",
|
|
};
|
|
return map[inst] || inst;
|
|
};
|
|
|
|
const newsIndex = new Map();
|
|
const maxItems = 20;
|
|
|
|
async function ladeNews() {
|
|
const container = document.getElementById("news-container");
|
|
if (!container) return;
|
|
|
|
const loadingEl = null;
|
|
let renderedCount = 0;
|
|
const order = [];
|
|
|
|
const getKey = (nachricht) => {
|
|
if (nachricht?.id) return `id:${nachricht.id}`;
|
|
if (nachricht?.link) return `link:${nachricht.link}`;
|
|
if (nachricht?.title) return `title:${stripTags(nachricht.title)}`;
|
|
if (nachricht?.publishedAt) return `ts:${nachricht.publishedAt}`;
|
|
return `idx:${newsIndex.size}`;
|
|
};
|
|
|
|
const buildItem = (nachricht) => {
|
|
const item = document.createElement("div");
|
|
item.className = "news-item";
|
|
|
|
const img = document.createElement("img");
|
|
img.className = "news-thumb";
|
|
img.loading = "lazy";
|
|
|
|
const content = document.createElement("div");
|
|
content.className = "news-content";
|
|
|
|
const titleEl = document.createElement("div");
|
|
titleEl.className = "news-title";
|
|
|
|
const textEl = document.createElement("div");
|
|
textEl.className = "news-text";
|
|
|
|
const linkWrap = document.createElement("div");
|
|
linkWrap.className = "news-link";
|
|
const linkEl = document.createElement("a");
|
|
linkEl.target = "_blank";
|
|
linkEl.rel = "noopener noreferrer";
|
|
linkWrap.appendChild(linkEl);
|
|
|
|
content.appendChild(titleEl);
|
|
content.appendChild(textEl);
|
|
content.appendChild(linkWrap);
|
|
item.appendChild(content);
|
|
|
|
return { item, img, titleEl, textEl, linkEl };
|
|
};
|
|
|
|
const updateFields = (state, nachricht) => {
|
|
const title = stripTags(nachricht.title) || "Kein Titel";
|
|
const link = nachricht.link || "#";
|
|
const rawText = stripTags(nachricht.text) || "[Kein Text verfügbar]";
|
|
const text = rawText.length > 140 ? `${rawText.slice(0, 140)}...` : rawText;
|
|
|
|
if (title && state.title !== title) {
|
|
state.title = title;
|
|
state.titleEl.textContent = title;
|
|
}
|
|
if (text && state.text !== text) {
|
|
state.text = text;
|
|
state.textEl.textContent = text;
|
|
}
|
|
if (link && state.link !== link) {
|
|
state.link = link;
|
|
state.linkEl.href = link;
|
|
state.linkEl.textContent = "🔗 Zum Artikel";
|
|
}
|
|
|
|
const isDataImg = typeof nachricht.image === "string" && nachricht.image.startsWith("data:image");
|
|
if (isDataImg && state.image !== nachricht.image) {
|
|
state.image = nachricht.image;
|
|
state.img.src = nachricht.image;
|
|
state.img.alt = title || "Artikelbild";
|
|
if (!state.img.isConnected) {
|
|
state.item.prepend(state.img);
|
|
}
|
|
}
|
|
};
|
|
|
|
const renderItem = (nachricht) => {
|
|
const key = getKey(nachricht);
|
|
const existing = newsIndex.get(key);
|
|
if (existing) {
|
|
updateFields(existing, nachricht);
|
|
order.push(key);
|
|
return false;
|
|
}
|
|
|
|
const state = buildItem(nachricht);
|
|
updateFields(state, nachricht);
|
|
newsIndex.set(key, state);
|
|
container.appendChild(state.item);
|
|
order.push(key);
|
|
renderedCount += 1;
|
|
return true;
|
|
};
|
|
|
|
const applyOrder = () => {
|
|
const keep = new Set(order);
|
|
for (const [key, state] of newsIndex) {
|
|
if (!keep.has(key)) {
|
|
if (state.item.isConnected) state.item.remove();
|
|
newsIndex.delete(key);
|
|
}
|
|
}
|
|
for (const key of order) {
|
|
const state = newsIndex.get(key);
|
|
if (state) container.appendChild(state.item);
|
|
}
|
|
};
|
|
|
|
try {
|
|
const res = await fetch("/api/artikeltext", {
|
|
headers: { "Accept": "application/x-ndjson" },
|
|
cache: "no-store",
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const t = await res.text().catch(() => "");
|
|
throw new Error(`HTTP ${res.status}: ${t.slice(0, 200)}`);
|
|
}
|
|
if (!res.body) throw new Error("Streaming wird vom Browser nicht unterstützt.");
|
|
|
|
let foundAny = false;
|
|
|
|
const reader = res.body.getReader();
|
|
const decoder = new TextDecoder("utf-8");
|
|
let buffer = "";
|
|
|
|
let stop = false;
|
|
while (true) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
const lines = buffer.split("\n");
|
|
buffer = lines.pop() ?? "";
|
|
|
|
for (const line of lines) {
|
|
const s = line.trim();
|
|
if (!s) continue;
|
|
try {
|
|
const added = renderItem(JSON.parse(s));
|
|
if (added) foundAny = true;
|
|
if (renderedCount >= maxItems) {
|
|
stop = true;
|
|
break;
|
|
}
|
|
} catch (e) {
|
|
console.warn("NDJSON parse error:", s.slice(0, 200));
|
|
}
|
|
}
|
|
if (stop) break;
|
|
}
|
|
if (stop) {
|
|
await reader.cancel();
|
|
}
|
|
|
|
const last = buffer.trim();
|
|
if (last && renderedCount < maxItems) {
|
|
try {
|
|
const added = renderItem(JSON.parse(last));
|
|
if (added) foundAny = true;
|
|
} catch {}
|
|
}
|
|
|
|
applyOrder();
|
|
|
|
if (loadingEl && loadingEl.isConnected) loadingEl.remove();
|
|
if (!foundAny && !container.hasChildNodes()) {
|
|
container.innerHTML = '<div class="empty">Keine News gefunden.</div>';
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error("ladeNews Fehler:", error);
|
|
if (loadingEl && loadingEl.isConnected) loadingEl.remove();
|
|
if (!container.hasChildNodes()) {
|
|
container.innerHTML = '<div class="empty">Fehler beim Laden der News.</div>';
|
|
}
|
|
}
|
|
}
|
|
|
|
async function ladeRMV() {
|
|
try {
|
|
const response = await fetch("/api/rmv");
|
|
if (!response.ok) throw new Error("RMV-JSON nicht erreichbar");
|
|
const daten = await response.json();
|
|
|
|
const infoBox = document.getElementById("abfahrt-info");
|
|
const wrapper = document.getElementById("abfahrt-wrapper");
|
|
wrapper.style.display = "block";
|
|
|
|
if (!daten.abfahrten || daten.abfahrten.length === 0) {
|
|
infoBox.innerHTML = "<p>Keine Abfahrten gefunden.</p>";
|
|
return;
|
|
}
|
|
|
|
const tripHTML = daten.abfahrten.map(trip => {
|
|
const legsHTML = trip.map(leg => `
|
|
<div>
|
|
<strong>${leg.linie}</strong><br>
|
|
${leg.abfahrt} → ${leg.ankunft}<br>
|
|
${leg.von} → ${leg.nach}
|
|
</div>
|
|
`).join("<hr style='border-color: rgba(255,255,255,0.2);'>");
|
|
|
|
return `<div class="trip-block"><div class="abfahrt-eintrag">${legsHTML}</div></div>`;
|
|
}).join("");
|
|
|
|
infoBox.innerHTML = tripHTML;
|
|
|
|
} catch (error) {
|
|
console.warn("RMV-Info nicht geladen:", error);
|
|
document.getElementById("abfahrt-info").innerText = "Fehler beim Laden.";
|
|
}
|
|
}
|
|
|
|
function toggleMensaIframe() {
|
|
const container = document.getElementById("mensa-iframe-container");
|
|
container.style.display = (container.style.display === "none") ? "block" : "none";
|
|
}
|
|
|
|
function zeigeGongVideo() {
|
|
const overlay = document.createElement("div");
|
|
overlay.style.position = "fixed";
|
|
overlay.style.top = 0;
|
|
overlay.style.left = 0;
|
|
overlay.style.width = "100vw";
|
|
overlay.style.height = "100vh";
|
|
overlay.style.backgroundColor = "rgba(0, 0, 0, 0.9)";
|
|
overlay.style.display = "flex";
|
|
overlay.style.alignItems = "center";
|
|
overlay.style.justifyContent = "center";
|
|
overlay.style.zIndex = 9999;
|
|
|
|
const video = document.createElement("video");
|
|
video.src = "/media/gong";
|
|
video.autoplay = true;
|
|
video.controls = true;
|
|
video.style.maxWidth = "90%";
|
|
video.style.maxHeight = "90%";
|
|
|
|
overlay.appendChild(video);
|
|
document.body.appendChild(overlay);
|
|
|
|
video.onended = () => document.body.removeChild(overlay);
|
|
overlay.onclick = () => document.body.removeChild(overlay);
|
|
}
|
|
|
|
let fxCache = [];
|
|
let fxIndex = 0;
|
|
function rotiereFX() {
|
|
const el = document.getElementById("fx-rotator");
|
|
if (!el || fxCache.length === 0) return;
|
|
const item = fxCache[fxIndex % fxCache.length];
|
|
el.textContent = `${instrumentEmoji(item.instrument)} ${formatBid(item.bid)}`;
|
|
fxIndex++;
|
|
}
|
|
|
|
function renderMarketTicker(quotes) {
|
|
const track = document.getElementById("market-ticker-track");
|
|
const container = document.getElementById("market-ticker");
|
|
if (!track || !container) return;
|
|
|
|
track.innerHTML = "";
|
|
if (!quotes || quotes.length === 0) {
|
|
track.textContent = "Keine Marktdaten verfügbar";
|
|
track.style.animation = "none";
|
|
return;
|
|
}
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
const addItems = () => quotes.forEach(q => {
|
|
const span = document.createElement("span");
|
|
span.className = "ticker-item";
|
|
span.textContent = `${instrumentEmoji(q.instrument)} ${formatBid(q.bid)}`;
|
|
fragment.appendChild(span);
|
|
});
|
|
addItems();
|
|
addItems();
|
|
|
|
track.appendChild(fragment);
|
|
const duration = Math.max(18, Math.min(60, quotes.length * 4));
|
|
track.style.setProperty("--ticker-duration", `${duration}s`);
|
|
track.style.animation = "none";
|
|
void track.offsetWidth;
|
|
track.style.animation = `ticker-move var(--ticker-duration, ${duration}s) linear infinite`;
|
|
}
|
|
|
|
async function ladeMarketDaten() {
|
|
try {
|
|
const res = await fetch("/api/market", { cache: "no-store" });
|
|
if (!res.ok) throw new Error("Market API nicht verfügbar");
|
|
const data = await res.json();
|
|
if (!Array.isArray(data)) throw new Error("Ungültige Marktdaten");
|
|
fxCache = data;
|
|
rotiereFX();
|
|
renderMarketTicker(data);
|
|
} catch (err) {
|
|
console.warn("Marktdaten Fehler:", err);
|
|
const el = document.getElementById("fx-rotator");
|
|
if (el) el.textContent = "FX Fehler";
|
|
renderMarketTicker([]);
|
|
}
|
|
}
|
|
|
|
function zeigeYouTubeVideo() {
|
|
const overlay = document.createElement("div");
|
|
overlay.style.position = "fixed";
|
|
overlay.style.top = 0;
|
|
overlay.style.left = 0;
|
|
overlay.style.width = "100vw";
|
|
overlay.style.height = "100vh";
|
|
overlay.style.backgroundColor = "rgba(0, 0, 0, 0.9)";
|
|
overlay.style.display = "flex";
|
|
overlay.style.alignItems = "center";
|
|
overlay.style.justifyContent = "center";
|
|
overlay.style.zIndex = 9999;
|
|
|
|
const iframe = document.createElement("iframe");
|
|
const videoIds = [
|
|
"zhDwjnYZiCo",
|
|
"Na0w3Mz46GA",
|
|
"OO2kPK5-qno",
|
|
"Yqk13qPcXis",
|
|
"MZhivjxcF-M",
|
|
"uMEvzhckqBw",
|
|
"uMEvzhckqBw",
|
|
"TGan48YE9Us",
|
|
"-Xh4BNbxpI8",
|
|
"r7kxh_vuBpo",
|
|
"bdUbACCWmoY",
|
|
];
|
|
const zufallsId = videoIds[Math.floor(Math.random() * videoIds.length)];
|
|
iframe.src = `https://www.youtube.com/embed/${zufallsId}?autoplay=1`;
|
|
iframe.allow = "autoplay; fullscreen";
|
|
iframe.allowFullscreen = true;
|
|
iframe.style.width = "90%";
|
|
iframe.style.height = "90%";
|
|
iframe.style.border = "none";
|
|
|
|
overlay.appendChild(iframe);
|
|
overlay.onclick = () => document.body.removeChild(overlay);
|
|
document.body.appendChild(overlay);
|
|
|
|
if (overlay.requestFullscreen) {
|
|
overlay.requestFullscreen();
|
|
} else if (overlay.webkitRequestFullscreen) {
|
|
overlay.webkitRequestFullscreen();
|
|
} else if (overlay.msRequestFullscreen) {
|
|
overlay.msRequestFullscreen();
|
|
}
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
const hazard = document.getElementById("hazard-symbol");
|
|
if (hazard) {
|
|
hazard.addEventListener("click", () => {
|
|
const elem = document.documentElement;
|
|
if (elem.requestFullscreen) {
|
|
elem.requestFullscreen();
|
|
} else if (elem.webkitRequestFullscreen) {
|
|
elem.webkitRequestFullscreen();
|
|
} else if (elem.msRequestFullscreen) {
|
|
elem.msRequestFullscreen();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
let currentDefcon = 5;
|
|
let letzteDefconÄnderung = Date.now();
|
|
|
|
function aktualisiereDefconButton(level) {
|
|
const btn = document.getElementById("defcon-button");
|
|
if (!btn) return;
|
|
const farben = {
|
|
1: "#ff0000",
|
|
2: "#ff9900",
|
|
3: "#ffff00",
|
|
4: "#0000ff",
|
|
5: "#000000",
|
|
};
|
|
const farbe = farben[level] || "#555";
|
|
btn.textContent = `⚠️ DEFCON ${level}`;
|
|
btn.style.backgroundColor = farbe;
|
|
btn.style.color = level === 3 ? "black" : "white";
|
|
btn.style.fontWeight = "bold";
|
|
btn.style.transition = "background-color 0.5s, color 0.5s";
|
|
|
|
if (level === 1) {
|
|
document.body.style.backgroundColor = farbe;
|
|
} else {
|
|
document.body.style.backgroundColor = "#000000";
|
|
}
|
|
}
|
|
|
|
async function ladeDefconStatus() {
|
|
try {
|
|
const res = await fetch("/api/defcon");
|
|
if (!res.ok) throw new Error("Fehler beim Abruf");
|
|
const data = await res.json();
|
|
currentDefcon = data.level;
|
|
aktualisiereDefconButton(currentDefcon);
|
|
} catch (err) {
|
|
console.warn("DEFCON Abruf fehlgeschlagen:", err);
|
|
}
|
|
}
|
|
|
|
async function sendeNeuesDefconLevel(level) {
|
|
try {
|
|
const response = await fetch("/api/defcon", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ level: level }),
|
|
});
|
|
|
|
if (!response.ok) throw new Error("Fehler beim Senden des neuen DEFCON-Levels");
|
|
|
|
await response.json();
|
|
} catch (err) {
|
|
console.error("Fehler beim POST:", err);
|
|
}
|
|
}
|
|
|
|
function newDefconStatus() {
|
|
currentDefcon -= 1;
|
|
if (currentDefcon < 1) {
|
|
currentDefcon = 5;
|
|
}
|
|
sendeNeuesDefconLevel(currentDefcon);
|
|
aktualisiereDefconButton(currentDefcon);
|
|
}
|
|
|
|
async function ladeBunkerStatus() {
|
|
try {
|
|
const res = await fetch("/api/bunker-status");
|
|
if (!res.ok) throw new Error("Fehler beim Abruf");
|
|
const data = await res.json();
|
|
|
|
const icon = document.getElementById("hazard-symbol");
|
|
if (!icon) return;
|
|
|
|
const inMainzNetz = Boolean(data && data.online);
|
|
|
|
if (inMainzNetz) {
|
|
icon.style.backgroundColor = "limegreen";
|
|
icon.style.color = "black";
|
|
icon.style.padding = "4px 8px";
|
|
icon.style.borderRadius = "4px";
|
|
} else {
|
|
icon.style.backgroundColor = "";
|
|
icon.style.color = "";
|
|
icon.title = "";
|
|
}
|
|
} catch (err) {
|
|
console.warn("Bunker-Status nicht abrufbar:", err);
|
|
}
|
|
}
|
|
|
|
ladeBunkerStatus();
|
|
setInterval(ladeBunkerStatus, 60 * 1000);
|
|
|
|
ladeDefconStatus();
|
|
ladeNews();
|
|
ladeRMV();
|
|
ladeMarketDaten();
|
|
|
|
setInterval(ladeNews, 30 * 1000);
|
|
setInterval(ladeRMV, 5 * 60 * 1000);
|
|
setInterval(ladeDefconStatus, 60 * 1000);
|
|
setInterval(() => {
|
|
rotiereFX();
|
|
}, 8 * 1000);
|
|
setInterval(ladeMarketDaten, 30 * 1000);
|
|
|
|
const defconBtn = document.getElementById("defcon-button");
|
|
if (defconBtn) {
|
|
defconBtn.addEventListener("click", newDefconStatus);
|
|
}
|
|
|
|
(function initLpaButton() {
|
|
const TARGET_LPA = "https://aerztepruefung.service24.rlp.de/intelliform/admin/intelliForm-Spaces/LPA/Studentenbereich";
|
|
const btn = document.getElementById("lpa-button");
|
|
if (!btn) return;
|
|
|
|
btn.addEventListener("click", () => window.open(TARGET_LPA, "_blank"));
|
|
})();
|