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
+141 -8
View File
@@ -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));
},
};