Please enter the commit message for your changes. Lines starting
with '#' will be ignored, and an empty message aborts the commit. On branch main Initial commit Changes to be committed: new file: .DS_Store new file: .env new file: .gitignore new file: ai-worker/Dockerfile new file: ai-worker/requirements.txt new file: ai-worker/worker.py new file: background-worker/Dockerfile new file: background-worker/go.mod new file: background-worker/go.sum new file: background-worker/main.go new file: background-worker/market.go new file: background-worker/rmv.go new file: background-worker/rss.go new file: background-worker/sql_work.go new file: db/Dockerfile new file: db/init.sql new file: docker-compose.yml new file: server-app/dockerfile new file: server-app/go.mod new file: server-app/go.sum new file: server-app/main.go new file: volumes/.DS_Store new file: volumes/db-init/.DS_Store new file: volumes/db-init/data/news_rss_feeds.csv new file: volumes/web/.DS_Store new file: volumes/web/static/css/blog.css new file: volumes/web/static/css/index-lite.css new file: volumes/web/static/css/index.css new file: volumes/web/static/css/mandelbrot.css new file: volumes/web/static/img/minecraft.png new file: volumes/web/static/js/blog.js new file: volumes/web/static/js/index-lite.js new file: volumes/web/static/js/index.js new file: volumes/web/static/js/mandelbrot.js new file: volumes/web/static/media/cantina.mp3 new file: volumes/web/static/media/countdowns.json new file: volumes/web/static/media/gong.mp4 new file: volumes/web/template/blog.html new file: volumes/web/template/index-lite.html new file: volumes/web/template/index.html new file: volumes/web/template/mandelbrot.html
This commit is contained in:
372
volumes/web/static/js/index-lite.js
Normal file
372
volumes/web/static/js/index-lite.js
Normal file
@@ -0,0 +1,372 @@
|
||||
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 = [];
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const parsed = JSON.parse(line);
|
||||
items.push({
|
||||
title: stripTags(parsed.title),
|
||||
text: stripTags(parsed.text),
|
||||
link: parsed.link,
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn("NDJSON parse error", err);
|
||||
}
|
||||
if (items.length >= 60) 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);
|
||||
}
|
||||
Reference in New Issue
Block a user