Agentur-Events: die Karte als Aushang, Aufgaben zum Abhaken
Filipe, mit einem Bildschirmfoto des Oktober-Events: "ich will das viel geiler viel perfekter. und auch wenn man die eintraegt und so soll viel mehr perfektioniert und uebersichtlicher sein." VORHER GEMESSEN (1280 px, eine einzige Eventkarte): Kartenhoehe 1196 px -- das Fenster ist 900 px davon Banner 638 px -- 53 %, ohne einen Buchstaben Titel steht bei 1482 px -- zweimal scrollen bis zum Namen Titelgroesse 15,36 px -- 1,4 px mehr als der Fliesstext Etikett 10,88 px -- unter der eigenen Grenze (11,5) Datum 3 x -- in drei Zeilen untereinander Knoepfe 4 x gleich -- "loeschen" wie "zur Aufgabe" NACHHER, dieselbe Messung: Karte 618 px, Buehne 203 px, Titel 75 px unter der Kartenkante und 24,8 px gross, Etikett 12 px, das Datum einmal, "loeschen" abgesetzt. DIE KARTE Das Banner ist nicht mehr ein Anhang ueber dem Titel, sondern die Buehne dahinter -- Hoehe gedeckelt, Titel darauf. Dazu die Frage, die bei einem Wettbewerb wirklich zaehlt und bisher nirgends beantwortet wurde: "noch 12 Tage", mit Zeitbalken. Ein Knopf "Banner ganz" holt das vollstaendige Bild zurueck, das beim Umbau sonst verloren gegangen waere -- bei Filipes Banner steht die Ansage IM Bild. DIE AUFGABEN Aus Fliesstext wird eine Liste, und jeder hakt fuer sich ab (neue Tabelle event_punkte, Schluessel aus dem Zeilentext). Umsortieren laesst den Haken, wo er ist; wird die Bedingung selbst umgeschrieben, faellt er -- beides nachgemessen. Die Leitung sieht, wer wie weit ist, eine Creatorin sieht diese Liste gar nicht erst. DAS FORMULAR Ein Feld je Zeile statt eines leeren Textfeldes. Im echten Event stand ".Jeden Tag Live gehen" neben ". 33k Diamanten erreichen" -- einmal mit Leerzeichen, einmal ohne. Das ist die zwangslaeufige Folge davon, dass die Aufzaehlungszeichen von Hand getippt werden; jetzt setzt sie das Formular. Dazu "Laeuft 14 Tage.", eine Warnung bei verdrehtem Zeitraum und eine Vorschau, die dieselbe Funktion benutzt wie die echte Karte. NEBENBEI GEFUNDEN UND BEHOBEN * Das Banner kam bei Creatorn mit 404 zurueck -- also an genau der Karte, die fuer sie gemacht ist. Die Regel "wer das Brett sieht, sieht das Bild" galt nur fuer die sieben Community-Bretter. Jetzt fragt der Bildweg dieselbe Regel wie das Brett (darfEintragSehen); die Gegenprobe zeigt, dass ein fremder Eintrag weiterhin 404 gibt. * Zwei Stellen setzten das Formular zurueck, die kuerzere liess Vorschau und Zeilen-Editor stehen -- beim naechsten "Neuer Eintrag" standen die Aufgaben des vorigen Events noch da. * Der Schriftgrund ragte auf dem Handy 6 px ueber die Karte (feste -20 px gegen 14 px Polsterung); jetzt an die Polsterung gekoppelt. GEPRUEFT pruef-eventkarte.mjs (neu): 77 Pruefungen, 0 Fehler -- mit Gegenproben zu jeder Zusage und einer Kontrastmessung am Bildpunkt auf einem weissen Banner (14,7:1; ohne den Schriftgrund 1,05:1). pruef-agentur.mjs auf die neuen Bausteine nachgezogen, Zahl unveraendert bei 62. pruef-eintrag-bild.mjs 24/0. OFFEN, NICHT AUS DIESEM UMBAU: Das Brett `agentur` ist auf der Crew-Adresse erreichbar. Zweimal gemessen, mit und ohne diese Aenderung -- gleiches Ergebnis. Gehoert zur Trennungsregel vom 24.09.2026 und wird getrennt entschieden. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -20,7 +20,7 @@ import express from "express";
|
||||
Fassung, und an ihr haengt die Sicherheit aller Uploads im Haus. */
|
||||
import { writeFileSync, mkdirSync, unlinkSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { randomBytes, createHash } from "node:crypto";
|
||||
import { dateiErkennen } from "./workspace-chat.js";
|
||||
import { TREFF_START, VORSCHLAG_HINWEIS } from "./workspace-treff-start.js";
|
||||
import { vorlagenFuer } from "./workspace-treff-vorlagen.js";
|
||||
@@ -752,6 +752,89 @@ const MIT_ANTWORTEN = new Set(["treff", "wunsch"]);
|
||||
const TITEL_MAX = 160;
|
||||
const TEXT_MAX = 6000;
|
||||
|
||||
/* =====================================================================
|
||||
DIE PUNKTE EINES AGENTUR-EVENTS (03.10.2026)
|
||||
|
||||
`event_aufgaben` und `event_regeln` sind Fliesstext, den DogFather
|
||||
tippt -- eine Zeile je Punkt, meistens mit einem Aufzaehlungszeichen
|
||||
davor. Dass er von Hand gesetzt wird, sieht man dem Ergebnis an: Im
|
||||
echten Oktober-Event stand „·Jeden Tag Live gehen" neben
|
||||
„· 33k Diamanten erreichen", einmal mit Leerzeichen, einmal ohne.
|
||||
|
||||
HIER WIRD DARAUS EINE LISTE. Die Oberflaeche bekommt Punkte, keinen
|
||||
Absatz -- damit sie Haken daran haengen kann und die Einrueckung
|
||||
nicht mehr davon abhaengt, wie jemand getippt hat.
|
||||
|
||||
DER SPEICHERTEXT BLEIBT UNVERAENDERT. Zerlegt wird beim LESEN, nicht
|
||||
beim Schreiben: Wer das Event spaeter bearbeitet, soll genau das
|
||||
wiederfinden, was er geschrieben hat. Und ein Textfeld, das beim
|
||||
Speichern umformatiert wird, ist ein Textfeld, dem man nicht traut.
|
||||
===================================================================== */
|
||||
|
||||
/* Die Zeichen, mit denen Menschen Aufzaehlungen anfangen. Sie gehoeren
|
||||
nicht zum Inhalt des Punktes -- und wer heute „·" tippt und morgen
|
||||
„-", meint denselben Punkt. */
|
||||
const PUNKT_ZEICHEN = /^[\s]*[-–—•·*▪◦‣>+]+[\s]*/u;
|
||||
|
||||
/** Der Kern einer Aufgabenzeile, ohne Zierde.
|
||||
* Nur fuer den Vergleich gedacht -- angezeigt wird der Originaltext. */
|
||||
function punktKern(zeile) {
|
||||
return String(zeile || "")
|
||||
.replace(PUNKT_ZEICHEN, "")
|
||||
/* Eine fuehrende Nummerierung („1.", „2)") ebenfalls weg: Wer
|
||||
einen Punkt davorsetzt, verschiebt sonst alle Nummern und damit
|
||||
alle Schluessel -- genau der stille Haken-Umzug, den die Tabelle
|
||||
verhindern soll. */
|
||||
.replace(/^\s*\d+[.)]\s*/, "")
|
||||
.replace(/\s+/gu, " ")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
/** Der Schluessel, unter dem ein Haken gespeichert wird.
|
||||
*
|
||||
* AUS DEM TEXT, NICHT AUS DER ZEILENNUMMER -- die Begruendung steht
|
||||
* bei der Tabelle `event_punkte` in workspace.js. Hier nur das Wie:
|
||||
* Kern bilden, davon die ersten 16 Zeichen des SHA-256. 16 Zeichen
|
||||
* sind 64 Bit; bei hoechstens ein paar Dutzend Punkten je Event ist
|
||||
* eine Verwechslung ausgeschlossen, und der Schluessel bleibt kurz
|
||||
* genug, um in einer Adresse zu stehen. */
|
||||
export function punktSchluessel(zeile) {
|
||||
return createHash("sha256").update(punktKern(zeile), "utf8")
|
||||
.digest("hex").slice(0, 16);
|
||||
}
|
||||
|
||||
/** Zerlegt ein Textfeld in Punkte.
|
||||
*
|
||||
* LEERE ZEILEN FALLEN WEG, aber nicht die Absicht dahinter: Wer eine
|
||||
* Leerzeile zwischen zwei Bloecke setzt, trennt -- und das ergibt in
|
||||
* einer Liste ohnehin keinen eigenen Punkt.
|
||||
*
|
||||
* GLEICHE PUNKTE NUR EINMAL: Stuende „22 Live-Stunden" zweimal da,
|
||||
* haetten beide denselben Schluessel. Ein Haken wuerde dann an zwei
|
||||
* Stellen gleichzeitig erscheinen und die Zaehlung „2 von 3" waere
|
||||
* falsch. Der zweite faellt deshalb weg -- zusammen mit der Zeile,
|
||||
* denn eine doppelte Bedingung ist ohnehin ein Tippfehler. */
|
||||
export function punkteAusText(text) {
|
||||
const raus = [];
|
||||
const gesehen = new Set();
|
||||
for (const zeile of String(text || "").split(/\r?\n/)) {
|
||||
const kern = punktKern(zeile);
|
||||
if (!kern) continue;
|
||||
const schluessel = punktSchluessel(zeile);
|
||||
if (gesehen.has(schluessel)) continue;
|
||||
gesehen.add(schluessel);
|
||||
raus.push({
|
||||
schluessel,
|
||||
/* Angezeigt wird der Text OHNE das getippte Aufzaehlungszeichen:
|
||||
Die Liste setzt ihr eigenes, und zwei davon nebeneinander
|
||||
sehen nach einem Fehler aus. */
|
||||
text: String(zeile).replace(PUNKT_ZEICHEN, "").trim(),
|
||||
});
|
||||
}
|
||||
return raus;
|
||||
}
|
||||
|
||||
const jetzt = () => new Date().toISOString();
|
||||
|
||||
function angemeldet(req, res, next) {
|
||||
@@ -1525,6 +1608,73 @@ bereicheRouter.get("/workspace/api/bereich/:bereich", (req, res) => {
|
||||
}
|
||||
for (const e of eintraege) e.antworten = jeE.get(e.id) || [];
|
||||
}
|
||||
|
||||
/* ==== DIE PUNKTE EINES EVENTS (03.10.2026) ====================
|
||||
|
||||
Aus dem Fliesstext in event_aufgaben/event_regeln wird eine
|
||||
Liste, und an die Aufgaben kommen die Haken der eigenen
|
||||
Person. Zerlegt wird HIER und nicht im Browser: Der Schluessel
|
||||
eines Punktes entscheidet, wo ein Haken landet -- eine zweite
|
||||
Rechenvorschrift dafuer waere die, die beim naechsten Umbau
|
||||
auslaeuft, und zwar still.
|
||||
|
||||
IN EINER ABFRAGE FUER DIE GANZE LISTE, wie bei den Bildern und
|
||||
den Antworten darueber. Ein Event hat ein Dutzend Punkte und
|
||||
ein Brett ein Dutzend Events; einzeln gefragt waere das bei
|
||||
jedem Seitenaufbau ein Schwall kleiner Abfragen.
|
||||
|
||||
NUR DORT, WO ES EVENTS GIBT. Auf den uebrigen Brettern sind
|
||||
diese Felder leer, und eine Abfrage mit leerer Liste ist eine
|
||||
Abfrage ohne Zweck. */
|
||||
const events = eintraege.filter((e) => e.event_aufgaben || e.event_regeln);
|
||||
if (events.length) {
|
||||
const ichId = (req.sicht || req.person)?.id;
|
||||
const p5 = events.map(() => "?").join(", ");
|
||||
/* Meine eigenen Haken ... */
|
||||
const meine = new Map();
|
||||
for (const z of db().prepare(
|
||||
`SELECT eintrag_id, punkt FROM event_punkte
|
||||
WHERE eintrag_id IN (${p5}) AND person_id = ?`)
|
||||
.all(...events.map((e) => e.id), ichId)) {
|
||||
if (!meine.has(z.eintrag_id)) meine.set(z.eintrag_id, new Set());
|
||||
meine.get(z.eintrag_id).add(z.punkt);
|
||||
}
|
||||
|
||||
/* ... und fuer die Leitung: wer macht mit, und wie weit?
|
||||
|
||||
NUR FUER DIE LEITUNG. „Wer hat was geschafft" ist eine
|
||||
Auskunft ueber andere Personen; die gehoert nicht an jede
|
||||
Karte, die irgendwer aufmacht. Wer nicht zur Leitung gehoert,
|
||||
bekommt das Feld gar nicht erst -- nicht leer, sondern
|
||||
ueberhaupt nicht: Ein leeres Feld laedt dazu ein, es spaeter
|
||||
„versehentlich" zu fuellen. */
|
||||
const stand = new Map();
|
||||
if (istLeitung(req.person)) {
|
||||
for (const z of db().prepare(
|
||||
`SELECT ep.eintrag_id, ep.person_id, p.name, COUNT(*) AS n
|
||||
FROM event_punkte ep
|
||||
LEFT JOIN personen p ON p.id = ep.person_id
|
||||
WHERE ep.eintrag_id IN (${p5})
|
||||
GROUP BY ep.eintrag_id, ep.person_id
|
||||
ORDER BY n DESC, p.name`)
|
||||
.all(...events.map((e) => e.id))) {
|
||||
if (!stand.has(z.eintrag_id)) stand.set(z.eintrag_id, []);
|
||||
stand.get(z.eintrag_id).push({ id: z.person_id, name: z.name, geschafft: z.n });
|
||||
}
|
||||
}
|
||||
|
||||
for (const e of events) {
|
||||
const haken = meine.get(e.id) || new Set();
|
||||
e.event_punkte = punkteAusText(e.event_aufgaben)
|
||||
.map((p) => ({ ...p, meiner: haken.has(p.schluessel) }));
|
||||
e.event_regelpunkte = punkteAusText(e.event_regeln);
|
||||
/* DIE ZAHL KOMMT MIT, statt im Browser gezaehlt zu werden --
|
||||
sie steht sonst an zwei Stellen, und „2 von 3" ist genau
|
||||
die Sorte Angabe, die man nicht zweimal haben will. */
|
||||
e.event_geschafft = e.event_punkte.filter((p) => p.meiner).length;
|
||||
if (stand.size) e.event_stand = stand.get(e.id) || [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* DASS DIES EIN BRETT DES TREFFS IST, SAGT DER SERVER (11.09.2026).
|
||||
@@ -2555,6 +2705,41 @@ bereicheRouter.post("/workspace/api/bereich/:bereich", gleicheHerkunft, (req, re
|
||||
|
||||
/* ---------- Ändern und Löschen ------------------------------------------- */
|
||||
|
||||
/** Darf diese Person diesen Eintrag sehen?
|
||||
*
|
||||
* ENTSTANDEN AM 03.10.2026, weil das Banner eines Agentur-Events fuer
|
||||
* Creator mit 404 beantwortet wurde -- gemessen, nicht vermutet:
|
||||
*
|
||||
* workspace.dogfather-universe.com admin Bild=200
|
||||
* workspace.dogfather-universe.com creator Bild=404
|
||||
*
|
||||
* Der Bildweg (workspace-dateien.js) kannte bisher zwei Regeln: die
|
||||
* Treff-Regel ("wer das Brett sieht, sieht das Bild" -- aber nur fuer
|
||||
* die sieben Community-Bretter) und sonst die ABLAGE-Regel, die an
|
||||
* Creator-Zuordnungen haengt. Ein Agentur-Event ist weder das eine
|
||||
* noch das andere: Es gehoert niemandem, und `agentur` ist kein
|
||||
* Treff-Brett. Also fiel es durch beide.
|
||||
*
|
||||
* Vor dem Umbau fiel das kaum auf -- das Bild war ein Anhang unter
|
||||
* dem Text. Seit es die Buehne hinter dem Titel ist, saehe eine
|
||||
* Creatorin eine leere Flaeche an genau der Karte, die fuer sie
|
||||
* gemacht ist.
|
||||
*
|
||||
* WARUM EINE FUNKTION UND KEINE ZWEITE LISTE: Der naheliegende Weg
|
||||
* waere gewesen, in workspace-dateien.js `|| e.bereich === "agentur"`
|
||||
* zu ergaenzen. Das waere eine zweite Stelle, an der steht, wer was
|
||||
* sehen darf -- und die laeuft beim naechsten Brett aus. Hier wird
|
||||
* stattdessen dieselbe Regel gefragt, die auch das Brett selbst
|
||||
* benutzt (sichtbarEintrag). Was dort verborgen ist, bleibt es auch
|
||||
* hier; es gibt keine Rechteausweitung, nur keine Luecke mehr. */
|
||||
export function darfEintragSehen(person, eintragId) {
|
||||
if (!person || !Number.isInteger(Number(eintragId))) return false;
|
||||
const regel = sichtbarEintrag(person);
|
||||
if (!regel) return false;
|
||||
return !!db().prepare(`SELECT e.id ${VERBUND} WHERE ${regel.wo} AND e.id = ?`)
|
||||
.get(...regel.werte, Number(eintragId));
|
||||
}
|
||||
|
||||
function holen(req, id) {
|
||||
const regel = sichtbarEintrag(req.person);
|
||||
if (!regel) return null;
|
||||
@@ -2674,6 +2859,114 @@ function darfBildAnhaengen(req, res, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
|
||||
/* =====================================================================
|
||||
EINEN PUNKT ABHAKEN (03.10.2026)
|
||||
|
||||
Filipe: „ich will das viel geiler viel perfekter. und auch wenn man
|
||||
die eintraegt und so soll viel mehr perfektioniert und
|
||||
uebersichtlicher sein."
|
||||
|
||||
Ein Agentur-Event stellt Bedingungen -- „jeden Tag live",
|
||||
„22 Live-Stunden", „33k Diamanten". Wer mitmacht, will sehen, wo er
|
||||
steht, ohne nachzufragen. Genau das ist der Unterschied zwischen
|
||||
einem Aushang und einer Aktion, an der jemand teilnimmt.
|
||||
|
||||
JEDER HAKT NUR FUER SICH SELBST. Es gibt bewusst keinen Weg, einen
|
||||
Haken bei einer ANDEREN Person zu setzen -- auch nicht fuer die
|
||||
Leitung. „Ich habe meine 22 Stunden" ist eine Aussage ueber sich,
|
||||
und wer sie fuer andere treffen koennte, macht aus einer Selbstaus-
|
||||
kunft eine Bewertung. Die Person steht deshalb nirgends in der
|
||||
Adresse; sie kommt aus der Sitzung.
|
||||
|
||||
KEIN KOERPER BEIM DELETE (gemessen 11.09.2026, steht so schon in
|
||||
bereich.js): Nodes HTTP-Parser weist ein DELETE mit Koerper mit
|
||||
einem leeren 400 ab, bevor Express die Anfrage sieht.
|
||||
===================================================================== */
|
||||
function punktZugang(req, res, next) {
|
||||
const id = Number(req.params.id);
|
||||
const schluessel = String(req.params.punkt || "");
|
||||
/* Der Schluessel ist 16 Hexzeichen -- was anders aussieht, kommt
|
||||
nicht aus unserer Liste und hat in der Tabelle nichts zu suchen.
|
||||
Ohne diese Pruefung koennte jemand beliebigen Text ablegen und die
|
||||
Tabelle als Notizbuch missbrauchen. */
|
||||
if (!Number.isInteger(id) || !/^[0-9a-f]{16}$/.test(schluessel)) {
|
||||
return res.status(400).json({ fehler: "ungueltig" });
|
||||
}
|
||||
const eintrag = holen(req, id);
|
||||
if (!eintrag) return res.status(404).json({ fehler: "nicht_gefunden" });
|
||||
|
||||
/* GEHOERT DER PUNKT UEBERHAUPT ZU DIESEM EVENT?
|
||||
|
||||
Das ist der eigentliche Grund fuer diesen Waechter. Ohne ihn
|
||||
koennte man einen beliebigen gueltig aussehenden Schluessel
|
||||
setzen; die Zahl „3 von 3" waere danach erreichbar, ohne dass eine
|
||||
der drei Aufgaben je dastand. Eine Zaehlung, die man sich selbst
|
||||
ausdenken kann, zaehlt nichts.
|
||||
|
||||
Gefragt wird die LISTE, nicht der Text: punkteAusText ist die eine
|
||||
Stelle, die entscheidet, was ein Punkt ist. */
|
||||
const punkte = punkteAusText(eintrag.event_aufgaben);
|
||||
if (!punkte.some((p) => p.schluessel === schluessel)) {
|
||||
return res.status(404).json({ fehler: "punkt_unbekannt" });
|
||||
}
|
||||
req.eventEintrag = eintrag;
|
||||
req.eventPunkt = schluessel;
|
||||
next();
|
||||
}
|
||||
|
||||
bereicheRouter.put("/workspace/api/eintrag/:id(\\d+)/punkt/:punkt",
|
||||
angemeldet, gleicheHerkunft, punktZugang, (req, res) => {
|
||||
try {
|
||||
/* ON CONFLICT DO NOTHING und kein Fehler beim zweiten Mal: Zwei
|
||||
Klicks kurz hintereinander (oder ein zweites Geraet) sind kein
|
||||
Versehen, das man melden muesste -- das Ergebnis ist in beiden
|
||||
Faellen „der Haken ist gesetzt". */
|
||||
db().prepare(
|
||||
"INSERT INTO event_punkte (eintrag_id, person_id, punkt, gesetzt)"
|
||||
+ " VALUES (?,?,?,?) ON CONFLICT DO NOTHING")
|
||||
.run(req.eventEintrag.id, req.person.id, req.eventPunkt, jetzt());
|
||||
res.json(punktStand(req.eventEintrag, req.person.id));
|
||||
} catch (fehler) {
|
||||
console.error("[bereich] Punkt setzen:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
bereicheRouter.delete("/workspace/api/eintrag/:id(\\d+)/punkt/:punkt",
|
||||
angemeldet, gleicheHerkunft, punktZugang, (req, res) => {
|
||||
try {
|
||||
db().prepare(
|
||||
"DELETE FROM event_punkte WHERE eintrag_id = ? AND person_id = ? AND punkt = ?")
|
||||
.run(req.eventEintrag.id, req.person.id, req.eventPunkt);
|
||||
res.json(punktStand(req.eventEintrag, req.person.id));
|
||||
} catch (fehler) {
|
||||
console.error("[bereich] Punkt loeschen:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
|
||||
/** Was nach einem Haken zurueckgeht: der gueltige Stand, vom Server.
|
||||
*
|
||||
* NICHT „ok". Der Browser rechnet sonst selbst weiter („war 2, jetzt
|
||||
* 3") -- und liegt daneben, sobald zwei Fenster offen sind oder eine
|
||||
* Aufgabe inzwischen umgeschrieben wurde. Die Zahl, die auf der Karte
|
||||
* steht, kommt immer von hier. */
|
||||
function punktStand(eintrag, personId) {
|
||||
const punkte = punkteAusText(eintrag.event_aufgaben);
|
||||
const meine = new Set(db().prepare(
|
||||
"SELECT punkt FROM event_punkte WHERE eintrag_id = ? AND person_id = ?")
|
||||
.all(eintrag.id, personId).map((z) => z.punkt));
|
||||
return {
|
||||
gesamt: punkte.length,
|
||||
/* Nur zaehlen, was AUCH in der Liste steht: Wird eine Aufgabe
|
||||
umgeschrieben, bleibt ihr alter Haken als Zeile liegen (das ist
|
||||
richtig, siehe Tabelle) -- mitzaehlen darf er nicht. */
|
||||
geschafft: punkte.filter((p) => meine.has(p.schluessel)).length,
|
||||
punkte: punkte.map((p) => ({ ...p, meiner: meine.has(p.schluessel) })),
|
||||
};
|
||||
}
|
||||
|
||||
bereicheRouter.post("/workspace/api/bereich/:bereich/:id(\\d+)/bild",
|
||||
gleicheHerkunft,
|
||||
darfBildAnhaengen,
|
||||
|
||||
Reference in New Issue
Block a user