Files
HiveGrid-Marketing/Dashboard/app.js
T
dejanandClaude Opus 5.5 686ee36b4f HiveGrid Marketing: Hub mit Strategie, Redaktionsplan, Dashboard, Produktionsvorgaben und Webinar
Stand 16.10.2026. Enthält das Marketing-Dashboard, die Daten in daten/,
die Produktionsvorgaben, Reels und das Webinar mit Skript, Sprechzettel
und Präsentation (HTML, PowerPoint, PDF).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:09:07 +02:00

209 lines
7.5 KiB
JavaScript

// 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 = `<section class="block warnung"><h2>Diese Seite ließ sich nicht zeichnen</h2><p>${esc(e.message)}</p></section>`;
}
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 = `<button class="zu" title="Esc">Schließen</button>${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) => `<a href="${esc(l.url)}" target="_blank" rel="noopener"${l.lokal ? " data-lokal" : ""}>${l.logo ? `<img src="${esc(l.logo)}" alt="">` : "<span class=\"ohne-logo\"></span>"}<span>${esc(l.name)}</span></a>`).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 =
`<section class="block warnung"><h2>Die Daten ließen sich nicht laden</h2><p>${esc(e.message)}</p><p>Läuft der Server? Im Hub-Ordner <code>./serve.sh</code> starten.</p></section>`;
return;
}
await zeichnen();
setInterval(nachsehen, 4000);
setInterval(lokaleLinksPruefen, 15000);
})();