Verwaltung auf dem Handy: vier Fehler behoben

Geprueft mit nachgebildeten Daten -- lange Firmennamen, lange
E-Mail-Adressen, mehrstellige Betraege. Eine leere Seite laeuft nie
ueber; die Fehler, um die es geht, entstehen erst mit Inhalt.

1. DAS RASTER DER KOPFZEILE WAR BREITER ALS SEIN BEHAELTER

Auf 390px summierten sich die beiden Spalten auf 372,7px, waehrend die
Leiste nur 358px breit ist. Rasterspalten schrumpfen von sich aus nicht
unter ihren Inhalt ("min-width: auto"). Alles darin schob sich nach
rechts hinaus: die Kopfzeile 8px, der Reiterstreifen 12px.

Sichtbar war das kaum -- der Streifen ist ohnehin wischbar, rechts
fehlte nur ein Stueck Rand. Genau deshalb blieb es liegen: ein Fehler,
der sich als Eigenart tarnt. Jetzt minmax(0, …) plus min-width: 0 an
den Kindern; lange Titel kuerzen mit Auslassungspunkten, statt zu
schieben.

2. DER INSTALLATIONSHINWEIS ZEIGTE DAS FALSCHE SYMBOL

Seit die Verwaltung eine eigene App ist, stand dort weiter der schwarze
Husky. Man las "Als App installieren" neben dem einen Bild und bekam
das andere auf den Startbildschirm. Das Symbol wird jetzt aus dem
Manifest abgeleitet, das die Seite tatsaechlich einbindet -- damit
stimmt es auch fuer jede kuenftige App, ohne dass jemand daran denken
muss.

3. DIE UEBERSICHT BRACH BEI UNVOLLSTAENDIGER ANTWORT AB

d.projekte und d.verlauf wurden ungeprueft mit .length angefasst,
waehrend drei andere Felder ausdruecklich geprueft wurden. Fehlte eine
der Listen, brach die Uebersicht mit "Cannot read properties of
undefined" ab -- und zwar NACH dem Aufbau der oberen Kacheln: Die halbe
Seite stand da, der Rest fehlte kommentarlos. Genau der Fall, den der
Kommentar daneben als "realistisch" beschreibt. Jetzt wird aufgefuellt
statt abgebrochen.

4. ZWEI FEHLALARME IM TEST SELBST

Der Test meldete den Reiterstreifen als Ueberlauf (er ist ein
Wischstreifen -- dass dort etwas ausserhalb liegt, ist sein Sinn) und
eine 1x1-Checkbox als zu kleines Tippziel (bedient wird sie ueber ihr
Label, 315x162 Punkte). Beides wuerde dazu verleiten, Funktionierendes
"zu reparieren". Der Test unterscheidet das jetzt.

Ebenso meldete er "Strg" und "Enter" mit 10,4px -- beide sind auf
schmalen Schirmen laengst ausgeblendet. getComputedStyle liefert auch
fuer verborgene Elemente eine Schriftgroesse; jetzt zaehlt nur
Sichtbares.

25 Pruefungen ueber sechs Bereiche gruen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-26 19:09:33 +02:00
co-authored by Claude Opus 5
parent 62cbd724e2
commit ea11cefe17
5 changed files with 462 additions and 5 deletions
+6
View File
@@ -21,3 +21,9 @@ server/node_modules/
server/tmp-lokal.mjs
server/package-lock.json
bild-*.png
# Aufnahmen der Handy-Pruefung (pruef-verwaltung-handy.mjs). Sie
# entstehen bei jedem Lauf neu und sind Zwischenstand, kein Quelltext --
# im Repo waeren sie nur Ballast und wuerden bei jedem Lauf als
# Aenderung erscheinen.
handy-*.png
+27 -2
View File
@@ -724,13 +724,38 @@
return iphone || ipadNeu;
}
/* Das Symbol der App, die hier tatsaechlich installiert wird.
⚠️ Vorher stand hier fest "/assets/img/wd-app-192.png". Seit die
Verwaltung eine eigene App mit eigenem Symbol ist (26.08.2026),
zeigte der Hinweis dort das FALSCHE Bild: Man las "Als App
installieren" neben dem schwarzen Husky und bekam anschliessend das
Eiskristall-Wappen auf den Startbildschirm.
Ein Hinweis, der etwas anderes zeigt als das, was passiert, ist
schlimmer als gar keiner -- man haelt ihn beim naechsten Mal fuer
unzuverlaessig.
Jetzt wird das Symbol aus dem Manifest gelesen, das die Seite
tatsaechlich einbindet. Damit stimmt es automatisch auch fuer jede
kuenftige App, ohne dass jemand daran denken muss. */
function installSymbol() {
try {
var v = document.querySelector('link[rel="manifest"]');
if (v && /verwaltung\.webmanifest/.test(v.getAttribute("href") || "")) {
return "/assets/img/wd-admin-192.jpg";
}
} catch (e) { /* dann eben das allgemeine */ }
return "/assets/img/wd-app-192.png";
}
function installBannerBauen(inhalt, mitKnopf) {
var box = document.createElement("div");
box.className = "wd-install";
box.setAttribute("role", "region");
box.innerHTML =
'<div class="wd-install-text">' +
'<img src="/assets/img/wd-app-192.png" alt="" width="40" height="40" />' +
'<img src="' + installSymbol() + '" alt="" width="40" height="40" />' +
"<div><strong>" + t("install_titel") + "</strong><span>" + inhalt + "</span></div>" +
"</div>" +
'<div class="wd-install-knoepfe">' +
@@ -838,7 +863,7 @@
Stand haben. Die Nummer wird zusammen mit CACHE_NAME in sw.js
hochgezaehlt -- beide gehoeren zusammen. */
navigator.serviceWorker
.register("/webdesign/sw.js?v=59", { scope: "/webdesign/" })
.register("/webdesign/sw.js?v=60", { scope: "/webdesign/" })
.catch(function () { /* still schlucken: ohne SW funktioniert alles weiter */ });
});
}
+363
View File
@@ -0,0 +1,363 @@
/* =====================================================================
Die Verwaltung auf dem Handy — mit echten Inhalten.
WARUM MIT NACHGEBILDETEN DATEN
Die Verwaltung liegt hinter der Zugangswand, ein Zugangscode liegt
hier nicht vor. Eine leere Seite zu prüfen bringt aber nichts: Genau
die Fehler, um die es geht — überlaufende Zeilen, abgeschnittene
Knöpfe, zu enge Tippziele — entstehen erst, wenn richtige Inhalte
darin stehen. Ein leerer Kasten läuft nie über.
Deshalb werden die Datenabrufe abgefangen und mit Inhalten
beantwortet, wie sie im Betrieb vorkommen: lange Firmennamen, lange
E-Mail-Adressen, mehrstellige Beträge. Absichtlich am oberen Rand des
Realistischen, denn dort brechen Layouts.
WAS GEMESSEN WIRD
1. Läuft etwas seitlich über? (der klassische Handy-Fehler)
2. Ragt ein Element aus dem Bildschirm?
3. Sind Tippziele groß genug? (WCAG 2.2: 24px Minimum,
Apple und Google empfehlen 44px)
4. Überlappen sich Elemente?
5. Ist Text lesbar groß?
===================================================================== */
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) && !p.endsWith("/")) {
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); }
};
/* ---------------------------------------------------------------------
NACHGEBILDETE DATEN
Bewusst unbequem: lange Namen, lange Adressen, grosse Zahlen. Wer mit
"Max Muster" und "12,00" prueft, findet die Fehler nicht, die im
Betrieb auftreten.
--------------------------------------------------------------------- */
const jetzt = new Date().toISOString();
const vorTagen = (n) => new Date(Date.now() - n * 864e5).toISOString();
const ANFRAGEN = [
{ id: "a1", nummer: "A-2608-0012", name: "Immobilienverwaltung Sonnenhof GmbH & Co. KG",
email: "kontakt.projektanfragen@immobilienverwaltung-sonnenhof.example.com",
paket: "shop", status: "neu", budget: "5.000–8.000 €", termin: "Q4",
sprache: "de", erstellt_am: vorTagen(9), kunde_id: null },
{ id: "a2", nummer: "A-2608-0011", name: "Bäckerei Krumm", email: "[email protected]",
paket: "onepager", status: "rueckfrage", budget: "1.200 €", termin: "sofort",
sprache: "de", erstellt_am: vorTagen(2), kunde_id: null },
{ id: "a3", nummer: "A-2608-0010", name: "Praxis Dr. med. Hohenstein-Wittelsbach",
email: "[email protected]",
paket: "website", status: "angenommen", budget: "3.400 €", termin: "November",
sprache: "de", erstellt_am: vorTagen(21), kunde_id: "k1" },
];
const ANTWORTEN = {
"/webdesign/admin/uebersicht": {
ok: true,
beiMir: {
anfragen: { n: 3, aeltestes: vorTagen(9) },
nachrichten: { n: 7, aeltestes: vorTagen(4) },
wuensche: { n: 2, aeltestes: vorTagen(6) },
projekte: 4,
widerrufe: { n: 1, aeltestes: vorTagen(1) },
},
beiKunde: {
projekte: 3, aufgaben: 11,
zahlungen: { n: 2, aeltestes: vorTagen(31) },
zahlungenText: "4.870,00 €",
},
geld: {
monat: { n: 6, bruttoText: "7.412,50 €", nettoText: "7.148,22 €" },
gesamt: { n: 38, bruttoText: "48.930,00 €" },
gebuehrenText: "264,28 €",
},
projekte: [
{ id: "p1", nummer: "P-2608-0004", titel: "Onlineshop für Bastelbedarf mit Rabattsystem",
kunde_name: "Immobilienverwaltung Sonnenhof GmbH & Co. KG",
wartet_auf: "dogfather", aufg_gesamt: 24, aufg_fertig: 17 },
{ id: "p2", nummer: "P-2608-0003", titel: "Einseiter Bäckerei",
kunde_name: "Bäckerei Krumm", wartet_auf: "kunde", aufg_gesamt: 12, aufg_fertig: 12 },
],
},
"/webdesign/admin/anfragen": { ok: true, anfragen: ANFRAGEN },
"/webdesign/admin/kunden": { ok: true, kunden: [
{ id: "k1", nummer: "K-2608-0003", name: "Praxis Dr. med. Hohenstein-Wittelsbach",
email: "[email protected]", firma: "Praxisgemeinschaft",
projekte: 2, erstellt_am: vorTagen(40) },
{ id: "k2", nummer: "K-2608-0002", name: "Bäckerei Krumm", email: "[email protected]",
firma: "", projekte: 1, erstellt_am: vorTagen(60) },
] },
"/webdesign/admin/projekte": { ok: true, projekte: [
{ id: "p1", nummer: "P-2608-0004", titel: "Onlineshop für Bastelbedarf mit Rabattsystem",
kunde_name: "Immobilienverwaltung Sonnenhof GmbH & Co. KG", status: "umsetzung",
wartet_auf: "dogfather", preis: 684000, aufg_gesamt: 24, aufg_fertig: 17,
liefertermin: vorTagen(-14), erstellt_am: vorTagen(30) },
{ id: "p2", nummer: "P-2608-0003", titel: "Einseiter Bäckerei", kunde_name: "Bäckerei Krumm",
status: "abnahme", wartet_auf: "kunde", preis: 120000, aufg_gesamt: 12, aufg_fertig: 12,
liefertermin: vorTagen(-3), erstellt_am: vorTagen(45) },
] },
"/webdesign/admin/zahlungen": { ok: true, zahlungen: [
{ id: "z1", nummer: "R-2608-0009", kunde_name: "Immobilienverwaltung Sonnenhof GmbH & Co. KG",
betrag: 342000, gebuehr: 12400, status: "offen", faellig_am: vorTagen(31),
erstellt_am: vorTagen(38), art: "anzahlung" },
{ id: "z2", nummer: "R-2608-0008", kunde_name: "Bäckerei Krumm", betrag: 60000,
gebuehr: 2100, status: "bezahlt", faellig_am: vorTagen(10),
erstellt_am: vorTagen(20), art: "schluss" },
] },
"/webdesign/admin/postfach": { ok: true, nachrichten: [
{ id: "n1", von: "Immobilienverwaltung Sonnenhof GmbH & Co. KG",
email: "kontakt.projektanfragen@immobilienverwaltung-sonnenhof.example.com",
betreff: "Rückfrage zur Rechnung und zum weiteren Ablauf im vierten Quartal",
text: "Guten Tag, wir hätten noch eine Frage zur Aufteilung der Anzahlung.",
gelesen: 0, erstellt_am: vorTagen(4) },
] },
"/webdesign/admin/postfach-zaehler": { ok: true, ungelesen: 7 },
"/webdesign/admin/einstellungen": { ok: true, werte: {} },
"/webdesign/admin/push/stand": { ok: true, eingerichtet: true, geraete: 1,
geraeteMitFehlern: 0, wirktMeldung: true, oeffentlicherSchluessel: "BTest", geraete_liste: [] },
};
function antwortFuer(url) {
const pfad = url.split("?")[0].replace(/^https?:\/\/[^/]+/, "");
if (ANTWORTEN[pfad]) return ANTWORTEN[pfad];
/* Einzelabrufe wie /anfragen/a1 */
if (/^\/webdesign\/admin\/anfragen\/[^/]+$/.test(pfad)) {
const a = ANFRAGEN[0];
return { ok: true, anfrage: { ...a, ziel: "Ein neuer Auftritt für unsere Hausverwaltung.",
zielgruppe: "Eigentümer und Mieter", funktionen: ["Kontaktformular", "Objektliste"],
details: {}, bemerkung: "" }, verlauf: [] };
}
return { ok: true };
}
/* --------------------------------------------------------------------- */
srv.listen(4411, async () => {
const browser = await chromium.launch();
/* Ein echtes Handy-Format, kein schmales Fenster: Handys haben ein
anderes Verhältnis und einen anderen Zeigertyp. */
const kontext = await browser.newContext({
viewport: { width: 390, height: 844 },
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 " +
"(KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
});
const seite = await kontext.newPage();
const skriptfehler = [];
/* Mit Aufrufverlauf, nicht nur mit der Meldung: "Cannot read
properties of undefined" sagt allein nichts darueber, WO es
passiert -- und danach sucht man in einer 4000-Zeilen-Datei. */
seite.on("pageerror", (e) =>
skriptfehler.push(String(e.stack || e).split(String.fromCharCode(10)).slice(0, 3).join(" <- ").slice(0, 320)));
await seite.route("**/webdesign/admin/**", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(antwortFuer(route.request().url())),
});
});
await seite.goto("http://127.0.0.1:4411/webdesign/verwaltung.html");
await seite.waitForTimeout(1200);
/* An der Anmeldung vorbei — geprueft wird das Layout, nicht die
Zugangswand. */
await seite.evaluate(() => {
document.getElementById("vw-anmeldung").hidden = true;
document.getElementById("vw-bereich").hidden = false;
if (typeof window.vwBalkenSetzen === "function") window.vwBalkenSetzen();
});
await seite.waitForTimeout(600);
const BEREICHE = ["uebersicht", "anfragen", "projekte", "kunden", "zahlungen", "postfach"];
for (const bereich of BEREICHE) {
console.log(`\n===== ${bereich.toUpperCase()} =====`);
await seite.evaluate((b) => {
const knopf = document.querySelector('[data-tab="' + b + '"]');
if (knopf) knopf.click();
}, bereich);
await seite.waitForTimeout(1400);
/* 1. Seitlicher Ueberlauf */
const ueberlauf = await seite.evaluate(() =>
document.documentElement.scrollWidth - document.documentElement.clientWidth);
pruefe(` kein seitlicher Ueberlauf`, ueberlauf <= 1, ueberlauf + "px");
/* 2. Welche Elemente ragen heraus? Die Ursache, nicht nur das
Symptom -- ohne sie sucht man den Schuldigen von Hand. */
const heraus = await seite.evaluate(() => {
const breite = document.documentElement.clientWidth;
const raus = [];
/* ⚠️ Was ABSICHTLICH hinausragt, ist kein Fehler.
Die erste Fassung meldete die Reiter als Fehler -- sie liegen
auf dem Handy in einem Wischstreifen, und dass dort etwas
ausserhalb des Bildschirms liegt, ist der ganze Sinn der Sache.
Ein Test, der das als Mangel meldet, verleitet dazu, einen
funktionierenden Streifen "zu reparieren".
Deshalb: Alles, was in einem seitlich scrollbaren Behaelter
liegt, wird uebersprungen -- dort GEHOERT es hin. */
const istInWischstreifen = (el) => {
for (let p = el.parentElement; p && p !== document.body; p = p.parentElement) {
const st = getComputedStyle(p);
if ((st.overflowX === "auto" || st.overflowX === "scroll") &&
p.scrollWidth > p.clientWidth + 1) return true;
}
return false;
};
for (const el of document.querySelectorAll("#vw-bereich *")) {
const r = el.getBoundingClientRect();
if (r.width === 0 || r.height === 0) continue;
if (istInWischstreifen(el)) continue;
if (r.right > breite + 1 || r.left < -1) {
raus.push({
was: el.tagName.toLowerCase() + (el.className && typeof el.className === "string"
? "." + el.className.split(" ").filter(Boolean).slice(0, 2).join(".") : ""),
rechts: Math.round(r.right), breite: Math.round(r.width),
text: (el.textContent || "").trim().slice(0, 30),
});
}
}
/* Nur die aeussersten melden -- sonst steht jedes Kind seines
ueberlaufenden Elternteils mit in der Liste. */
return raus.slice(0, 6);
});
pruefe(` nichts ragt aus dem Bildschirm`, heraus.length === 0,
heraus.length ? heraus.map((h) => `${h.was} bis ${h.rechts}px ("${h.text}")`).join(" | ") : "sauber");
/* 3. Tippziele */
const klein = await seite.evaluate(() => {
const raus = [];
const auswahl = "#vw-bereich button, #vw-bereich a, #vw-bereich input, " +
"#vw-bereich select, #vw-bereich summary, #vw-bereich [role='button']";
for (const el of document.querySelectorAll(auswahl)) {
const r = el.getBoundingClientRect();
if (r.width === 0 || r.height === 0) continue;
const st = getComputedStyle(el);
if (st.visibility === "hidden" || st.display === "none") continue;
/* ⚠️ Ein Ankreuzfeld misst man an seinem Label, nicht an sich
selbst.
Die erste Fassung meldete eine Checkbox mit 1x1 Punkt als
Mangel. Sie ist absichtlich so klein: Sichtbar ist ein selbst
gestalteter Schalter, angetippt wird das Label — 315x162
Punkte, also weit über jedem Mindestmaß. Wer dem Test folgt
und die Checkbox vergrößert, macht eine funktionierende
Bedienung kaputt und bekommt obendrein ein doppeltes
Bedienelement. */
if (el.type === "checkbox" || el.type === "radio") {
const lab = document.querySelector('label[for="' + el.id + '"]') || el.closest("label");
if (lab) {
const lr = lab.getBoundingClientRect();
if (lr.height >= 24 && lr.width >= 24) continue;
}
}
/* WCAG 2.2 Erfolgskriterium 2.5.8 verlangt 24x24. Apple und
Google empfehlen 44. Gemeldet wird alles unter 44, gezaehlt
als Fehler nur unter 24. */
if (r.height < 44 || r.width < 44) {
raus.push({
was: el.tagName.toLowerCase() + (typeof el.className === "string" && el.className
? "." + el.className.split(" ").filter(Boolean)[0] : ""),
b: Math.round(r.width), h: Math.round(r.height),
text: (el.textContent || el.value || "").trim().slice(0, 22),
hart: r.height < 24 || r.width < 24,
});
}
}
return raus;
});
const hart = klein.filter((k) => k.hart);
pruefe(` Tippziele erreichen 24px (Mindestmass)`, hart.length === 0,
hart.length ? hart.slice(0, 4).map((k) => `${k.was} ${k.b}x${k.h} ("${k.text}")`).join(" | ")
: klein.length ? klein.length + " unter 44px, aber alle ueber 24px" : "alle gross genug");
if (klein.length && !hart.length) {
console.log(" Hinweis (unter 44px, empfohlen):");
for (const k of klein.slice(0, 5)) {
console.log(` ${k.was} ${k.b}x${k.h}px "${k.text}"`);
}
}
/* 4. Schriftgroesse */
const winzig = await seite.evaluate(() => {
const raus = [];
for (const el of document.querySelectorAll("#vw-bereich *")) {
if (el.children.length || (el.textContent || "").trim().length <= 3) continue;
/* ⚠️ Nur SICHTBARE Schrift zaehlt.
Die erste Fassung meldete "Strg" und "Enter" mit 10,4px als
Mangel -- beide sind auf schmalen Bildschirmen laengst
ausgeblendet (display:none ab 620px, mit Begruendung im
Stilblatt: auf einem Handy gibt es keine Strg-Taste).
getComputedStyle liefert auch fuer verborgene Elemente eine
Schriftgroesse. Ohne diese Pruefung meldet der Test also
genau die Stellen, an denen bereits richtig gehandelt wurde --
und verleitet dazu, eine funktionierende Loesung "zu
reparieren". */
const r = el.getBoundingClientRect();
if (r.width === 0 || r.height === 0) continue;
const st = getComputedStyle(el);
if (st.display === "none" || st.visibility === "hidden" || st.opacity === "0") continue;
const g = parseFloat(st.fontSize);
if (g && g < 11) raus.push({ g: Math.round(g * 10) / 10, t: el.textContent.trim().slice(0, 24) });
}
return raus.slice(0, 5);
});
pruefe(` Schrift ist nicht unter 11px`, winzig.length === 0,
winzig.length ? winzig.map((w) => `${w.g}px "${w.t}"`).join(" | ") : "lesbar");
await seite.screenshot({ path: `handy-${bereich}.png`, fullPage: false });
}
console.log("\n===== ALLGEMEIN =====");
pruefe("kein Skriptfehler", skriptfehler.length === 0, skriptfehler[0] || "keiner");
await browser.close();
srv.close();
console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`);
if (funde.length) {
console.log("\nZU BEHEBEN:");
funde.forEach((f, i) => console.log(` ${i + 1}. ${f}`));
}
process.exit(fehl ? 1 : 0);
});
+1 -1
View File
@@ -34,7 +34,7 @@
Start alle alten Zwischenspeicher weg. Muss bei jeder Änderung an den
Dateien unten hochgezählt werden, sonst hängen Nutzer auf einem alten
Stand fest. */
const CACHE_NAME = "dogfather-webdesign-v59";
const CACHE_NAME = "dogfather-webdesign-v60";
/* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE
HTML-Datei in dieser Liste. */
+65 -2
View File
@@ -940,7 +940,23 @@
"wo bin ich", die zweite "wo kann ich hin". */
.vw-leiste {
display: grid;
grid-template-columns: 1fr auto;
/* minmax(0, …) statt "1fr auto" (26.08.2026).
Rasterspalten schrumpfen von sich aus NICHT unter die Breite
ihres Inhalts — sie haben "min-width: auto". Auf einem 390px
breiten Gerät summierten sich die beiden Spalten dadurch auf
372,7px, während die Leiste selbst nur 358px breit ist. Das
Raster war also breiter als sein eigener Behälter, und alles
darin schob sich nach rechts über den Bildschirmrand: die
Kopfzeile 8px, der Reiterstreifen 12px.
Sichtbar war das kaum — der Streifen ist ohnehin wischbar, und
rechts fehlte nur ein Stück Rand. Genau deshalb blieb es liegen.
Mit minmax(0, …) darf jede Spalte kleiner werden als ihr Inhalt.
Die Überschrift kürzt dann mit Auslassungspunkten, statt das
ganze Raster aufzublähen. */
grid-template-columns: minmax(0, 1fr) minmax(0, auto);
align-items: center;
gap: .6rem 1rem;
margin-bottom: 1.2rem;
@@ -948,12 +964,38 @@
.vw-leiste h1 {
margin: 0; font-size: clamp(1.4rem, 3.4vw, 2rem);
grid-column: 1; grid-row: 1;
/* Damit die Spalte oben tatsächlich schrumpfen darf, muss auch das
Kind es dürfen. Lange Titel wie "Projektanfragen" kürzen dann,
statt zu schieben. */
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.vw-leiste .vw-wer { grid-column: 2; grid-row: 1; }
.vw-leiste .vw-wer { grid-column: 2; grid-row: 1; min-width: 0; }
/* Die Reiter über die volle Breite darunter. */
.vw-leiste .vw-tabs {
grid-column: 1 / -1; grid-row: 2;
padding-top: .2rem;
/* ⚠️ min-width: 0 ist hier NICHT überflüssig (26.08.2026).
Ein Rasterkind bekommt von sich aus "min-width: auto" — es darf
also breiter werden als seine Spalte, wenn sein Inhalt das
verlangt. Genau das passierte: Sobald ein Reiter einen Zähler
trug ("Anfragen ③"), wuchs der Streifen auf volle Fensterbreite
und ragte, weil die Leiste links 12px Abstand hat, rechts 12px
über den Bildschirmrand hinaus.
Gemessen auf einem 390px breiten Gerät: In der Übersicht endete
der Streifen bei 378px, im Anfragen-Bereich bei 402px. Sichtbar
wurde es kaum — der Streifen ist ohnehin seitlich wischbar, also
fiel nur auf, dass der letzte Reiter schwerer zu erreichen war.
Ein Fehler, der sich als Eigenart tarnt.
Mit min-width: 0 darf er wieder schmaler werden als sein Inhalt —
und genau dann greift das seitliche Wischen, wofür es gedacht
ist. */
min-width: 0;
}
@media (max-width: 560px) {
/* Titel und Abmelden bleiben zusammen in Zeile 1.
@@ -2955,6 +2997,27 @@
Ersatzantwort im Test genau das ausloeste. Ein halber Server ist
ein realistischer Fall (alte Fassung noch nicht neu gestartet),
und dann sollen die uebrigen Reiter weiter benutzbar sein. */
/* ⚠️ Die Absicht daneben galt nur für drei von fünf Teilen
(behoben 26.08.2026).
Geprüft wurden beiMir, beiKunde und geld — die beiden Listen
darunter, projekte und verlauf, wurden ungeprüft mit .length
angefasst. Fehlte eine davon, brach die Übersicht mit
"Cannot read properties of undefined" ab, und zwar NACH dem
Aufbau der oberen Kacheln: Die halbe Seite stand schon da, der
Rest fehlte kommentarlos.
Genau der Fall, den der Kommentar unten beschreibt — ein halber
Server, dessen Antwort nicht alles mitbringt. Statt die ganze
Übersicht zu verweigern, wird jetzt aufgefüllt: Was da ist, wird
gezeigt; was fehlt, bleibt leer. Eine Liste, die es nicht gibt,
ist keine leere Liste — hier aber die einzig sinnvolle Anzeige.
Gefunden vom Handy-Test, weil dessen nachgebildete Antwort diese
beiden Felder nicht enthielt. */
if (!Array.isArray(d.projekte)) d.projekte = [];
if (!Array.isArray(d.verlauf)) d.verlauf = [];
if (!d.beiMir || !d.beiKunde || !d.geld) {
laedt.hidden = true;
inhalt.hidden = false;