// Dashboard: Daten laden, Seiten wechseln, Änderungen von selbst nachladen. // // Die Daten liegen in daten/*.json. Ändert Claude dort etwas, merkt das // Dashboard es über /api/stand und zeichnet die Seite neu, ohne dass jemand // neu laden muss. import { esc, zeitstempel } from "./hilfen.js"; import * as start from "./seiten/start.js"; import * as plan from "./seiten/plan.js"; import * as roadmap from "./seiten/roadmap.js"; import * as formate from "./seiten/formate.js"; import * as dok from "./seiten/dokumente.js"; import * as webinar from "./seiten/webinar.js"; const SEITEN = { start, plan, roadmap, formate, webinar, dok }; export const S = { plan: null, roadmap: null, kanaele: null, webinar: null, dokumente: [], stand: null, slots: [], schritte: [], }; async function holeJson(url) { const r = await fetch(url, { cache: "no-store" }); if (!r.ok) throw new Error(`${url}: ${r.status}`); return r.json(); } export async function laden() { const [p, r, k, w, d, st] = await Promise.all([ holeJson("/daten/redaktionsplan.json"), holeJson("/daten/roadmap.json"), holeJson("/daten/kanaele.json"), holeJson("/daten/webinar.json").catch(() => null), holeJson("/api/dokumente"), holeJson("/api/stand"), ]); Object.assign(S, { plan: p, roadmap: r, kanaele: k, webinar: w, dokumente: d, stand: st }); // Nach Datum sortiert. Am selben Tag gilt die Reihenfolge in der Datei. S.slots = p.slots.map((s, i) => [s, i]) .sort(([a, i], [b, j]) => a.datum.localeCompare(b.datum) || i - j) .map(([s]) => s); S.schritte = r.pakete.flatMap((pk) => pk.schritte.map((s) => ({ ...s, paket: pk.id, paketTitel: pk.titel }))); standZeigen(); } // ---------------------------------------------------------------- Router export function route() { const roh = location.hash.replace(/^#/, "") || "/"; const [pfad, abfrage] = roh.split("?"); const teile = pfad.split("/").filter(Boolean).map((t) => { try { return decodeURIComponent(t); } catch { return t; } }); const seite = SEITEN[teile[0]] ? teile[0] : "start"; return { seite, rest: teile.slice(1), query: new URLSearchParams(abfrage || "") }; } let letzterSchluessel = null; export async function zeichnen() { const r = route(); const main = document.getElementById("inhalt"); // Gleiche Seite: Scrollstand behalten (zum Beispiel beim Öffnen eines Slots // oder wenn sich Daten ändern). Andere Seite: nach oben. const schluessel = r.seite === "dok" ? `dok/${r.rest.join("/")}?${r.query}` : r.seite; const gleich = schluessel === letzterSchluessel; const y = window.scrollY; for (const a of document.querySelectorAll("#navigation a")) { a.classList.toggle("aktiv", a.dataset.seite === r.seite); } schubladeWeg(); // Erst nach oben, dann zeichnen: so darf die Seite selbst noch zu einer // Stelle springen (laufende Woche, Abschnitt, Suchtreffer). if (!gleich) window.scrollTo(0, 0); letzterSchluessel = schluessel; try { await SEITEN[r.seite].zeichnen(main, r, { erstesMal: !gleich }); } catch (e) { console.error(e); main.innerHTML = `

Diese Seite ließ sich nicht zeichnen

${esc(e.message)}

`; } if (gleich) window.scrollTo(0, y); document.title = `${document.querySelector("#navigation a.aktiv span")?.textContent.trim() || "Übersicht"}, HiveGrid Marketing`; } // ---------------------------------------------------------------- Schublade export function schublade(html, { zurueck, breit = false } = {}) { const el = document.getElementById("schublade"); const schleier = document.getElementById("schleier"); el.className = "schublade" + (breit ? " breit" : ""); el.innerHTML = `${html}`; el.hidden = false; schleier.hidden = false; const zu = () => { location.hash = zurueck; }; el.querySelector(".zu").onclick = zu; schleier.onclick = zu; el.dataset.zurueck = zurueck; return el; } export function schubladeWeg() { document.getElementById("schublade").hidden = true; document.getElementById("schleier").hidden = true; } // ---------------------------------------------------------------- Meldung let meldungZeit = null; export function meldung(text, fehler = false) { const el = document.getElementById("meldung"); el.textContent = text; el.className = "meldung" + (fehler ? " fehler" : ""); el.hidden = false; clearTimeout(meldungZeit); meldungZeit = setTimeout(() => { el.hidden = true; }, fehler ? 6000 : 2200); } // ---------------------------------------------------------------- Schreiben export async function statusSetzen(datei, id, status) { try { const r = await fetch("/api/status", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ datei, id, status }), }); const j = await r.json().catch(() => ({})); if (!r.ok || !j.ok) throw new Error(j.fehler || j.ausgabe || `Fehler ${r.status}`); meldung(`${id}: ${status}`); } catch (e) { meldung(`Nicht gespeichert: ${e.message}`, true); } await laden(); await zeichnen(); } // ---------------------------------------------------------------- Stand und Nachladen function standZeigen() { const st = S.stand; const hinweis = document.getElementById("stand-hinweis"); hinweis.textContent = S.plan ? `${S.plan.status} vom ${S.plan.stand.split("-").reverse().join(".")}` : ""; hinweis.title = st?.aenderung ? `Letzte Änderung im Hub: ${zeitstempel(st.aenderung)}` : ""; const links = document.getElementById("links"); if (S.kanaele && !links.childElementCount) { links.innerHTML = S.kanaele.links.map((l) => `${l.logo ? `` : ""}${esc(l.name)}`).join(""); lokaleLinksPruefen(); } } // Lokale Werkzeuge wie PostGrid laufen nur, wenn ihr Server gestartet ist. // Ist er aus, wird der Link blass und sagt das, statt ins Leere zu führen. async function lokaleLinksPruefen() { for (const a of document.querySelectorAll("#links a[data-lokal]")) { let da = true; try { await fetch(a.href, { method: "HEAD", mode: "no-cors", cache: "no-store" }); } catch { da = false; } a.classList.toggle("aus", !da); a.title = da ? "" : "Läuft gerade nicht. In posts ./serve.sh starten."; } } let getrennt = false; async function nachsehen() { let st; try { st = await holeJson("/api/stand"); } catch { if (!getrennt) meldung("Server nicht erreichbar. Läuft ./serve.sh noch?", true); getrennt = true; return; } getrennt = false; if (S.stand && st.aenderung !== S.stand.aenderung) { await laden(); // Nicht neu zeichnen, solange jemand in ein Feld tippt. if (!document.activeElement?.matches("input, textarea")) { await zeichnen(); meldung("Aktualisiert"); } } else { S.stand = st; standZeigen(); } } document.addEventListener("keydown", (e) => { if (e.key !== "Escape") return; const el = document.getElementById("schublade"); if (!el.hidden && el.dataset.zurueck) location.hash = el.dataset.zurueck; }); window.addEventListener("hashchange", zeichnen); (async () => { try { await laden(); } catch (e) { document.getElementById("inhalt").innerHTML = `

Die Daten ließen sich nicht laden

${esc(e.message)}

Läuft der Server? Im Hub-Ordner ./serve.sh starten.

`; return; } await zeichnen(); setInterval(nachsehen, 4000); setInterval(lokaleLinksPruefen, 15000); })();