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:
hubble_dubble
2026-01-26 00:19:54 +01:00
commit 3667c678e4
41 changed files with 3556 additions and 0 deletions

View File

@@ -0,0 +1,188 @@
let timersData = [];
let detailOpenFor = null;
function formatDiff(target) {
const targetDate = target instanceof Date ? target : new Date(target);
const now = new Date();
const diff = targetDate - now;
if (diff <= 0) return "Abgelaufen";
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff / (1000 * 60 * 60)) % 24);
const minutes = Math.floor((diff / (1000 * 60)) % 60);
const seconds = Math.floor((diff / 1000) % 60);
return `${days}d ${hours}h ${minutes}m ${seconds}s`;
}
fetch("/api/countdowns")
.then(res => res.json())
.then(data => {
timersData = data.timers || [];
const mainTargetDate = new Date(data.main.target);
const mainLabel = data.main.label || "";
const mainLabelEl = document.getElementById("main-label");
const mainCountdownEl = document.getElementById("main-countdown");
function updateMainTimer() {
const now = new Date();
const diff = mainTargetDate - now;
if (diff <= 0) {
mainLabelEl.textContent = mainLabel;
mainCountdownEl.textContent = "Abgelaufen";
return;
}
mainLabelEl.textContent = mainLabel;
mainCountdownEl.textContent = formatDiff(mainTargetDate);
}
function updateSmallTimers() {
timersData.forEach(t => {
const el = document.getElementById(t.id);
if (!el) return;
const now = new Date();
const nextEntry = t.targets
.map(e => ({ ...e, date: new Date(e.time) }))
.find(e => e.date > now);
if (nextEntry) {
el.textContent = `${nextEntry.label}\n${formatDiff(nextEntry.date)}`;
} else {
el.textContent = `${t.label}\nAbgelaufen`;
}
});
}
function updateDetailsPanel() {
if (!detailOpenFor) return;
const timer = timersData.find(t => t.id === detailOpenFor);
if (!timer) return;
timer.targets.forEach((target, idx) => {
const el = document.getElementById(`detail-${timer.id}-${idx}`);
if (!el) return;
el.textContent = formatDiff(target.time);
});
}
function renderDetails(timer) {
const detailEl = document.getElementById("timer-details");
if (!detailEl) return;
detailOpenFor = timer.id;
const entries = timer.targets.map((target, idx) => {
const entryId = `detail-${timer.id}-${idx}`;
return `
<div class="timer-entry">
<div class="label">${target.label}</div>
<div class="countdown" id="${entryId}"></div>
</div>
`;
}).join("");
detailEl.innerHTML = `
<button class="close-btn" id="timer-details-close">Schließen</button>
<h3>${timer.label}</h3>
<div class="timer-meta">${timer.targets.length} Termine</div>
${entries}
`;
detailEl.classList.remove("hidden");
const closeBtn = document.getElementById("timer-details-close");
if (closeBtn) {
closeBtn.addEventListener("click", () => {
detailOpenFor = null;
detailEl.classList.add("hidden");
});
}
updateDetailsPanel();
}
setInterval(() => {
updateMainTimer();
updateSmallTimers();
updateDetailsPanel();
}, 1000);
updateMainTimer();
updateSmallTimers();
document.querySelectorAll(".small-circle").forEach(circle => {
circle.addEventListener("click", () => {
const timerId = circle.getAttribute("data-timer");
const timer = timersData.find(t => t.id === timerId);
if (timer) renderDetails(timer);
});
});
});
document.getElementById("fullscreen-btn").addEventListener("click", () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else {
document.exitFullscreen();
}
});
const cantinaAudio = document.getElementById("cantina-audio");
const circleContainer = document.querySelector(".circle-container");
function playCantina() {
if (!cantinaAudio) return;
cantinaAudio.currentTime = 0;
cantinaAudio.play().catch(err => console.warn("Cantina playback blocked:", err));
}
if (circleContainer) {
circleContainer.addEventListener("click", playCantina);
}
function scheduleNextFullHour() {
const now = new Date();
const next = new Date(now);
next.setMinutes(0, 0, 0);
if (next <= now) {
next.setHours(next.getHours() + 1);
}
const delay = next - now;
setTimeout(() => {
playCantina();
scheduleNextFullHour();
}, delay);
}
scheduleNextFullHour();
const overlay = document.getElementById("twitter-overlay");
function showOverlay() {
overlay.style.display = "flex";
const gongVideo = document.getElementById("gong-video-fullscreen");
if (gongVideo) {
gongVideo.currentTime = 0;
gongVideo.play().catch(err => console.warn("Autoplay-Fehler:", err));
}
setTimeout(() => {
overlay.style.display = "none";
}, 60000);
}
(() => {
const now = new Date();
const target = new Date();
target.setHours(23, 40, 0, 0);
if (now > target && now - target < 60000) {
showOverlay();
} else if (now <= target) {
const delay = target - now;
setTimeout(showOverlay, delay);
}
})();