Dogi hat die Lockerung vom 04.08.2026 ("Code nur einmal am Tag") heute
zurueckgenommen. Gewaehlte Auspraegung: Code beim OEFFNEN der Seite/App,
Neuladen im selben Tab wirft nicht raus, keine Abmeldung bei Untaetigkeit.
- admin-auth.js legt die Sitzung jetzt in sessionStorage statt in
localStorage: gehoert zum Tab/App-Fenster, uebersteht F5 und Uploads,
ist beim naechsten Oeffnen weg. Zweiter Tab = eigener Code.
- Alte localStorage-Sitzung wird beim ersten Laden einmalig entfernt --
sonst waere Dogi trotz Umstellung mit dem alten Token weiter drin und
ein Token laege monatelang im Browser herum.
- Versionsnummer des Skripts hochgesetzt: Caddy liefert Assets mit
max-age=14400, der Browser haette die alte Anmelde-Logik sonst bis zu
vier Stunden weiterbenutzt.
- Der 24-Std-Ablauf bleibt als Rueckfallsicherung fuer tagelang offene
Tabs; der Server erzwingt dieselbe Grenze weiterhin selbst.
- Neuer Test pruef-verwaltung-anmeldung.mjs (12 Pruefungen: Gate beim
Oeffnen, alte Sitzung wirkungslos, falscher/richtiger Code, F5 bleibt
drin, neuer Tab verlangt Code, Abmelden raeumt auf, Untaetigkeit meldet
NICHT ab).
- pruef-verwaltung-kacheln.mjs: echten Aufruf ans Live-Backend abgefangen
und auf die Supporter-Zeilen gewartet -- der Test hatte dadurch
gelegentlich falsch gemeldet.
Co-Authored-By: Claude Opus 5 <[email protected]>
208 lines
11 KiB
JavaScript
208 lines
11 KiB
JavaScript
/* =====================================================================
|
|
Verwaltung: Stimmen-Kacheln + Supporter-Liste ein-/ausklappbar
|
|
(Nutzer-Wunsch 27.08.2026)
|
|
|
|
Geprüft wird mit nachgebildeten Daten, weil die Verwaltung hinter der
|
|
Zugangswand liegt: admin-auth.js wird durch eine Attrappe ersetzt, die
|
|
eine gültige Sitzung und feste Antworten liefert. Absichtlich lange
|
|
Namen/Texte — genau daran brechen Kachel-Layouts.
|
|
|
|
Gemessen wird:
|
|
1. Stimmen: im zugeklappten Zustand ist genau die erste Reihe
|
|
sichtbar (Anzahl = Spaltenzahl des Rasters), am Handy also 1.
|
|
2. Knopf klappt auf (alle sichtbar) und wieder zu (wieder eine Reihe).
|
|
3. Supporter: nur die 4 zuletzt Angemeldeten, Knopf zeigt alle, Knopf
|
|
klappt wieder zu.
|
|
4. Reihenfolge stimmt: die 4 sichtbaren sind die neuesten.
|
|
5. Nichts läuft am Handy seitlich über.
|
|
===================================================================== */
|
|
import { chromium } from "playwright";
|
|
import http from "http";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
|
|
const TYP = {
|
|
".html": "text/html", ".css": "text/css", ".js": "text/javascript",
|
|
".png": "image/png", ".svg": "image/svg+xml", ".webp": "image/webp",
|
|
".json": "application/json", ".ico": "image/x-icon", ".jpg": "image/jpeg",
|
|
".webmanifest": "application/manifest+json",
|
|
};
|
|
|
|
const srv = http.createServer((q, r) => {
|
|
const p = path.join(process.cwd(), decodeURIComponent(q.url.split("?")[0]));
|
|
if (fs.existsSync(p) && fs.statSync(p).isFile()) {
|
|
r.writeHead(200, { "content-type": TYP[path.extname(p)] || "text/plain" });
|
|
return r.end(fs.readFileSync(p));
|
|
}
|
|
r.writeHead(404);
|
|
r.end("");
|
|
});
|
|
|
|
let ok = 0, fehl = 0;
|
|
const funde = [];
|
|
const pruefe = (name, gut, info) => {
|
|
if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); }
|
|
else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); funde.push(name + " — " + info); }
|
|
};
|
|
|
|
const vorTagen = (n) => new Date(Date.now() - n * 864e5).toISOString();
|
|
|
|
const STIMMEN = [
|
|
"Team Dogi bedeutet für mich Zusammenhalt, echte Freundschaft und Menschen auf die man sich verlassen kann. In erster Linie bin ich sehr stolz und auch gleichzeitig dankbar das Dogi weiter gemacht hat.",
|
|
"Team Dogi bedeutet für uns Zusammenhalt, echte Freundschaft und Menschen, auf die man sich verlassen kann. Hinter all dem steht für uns vor allem ein ganz besonderer Mensch, Dogi.",
|
|
"Kurz und knapp: beste Community.",
|
|
"Ich bin seit dem ersten Tag dabei und habe hier Freunde fürs Leben gefunden — danke an alle, die das jeden Tag mittragen.",
|
|
"Ein Wahnsinns-Team mit einem Riesenherz. Jede Woche neue Aktionen, immer fair, immer ehrlich.",
|
|
"Danke für alles!",
|
|
"Hier fühlt man sich sofort willkommen, egal ob man 5 Minuten oder 5 Monate dabei ist.",
|
|
];
|
|
|
|
const TESTIMONIALS = STIMMEN.map((nachricht, i) => ({
|
|
id: "t" + i,
|
|
name: ["Sabrina-Charlotte Winterfeld-Baumgartner", "Klabine", "Ghost", "DjBeatim", "VanVan", "Miss", "Marina"][i],
|
|
tiktokUsername: ["sabrina_winterfeld_offiziell", "klabine", "_sven_85", "mairetim", "o.o.vanessa.o.o", "seelen_staub", "flauschirexi"][i],
|
|
nachricht,
|
|
status: i < 2 ? "freigegeben" : i === 2 ? "eingereicht" : "freigegeben",
|
|
avatar: null,
|
|
createdAt: vorTagen(i + 1),
|
|
}));
|
|
|
|
/* 9 Supporter, absteigend nach Anmeldedatum: s0 ist der neueste. */
|
|
const SUPPORTER = Array.from({ length: 9 }, (_, i) => ({
|
|
id: "s" + i,
|
|
display_name: ["Marina", "Miss", "Diene", "Ghost", "DjBeatim", "VanVan", "Dogi", "DogFather", "Immobilienverwaltung Sonnenhof"][i],
|
|
tiktok_username: ["flauschirexi", "seelen_staub", "_ditschi", "_sven_85", "mairetim", "o.o.vanessa.o.o", "dogfather0804", "dogfather0804", "sonnenhof_immobilienverwaltung"][i],
|
|
email: ["[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "kontakt.projektanfragen@immobilienverwaltung-sonnenhof.example.com"][i],
|
|
sub_status: "none",
|
|
total_paid_months: 0,
|
|
created_at: vorTagen(i),
|
|
progress: { cycleNumber: 1, currentCycleMonth: 0 },
|
|
}));
|
|
|
|
const ATTRAPPE = `
|
|
const SESSION = { name: "Dogi", isOwner: true, permissions: ["*"], token: "test" };
|
|
const TESTIMONIALS = ${JSON.stringify(TESTIMONIALS)};
|
|
const SUPPORTER = ${JSON.stringify(SUPPORTER)};
|
|
window.AdminAuth = {
|
|
getSession: () => SESSION,
|
|
setSession() {}, clearSession() {},
|
|
sessionHasPermission: () => true,
|
|
async apiCall(base, pfad) {
|
|
if (pfad === "/admin/testimonials/list") return { ok: true, testimonials: TESTIMONIALS };
|
|
if (pfad === "/admin/supporter/list") return { ok: true, supporters: SUPPORTER, supporter: SUPPORTER };
|
|
if (pfad === "/admin/overview") return { ok: true, premiums: [], payments: [] };
|
|
if (pfad === "/admin/events/list") return { ok: true, events: [null, null] };
|
|
if (pfad === "/admin/polls/list") return { ok: true, polls: [] };
|
|
if (pfad === "/admin/team/list") return { ok: true, members: [], team: [] };
|
|
if (pfad === "/admin/special-event") return { ok: true, event: null };
|
|
return { ok: true };
|
|
},
|
|
async doLogin() { return { ok: true }; },
|
|
async doLogout() {},
|
|
};
|
|
`;
|
|
|
|
const zustand = (seite) => seite.evaluate(() => {
|
|
const grid = document.getElementById("testimonials-liste");
|
|
const karten = Array.from(grid.querySelectorAll("[data-testimonial-id]"));
|
|
const sichtbar = (el) => !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
|
|
const spur = getComputedStyle(grid).gridTemplateColumns;
|
|
return {
|
|
spalten: spur && spur !== "none" ? spur.split(" ").filter(Boolean).length : 1,
|
|
stimmenGesamt: karten.length,
|
|
stimmenSichtbar: karten.filter(sichtbar).length,
|
|
stimmenKnopf: (() => { const b = document.getElementById("stimmen-mehr-btn"); return { da: !b.hidden, text: b.textContent.trim() }; })(),
|
|
zeilen: Array.from(document.querySelectorAll("#supporter-tbody tr[data-id]")).map((tr) => tr.querySelector(".vw-name-cell")?.textContent || ""),
|
|
supporterKnopf: (() => { const b = document.getElementById("supporter-mehr-btn"); return { da: !b.hidden, text: b.textContent.trim() }; })(),
|
|
ueberlauf: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
|
};
|
|
});
|
|
|
|
async function laden(browser, viewport, dateiname) {
|
|
const seite = await browser.newPage({ viewport });
|
|
await seite.route("**/assets/js/admin-auth.js*", (route) =>
|
|
route.fulfill({ status: 200, contentType: "text/javascript", body: ATTRAPPE })
|
|
);
|
|
/* Die Seite holt die Website-Texte mit einem eigenen fetch() direkt vom
|
|
Live-Backend — das ginge im Test wirklich ins Netz und verzögerte je
|
|
nach Laufzeit alles, was danach kommt (die Supporter-Liste wird erst
|
|
danach gefüllt). Deshalb hier abfangen: der Test soll messen, was die
|
|
Seite tut, nicht wie schnell der Server gerade antwortet. */
|
|
await seite.route("https://postfach.dogfather-universe.com/**", (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
headers: { "access-control-allow-origin": "*", "content-type": "application/json" },
|
|
body: JSON.stringify({ ok: true, texte: {} }),
|
|
})
|
|
);
|
|
seite.on("pageerror", (e) => pruefe("Kein JS-Fehler (" + dateiname + ")", false, (e.stack || e.message).split("\n").slice(0, 4).join(" | ")));
|
|
await seite.goto("http://127.0.0.1:4422/verwaltung.html");
|
|
await seite.waitForSelector("[data-testimonial-id]", { timeout: 8000 });
|
|
await seite.waitForSelector("#supporter-tbody tr[data-id]", { timeout: 8000 });
|
|
await seite.waitForTimeout(400);
|
|
return seite;
|
|
}
|
|
|
|
(async () => {
|
|
await new Promise((r) => srv.listen(4422, "127.0.0.1", r));
|
|
const browser = await chromium.launch();
|
|
|
|
/* ---------- Computer ---------- */
|
|
console.log("\n== Computer (1440x900) ==");
|
|
const pc = await laden(browser, { width: 1440, height: 900 }, "computer");
|
|
let z = await zustand(pc);
|
|
console.log(" Raster-Spalten:", z.spalten);
|
|
pruefe("Stimmen stehen nebeneinander", z.spalten >= 2, z.spalten + " Kacheln pro Reihe");
|
|
pruefe("Nur die erste Reihe sichtbar", z.stimmenSichtbar === z.spalten, z.stimmenSichtbar + " von " + z.stimmenGesamt + " sichtbar");
|
|
pruefe("Aufklapp-Knopf da", z.stimmenKnopf.da, z.stimmenKnopf.text);
|
|
pruefe("Supporter: nur 4 Zeilen", z.zeilen.length === 4, z.zeilen.join(", "));
|
|
pruefe("Supporter: die 4 neuesten", z.zeilen.join("|") === "Marina|Miss|Diene|Ghost", z.zeilen.join("|"));
|
|
pruefe("Supporter-Knopf da", z.supporterKnopf.da, z.supporterKnopf.text);
|
|
|
|
await pc.screenshot({ path: "bild-kacheln-computer-zu.png", fullPage: true });
|
|
|
|
await pc.click("#stimmen-mehr-btn");
|
|
await pc.click("#supporter-mehr-btn");
|
|
await pc.waitForTimeout(300);
|
|
z = await zustand(pc);
|
|
pruefe("Aufgeklappt: alle Stimmen sichtbar", z.stimmenSichtbar === z.stimmenGesamt, z.stimmenSichtbar + "/" + z.stimmenGesamt);
|
|
pruefe("Aufgeklappt: alle Supporter sichtbar", z.zeilen.length === 9, z.zeilen.length + " Zeilen");
|
|
pruefe("Knopftext wechselt (Stimmen)", /Weniger/.test(z.stimmenKnopf.text), z.stimmenKnopf.text);
|
|
pruefe("Knopftext wechselt (Supporter)", /Weniger/.test(z.supporterKnopf.text), z.supporterKnopf.text);
|
|
await pc.screenshot({ path: "bild-kacheln-computer-offen.png", fullPage: true });
|
|
|
|
await pc.click("#stimmen-mehr-btn");
|
|
await pc.click("#supporter-mehr-btn");
|
|
await pc.waitForTimeout(400);
|
|
z = await zustand(pc);
|
|
pruefe("Wieder zugeklappt: eine Reihe Stimmen", z.stimmenSichtbar === z.spalten, z.stimmenSichtbar + "/" + z.stimmenGesamt);
|
|
pruefe("Wieder zugeklappt: 4 Supporter", z.zeilen.length === 4, z.zeilen.length + " Zeilen");
|
|
|
|
/* ---------- Handy ---------- */
|
|
console.log("\n== Handy (390x844) ==");
|
|
const handy = await laden(browser, { width: 390, height: 844 }, "handy");
|
|
let h = await zustand(handy);
|
|
pruefe("Handy: eine Kachel pro Reihe", h.spalten === 1, h.spalten + " Spalte(n)");
|
|
pruefe("Handy: nur die erste Kachel sichtbar", h.stimmenSichtbar === 1, h.stimmenSichtbar + " sichtbar");
|
|
pruefe("Handy: nur 4 Supporter", h.zeilen.length === 4, h.zeilen.length + " Zeilen");
|
|
pruefe("Handy: kein seitlicher Überlauf", h.ueberlauf <= 1, h.ueberlauf + "px");
|
|
await handy.screenshot({ path: "bild-kacheln-handy-zu.png", fullPage: true });
|
|
await handy.click("#stimmen-mehr-btn");
|
|
await handy.waitForTimeout(250);
|
|
h = await zustand(handy);
|
|
pruefe("Handy: aufgeklappt alle Stimmen", h.stimmenSichtbar === h.stimmenGesamt, h.stimmenSichtbar + "/" + h.stimmenGesamt);
|
|
|
|
/* ---------- Breitenwechsel im zugeklappten Zustand ---------- */
|
|
console.log("\n== Fenster verkleinern (zugeklappt) ==");
|
|
await pc.setViewportSize({ width: 760, height: 900 });
|
|
await pc.waitForTimeout(400);
|
|
z = await zustand(pc);
|
|
pruefe("Nach Größenänderung stimmt die erste Reihe", z.stimmenSichtbar === z.spalten, z.stimmenSichtbar + " sichtbar bei " + z.spalten + " Spalten");
|
|
|
|
await browser.close();
|
|
srv.close();
|
|
console.log("\n===== " + ok + " ok, " + fehl + " Fehler =====");
|
|
if (funde.length) { console.log("\nOffene Punkte:"); funde.forEach((f) => console.log(" - " + f)); }
|
|
process.exit(fehl ? 1 : 0);
|
|
})();
|