Live-Punkt laeuft jetzt automatisch ueber TikTok

- Neues Modul lib/tiktok-live.js liest den Live-Status direkt von der
  oeffentlichen TikTok-Live-Seite (SIGI_STATE -> liveRoom.status,
  2 = live, alles andere = offline; bestaetigt durch yt-dlp)
- Cron-Trigger im Worker prueft jede Minute und schreibt das Ergebnis
  nach app_settings; schlaegt ein Abruf fehl, bleibt der letzte gute
  Stand stehen statt faelschlich auf offline zu springen
- /live-status liefert jetzt Quelle, Titel und Zeitpunkt der Pruefung
- Punkt ist rot wenn offline, gruen+pulsierend wenn live, grau solange
  der Status laedt; Startseite aktualisiert alle 30 s ohne Neuladen
- Handschaltung bleibt als Notfall-Vorrang mit 3-Stunden-Ablauf, plus
  Knopf "Zurueck zur Automatik" im Postfach

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-07-31 03:03:31 +02:00
co-authored by Claude Opus 5
parent 597988a9a2
commit 7b4e1352ca
7 changed files with 361 additions and 36 deletions
+9 -1
View File
@@ -777,14 +777,22 @@ footer.site-footer {
width: 10px;
height: 10px;
border-radius: 50%;
background: #6b7280; /* grau = aktuell offline */
background: #e05260; /* rot = offline (Standard) */
box-shadow: 0 0 8px rgba(224, 82, 96, .55);
margin-right: .45rem;
vertical-align: middle;
transition: background .3s ease, box-shadow .3s ease;
}
.live-dot.is-live {
background: #22c55e; /* grün = live */
animation: live-pulse 1.6s infinite;
}
/* Solange der Status noch geladen wird: neutral grau, damit nie kurz
fälschlich "offline" aufblitzt. */
.live-dot.is-unknown {
background: #6b7280;
box-shadow: none;
}
@keyframes live-pulse {
0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, .55); }
70% { box-shadow: 0 0 0 10px rgba(34, 197, 94, 0); }
+57 -7
View File
@@ -162,17 +162,56 @@ async function loadNotes(applicationId) {
.join("") || `<p class="small muted">Noch keine Notizen.</p>`;
}
/* ---------------- Live-Status ---------------- */
function renderLiveStatus(live) {
$("admin-live-dot").classList.toggle("is-live", !!live);
$("admin-live-text").textContent = live ? "Live jetzt!" : "Aktuell offline";
/* ---------------- Live-Status ----------------
Der Status kommt automatisch von TikTok (Cron im Worker, jede Minute).
Die Handschaltung ist nur ein zeitlich begrenzter Vorrang.
------------------------------------------------------------------- */
function formatSince(iso) {
if (!iso) return null;
const diffMin = Math.floor((Date.now() - new Date(iso).getTime()) / 60000);
if (diffMin < 1) return "gerade eben";
if (diffMin === 1) return "vor 1 Minute";
if (diffMin < 60) return `vor ${diffMin} Minuten`;
const h = Math.floor(diffMin / 60);
return h === 1 ? "vor 1 Stunde" : `vor ${h} Stunden`;
}
function renderLiveStatus(data) {
const live = !!(data && data.live);
const dot = $("admin-live-dot");
dot.classList.remove("is-unknown");
dot.classList.toggle("is-live", live);
$("admin-live-text").textContent = live ? "Jetzt live" : "Gerade offline";
const manual = data && data.source === "manuell";
$("go-auto-btn").style.display = manual ? "inline-flex" : "none";
const lines = [];
if (manual) {
lines.push("⚠️ Von Hand gesetzt — die Automatik ist gerade übersteuert.");
if (data.overrideUntil) {
const until = new Date(data.overrideUntil);
lines.push(`Läuft aus um ${until.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" })} Uhr.`);
}
if (data.autoLive !== null && data.autoLive !== undefined) {
lines.push(`TikTok meldet gerade: ${data.autoLive ? "live" : "offline"}.`);
}
} else {
if (data && data.title && live) lines.push(`Titel: ${data.title}`);
const since = formatSince(data && data.autoCheckedAt);
if (since) lines.push(`Automatisch geprüft ${since}.`);
if (data && data.autoHealthy === false) {
lines.push("⚠️ Der letzte Abruf bei TikTok hat nicht geklappt — angezeigt wird der letzte bekannte Stand.");
}
}
$("admin-live-detail").innerHTML = lines.join("<br>");
}
async function loadLiveStatus() {
try {
const res = await fetch(`${API_BASE_URL}/live-status`);
const res = await fetch(`${API_BASE_URL}/live-status`, { cache: "no-store" });
const data = await res.json();
if (res.ok && data.ok) renderLiveStatus(data.live);
if (res.ok && data.ok) renderLiveStatus(data);
} catch {
/* keine Blockade */
}
@@ -180,7 +219,12 @@ async function loadLiveStatus() {
async function setLiveStatus(live) {
const res = await apiCall(API_BASE_URL, "/live-status/set", { live });
if (res.ok) renderLiveStatus(res.live);
if (res.ok) renderLiveStatus(res);
}
async function backToAutoLive() {
const res = await apiCall(API_BASE_URL, "/live-status/set", { auto: true });
if (res.ok) renderLiveStatus(res);
}
/* ---------------- Login / Logout ---------------- */
@@ -213,4 +257,10 @@ document.addEventListener("DOMContentLoaded", () => {
$("show-archived").addEventListener("change", loadInbox);
$("go-live-btn").addEventListener("click", () => setLiveStatus(true));
$("go-offline-btn").addEventListener("click", () => setLiveStatus(false));
$("go-auto-btn").addEventListener("click", backToAutoLive);
// Anzeige alle 30 Sekunden auffrischen, damit man die Automatik arbeiten sieht.
setInterval(() => {
if (!document.hidden && getSession()) loadLiveStatus();
}, 30000);
});