-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/web/js/app.js b/web/js/app.js
deleted file mode 100644
index 45ba8ae..0000000
--- a/web/js/app.js
+++ /dev/null
@@ -1,122 +0,0 @@
-"use strict";
-
-// --- Headerbar (eingeloggt?) ---
-let loggedIn = false;
-
-async function refreshHeader() {
- const me = await api.get("/user/info");
- loggedIn = me.ok;
-
- document.getElementById("auth-box").hidden = loggedIn;
- document.getElementById("compose").hidden = !loggedIn;
-
- const bar = document.getElementById("headerbar");
- if (loggedIn) {
- const user = encodeURIComponent(me.data.username);
- bar.innerHTML = `mein Profil `;
- document.getElementById("logout-btn").addEventListener("click", async () => {
- // POST statt GET: als GET wäre Logout per fremdem Link auslösbar (CSRF).
- await api.post("/auth/logout", new FormData());
- await refreshHeader();
- });
- } else {
- bar.innerHTML = "";
- showAuth("login");
- }
-}
-
-// --- Login / Registrierung ---
-const loginForm = document.getElementById("login-form");
-const registerForm = document.getElementById("register-form");
-const authMsg = document.getElementById("auth-msg");
-
-function showAuth(which) {
- loginForm.hidden = which !== "login";
- registerForm.hidden = which !== "register";
- authMsg.textContent = "";
-}
-
-document.getElementById("show-register").addEventListener("click", (e) => {
- e.preventDefault();
- showAuth("register");
-});
-document.getElementById("show-login").addEventListener("click", (e) => {
- e.preventDefault();
- showAuth("login");
-});
-
-loginForm.addEventListener("submit", async (e) => {
- e.preventDefault();
- const res = await api.post("/auth/login", new FormData(e.target));
- if (res.ok) {
- await refreshHeader();
- } else {
- authMsg.textContent = errMsg(res.data, "Login fehlgeschlagen.");
- }
-});
-
-registerForm.addEventListener("submit", async (e) => {
- e.preventDefault();
- const data = new FormData(e.target);
- const res = await api.post("/auth/newuser", data);
- if (!res.ok) {
- // newuser meldet alle Feldfehler auf einmal (error.fields).
- authMsg.textContent = errMsg(res.data, "Registrierung fehlgeschlagen.");
- return;
- }
- // Direkt mit denselben Daten einloggen, damit der Nutzer gleich drin ist.
- const login = new FormData();
- login.append("user", data.get("user"));
- login.append("pass", data.get("pass1"));
- login.append("timeout", "86400");
- await api.post("/auth/login", login);
- await refreshHeader();
-});
-
-// --- Feed ---
-const feed = createFeed(
- document.getElementById("feed"),
- document.getElementById("sentinel"),
- (page) => `/entry/feed/${page}`
-);
-
-// --- Neuer Beitrag ---
-document.getElementById("entry-form").addEventListener("submit", async (e) => {
- e.preventDefault();
- const res = await api.post("/entry/create", new FormData(e.target));
- const msg = document.getElementById("entry-msg");
- if (res.ok) {
- msg.textContent = "";
- document.getElementById("paste-msg").textContent = "";
- e.target.reset();
- feed.reset();
- } else {
- msg.textContent = errMsg(res.data, "Beitrag fehlgeschlagen.");
- }
-});
-
-// --- Bild aus der Zwischenablage einfügen ---
-setupClipboardPaste(
- document.getElementById("entry-form"),
- document.getElementById("paste-msg")
-);
-
-// --- Statuszeile unter dem Titel ---
-// Tage ohne Datenverlust (fester Startpunkt, Kontinuität seit der alten
-// Version).
-const dataLossStart = new Date("2025-12-13T12:44:00").getTime();
-
-function plural(n, one, many) {
- return `${n} ${n === 1 ? one : many}`;
-}
-
-function renderStats() {
- const el = document.getElementById("stats");
- if (!el) return;
- const days = Math.floor((Date.now() - dataLossStart) / 86400000);
- el.textContent = `Ohne Datenverlust seit ${plural(days, "Tag", "Tagen")}`;
-}
-
-// --- Start ---
-refreshHeader();
-renderStats();
diff --git a/web/js/entry.js b/web/js/entry.js
deleted file mode 100644
index 77f729d..0000000
--- a/web/js/entry.js
+++ /dev/null
@@ -1,97 +0,0 @@
-"use strict";
-
-// pid aus dem Pfad /e/.
-const pid = location.pathname.split("/")[2] || "";
-
-const replyForm = document.getElementById("reply-form");
-setupClipboardPaste(replyForm, document.getElementById("reply-paste-msg"));
-
-replyForm.addEventListener("submit", async (e) => {
- e.preventDefault();
- const fd = new FormData(e.target);
- fd.append("reply_to", pid);
- const res = await api.post("/entry/create", fd);
- if (res.ok) {
- e.target.reset();
- document.getElementById("reply-msg").textContent = "";
- document.getElementById("reply-paste-msg").textContent = "";
- load();
- } else {
- document.getElementById("reply-msg").textContent =
- errMsg(res.data, "Antwort fehlgeschlagen.");
- }
-});
-
-async function load() {
- const res = await api.get(`/entry/${pid}/thread`);
-
- const focus = document.getElementById("focus");
- if (!res.ok) {
- focus.textContent = errMsg(res.data, "Beitrag konnte nicht geladen werden.");
- return;
- }
- const data = res.data;
-
- // Ahnenkette (Root zuerst), dann der fokussierte Beitrag.
- const ancestors = document.getElementById("ancestors");
- ancestors.innerHTML = "";
- data.ancestors.forEach((a) => ancestors.appendChild(renderEntry(a)));
-
- focus.innerHTML = "";
- focus.appendChild(renderEntry(data.entry));
-
- // Eingeloggt? Reply-Box zeigen; eigener, nicht gelöschter Beitrag -> Bearbeiten/Löschen.
- // 401 heißt nicht angemeldet -> me bleibt leer.
- const meRes = await api.get("/user/info");
- const me = meRes.ok ? meRes.data : {};
- document.getElementById("reply-box").hidden = !me.uid;
- if (me.uid === data.entry.uid && !data.entry.deleted) {
- const editBtn = document.createElement("button");
- editBtn.textContent = "Bearbeiten";
- editBtn.addEventListener("click", () => {
- editBtn.disabled = true;
- const form = document.createElement("form");
- form.innerHTML = `
-
-
-
- `;
- form.querySelector("textarea").value = data.entry.content;
- form.addEventListener("submit", async (ev) => {
- ev.preventDefault();
- const res = await api.post(`/entry/${pid}/edit`, new FormData(form));
- if (res.ok) load();
- else form.querySelector(".msg").textContent =
- errMsg(res.data, "Bearbeiten fehlgeschlagen.");
- });
- form.querySelector(".cancel").addEventListener("click", load);
- focus.appendChild(form);
- });
-
- const delBtn = document.createElement("button");
- delBtn.textContent = "Löschen";
- delBtn.addEventListener("click", async () => {
- if (!confirm("Beitrag wirklich löschen?")) return;
- const res = await api.post(`/entry/${pid}/delete`, new FormData());
- if (res.ok) load();
- else alert(errMsg(res.data, "Löschen fehlgeschlagen."));
- });
-
- focus.appendChild(editBtn);
- focus.appendChild(delBtn);
- }
-
- // Direkte Antworten.
- const replies = document.getElementById("replies");
- replies.innerHTML = "";
- if (!data.replies.length) {
- const p = document.createElement("p");
- p.className = "muted";
- p.textContent = "Noch keine Antworten.";
- replies.appendChild(p);
- } else {
- data.replies.forEach((e) => replies.appendChild(renderEntry(e)));
- }
-}
-
-load();
diff --git a/web/js/feed.js b/web/js/feed.js
deleted file mode 100644
index c3db940..0000000
--- a/web/js/feed.js
+++ /dev/null
@@ -1,289 +0,0 @@
-"use strict";
-
-// Die API liegt unter /api und antwortet ausschließlich mit JSON. Ob ein Aufruf
-// geklappt hat, sagt der HTTP-Status (ok/status); data ist bei Fehlern das
-// Fehlerobjekt {"error": {...}}. Pfade werden ohne /api übergeben.
-const JSON_HEADERS = { Accept: "application/json" };
-
-async function apiResult(r) {
- return { ok: r.ok, status: r.status, data: await r.json().catch(() => ({})) };
-}
-
-const api = {
- async get(path) {
- return apiResult(await fetch(`/api${path}`, {
- credentials: "same-origin",
- headers: JSON_HEADERS,
- }));
- },
- async post(path, formData) {
- return apiResult(await fetch(`/api${path}`, {
- method: "POST",
- body: formData,
- credentials: "same-origin",
- headers: JSON_HEADERS,
- }));
- },
-};
-
-// errMsg zieht die Nutzermeldung aus dem Fehlerformat der API:
-// {"error": {"code", "message", "field", "fields"}}. Liegen mehrere
-// Feldfehler vor (Registrierung), werden sie zu einem Satz zusammengezogen.
-// Der code ist bewusst nicht Teil der Anzeige -- er ist für Logik da, nicht
-// für Menschen.
-function errMsg(data, fallback) {
- const e = data && data.error;
- if (!e) return fallback;
- const fields = e.fields ? Object.values(e.fields) : [];
- if (fields.length) return fields.join(" ");
- return e.message || fallback;
-}
-
-// Escaped für Text- UND Attributkontext. Der frühere textContent->innerHTML-
-// Trick ließ Anführungszeichen stehen (in Textknoten harmlos); linkify steckt
-// den Wert aber in ein href="..."-Attribut, wo ein " ausbrechen würde.
-function escapeHtml(s) {
- return String(s ?? "")
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
-}
-
-// avatarUrl liefert den Bildpfad oder das Platzhalterbild bei leerem Avatar.
-function avatarUrl(path) {
- return path ? `/${path}` : "/static/assets/no_profile_pic.jpg";
-}
-
-function linkify(s) {
- const escaped = escapeHtml(s);
- const url = /https?:\/\/[^\s<]+/g;
- return escaped
- .replace(url, (m) => `${m}`)
- .replace(/\n/g, " ");
-}
-
-// setupClipboardPaste erlaubt Strg+V eines Bildes aus der Zwischenablage direkt
-// im Formular: das Bild wandert in den File-Input (über DataTransfer, weil sich
-// .files nicht direkt setzen lässt). Liegt kein Bild vor (Text o. Ä.), tun wir
-// nichts und lassen den normalen Paste durchlaufen -> preventDefault nur, wenn
-// wir wirklich ein Bild gegriffen haben. msgEl (optional) zeigt den Hinweis.
-function setupClipboardPaste(form, msgEl) {
- form.addEventListener("paste", (e) => {
- const item = [...(e.clipboardData?.items ?? [])].find(
- (it) => it.kind === "file" && it.type.startsWith("image/")
- );
- if (!item) return;
-
- const original = item.getAsFile();
- if (!original) return;
- e.preventDefault();
-
- // Browser benennt Clipboard-Bilder uneinheitlich ("grafik.png" o. Ä.) -> neu
- // verpacken mit sprechendem Namen. Endung aus dem MIME-Typ, sonst png.
- const ext = (original.type.split("/")[1] || "png").toLowerCase();
- const file = new File([original], `zwischenablage.${ext}`, { type: original.type });
-
- const input = form.querySelector("input[name='file']");
- const dt = new DataTransfer();
- dt.items.add(file);
- input.files = dt.files;
-
- if (msgEl) msgEl.textContent = "Bild aus Zwischenablage übernommen.";
- });
-}
-
-function renderEntry(e) {
- const el = document.createElement("article");
- el.className = "card";
-
- const when = new Date(e.created_at * 1000).toLocaleString("de-DE");
-
- // Soft-gelöschter Beitrag: nur Platzhalter, damit der Thread navigierbar bleibt.
- if (e.deleted) {
- el.innerHTML = `
-
[deleted] · ${when}
-
-
-
-
[deleted]
-
-
- `;
- el.addEventListener("click", (ev) => {
- if (!ev.target.closest("a, button")) location.href = `/e/${e.pid}`;
- });
- return el;
- }
-
- const user = encodeURIComponent(e.username);
- let media = "";
- if (e.filepath) {
- media = ``;
- }
-
- // Antwort- und Repost-Zahl wandern in die Buttons unten; in der Byline bleibt
- // nur Name, Zeit und (im Cooldown) der Hinweis bis zum nächsten Repost.
- el.innerHTML = `
-
- Seitenaufrufe werden mit anonymisierten Netzen gezählt (IPv4 /16, IPv6 /32).
- „Netze" ist eine grobe Annäherung an Besucher, keine Personenzählung.
-
-
-
-
Netze nach Betreiber
-
- Aufrufe je autonomem System (Netzbetreiber), aufgelöst über GeoLite2-ASN.
-