diff --git a/assets/css/webdesign.css b/assets/css/webdesign.css index b66453a7..1692adb3 100644 --- a/assets/css/webdesign.css +++ b/assets/css/webdesign.css @@ -1390,3 +1390,155 @@ a.wd-karte:hover, .wd-karte--klickbar:hover { .po-legende .po-l-2 { background: var(--wd-p2); } .po-legende .po-l-3 { background: var(--wd-p3); } .po-legende .po-l-4 { background: var(--wd-p4); } + +/* ===================================================================== + PORTAL — AUFGABENLISTE UND POSTFACH (22.08.2026) + + "wenn sie drauf drücken sollen sie auch sehen was ich schon gemacht + habe von dem was im plan war." + + Dieselben vier Prozessfarben wie auf der Ablaufseite, in der Vorschau + und in der Verwaltung. Das ist der eigentliche Wert: Eine Farbe + bedeutet im ganzen Bereich dasselbe — sonst wäre sie Dekoration. + ===================================================================== */ + +/* Die grosse Zahl. "9 von 14 erledigt" ist greifbar, "64 %" ist eine + Rechnung, die niemand fühlt. */ +.po-aufg-stand { margin: .9rem 0 .2rem; font-size: .92rem; color: var(--wd-text-soft); } +.po-aufg-stand b { + font-family: var(--font-head); font-weight: 800; + font-size: 1.7rem; color: var(--wd-text); margin-right: .3rem; +} + +.po-fortschritt { display: flex; gap: 4px; height: 8px; margin-bottom: 1.2rem; } +.po-fs-teil { flex: 1; border-radius: 999px; background: rgba(255,255,255,.07); position: relative; overflow: hidden; } +.po-fs-teil > i { position: absolute; inset: 0 auto 0 0; border-radius: 999px; } +.po-fs-teil[data-k="start"] > i { background: var(--wd-p1); } +.po-fs-teil[data-k="gestaltung"] > i { background: var(--wd-p2); } +.po-fs-teil[data-k="umsetzung"] > i { background: var(--wd-p3); } +.po-fs-teil[data-k="abschluss"] > i { background: var(--wd-p4); } + +/* ---------- "Das brauche ich noch von dir" ---------- + Ein EIGENER Kasten ganz oben, nicht nur eine farbige Zeile irgendwo + in der Liste. Wer eine lange Liste sieht, liest sie als Bericht über + fremde Arbeit und überliest seinen eigenen Teil — und genau daraus + entstehen die meisten Verzögerungen. + + Gold, weil es im ganzen Bereich für "das betrifft dich" steht. Nicht + Rot: Es ist eine Bitte, kein Fehler. */ +.po-dubist { + margin: 0 0 1.3rem; + padding: .9rem 1rem 1rem; + border-radius: 14px; + background: rgba(232,200,119,.07); + border: 1px solid rgba(232,200,119,.32); +} +.po-dubist h4 { + margin: 0; font-size: 1rem; color: var(--wd-gold); + font-family: var(--font-head); font-weight: 700; +} + +/* Der beruhigende Gegenpart. Wenn nichts offen ist, soll das auch + dastehen — Abwesenheit einer Aufforderung ist eine gute Nachricht und + darf ruhig ausgesprochen werden. */ +.po-allesgut { + margin: 0 0 1.3rem; padding: .75rem .95rem; + border-radius: 12px; font-size: .9rem; + background: rgba(111,214,163,.08); + border: 1px solid rgba(111,214,163,.28); + color: var(--wd-ok); +} + +/* ---------- Gruppen und Zeilen ---------- */ +.po-aufg-gruppe { margin-bottom: 1.1rem; } +.po-aufg-kopf { + display: flex; align-items: baseline; gap: .5rem; + font-size: .74rem; letter-spacing: .13em; text-transform: uppercase; + margin-bottom: .45rem; padding-bottom: .35rem; + border-bottom: 1px solid var(--wd-rand); +} +.po-aufg-zahl { margin-left: auto; letter-spacing: 0; text-transform: none; font-size: .8rem; color: var(--wd-text-soft); } +.po-aufg-gruppe[data-k="start"] .po-aufg-kopf { color: var(--wd-p1); } +.po-aufg-gruppe[data-k="gestaltung"] .po-aufg-kopf { color: var(--wd-p2); } +.po-aufg-gruppe[data-k="umsetzung"] .po-aufg-kopf { color: var(--wd-p3); } +.po-aufg-gruppe[data-k="abschluss"] .po-aufg-kopf { color: var(--wd-p4); } + +/* Zeile und Spalte ausdrücklich für JEDES Kind. + + Ohne Spaltenangabe geht es schief, und zwar auf eine Art, die man + nicht errät: Das Raster platziert zuerst alle Elemente mit + FESTGELEGTER Zeile und erst danach die freien. Kästchen und Marke + haben beide eine feste Zeile — sie würden zusammen nach vorne rutschen + und der Titel landete ganz rechts. Genau das ist am 22.08.2026 in der + Verwaltung passiert und erst am Screenshot aufgefallen. */ +.po-aufg { + display: grid; + grid-template-columns: auto minmax(0,1fr) auto; + gap: .12rem .7rem; align-items: start; + padding: .55rem .6rem; margin-bottom: .25rem; + border-radius: 10px; border: 1px solid transparent; +} +.po-aufg-haken { grid-row: 1 / span 2; grid-column: 1; } +.po-aufg-titel { grid-row: 1; grid-column: 2; font-size: .94rem; color: var(--wd-text); overflow-wrap: anywhere; } +.po-aufg-marken { grid-row: 1; grid-column: 3; } +.po-aufg-text { grid-row: 2; grid-column: 2 / span 2; font-size: .82rem; color: var(--wd-text-soft); line-height: 1.5; } + +.po-aufg--fertig .po-aufg-titel { color: var(--wd-text-soft); text-decoration: line-through; text-decoration-thickness: 1px; } +.po-aufg--weg .po-aufg-titel { color: var(--wd-text-soft); text-decoration: line-through; opacity: .55; } +.po-aufg--du { background: rgba(232,200,119,.06); } + +/* Im oberen Kasten stehen dieselben Punkte noch einmal. Dort brauchen + sie den eigenen Hintergrund nicht — der Kasten ist schon golden, ein + zweiter Goldton darin würde nur schmutzig wirken. */ +.po-aufg--hervor { background: transparent; padding-left: 0; padding-right: 0; } + +/* Das Kästchen. Bei eigenen Punkten ein echter Knopf (tastaturbedienbar, + 44px Trefferfläche über das Innenabstands-Polster), sonst nur Anzeige. + Ein Knopf, der nichts tut, lässt die Seite kaputt wirken. */ +.po-aufg-haken { + width: 26px; height: 26px; margin-top: .05rem; padding: 0; + border-radius: 8px; border: 2px solid var(--wd-rand-stark); + background: transparent; color: transparent; + display: grid; place-items: center; + font-size: .95rem; line-height: 1; + transition: all var(--wd-zeit-schnell) ease; +} +button.po-aufg-haken { cursor: pointer; } +button.po-aufg-haken:hover { border-color: var(--wd-gold); background: rgba(232,200,119,.1); } +.po-aufg-haken--fest { cursor: default; } +.po-aufg--fertig .po-aufg-haken { background: var(--wd-ok); border-color: var(--wd-ok); color: #06120c; } +.po-aufg--jetzt .po-aufg-haken { border-color: var(--wd-p2); color: var(--wd-p2); } +.po-aufg--weg .po-aufg-haken { border-style: dashed; color: var(--wd-text-soft); } +/* Offene EIGENE Punkte: der Rand ist golden, damit klar ist, dass hier + geklickt werden kann und soll. */ +.po-aufg--du button.po-aufg-haken { border-color: rgba(232,200,119,.55); } + +.po-aufg-marke { + display: inline-block; white-space: nowrap; + font-size: .68rem; letter-spacing: .05em; text-transform: uppercase; + padding: .14rem .45rem; border-radius: 999px; +} +.po-aufg-marke--ok { background: rgba(111,214,163,.13); color: var(--wd-ok); } +.po-aufg-marke--jetzt { background: rgba(183,157,255,.15); color: var(--wd-p2); } +.po-aufg-marke--du { background: rgba(232,200,119,.16); color: var(--wd-gold); } +.po-aufg-nicht { + display: inline-block; margin-left: .4rem; + font-size: .68rem; letter-spacing: .05em; text-transform: uppercase; + padding: .12rem .4rem; border-radius: 999px; + background: rgba(255,157,157,.12); color: var(--wd-fehler); +} + +/* Auf dem Handy rutscht die Marke unter den Titel — nebeneinander bliebe + für den Titel eine Spalte von zwei Wörtern Breite. */ +@media (max-width: 520px) { + .po-aufg { grid-template-columns: auto minmax(0,1fr); } + .po-aufg-marken { grid-row: 2; grid-column: 2; margin-bottom: .15rem; } + .po-aufg-text { grid-row: 3; grid-column: 2; } +} + +/* ---------- Postfach auf der Übersicht ---------- */ +.po-pf-verlauf { + max-height: 46vh; overflow-y: auto; + margin: .8rem 0 .2rem; padding-right: .3rem; + display: flex; flex-direction: column; gap: .5rem; +} diff --git a/assets/js/i18n-wd-portal.js b/assets/js/i18n-wd-portal.js index 0c41dd93..9b2ef314 100644 --- a/assets/js/i18n-wd-portal.js +++ b/assets/js/i18n-wd-portal.js @@ -205,7 +205,13 @@ window.I18N_WD = { po_senden: { de: "Senden", "de-CH": "Schicke", en: "Send", fr: "Envoyer", pt: "Enviar" }, /* ---------- Änderungswünsche ---------- */ - po_ideen: { de: "Ideen & Änderungswünsche", "de-CH": "Ideä & Änderigswünsch", en: "Ideas & change requests", fr: "Idées & demandes de modification", pt: "Ideias & pedidos de alteração" }, + /* Hier steht ein ECHTES "&", kein "&" -- anders als in den meisten + anderen i18n-Dateien. Der Grund: Texte, die über data-i18n gesetzt + werden, landen als HTML in der Seite, dort ist "&" richtig. + Dieser Text wird aber im JavaScript durch die Absicherung geschickt + (s(...)), und die kodiert das "&" ein zweites Mal -- auf dem + Bildschirm stand "Ideen & Änderungswünsche". */ + po_ideen: { de: "Ideen & Änderungswünsche", "de-CH": "Ideä & Änderigswünsch", en: "Ideas & change requests", fr: "Idées & demandes de modification", pt: "Ideias & pedidos de alteração" }, po_ideen_text: { de: "Trag hier ein, was dir noch einfällt. Ich prüfe Aufwand und Preis und melde mich — du entscheidest danach in Ruhe.", "de-CH": "Trag do ii, was dir no iifallt. Ich prüefe Ufwand und Priis und mälde mich — du entscheidsch dernoch i Rueh.", @@ -268,4 +274,183 @@ window.I18N_WD = { fr: "Ta session a expiré. Merci de te reconnecter.", pt: "A tua sessão expirou. Inicia sessão novamente.", }, + + /* ---------- Aufgabenliste (22.08.2026) ---------- + "wenn sie drauf drücken sollen sie auch sehen was ich schon gemacht + habe von dem was im plan war." + + Der Ton ist hier besonders wichtig. Der Kunde liest diese Liste, + wenn er unsicher ist, wie weit es steht — also im Zweifel schon + leicht angespannt. Sie muss beruhigen und klar sagen, was von IHM + erwartet wird, ohne zu drängen. "Wir warten auf dich" wäre Druck; + "Das brauche ich noch von dir" ist eine Bitte. */ + po_aufgaben: { + de: "Was schon erledigt ist", + "de-CH": "Was scho erledigt isch", + en: "What's already done", + fr: "Ce qui est déjà fait", + pt: "O que já está feito", + }, + po_aufg_text: { + de: "Jeder Punkt aus deinem Plan — abgehakt, sobald er fertig ist. Du siehst jederzeit, woran gerade gearbeitet wird und was noch von dir gebraucht wird.", + "de-CH": "Jede Punkt us dim Plan — abghakt, sobald er fertig isch. Du gsehsch jederziit, wo grad dra gschaffet wird und was no vo dir brucht wird.", + en: "Every point from your plan — ticked off as soon as it's finished. You can always see what's being worked on and what's still needed from you.", + fr: "Chaque point de ton plan — coché dès qu'il est terminé. Tu vois à tout moment ce sur quoi je travaille et ce qu'il me faut encore de ta part.", + pt: "Cada ponto do teu plano — assinalado assim que estiver pronto. Vês sempre no que se está a trabalhar e o que ainda é preciso da tua parte.", + }, + po_aufg_leer: { + de: "Die Punkte erscheinen hier, sobald dein Plan steht.", + "de-CH": "D Pünkt erschiined do, sobald din Plan stoht.", + en: "The points appear here as soon as your plan is set.", + fr: "Les points apparaîtront ici dès que ton plan sera établi.", + pt: "Os pontos aparecem aqui assim que o teu plano estiver definido.", + }, + po_aufg_fertig: { + de: "von {gesamt} erledigt", + "de-CH": "vo {gesamt} erledigt", + en: "of {gesamt} done", + fr: "sur {gesamt} terminés", + pt: "de {gesamt} concluídos", + }, + po_aufg_dubist: { + de: "Das brauche ich noch von dir", + "de-CH": "Das bruuch ich no vo dir", + en: "This is what I still need from you", + fr: "Voici ce qu'il me faut encore de ta part", + pt: "Isto é o que ainda preciso da tua parte", + }, + po_aufg_dubist_eins: { + de: "Ein Punkt wartet auf dich", + "de-CH": "Ei Punkt wartet uf dich", + en: "One point is waiting for you", + fr: "Un point t'attend", + pt: "Um ponto está à tua espera", + }, + po_aufg_dubist_viele: { + de: "{n} Punkte warten auf dich", + "de-CH": "{n} Pünkt warted uf dich", + en: "{n} points are waiting for you", + fr: "{n} points t'attendent", + pt: "{n} pontos estão à tua espera", + }, + po_aufg_allesbei: { + de: "Von dir wird gerade nichts gebraucht — lehn dich zurück.", + "de-CH": "Vo dir wird grad nüt brucht — lehn di zrugg.", + en: "Nothing is needed from you right now — sit back.", + fr: "Rien n'est attendu de toi pour l'instant — tu peux te détendre.", + pt: "Neste momento não é preciso nada da tua parte — descontrai.", + }, + po_aufg_erledigt_marke: { + de: "erledigt", + "de-CH": "erledigt", + en: "done", + fr: "terminé", + pt: "concluído", + }, + po_aufg_arbeit_marke: { + de: "läuft gerade", + "de-CH": "lauft grad", + en: "in progress", + fr: "en cours", + pt: "em curso", + }, + po_aufg_du_marke: { + de: "bei dir", + "de-CH": "bi dir", + en: "your turn", + fr: "à toi", + pt: "contigo", + }, + po_aufg_nicht: { + de: "nicht im Paket", + "de-CH": "nöd im Paket", + en: "not included", + fr: "hors forfait", + pt: "não incluído", + }, + po_aufg_hake: { + de: "Erledigt — als erledigt melden", + "de-CH": "Erledigt — als erledigt mälde", + en: "Done — mark as done", + fr: "Fait — marquer comme terminé", + pt: "Feito — marcar como concluído", + }, + po_aufg_zurueck: { + de: "Doch noch offen", + "de-CH": "Doch no offe", + en: "Still open after all", + fr: "Finalement pas terminé", + pt: "Afinal ainda em aberto", + }, + + /* ---------- Allgemeines Postfach (22.08.2026) ---------- + "auf dieser seite sollen die kunden mir auch nachrichten hinterlassen + können, für noch kleinigkeiten oder sachen die ich brauch." + + Bewusst niedrigschwellig formuliert. Wer glaubt, eine Nachricht + müsse eine "richtige" Anfrage sein, schreibt gar nicht erst — und + genau die kurzen Fragen sollen ja hier landen. */ + po_postfach: { + de: "Kurz etwas fragen", + "de-CH": "Churz öppis frooge", + en: "Ask something quickly", + fr: "Une question rapide", + pt: "Perguntar algo rápido", + }, + po_postfach_text: { + de: "Für alles Kleine zwischendurch — eine Frage, ein Hinweis, etwas Vergessenes. Auch wenn es zu keinem bestimmten Projekt gehört. Kein Anlass ist zu klein.", + "de-CH": "Für alles Chliine zwüschedure — e Frog, en Hiiwiis, öppis Vergässnigs. Au wenns zu kem bstimmte Projekt ghört. Kein Aalass isch z chlii.", + en: "For all the small things in between — a question, a note, something forgotten. Even if it doesn't belong to a particular project. No reason is too small.", + fr: "Pour tout ce qui est petit entre-temps — une question, une remarque, un oubli. Même si cela ne concerne aucun projet précis. Aucune raison n'est trop petite.", + pt: "Para todas as pequenas coisas pelo meio — uma pergunta, um aviso, algo esquecido. Mesmo que não pertença a um projeto específico. Nenhum motivo é pequeno demais.", + }, + po_postfach_leer: { + de: "Noch nichts geschrieben. Schreib einfach los.", + "de-CH": "No nüt gschriebe. Schriib eifach los.", + en: "Nothing written yet. Just go ahead.", + fr: "Rien d'écrit pour l'instant. Lance-toi.", + pt: "Ainda nada escrito. Escreve à vontade.", + }, + po_postfach_platzhalter: { + de: "Was möchtest du fragen oder mitteilen?", + "de-CH": "Was wotsch frooge oder mitteile?", + en: "What would you like to ask or tell me?", + fr: "Que veux-tu demander ou signaler ?", + pt: "O que queres perguntar ou dizer?", + }, + po_postfach_bezug: { + de: "Worum geht es? (freiwillig)", + "de-CH": "Um was gohts? (freiwillig)", + en: "What is it about? (optional)", + fr: "De quoi s'agit-il ? (facultatif)", + pt: "Sobre o que é? (opcional)", + }, + po_postfach_kein_bezug: { + de: "— nichts Bestimmtes —", + "de-CH": "— nüt Bstimmts —", + en: "— nothing in particular —", + fr: "— rien de particulier —", + pt: "— nada em particular —", + }, + po_postfach_leer_fehler: { + de: "Bitte schreib zuerst etwas.", + "de-CH": "Bitte schriib zerscht öppis.", + en: "Please write something first.", + fr: "Merci d'écrire quelque chose d'abord.", + pt: "Escreve algo primeiro.", + }, + po_postfach_gesendet: { + de: "Ist angekommen. Ich melde mich.", + "de-CH": "Isch aacho. Ich mälde mich.", + en: "Received. I'll get back to you.", + fr: "Bien reçu. Je reviens vers toi.", + pt: "Recebido. Volto a contactar-te.", + }, + po_von_dogfather: { + de: "Dogfather", + "de-CH": "Dogfather", + en: "Dogfather", + fr: "Dogfather", + pt: "Dogfather", + }, }; diff --git a/assets/js/wd-portal.js b/assets/js/wd-portal.js index 499fe259..b91f441e 100644 --- a/assets/js/wd-portal.js +++ b/assets/js/wd-portal.js @@ -144,6 +144,133 @@ s(window.WD.t(wartet === "kunde" ? "po_wartet_kunde" : "po_wartet_dogfather")) + ""; } + /* ---------------- Aufgabenliste (22.08.2026) ---------------- + "wenn sie drauf drücken sollen sie auch sehen was ich schon gemacht + habe von dem was im plan war." + + Bis hierher sah der Kunde nur die Phase ("Design · 3/7") — eine Zahl + ohne Inhalt. Sie beantwortet die eigentliche Frage nicht: WAS ist + denn schon fertig? + + Zwei Entscheidungen prägen diese Ansicht: + + 1. Punkte, die auf den KUNDEN warten, stehen ganz oben in einem + eigenen Kasten — nicht nur farblich markiert irgendwo mittendrin. + Wer eine lange Liste sieht, liest sie als Bericht über MEINE + Arbeit und übersieht seinen eigenen Teil. Genau daraus entstehen + die meisten Verzögerungen. + + 2. Der Kunde kann seine eigenen Punkte selbst abhaken. Das ist keine + Spielerei: Es gibt ihm einen sichtbaren Anteil am Fortschritt, + und ich sehe sofort, dass die Texte da sind, ohne dass er extra + schreiben muss. + ---------------------------------------------------------------- */ + var KATS = ["start", "gestaltung", "umsetzung", "abschluss"]; + var KAT_KEY = { start: "po_k1", gestaltung: "po_k2", umsetzung: "po_k3", abschluss: "po_k4" }; + + function aufgabeZeile(a, hervor) { + var klasse = "po-aufg"; + if (a.status === "erledigt") klasse += " po-aufg--fertig"; + else if (a.status === "in_arbeit") klasse += " po-aufg--jetzt"; + else if (a.status === "entfaellt") klasse += " po-aufg--weg"; + var duBistDran = a.wer_dran === "kunde" && a.status !== "erledigt" && a.status !== "entfaellt"; + if (duBistDran) klasse += " po-aufg--du"; + if (hervor) klasse += " po-aufg--hervor"; + + /* Nur EIGENE offene Punkte bekommen einen Knopf. Bei allen anderen + ist das Zeichen reine Anzeige — sonst klickt der Kunde darauf, + nichts passiert, und er hält die Seite für kaputt. */ + var eigen = a.wer_dran === "kunde" && !a.nicht_enthalten && a.status !== "entfaellt"; + var zeichen = a.status === "erledigt" ? "✓" : a.status === "in_arbeit" ? "•" : a.status === "entfaellt" ? "–" : ""; + + var kaestchen = eigen + ? '" + : '"; + + var marke = a.status === "erledigt" + ? '' + s(window.WD.t("po_aufg_erledigt_marke")) + "" + : a.status === "in_arbeit" + ? '' + s(window.WD.t("po_aufg_arbeit_marke")) + "" + : duBistDran + ? '' + s(window.WD.t("po_aufg_du_marke")) + "" + : ""; + + return '
' + kaestchen + + '' + s(a.titel) + + (a.nicht_enthalten ? '' + s(window.WD.t("po_aufg_nicht")) + "" : "") + + "" + + '' + marke + "" + + (a.beschreibung || a.hinweis + ? '' + s(a.beschreibung || "") + + (a.hinweis ? " " + s(a.hinweis) : "") + "" + : "") + + "
"; + } + + function aufgabenKarte(d) { + var f = d.fortschritt || { gesamt: 0, erledigt: 0, beimKunden: 0, jeKategorie: {} }; + var alle = d.aufgaben || []; + + var html = '

' + + s(window.WD.t("po_aufgaben")) + "

"; + + if (!alle.length) { + return html + '

' + s(window.WD.t("po_aufg_leer")) + "

"; + } + + html += '

' + + s(window.WD.t("po_aufg_text")) + "

"; + + /* Grosse Zahl statt Prozent. "9 von 14 erledigt" ist greifbar, + "64 %" ist eine Rechnung, die niemand fuehlt. */ + html += '
' + f.erledigt + " " + + s(window.WD.t("po_aufg_fertig").replace("{gesamt}", f.gesamt)) + "
"; + + /* Der Balken in den vier Prozessfarben — dieselben wie auf der + Ablaufseite und in der Verwaltung. */ + html += '
' + KATS.map(function (k) { + var z = (f.jeKategorie && f.jeKategorie[k]) || { gesamt: 0, erledigt: 0 }; + var breite = z.gesamt ? Math.round((z.erledigt / z.gesamt) * 100) : 0; + return ''; + }).join("") + "
"; + + /* --- Was auf den Kunden wartet, ZUERST und in einem eigenen Kasten. --- */ + var meine = alle.filter(function (a) { + return a.wer_dran === "kunde" && a.status !== "erledigt" && a.status !== "entfaellt" && !a.nicht_enthalten; + }); + + if (meine.length) { + html += '

' + + s(window.WD.t(meine.length === 1 ? "po_aufg_dubist_eins" + : "po_aufg_dubist_viele").replace("{n}", meine.length)) + "

" + + '

' + + s(window.WD.t("po_aufg_dubist")) + "

" + + meine.map(function (a) { return aufgabeZeile(a, true); }).join("") + + "
"; + } else { + html += '

' + s(window.WD.t("po_aufg_allesbei")) + "

"; + } + + /* --- Danach die vollstaendige Liste nach Abschnitten. --- */ + html += KATS.map(function (k) { + var drin = alle.filter(function (a) { return a.kategorie === k; }); + if (!drin.length) return ""; + var z = (f.jeKategorie && f.jeKategorie[k]) || { gesamt: 0, erledigt: 0 }; + return '
' + + '
' + s(window.WD.t(KAT_KEY[k])) + + '' + z.erledigt + " / " + z.gesamt + "
" + + drin.map(function (a) { return aufgabeZeile(a, false); }).join("") + + "
"; + }).join(""); + + return html + ""; + } + async function ladeUebersicht() { zeige("po-uebersicht"); var ziel = document.getElementById("po-inhalt"); @@ -261,10 +388,23 @@ }).join("") + ""; } + /* Der allgemeine Nachrichtenbereich steht auf der ÜBERSICHT, nicht + im Projekt. Genau das war der Punkt: Wer kein Projekt hat — oder + eine Frage, die zu keinem gehört — konnte vorher gar nicht + schreiben und musste zur E-Mail greifen. Damit war der Verlauf + weg, sobald man ihn brauchte. + + Er wird nachgeladen, nicht mitgeliefert: Die Übersicht soll + sofort dastehen. Ein paar Zehntelsekunden später erscheinen die + Nachrichten — das ist besser, als die ganze Seite auf sie warten + zu lassen. */ + html += '
'; + ziel.innerHTML = html; ziel.querySelectorAll("[data-projekt]").forEach(function (b) { b.addEventListener("click", function () { ladeProjekt(b.dataset.projekt); }); }); + ladePostfach(); } catch (fehler) { if (behandleFehler(fehler)) return; ziel.innerHTML = ""; @@ -273,6 +413,100 @@ } /* ---------------- Projektdetail ---------------- */ + /* ---------------- Allgemeines Postfach ---------------- + "auf dieser seite sollen die kunden mir auch nachrichten hinterlassen + können, für noch kleinigkeiten oder sachen die ich brauch." + + Bewusst niedrigschwellig: kein Betreff, kein Pflichtfeld, der Bezug + zu einem Projekt ist freiwillig. Wer glaubt, eine Nachricht müsse + eine "richtige" Anfrage sein, schreibt gar nicht erst — und genau + die kurzen Fragen sollen hier landen. + ---------------------------------------------------------------- */ + async function ladePostfach() { + var kasten = document.getElementById("po-postfach"); + if (!kasten) return; + + var d; + try { + d = await api("/webdesign/portal/postfach"); + } catch (fehler) { + /* Still bleiben. Der Bereich ist eine Ergänzung — eine rote + Fehlermeldung über der Projektübersicht wäre alarmierender, als + der Anlass es hergibt. */ + return; + } + + var html = "

" + s(window.WD.t("po_postfach")) + "

" + + '
' + + '

' + s(window.WD.t("po_postfach_text")) + "

"; + + if (!d.nachrichten.length) { + html += '

' + s(window.WD.t("po_postfach_leer")) + "

"; + } else { + html += '
' + d.nachrichten.map(function (n) { + var vonMir = n.autor === "kunde"; + return '
' + + '
' + + (vonMir ? "" : s(window.WD.t("po_von_dogfather")) + " · ") + + window.WD.datum(n.erstellt_am) + + (n.projekt_titel ? " · " + s(n.projekt_titel) : "") + + "

" + s(n.text) + "

"; + }).join("") + "
"; + } + + html += '
' + + '' + + '

'; + + if (d.projekte && d.projekte.length) { + html += '
' + + '" + + '
"; + } + + html += '
"; + + kasten.innerHTML = html; + + var lauf = document.getElementById("po-pf-verlauf"); + if (lauf) lauf.scrollTop = lauf.scrollHeight; + + var eingabe = document.getElementById("po-pf-text"); + var feld = document.getElementById("po-pf-feld"); + var knopf = document.getElementById("po-pf-senden"); + + knopf.addEventListener("click", async function () { + var inhalt = eingabe.value.trim(); + feld.classList.remove("wd-feld--fehler"); + if (!inhalt) { + feld.classList.add("wd-feld--fehler"); + feld.querySelector(".wd-feld-fehler").textContent = window.WD.t("po_postfach_leer_fehler"); + eingabe.focus(); + return; + } + var wahl = document.getElementById("po-pf-projekt"); + window.WD.knopfSperren(knopf, true, "…"); + try { + await api("/webdesign/portal/postfach", { + method: "POST", + body: { text: inhalt, projektId: wahl ? wahl.value : null }, + }); + window.WD.meldung(meldung, window.WD.t("po_postfach_gesendet"), "ok"); + await ladePostfach(); + } catch (fehler) { + if (behandleFehler(fehler)) return; + window.WD.knopfSperren(knopf, false); + window.WD.meldung(meldung, fehler.message, "fehler"); + } + }); + } + var aktuellesProjekt = null; document.getElementById("po-zurueck").addEventListener("click", function () { @@ -287,7 +521,21 @@ ziel.innerHTML = '
' + s(window.WD.t("laedt")) + "
"; try { - var d = await api("/webdesign/portal/projekt/" + encodeURIComponent(id)); + /* Beides gleichzeitig anfordern statt nacheinander. Nacheinander + waere die Wartezeit die Summe beider Anfragen — bei einer + Mobilfunkverbindung deutlich spuerbar. + + Die Aufgabenliste darf dabei fehlschlagen, ohne die ganze Seite + mitzureissen: Sie ist eine Ergaenzung, keine Voraussetzung. Wer + wegen einer leeren Liste seine Zahlungen und Dateien nicht mehr + saehe, waere schlechter dran als vorher. */ + var beides = await Promise.all([ + api("/webdesign/portal/projekt/" + encodeURIComponent(id)), + api("/webdesign/portal/projekt/" + encodeURIComponent(id) + "/aufgaben") + .catch(function () { return { aufgaben: [], fortschritt: null }; }), + ]); + var d = beides[0]; + var aufg = beides[1]; var p = d.projekt; var html = ""; @@ -302,6 +550,12 @@ : "") + ""; + /* Die Aufgabenliste steht GANZ OBEN, direkt nach dem Projektkopf. + Sie beantwortet die Frage, mit der man das Projekt oeffnet + ("wie weit ist es?"). Zahlungen und Dateien sind wichtig, aber + sie sind nicht der Grund fuer den Klick. */ + html += aufgabenKarte(aufg); + /* --- Zahlungen --- */ html += '

' + s(window.WD.t("po_zahlungen")) + "

"; if (!d.zahlungen.length) { @@ -420,6 +674,26 @@ }); }); + /* Eigene Punkte abhaken. Der Punkt erscheint in der Liste zweimal + (oben unter "Das brauche ich noch von dir" und unten im + Abschnitt), deshalb wird nach dem Umschalten die ganze Ansicht + neu aufgebaut — sonst stuenden beide Fassungen widerspruechlich + nebeneinander. */ + ziel.querySelectorAll("[data-haken]").forEach(function (b) { + b.addEventListener("click", async function () { + window.WD.knopfSperren(b, true, "…"); + try { + await api("/webdesign/portal/aufgabe/" + encodeURIComponent(b.dataset.haken) + "/erledigt", + { method: "POST", body: {} }); + ladeProjekt(id); + } catch (f) { + if (behandleFehler(f)) return; + window.WD.knopfSperren(b, false); + window.WD.meldung(meldung, f.message, "fehler"); + } + }); + }); + ziel.querySelector("#po-idee-senden").addEventListener("click", async function (e) { var titel = ziel.querySelector("#po-idee-titel").value.trim(); var beschreibung = ziel.querySelector("#po-idee-text").value.trim(); diff --git a/pruef-portal.mjs b/pruef-portal.mjs new file mode 100644 index 00000000..2acca249 --- /dev/null +++ b/pruef-portal.mjs @@ -0,0 +1,224 @@ +/* Prüft die Aufgabenliste und das Postfach im Kundenportal. + + Wichtig sind hier drei Dinge, die man leicht falsch baut: + + 1. Der Kunde darf NUR seine eigenen Punkte anklicken. Ein Knopf, der + nichts tut, lässt die Seite kaputt wirken. + 2. Offene Kundenpunkte müssen VOR der langen Liste stehen — sonst + liest man die Liste als Bericht und überliest seinen Teil. + 3. Alle fünf Sprachen müssen vollständig sein. Ein fehlender + Schlüssel fällt sonst erst einem französischen Kunden auf. +*/ +import { chromium } from "playwright"; +import { createServer } from "node:http"; +import { readFileSync, existsSync } from "node:fs"; +import { join, extname } from "node:path"; + +const W = process.cwd(); +const T = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript", + ".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml", + ".webp": "image/webp", ".ico": "image/x-icon", ".json": "application/json" }; +const srv = createServer((q, r) => { + const p = join(W, decodeURIComponent(q.url.split("?")[0])); + if (existsSync(p) && !p.endsWith("/")) { + r.writeHead(200, { "content-type": T[extname(p)] || "text/plain" }); + return r.end(readFileSync(p)); + } + r.writeHead(404).end(""); +}); +await new Promise((r) => srv.listen(4326, r)); + +const AUFGABEN = [ + { id: "a1", titel: "Briefing-Gespräch führen", beschreibung: "Ziel und Umfang klären.", + kategorie: "start", status: "erledigt", wer_dran: "dogfather", nicht_enthalten: 0, hinweis: null }, + { id: "a2", titel: "Texte und Bilder zusammenstellen", beschreibung: "Lieber früh als vollständig.", + kategorie: "start", status: "offen", wer_dran: "kunde", nicht_enthalten: 0, hinweis: null }, + { id: "a3", titel: "Entwurf der Startseite", beschreibung: null, + kategorie: "gestaltung", status: "erledigt", wer_dran: "dogfather", nicht_enthalten: 0, hinweis: null }, + { id: "a4", titel: "Entwurf ansehen und Rückmeldung geben", beschreibung: null, + kategorie: "gestaltung", status: "offen", wer_dran: "kunde", nicht_enthalten: 0, hinweis: null }, + { id: "a5", titel: "Alle Unterseiten bauen", beschreibung: null, + kategorie: "umsetzung", status: "in_arbeit", wer_dran: "dogfather", nicht_enthalten: 0, hinweis: null }, + { id: "a6", titel: "Mehrsprachigkeit", beschreibung: null, + kategorie: "umsetzung", status: "offen", wer_dran: "dogfather", nicht_enthalten: 1, + hinweis: "Auf Wunsch als Zusatz." }, + { id: "a7", titel: "Seite live schalten", beschreibung: null, + kategorie: "abschluss", status: "offen", wer_dran: "dogfather", nicht_enthalten: 0, hinweis: null }, +]; + +const FORTSCHRITT = { gesamt: 6, erledigt: 2, inArbeit: 1, beimKunden: 2, prozent: 33, + jeKategorie: { start: { gesamt: 2, erledigt: 1 }, gestaltung: { gesamt: 2, erledigt: 1 }, + umsetzung: { gesamt: 1, erledigt: 0 }, abschluss: { gesamt: 1, erledigt: 0 } } }; + +const PROJEKT = { id: "p1", nummer: "P-0001", titel: "Website für Musterbau", + status: "design", wartet_auf: "kunde", naechster_schritt: "Deine Rückmeldung zum Entwurf." }; + +const ergebnisse = []; +function pruefe(name, ok, zusatz) { + ergebnisse.push({ name, ok }); + console.log((ok ? " ok " : " FEHL ") + name + (zusatz ? " -> " + zusatz : "")); +} + +const browser = await chromium.launch(); + +/* ---- 1. Vollständigkeit aller fünf Sprachen ---- */ +{ + const p = await browser.newPage(); + await p.goto("http://127.0.0.1:4326/webdesign/portal.html"); + await p.waitForTimeout(600); + const luecken = await p.evaluate(() => { + const w = window.I18N_WD || {}; + const sprachen = ["de", "de-CH", "en", "fr", "pt"]; + const fehlt = []; + for (const [k, v] of Object.entries(w)) { + for (const sp of sprachen) { + if (!v[sp] || !String(v[sp]).trim()) fehlt.push(k + "/" + sp); + } + } + return { fehlt, anzahl: Object.keys(w).length }; + }); + console.log("\n=== Sprachen ==="); + pruefe("alle Texte in allen fünf Sprachen vorhanden", luecken.fehlt.length === 0, + luecken.fehlt.slice(0, 6).join(", ")); + console.log(" (" + luecken.anzahl + " Texte geprüft)"); + await p.close(); +} + +for (const [wo, breite, hoehe] of [["Computer", 1280, 950], ["Handy", 390, 844]]) { + const seite = await browser.newPage({ viewport: { width: breite, height: hoehe } }); + const fehlerImProtokoll = []; + seite.on("pageerror", (e) => fehlerImProtokoll.push(e.message)); + + let abgehakt = null; + // Nur die API abfangen, nicht die Seite selbst. + await seite.route("**/webdesign/portal/**", (route) => { + /* Auf den PFAD prüfen, nicht auf die ganze Adresse. + + Der API-Server heisst "postfach.dogfather-universe.com" -- ein + url.includes("/postfach") trifft also den HOSTNAMEN und damit + JEDE Anfrage. Beim ersten Lauf bekam deshalb auch die Übersicht + die Postfach-Antwort, und der Fehler sah aus wie ein Fehler im + Portal ("Cannot read properties of undefined"). Er lag im Test. */ + const u = new URL(route.request().url()).pathname; + if (u.includes("/projekt/p1/aufgaben")) { + return route.fulfill({ json: { ok: true, projekt: PROJEKT, aufgaben: AUFGABEN, fortschritt: FORTSCHRITT } }); + } + if (u.includes("/aufgabe/") && u.includes("/erledigt")) { + abgehakt = u.split("/aufgabe/")[1].split("/")[0]; + return route.fulfill({ json: { ok: true, status: "erledigt", fortschritt: FORTSCHRITT } }); + } + if (u.includes("/projekt/p1")) { + return route.fulfill({ json: { ok: true, projekt: PROJEKT, + zahlungen: [], dateien: [], aenderungen: [], nachrichten: [] } }); + } + if (u.includes("/postfach")) { + if (route.request().method() === "POST") return route.fulfill({ json: { ok: true, id: "x" } }); + return route.fulfill({ json: { ok: true, projekte: [{ id: "p1", titel: "Website für Musterbau" }], + nachrichten: [ + { id: "m1", autor: "kunde", text: "Kurze Frage zur Domain.", projekt_titel: null, + erstellt_am: new Date(Date.now() - 7200000).toISOString() }, + { id: "m2", autor: "dogfather", text: "Die läuft schon auf dich, alles gut.", + projekt_titel: null, erstellt_am: new Date(Date.now() - 3600000).toISOString() }, + ] } }); + } + if (u.includes("/uebersicht")) { + return route.fulfill({ json: { ok: true, kunde: { name: "Anna" }, + projekte: [PROJEKT], offeneZahlungen: [], abos: [] } }); + } + return route.fulfill({ json: { ok: true } }); + }); + await seite.addInitScript(() => { + sessionStorage.setItem("wd-portal-token", "t"); + sessionStorage.setItem("wd-sitzung-aktiv", "1"); + }); + + await seite.goto("http://127.0.0.1:4326/webdesign/portal.html"); + await seite.waitForTimeout(1000); + console.log("\n=== " + wo + " ==="); + + // ---- Postfach auf der Übersicht ---- + pruefe(wo + ": Postfach steht auf der Übersicht", await seite.locator("#po-pf-senden").count() === 1); + pruefe(wo + ": bisherige Nachrichten sind da", await seite.locator("#po-postfach .po-nachricht").count() === 2); + + // Leere Nachricht muss abgelehnt werden, nicht stillschweigend gesendet + await seite.click("#po-pf-senden"); + await seite.waitForTimeout(300); + pruefe(wo + ": leere Nachricht wird abgelehnt", + await seite.locator("#po-pf-feld.wd-feld--fehler").count() === 1); + + await seite.screenshot({ path: `pruef-portal-postfach-${wo.toLowerCase()}.png`, fullPage: true }); + + // ---- Projekt öffnen ---- + await seite.click("[data-projekt]"); + await seite.waitForTimeout(900); + + const m = await seite.evaluate(() => { + const gruppen = Array.from(document.querySelectorAll(".po-aufg-gruppe")); + const farben = gruppen.map((g) => getComputedStyle(g.querySelector(".po-aufg-kopf")).color); + const dubist = document.querySelector(".po-dubist"); + const ersteGruppe = gruppen[0]; + const knoepfe = document.querySelectorAll("button.po-aufg-haken"); + const feste = document.querySelectorAll(".po-aufg-haken--fest"); + const aufgKarte = document.querySelector(".po-aufg-stand"); + const zahlKarte = Array.from(document.querySelectorAll(".wd-karte h3")) + .find((h) => h.textContent.includes("Zahlung")); + + // Anordnung einer Zeile pruefen (Lehre aus der Verwaltung) + const z = document.querySelector(".po-aufg-gruppe .po-aufg"); + const k = z?.querySelector(".po-aufg-haken")?.getBoundingClientRect(); + const t = z?.querySelector(".po-aufg-titel")?.getBoundingClientRect(); + + return { + gruppen: gruppen.map((g) => g.dataset.k), + farbenVerschieden: new Set(farben).size, + dubistDa: !!dubist, + dubistVorListe: dubist && ersteGruppe + ? dubist.getBoundingClientRect().top < ersteGruppe.getBoundingClientRect().top : false, + knoepfe: knoepfe.length, + feste: feste.length, + standDa: !!aufgKarte, + aufgabenVorZahlungen: aufgKarte && zahlKarte + ? aufgKarte.getBoundingClientRect().top < zahlKarte.getBoundingClientRect().top : false, + kaestchenVorTitel: k && t ? k.right <= t.left + 1 : false, + balkenTeile: document.querySelectorAll(".po-fs-teil").length, + }; + }); + + pruefe(wo + ": alle vier Abschnitte erscheinen", + m.gruppen.join(",") === "start,gestaltung,umsetzung,abschluss", m.gruppen.join(",")); + pruefe(wo + ": jeder Abschnitt hat eine eigene Farbe", m.farbenVerschieden === 4, "n=" + m.farbenVerschieden); + pruefe(wo + ": Fortschrittsbalken hat vier Teile", m.balkenTeile === 4); + pruefe(wo + ": „Das brauche ich von dir“ ist da", m.dubistDa); + pruefe(wo + ": … und steht VOR der langen Liste", m.dubistVorListe); + pruefe(wo + ": Aufgabenliste steht vor den Zahlungen", m.aufgabenVorZahlungen); + pruefe(wo + ": grosse Zahl statt Prozent", m.standDa); + pruefe(wo + ": Kästchen steht links vom Titel", m.kaestchenVorTitel); + + /* Der Kunde hat 2 offene eigene Punkte. Sie erscheinen zweimal (oben + und in der Gruppe) -> 4 Knöpfe. Alle übrigen Kästchen sind fest. */ + pruefe(wo + ": nur eigene Punkte sind anklickbar", m.knoepfe === 4, "Knöpfe=" + m.knoepfe + ", fest=" + m.feste); + pruefe(wo + ": fremde Punkte haben KEINEN Knopf", m.feste === 5, "n=" + m.feste); + + // Abhaken loest den richtigen Aufruf aus + await seite.locator("button.po-aufg-haken").first().click(); + await seite.waitForTimeout(700); + pruefe(wo + ": Abhaken meldet den richtigen Punkt", abgehakt === "a2", "gemeldet: " + abgehakt); + + if (breite < 900) { + const zuKlein = await seite.evaluate(() => + Array.from(document.querySelectorAll("button.po-aufg-haken")) + .filter((b) => { const r = b.getBoundingClientRect(); return r.width < 24 || r.height < 24; }).length); + pruefe(wo + ": Kästchen sind gross genug zum Antippen", zuKlein === 0, "zu klein: " + zuKlein); + } + + pruefe(wo + ": keine Fehler im Protokoll", fehlerImProtokoll.length === 0, fehlerImProtokoll.join(" | ")); + + await seite.screenshot({ path: `pruef-portal-aufgaben-${wo.toLowerCase()}.png`, fullPage: true }); + await seite.close(); +} + +await browser.close(); +srv.close(); +const fehler = ergebnisse.filter((e) => !e.ok).length; +console.log(`\n===== ${ergebnisse.length - fehler} bestanden, ${fehler} fehlgeschlagen =====`); +process.exitCode = fehler ? 1 : 0; diff --git a/pruef-texte.mjs b/pruef-texte.mjs new file mode 100644 index 00000000..cb8d3cd8 --- /dev/null +++ b/pruef-texte.mjs @@ -0,0 +1,182 @@ +/* Sucht doppelt kodierte Zeichen auf ALLEN Webdesign-Seiten in ALLEN + fünf Sprachen. + + Anlass: Im Portal stand sichtbar "Ideen &amp; Änderungswünsche". + Ursache ist eine Falle, die sich leicht wiederholt — es gibt zwei + Wege, wie ein Text in die Seite kommt: + + data-i18n="schluessel" -> wird als HTML eingesetzt. + Dort ist "&" RICHTIG. + s(window.WD.t("schluessel")) -> wird abgesichert eingesetzt. + Dort wird "&" ein zweites Mal + kodiert und erscheint wörtlich. + + Derselbe Textbaustein kann also je nach Verwendungsort richtig oder + falsch sein. Das kann man nicht zuverlässig im Kopf behalten — + deshalb prüft es der Browser: Was am Ende auf dem Bildschirm steht, + darf keine Kodierungsreste enthalten. +*/ +import { chromium } from "playwright"; +import { createServer } from "node:http"; +import { readFileSync, existsSync, readdirSync } from "node:fs"; +import { join, extname } from "node:path"; + +const W = process.cwd(); +const T = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript", + ".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml", + ".webp": "image/webp", ".ico": "image/x-icon", ".json": "application/json" }; +const srv = createServer((q, r) => { + const p = join(W, decodeURIComponent(q.url.split("?")[0])); + if (existsSync(p) && !p.endsWith("/")) { + r.writeHead(200, { "content-type": T[extname(p)] || "text/plain" }); + return r.end(readFileSync(p)); + } + r.writeHead(404).end(""); +}); +await new Promise((r) => srv.listen(4327, r)); + +const SEITEN = readdirSync(join(W, "webdesign")) + .filter((f) => f.endsWith(".html")); +const SPRACHEN = ["de", "de-CH", "en", "fr", "pt"]; +/* Reste, die eine doppelte Kodierung hinterlässt. Sie dürfen im + sichtbaren Text nie vorkommen. */ +const RESTE = ["&", "<", ">", """, "'", " "]; + +/* ===================================================================== + TEIL 1 — Prüfung am QUELLTEXT + + Die Browserprüfung weiter unten sieht nur, was gerade auf dem + Bildschirm steht. Grosse Teile des Portals und der Verwaltung + erscheinen aber erst NACH einer Anmeldung — genau dort sass der + Fehler, den es zu finden galt ("Ideen &amp; Änderungswünsche"). + Gegengeprüft: Die Browserprüfung allein hätte ihn NICHT gefunden. + + Diese Prüfung braucht keine Anzeige. Sie fragt nur: + Gibt es einen Textbaustein mit "&", der irgendwo durch die + Absicherung s(...) geschickt wird? Dann ist er dort doppelt kodiert. + ===================================================================== */ +function quelltextPruefen() { + const i18nDateien = readdirSync(join(W, "assets", "js")) + .filter((f) => f.startsWith("i18n-wd-") && f.endsWith(".js")); + + // Alle Schlüssel einsammeln, deren Text ein "&" enthält. + const verdaechtig = new Map(); // schluessel -> datei + for (const datei of i18nDateien) { + const inhalt = readFileSync(join(W, "assets", "js", datei), "utf-8"); + /* Der Block eines Schlüssels endet dort, wo seine geschweifte + Klammer wieder zugeht — nicht erst beim nächsten Schlüssel. + + Der Unterschied ist nicht theoretisch: Zwischen zwei Schlüsseln + stehen Kommentare, und einer davon lautet "Ideen & Änderungs- + wünsche". Mit der Regel "bis zum nächsten Schlüssel" hat der + VORHERIGE Schlüssel (po_senden, ein schlichtes "Senden") diesen + Kommentar mitgeschluckt und wurde fälschlich gemeldet. Ein + Fehlalarm im Prüfwerkzeug ist fast so schädlich wie ein + übersehener Fehler — man gewöhnt sich daran, ihn wegzusehen. */ + const zeilen = inhalt.split("\n"); + let aktuell = null; + let text = ""; + let tiefe = 0; + const abschliessen = () => { + if (aktuell && text.includes("&")) verdaechtig.set(aktuell, datei); + aktuell = null; text = ""; tiefe = 0; + }; + for (const z of zeilen) { + if (!aktuell) { + const m = z.match(/^ ([a-zA-Z0-9_]+):\s*\{/); + if (!m) continue; + aktuell = m[1]; text = z; + tiefe = (z.match(/\{/g) || []).length - (z.match(/\}/g) || []).length; + if (tiefe <= 0) abschliessen(); + } else { + text += "\n" + z; + tiefe += (z.match(/\{/g) || []).length - (z.match(/\}/g) || []).length; + if (tiefe <= 0) abschliessen(); + } + } + abschliessen(); + } + + // Wo werden sie abgesichert eingesetzt? + const quellen = [ + ...readdirSync(join(W, "assets", "js")).filter((f) => f.startsWith("wd-") && f.endsWith(".js")) + .map((f) => ["assets/js/" + f, readFileSync(join(W, "assets", "js", f), "utf-8")]), + ...SEITEN.map((f) => ["webdesign/" + f, readFileSync(join(W, "webdesign", f), "utf-8")]), + ]; + + const funde = []; + for (const [schluessel, datei] of verdaechtig) { + // s(window.WD.t("key")) oder s(t("key")) + const muster = new RegExp( + "\\bs\\(\\s*(?:window\\.WD\\.)?t\\(\\s*[\"']" + schluessel + "[\"']", ""); + for (const [name, inhalt] of quellen) { + if (muster.test(inhalt)) funde.push({ schluessel, datei, benutztIn: name }); + } + } + + console.log(`Quelltext: ${verdaechtig.size} Textbausteine enthalten "&".`); + if (funde.length) { + for (const f of funde) { + console.log(` FEHL ${f.schluessel} (${f.datei}) wird in ${f.benutztIn} abgesichert eingesetzt`); + console.log(` -> dort muss ein echtes "&" stehen, sonst erscheint "&" wörtlich.`); + } + } else { + console.log(" ok keiner davon wird durch die Absicherung geschickt.\n"); + } + return funde.length; +} + +let quellFehler = quelltextPruefen(); + +const browser = await chromium.launch(); +const seite = await browser.newPage({ viewport: { width: 1280, height: 900 } }); +// Die API gibt es hier nicht -- leere, gültige Antworten genügen. +await seite.route("**/webdesign/admin/**", (r) => r.fulfill({ json: { ok: true, anfragen: [], kunden: [], projekte: [], verlaeufe: [] } })); +await seite.route("**/webdesign/portal/**", (r) => r.fulfill({ json: { ok: true } })); +await seite.route("**/webdesign/api-ausweis", (r) => r.fulfill({ json: {} })); +await seite.addInitScript(() => { sessionStorage.setItem("wd-sitzung-aktiv", "1"); }); + +let treffer = 0; +let geprueft = 0; + +for (const datei of SEITEN) { + for (const sprache of SPRACHEN) { + await seite.addInitScript(`localStorage.setItem("dogi-sprache", ${JSON.stringify(sprache)})`); + await seite.goto("http://127.0.0.1:4327/webdesign/" + datei); + await seite.waitForTimeout(320); + geprueft++; + + const gefunden = await seite.evaluate((reste) => { + const raus = []; + const lauf = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); + let n; + while ((n = lauf.nextNode())) { + const eltern = n.parentElement; + if (!eltern || eltern.closest("script, style, code, pre")) continue; + const t = n.nodeValue; + for (const r of reste) { + if (t.includes(r)) { + raus.push({ rest: r, text: t.trim().slice(0, 70), wo: eltern.tagName.toLowerCase() }); + } + } + } + return raus; + }, RESTE); + + if (gefunden.length) { + treffer += gefunden.length; + for (const g of gefunden) { + console.log(` FEHL ${datei} [${sprache}] <${g.wo}> ${g.rest} in: "${g.text}"`); + } + } + } +} + +await browser.close(); +srv.close(); +console.log(`\n${geprueft} Seiten-Sprach-Kombinationen geprüft (${SEITEN.length} Seiten x ${SPRACHEN.length} Sprachen).`); +const gesamt = treffer + quellFehler; +console.log(gesamt === 0 + ? "===== keine doppelt kodierten Zeichen gefunden (Quelltext + Anzeige) =====" + : `===== ${gesamt} Fundstellen (${quellFehler} im Quelltext, ${treffer} in der Anzeige) =====`); +process.exitCode = gesamt ? 1 : 0; diff --git a/webdesign/ablauf.html b/webdesign/ablauf.html index b06092b6..02e65249 100644 --- a/webdesign/ablauf.html +++ b/webdesign/ablauf.html @@ -13,8 +13,8 @@ - - + +