Files
dogfather-universe/assets/js/postfach.js
T
DogFatherGitandClaude Opus 5 7b4e1352ca 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]>
2026-07-31 03:03:31 +02:00

267 lines
11 KiB
JavaScript

/* =====================================================================
postfach.js — Logik für postfach.html
===================================================================== */
const API_BASE_URL = "https://dogfather-universe-postfach.dogfather1608.workers.dev";
const { getSession, sessionHasPermission, apiCall, doLogin, doLogout } = window.AdminAuth;
const STATUS_OPTIONS = ["neu", "in_bearbeitung", "beantwortet"];
function $(id) { return document.getElementById(id); }
function showApp(session) {
$("gate-container").style.display = "none";
$("session-bar").style.display = "block";
$("inbox-section").style.display = "block";
$("role-badge").textContent = session.isOwner ? "Eingeloggt als: Dogi (Owner)" : `Eingeloggt als: ${session.name} (${session.roleName})`;
const canSeeTeam = session.isOwner || sessionHasPermission(session, "TEAM_VIEW");
$("team-link").style.display = canSeeTeam ? "inline-flex" : "none";
if (session.isOwner) {
$("live-toggle-section").style.display = "block";
loadLiveStatus();
}
loadInbox();
}
function showGate() {
$("gate-container").style.display = "block";
$("session-bar").style.display = "none";
$("live-toggle-section").style.display = "none";
$("inbox-section").style.display = "none";
}
/* ---------------- Bewerbungen ---------------- */
async function loadInbox() {
const session = getSession();
const includeArchived = $("show-archived").checked;
const res = await apiCall(API_BASE_URL, "/admin/applications/list", { includeArchived });
if (!res.ok) {
if (!getSession()) showGate();
return;
}
renderInbox(res.applications, session);
}
function renderInbox(applications, session) {
const list = $("inbox-list");
if (!applications.length) {
list.innerHTML = `<p class="muted">Keine Bewerbungen in dieser Ansicht.</p>`;
return;
}
list.innerHTML = applications
.map((a) => {
const fields = a.data
? Object.entries(a.data).map(([k, v]) => `<p class="small"><strong>${k}:</strong> ${v}</p>`).join("")
: `<p class="small muted">Keine Berechtigung, den vollen Inhalt zu öffnen.</p>`;
const date = new Date(a.createdAt).toLocaleString("de-DE");
const actions = [];
if (sessionHasPermission(session, "APPLICATIONS_TAKE_OVER")) {
actions.push(`<button class="btn btn-outline take-btn" data-id="${a.id}">Übernehmen</button>`);
}
if (sessionHasPermission(session, "APPLICATIONS_MARK_ANSWERED")) {
actions.push(`<button class="btn btn-outline answered-btn" data-id="${a.id}">Als beantwortet markieren</button>`);
}
if (sessionHasPermission(session, "APPLICATIONS_ARCHIVE") && !a.archived) {
actions.push(`<button class="btn btn-outline archive-btn" data-id="${a.id}">Archivieren</button>`);
}
if (sessionHasPermission(session, "APPLICATIONS_RESTORE") && a.archived) {
actions.push(`<button class="btn btn-outline restore-btn" data-id="${a.id}">Wiederherstellen</button>`);
}
if (sessionHasPermission(session, "APPLICATIONS_DELETE")) {
actions.push(`<button class="btn btn-outline delete-btn" data-id="${a.id}" style="color:var(--accent);">Löschen</button>`);
}
if (sessionHasPermission(session, "NOTES_READ")) {
actions.push(`<button class="btn btn-outline notes-toggle-btn" data-id="${a.id}">Notizen</button>`);
}
const statusSelect = sessionHasPermission(session, "APPLICATIONS_CHANGE_STATUS")
? `<select class="status-select" data-id="${a.id}">
${STATUS_OPTIONS.map((s) => `<option value="${s}" ${s === a.status ? "selected" : ""}>${s}</option>`).join("")}
</select>`
: `<span class="tag">${a.status}</span>`;
return `
<div class="card app-card">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span class="tag">${a.type}${a.archived ? " · archiviert" : ""}</span>
${statusSelect}
</div>
<p class="small muted" style="margin:0;">${date}</p>
<div class="fields">${fields}</div>
<div class="actions">${actions.join("")}</div>
<div class="notes-box" id="notes-${a.id}">
<div class="notes-list" id="notes-list-${a.id}"><p class="small muted">Lädt…</p></div>
${
sessionHasPermission(session, "NOTES_WRITE")
? `<div class="field" style="margin-top:.6rem;">
<textarea class="note-input" data-id="${a.id}" placeholder="Neue Notiz…" style="min-height:60px;"></textarea>
<button class="btn btn-outline add-note-btn" data-id="${a.id}" style="margin-top:.4rem;">Notiz speichern</button>
</div>`
: ""
}
</div>
</div>`;
})
.join("");
list.querySelectorAll(".take-btn").forEach((b) => b.addEventListener("click", () => appAction("/admin/applications/take-over", b.dataset.id)));
list.querySelectorAll(".answered-btn").forEach((b) => b.addEventListener("click", () => appAction("/admin/applications/mark-answered", b.dataset.id)));
list.querySelectorAll(".archive-btn").forEach((b) => b.addEventListener("click", () => appAction("/admin/applications/archive", b.dataset.id)));
list.querySelectorAll(".restore-btn").forEach((b) => b.addEventListener("click", () => appAction("/admin/applications/restore", b.dataset.id)));
list.querySelectorAll(".delete-btn").forEach((b) =>
b.addEventListener("click", () => {
if (confirm("Diese Bewerbung wirklich endgültig löschen?")) appAction("/admin/applications/delete", b.dataset.id);
})
);
list.querySelectorAll(".status-select").forEach((sel) =>
sel.addEventListener("change", () => appAction("/admin/applications/status", sel.dataset.id, { status: sel.value }, false))
);
list.querySelectorAll(".notes-toggle-btn").forEach((b) =>
b.addEventListener("click", () => {
const box = $(`notes-${b.dataset.id}`);
box.classList.toggle("open");
if (box.classList.contains("open")) loadNotes(b.dataset.id);
})
);
list.querySelectorAll(".add-note-btn").forEach((b) =>
b.addEventListener("click", async () => {
const textarea = document.querySelector(`.note-input[data-id="${b.dataset.id}"]`);
const text = textarea.value.trim();
if (!text) return;
const res = await apiCall(API_BASE_URL, "/admin/notes/add", { applicationId: b.dataset.id, text });
if (res.ok) {
textarea.value = "";
loadNotes(b.dataset.id);
}
})
);
}
async function appAction(path, id, extra, reload = true) {
const res = await apiCall(API_BASE_URL, path, { id, ...(extra || {}) });
if (res.ok && reload) loadInbox();
else if (!res.ok) alert(res.error || "Fehler.");
}
async function loadNotes(applicationId) {
const box = $(`notes-list-${applicationId}`);
const res = await apiCall(API_BASE_URL, "/admin/notes/list", { applicationId });
if (!res.ok) {
box.innerHTML = `<p class="small muted">Konnte Notizen nicht laden.</p>`;
return;
}
box.innerHTML =
res.notes
.map((n) => `<div class="note-item"><strong>${n.author}</strong>: ${n.text}<br><span class="small muted">${new Date(n.created_at).toLocaleString("de-DE")}</span></div>`)
.join("") || `<p class="small muted">Noch keine Notizen.</p>`;
}
/* ---------------- 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`, { cache: "no-store" });
const data = await res.json();
if (res.ok && data.ok) renderLiveStatus(data);
} catch {
/* keine Blockade */
}
}
async function setLiveStatus(live) {
const res = await apiCall(API_BASE_URL, "/live-status/set", { 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 ---------------- */
document.addEventListener("DOMContentLoaded", () => {
const existing = getSession();
if (existing) showApp(existing);
else showGate();
$("code-gate").addEventListener("submit", async (e) => {
e.preventDefault();
const status = $("gate-status");
status.className = "form-status show";
status.textContent = "Prüfe Code …";
const res = await doLogin(API_BASE_URL, $("code-input").value);
if (res.ok) {
status.className = "form-status";
showApp(getSession());
} else {
status.textContent = res.error || "Falscher Code.";
status.className = "form-status show error";
}
});
$("logout-btn").addEventListener("click", async () => {
await doLogout(API_BASE_URL);
showGate();
});
$("refresh-btn").addEventListener("click", loadInbox);
$("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);
});