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:
+9
-1
@@ -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
@@ -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);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/* =====================================================================
|
||||
lib/tiktok-live.js — Automatische Live-Erkennung für TikTok
|
||||
|
||||
TikTok bietet KEINE offizielle API, die einem sagt "ist Nutzer X gerade
|
||||
live?". Was aber öffentlich funktioniert: die Seite
|
||||
https://www.tiktok.com/@<name>/live liefert im HTML ein JSON-Objekt
|
||||
(<script id="SIGI_STATE">), in dem der Raum-Status steht.
|
||||
|
||||
Bedeutung von liveRoom.status (aus Beobachtung, nicht offiziell):
|
||||
2 = läuft gerade (LIVE)
|
||||
4 = beendet / offline
|
||||
Alles andere behandeln wir sicherheitshalber als "offline".
|
||||
|
||||
Weil das eine inoffizielle Quelle ist, kann TikTok sie jederzeit ändern.
|
||||
Deshalb: jeder Fehler wird sauber gemeldet (ok:false + reason) und der
|
||||
zuletzt bekannte Status bleibt stehen, statt fälschlich auf offline zu
|
||||
springen. Zusätzlich gibt es im Postfach immer den manuellen Schalter.
|
||||
===================================================================== */
|
||||
|
||||
const LIVE_STATUS_RUNNING = 2;
|
||||
|
||||
const BROWSER_HEADERS = {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
|
||||
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "de-DE,de;q=0.9,en;q=0.8",
|
||||
"Cache-Control": "no-cache",
|
||||
Pragma: "no-cache",
|
||||
};
|
||||
|
||||
/** Schneidet den Inhalt eines <script id="..."> aus dem HTML heraus. */
|
||||
function extractScriptJson(html, id) {
|
||||
const marker = `<script id="${id}"`;
|
||||
const start = html.indexOf(marker);
|
||||
if (start === -1) return null;
|
||||
const open = html.indexOf(">", start);
|
||||
if (open === -1) return null;
|
||||
const end = html.indexOf("</script>", open);
|
||||
if (end === -1) return null;
|
||||
try {
|
||||
return JSON.parse(html.slice(open + 1, end));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fragt bei TikTok nach, ob der Kanal gerade live ist.
|
||||
* @returns {Promise<{ok:boolean, live?:boolean, title?:string, roomId?:string,
|
||||
* followers?:number, rawStatus?:number, reason?:string}>}
|
||||
*/
|
||||
export async function checkTikTokLive(username) {
|
||||
const handle = String(username || "").replace(/^@/, "").trim();
|
||||
if (!handle) return { ok: false, reason: "kein-benutzername" };
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(`https://www.tiktok.com/@${encodeURIComponent(handle)}/live`, {
|
||||
headers: BROWSER_HEADERS,
|
||||
redirect: "follow",
|
||||
cf: { cacheTtl: 0, cacheEverything: false },
|
||||
});
|
||||
} catch (err) {
|
||||
return { ok: false, reason: `netzwerkfehler: ${String(err).slice(0, 120)}` };
|
||||
}
|
||||
|
||||
if (!res.ok) return { ok: false, reason: `http-${res.status}` };
|
||||
|
||||
const html = await res.text();
|
||||
if (html.length < 5000) return { ok: false, reason: "seite-zu-klein-vermutlich-blockiert" };
|
||||
|
||||
// Bevorzugt SIGI_STATE, ersatzweise das neuere Rehydration-Objekt.
|
||||
const sigi = extractScriptJson(html, "SIGI_STATE");
|
||||
let info = sigi?.LiveRoom?.liveRoomUserInfo;
|
||||
|
||||
if (!info) {
|
||||
const universal = extractScriptJson(html, "__UNIVERSAL_DATA_FOR_REHYDRATION__");
|
||||
info = universal?.__DEFAULT_SCOPE__?.["webapp.live-detail"]?.liveRoomUserInfo;
|
||||
}
|
||||
if (!info) return { ok: false, reason: "struktur-nicht-gefunden" };
|
||||
|
||||
const status = info.liveRoom?.status;
|
||||
if (typeof status !== "number") return { ok: false, reason: "kein-status-feld" };
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
live: status === LIVE_STATUS_RUNNING,
|
||||
rawStatus: status,
|
||||
title: info.liveRoom?.title || null,
|
||||
roomId: info.user?.roomId ? String(info.user.roomId) : null,
|
||||
followers: info.stats?.followerCount ?? null,
|
||||
};
|
||||
}
|
||||
@@ -39,21 +39,149 @@ import {
|
||||
listNotes, addNote, deleteNote, listDrafts, saveDraft,
|
||||
} from "./routes/applications.js";
|
||||
import { listAuditLog } from "./routes/audit.js";
|
||||
import { checkTikTokLive } from "./lib/tiktok-live.js";
|
||||
import { logAction } from "./lib/audit.js";
|
||||
|
||||
const LIVE_STATUS_KEY = "meta:live";
|
||||
/* =====================================================================
|
||||
LIVE-STATUS
|
||||
|
||||
async function handleLiveStatusGet(env) {
|
||||
const raw = await env.BEWERBUNGEN.get(LIVE_STATUS_KEY);
|
||||
const data = raw ? JSON.parse(raw) : { live: false, updatedAt: null };
|
||||
return json({ ok: true, ...data });
|
||||
Der grüne Punkt auf der Startseite läuft automatisch: ein Cron-Auftrag
|
||||
fragt jede Minute bei TikTok nach, ob @dogfather0804 gerade sendet, und
|
||||
schreibt das Ergebnis in die Tabelle app_settings.
|
||||
|
||||
Zwei Schlüssel:
|
||||
live_auto – letzter ERFOLGREICH ermittelter Stand (wird bei einem
|
||||
fehlgeschlagenen Abruf bewusst NICHT überschrieben,
|
||||
damit ein TikTok-Aussetzer den Punkt nicht fälschlich
|
||||
auf offline zieht)
|
||||
live_override – manueller Vorrang aus dem Postfach, mit Ablaufzeit.
|
||||
Solange er gilt, schlägt er die Automatik.
|
||||
===================================================================== */
|
||||
|
||||
const DEFAULT_OVERRIDE_MINUTES = 180;
|
||||
|
||||
async function readSetting(env, key) {
|
||||
const row = await env.DB.prepare(`SELECT value FROM app_settings WHERE key = ?`)
|
||||
.bind(key)
|
||||
.first()
|
||||
.catch(() => null);
|
||||
if (!row || !row.value) return null;
|
||||
try {
|
||||
return JSON.parse(row.value);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeSetting(env, key, value) {
|
||||
await env.DB.prepare(
|
||||
`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
|
||||
)
|
||||
.bind(key, JSON.stringify(value), new Date().toISOString())
|
||||
.run();
|
||||
}
|
||||
|
||||
/** Ermittelt den Stand, der nach außen gilt (Vorrang: manuell vor Automatik). */
|
||||
async function resolveLiveStatus(env) {
|
||||
const auto = (await readSetting(env, "live_auto")) || {};
|
||||
const override = await readSetting(env, "live_override");
|
||||
|
||||
const overrideActive =
|
||||
override && override.until && new Date(override.until).getTime() > Date.now();
|
||||
|
||||
if (overrideActive) {
|
||||
return {
|
||||
live: !!override.live,
|
||||
source: "manuell",
|
||||
updatedAt: override.setAt || null,
|
||||
overrideUntil: override.until,
|
||||
autoLive: auto.live ?? null,
|
||||
autoCheckedAt: auto.checkedAt || null,
|
||||
autoHealthy: auto.ok !== false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
live: !!auto.live,
|
||||
source: "automatisch",
|
||||
updatedAt: auto.changedAt || auto.checkedAt || null,
|
||||
title: auto.title || null,
|
||||
autoLive: auto.live ?? null,
|
||||
autoCheckedAt: auto.checkedAt || null,
|
||||
autoHealthy: auto.ok !== false,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleLiveStatusGet(env) {
|
||||
const state = await resolveLiveStatus(env);
|
||||
return json({ ok: true, ...state });
|
||||
}
|
||||
|
||||
/**
|
||||
* Manueller Eingriff aus dem Postfach.
|
||||
* { live: true|false, minutes?: number } → Vorrang setzen
|
||||
* { auto: true } → Vorrang aufheben, Automatik übernimmt
|
||||
*/
|
||||
async function handleLiveStatusSet(request, env, session) {
|
||||
if (!session?.isOwner) return json({ ok: false, error: "Nur Dogi darf den Live-Status ändern." }, 403);
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const record = { live: !!body.live, updatedAt: new Date().toISOString() };
|
||||
await env.BEWERBUNGEN.put(LIVE_STATUS_KEY, JSON.stringify(record));
|
||||
return json({ ok: true, ...record });
|
||||
|
||||
if (body.auto === true) {
|
||||
await env.DB.prepare(`DELETE FROM app_settings WHERE key = 'live_override'`).run();
|
||||
await logAction(env, "owner", "live.override_cleared", null, null);
|
||||
return json({ ok: true, ...(await resolveLiveStatus(env)) });
|
||||
}
|
||||
|
||||
const minutes = Number.isFinite(body.minutes) ? Math.min(Math.max(body.minutes, 5), 1440) : DEFAULT_OVERRIDE_MINUTES;
|
||||
const now = new Date();
|
||||
await writeSetting(env, "live_override", {
|
||||
live: !!body.live,
|
||||
setAt: now.toISOString(),
|
||||
until: new Date(now.getTime() + minutes * 60_000).toISOString(),
|
||||
});
|
||||
await logAction(env, "owner", "live.override_set", null, { live: !!body.live, minutes });
|
||||
return json({ ok: true, ...(await resolveLiveStatus(env)) });
|
||||
}
|
||||
|
||||
/** Wird vom Cron-Auftrag aufgerufen — fragt TikTok ab und schreibt das Ergebnis. */
|
||||
async function refreshLiveFromTikTok(env) {
|
||||
const username = env.TIKTOK_USERNAME || "dogfather0804";
|
||||
const result = await checkTikTokLive(username);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
if (!result.ok) {
|
||||
// Fehlgeschlagen: letzten guten Stand behalten, nur den Fehler vermerken.
|
||||
const previous = (await readSetting(env, "live_auto")) || {};
|
||||
await writeSetting(env, "live_auto", {
|
||||
...previous,
|
||||
ok: false,
|
||||
lastError: result.reason,
|
||||
checkedAt: now,
|
||||
});
|
||||
return { ok: false, reason: result.reason };
|
||||
}
|
||||
|
||||
const previous = (await readSetting(env, "live_auto")) || {};
|
||||
const changed = previous.live !== result.live;
|
||||
|
||||
await writeSetting(env, "live_auto", {
|
||||
ok: true,
|
||||
live: result.live,
|
||||
title: result.title,
|
||||
roomId: result.roomId,
|
||||
rawStatus: result.rawStatus,
|
||||
checkedAt: now,
|
||||
changedAt: changed ? now : previous.changedAt || now,
|
||||
lastError: null,
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
await logAction(env, "system", result.live ? "live.started" : "live.ended", null, {
|
||||
title: result.title,
|
||||
});
|
||||
}
|
||||
return { ok: true, live: result.live, changed };
|
||||
}
|
||||
|
||||
// Routen, die eine gültige Session brauchen. `session` wird injiziert.
|
||||
@@ -130,4 +258,9 @@ export default {
|
||||
|
||||
return json({ ok: false, error: "Not found." }, 404);
|
||||
},
|
||||
|
||||
/* Cron-Auftrag: läuft jede Minute und hält den Live-Punkt aktuell. */
|
||||
async scheduled(event, env, ctx) {
|
||||
ctx.waitUntil(refreshLiveFromTikTok(env));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,6 +7,14 @@ name = "dogfather-universe-postfach"
|
||||
main = "src/worker.js"
|
||||
compatibility_date = "2024-01-01"
|
||||
|
||||
# TikTok-Kanal, dessen Live-Status automatisch geprüft wird.
|
||||
[vars]
|
||||
TIKTOK_USERNAME = "dogfather0804"
|
||||
|
||||
# Cron: jede Minute bei TikTok nachsehen, ob gerade gesendet wird.
|
||||
[triggers]
|
||||
crons = ["* * * * *"]
|
||||
|
||||
[[kv_namespaces]]
|
||||
binding = "BEWERBUNGEN"
|
||||
id = "f8f8a692150f452cab5e334743194fe2"
|
||||
|
||||
+36
-15
@@ -53,7 +53,7 @@
|
||||
<p class="lead worlds">Drei Welten, eine Familie: Team Dogi.</p>
|
||||
|
||||
<div class="card" style="max-width:420px;margin:1.4rem auto 0;padding:1rem 1.4rem;">
|
||||
<span class="badge rank-1"><span class="live-dot" id="live-dot"></span><span id="live-text">Aktuell offline</span></span>
|
||||
<span class="badge rank-1"><span class="live-dot is-unknown" id="live-dot"></span><span id="live-text">Status wird geladen …</span></span>
|
||||
<p class="small" style="margin:.5rem 0 0;">Dogfather geht <strong>jeden Tag ab 20 Uhr</strong> live — oft schon tagsüber, zu wechselnden Zeiten. <a href="streamplan.html">Streamplan ansehen →</a></p>
|
||||
</div>
|
||||
|
||||
@@ -268,23 +268,44 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// Live-Status: grüner Punkt, wenn Dogfather gerade live ist
|
||||
// Live-Status: grün = läuft gerade, rot = offline.
|
||||
// Der Wert kommt automatisch von TikTok (Cron im Worker prüft jede Minute).
|
||||
// Die Seite fragt alle 30 Sekunden nach, damit der Punkt ohne Neuladen umspringt.
|
||||
const liveDot = document.getElementById("live-dot");
|
||||
const liveText = document.getElementById("live-text");
|
||||
|
||||
if (API_BASE_URL && liveDot && liveText) {
|
||||
fetch(`${API_BASE_URL}/live-status`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data && data.live) {
|
||||
liveDot.classList.add("is-live");
|
||||
liveText.textContent = "Live jetzt!";
|
||||
} else {
|
||||
liveText.textContent = "Aktuell offline";
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
liveText.textContent = "Aktuell offline";
|
||||
});
|
||||
liveDot.classList.add("is-unknown");
|
||||
liveText.textContent = "Status wird geladen …";
|
||||
|
||||
const applyLiveStatus = (data) => {
|
||||
const isLive = !!(data && data.live);
|
||||
liveDot.classList.remove("is-unknown");
|
||||
liveDot.classList.toggle("is-live", isLive);
|
||||
|
||||
if (isLive) {
|
||||
liveText.textContent = data.title ? `Jetzt live: ${data.title}` : "Jetzt live!";
|
||||
} else {
|
||||
liveText.textContent = "Gerade offline";
|
||||
}
|
||||
};
|
||||
|
||||
const refreshLiveStatus = () =>
|
||||
fetch(`${API_BASE_URL}/live-status`, { cache: "no-store" })
|
||||
.then((res) => res.json())
|
||||
.then(applyLiveStatus)
|
||||
.catch(() => {
|
||||
liveDot.classList.remove("is-live");
|
||||
liveDot.classList.add("is-unknown");
|
||||
liveText.textContent = "Status gerade nicht abrufbar";
|
||||
});
|
||||
|
||||
refreshLiveStatus();
|
||||
setInterval(refreshLiveStatus, 30000);
|
||||
// Beim Zurückwechseln zum Tab sofort aktualisieren statt bis zu 30 s warten.
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (!document.hidden) refreshLiveStatus();
|
||||
});
|
||||
}
|
||||
|
||||
// Stats: Team-Größe aus den Datenobjekten berechnen
|
||||
|
||||
+17
-5
@@ -67,13 +67,25 @@
|
||||
<section class="section-tight" id="live-toggle-section" style="display:none;">
|
||||
<div class="container text-center">
|
||||
<h2>Live-Status</h2>
|
||||
<p class="lead" style="max-width:50ch;margin:0 auto;">Steuert den grünen Punkt auf der Startseite.</p>
|
||||
<div class="card" style="max-width:360px;margin:1rem auto 0;">
|
||||
<span class="badge rank-1"><span class="live-dot" id="admin-live-dot"></span><span id="admin-live-text">Aktuell offline</span></span>
|
||||
<p class="lead" style="max-width:56ch;margin:0 auto;">
|
||||
Läuft <strong>automatisch</strong>: Das System schaut jede Minute bei TikTok nach,
|
||||
ob du gerade sendest. Du musst nichts melden.
|
||||
</p>
|
||||
<div class="card" style="max-width:420px;margin:1rem auto 0;">
|
||||
<span class="badge rank-1"><span class="live-dot is-unknown" id="admin-live-dot"></span><span id="admin-live-text">Wird geladen …</span></span>
|
||||
<p class="small muted" style="margin:.7rem 0 0;" id="admin-live-detail"></p>
|
||||
|
||||
<div class="btn-row" style="justify-content:center;margin-top:1rem;">
|
||||
<button class="btn btn-primary" id="go-live-btn" type="button">🟢 Live schalten</button>
|
||||
<button class="btn btn-outline" id="go-offline-btn" type="button">Offline schalten</button>
|
||||
<button class="btn btn-primary" id="go-live-btn" type="button">🟢 Von Hand live</button>
|
||||
<button class="btn btn-outline" id="go-offline-btn" type="button">Von Hand offline</button>
|
||||
</div>
|
||||
<button class="btn btn-outline" id="go-auto-btn" type="button" style="margin-top:.6rem;display:none;">
|
||||
↩︎ Zurück zur Automatik
|
||||
</button>
|
||||
<p class="small muted" style="margin:.8rem 0 0;">
|
||||
Die Handschaltung ist nur ein Notfall-Vorrang (z. B. wenn TikTok mal klemmt)
|
||||
und läuft nach 3 Stunden von selbst wieder aus.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user