- 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]>
267 lines
11 KiB
JavaScript
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);
|
|
});
|