379 lines
11 KiB
JavaScript
379 lines
11 KiB
JavaScript
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 newsContainer = document.getElementById("news-container");
|
|
const abfahrtWrapper = document.getElementById("abfahrt-wrapper");
|
|
const abfahrtInfo = document.getElementById("abfahrt-info");
|
|
const marketTrack = document.getElementById("market-ticker-track");
|
|
const marketContainer = document.getElementById("market-ticker");
|
|
|
|
async function loadNews() {
|
|
if (!newsContainer) return;
|
|
try {
|
|
const res = await fetch("/api/artikeltext", {
|
|
headers: { "Accept": "application/x-ndjson" },
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
|
|
const text = await res.text();
|
|
const lines = text.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
|
|
const items = [];
|
|
const seen = new Set();
|
|
for (const line of lines) {
|
|
try {
|
|
const parsed = JSON.parse(line);
|
|
const fallbackTitle = stripTags(parsed.title);
|
|
const id = parsed.id || parsed.link || fallbackTitle || `idx:${items.length}`;
|
|
if (seen.has(id)) continue;
|
|
seen.add(id);
|
|
items.push({
|
|
id,
|
|
title: stripTags(parsed.title),
|
|
text: stripTags(parsed.text),
|
|
link: parsed.link,
|
|
});
|
|
} catch (err) {
|
|
console.warn("NDJSON parse error", err);
|
|
}
|
|
if (items.length >= 20) break;
|
|
}
|
|
|
|
newsContainer.innerHTML = "";
|
|
if (!items.length) {
|
|
newsContainer.innerHTML = '<div class="empty">Keine News gefunden.</div>';
|
|
return;
|
|
}
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
items.forEach((item) => {
|
|
const wrapper = document.createElement("div");
|
|
wrapper.className = "news-item";
|
|
|
|
const title = document.createElement("div");
|
|
title.className = "news-title";
|
|
title.textContent = item.title || "Kein Titel";
|
|
|
|
const textEl = document.createElement("div");
|
|
textEl.className = "news-text";
|
|
textEl.textContent = item.text || "[Kein Text verfuegbar]";
|
|
|
|
const linkWrap = document.createElement("div");
|
|
linkWrap.className = "news-link";
|
|
const link = document.createElement("a");
|
|
link.href = item.link || "#";
|
|
link.textContent = "Zum Artikel";
|
|
link.target = "_blank";
|
|
link.rel = "noopener noreferrer";
|
|
linkWrap.appendChild(link);
|
|
|
|
wrapper.appendChild(title);
|
|
wrapper.appendChild(textEl);
|
|
wrapper.appendChild(linkWrap);
|
|
fragment.appendChild(wrapper);
|
|
});
|
|
|
|
newsContainer.appendChild(fragment);
|
|
} catch (err) {
|
|
console.warn("News Fehler:", err);
|
|
newsContainer.innerHTML = '<div class="empty">Fehler beim Laden der News.</div>';
|
|
}
|
|
}
|
|
|
|
function renderMarketTicker(quotes) {
|
|
if (!marketTrack || !marketContainer) return;
|
|
|
|
marketTrack.innerHTML = "";
|
|
if (!quotes || quotes.length === 0) {
|
|
marketTrack.textContent = "Keine Marktdaten verfuegbar";
|
|
marketTrack.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();
|
|
|
|
marketTrack.appendChild(fragment);
|
|
const duration = Math.max(90, Math.min(180, quotes.length * 10));
|
|
marketTrack.style.setProperty("--ticker-duration", `${duration}s`);
|
|
marketTrack.style.animation = "none";
|
|
void marketTrack.offsetWidth;
|
|
marketTrack.style.animation = `ticker-move var(--ticker-duration, ${duration}s) linear infinite`;
|
|
}
|
|
|
|
async function loadMarket() {
|
|
try {
|
|
const res = await fetch("/api/market", { cache: "no-store" });
|
|
if (!res.ok) throw new Error("Market API nicht verfuegbar");
|
|
const data = await res.json();
|
|
if (!Array.isArray(data)) throw new Error("Ungueltige Marktdaten");
|
|
renderMarketTicker(data);
|
|
} catch (err) {
|
|
console.warn("Marktdaten Fehler:", err);
|
|
renderMarketTicker([]);
|
|
}
|
|
}
|
|
|
|
async function loadRMV() {
|
|
if (!abfahrtInfo || !abfahrtWrapper) return;
|
|
try {
|
|
const response = await fetch("/api/rmv", { cache: "no-store" });
|
|
if (!response.ok) throw new Error("RMV-JSON nicht erreichbar");
|
|
const data = await response.json();
|
|
|
|
abfahrtWrapper.style.display = "block";
|
|
|
|
if (!data.abfahrten || data.abfahrten.length === 0) {
|
|
abfahrtInfo.innerHTML = "<p>Keine Abfahrten gefunden.</p>";
|
|
return;
|
|
}
|
|
|
|
const tripHTML = data.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("");
|
|
|
|
abfahrtInfo.innerHTML = tripHTML;
|
|
} catch (err) {
|
|
console.warn("RMV-Info nicht geladen:", err);
|
|
abfahrtInfo.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);
|
|
}
|
|
|
|
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;
|
|
|
|
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 loadDefconStatus() {
|
|
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 loadBunkerStatus() {
|
|
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);
|
|
}
|
|
}
|
|
|
|
loadBunkerStatus();
|
|
loadDefconStatus();
|
|
loadNews();
|
|
loadRMV();
|
|
loadMarket();
|
|
|
|
setInterval(loadBunkerStatus, 5 * 60 * 1000);
|
|
setInterval(loadDefconStatus, 2 * 60 * 1000);
|
|
setInterval(loadNews, 2 * 60 * 1000);
|
|
setInterval(loadRMV, 10 * 60 * 1000);
|
|
setInterval(loadMarket, 2 * 60 * 1000);
|
|
|
|
const defconBtn = document.getElementById("defcon-button");
|
|
if (defconBtn) {
|
|
defconBtn.addEventListener("click", newDefconStatus);
|
|
}
|