diff --git a/404.html b/404.html index c5598493..a32d2771 100644 --- a/404.html +++ b/404.html @@ -3,7 +3,8 @@ - + + diff --git a/abnahme-installiert.png b/abnahme-installiert.png new file mode 100644 index 00000000..989ce49b Binary files /dev/null and b/abnahme-installiert.png differ diff --git a/abonnieren.html b/abonnieren.html index c2f8a582..6f5d94f0 100644 --- a/abonnieren.html +++ b/abonnieren.html @@ -5,7 +5,8 @@ DogiCrew 🩵 — DOGFATHER UNIVERSE - + + diff --git a/agb.html b/agb.html index 5f5e3eb5..c4ddb96f 100644 --- a/agb.html +++ b/agb.html @@ -5,7 +5,8 @@ AGB — DogiCrew-Supporter-Abo — DOGFATHER UNIVERSE - + + diff --git a/aktion-detail.html b/aktion-detail.html index 93b11f9f..1645d67d 100644 --- a/aktion-detail.html +++ b/aktion-detail.html @@ -5,7 +5,8 @@ Aktion — DOGFATHER UNIVERSE - + + diff --git a/assets/img/app-symbole/analyse-180.png b/assets/img/app-symbole/analyse-180.png new file mode 100644 index 00000000..db1bfabf Binary files /dev/null and b/assets/img/app-symbole/analyse-180.png differ diff --git a/assets/img/app-symbole/analyse-192-maskable.png b/assets/img/app-symbole/analyse-192-maskable.png new file mode 100644 index 00000000..c2774ddd Binary files /dev/null and b/assets/img/app-symbole/analyse-192-maskable.png differ diff --git a/assets/img/app-symbole/analyse-192.png b/assets/img/app-symbole/analyse-192.png new file mode 100644 index 00000000..d213e1a4 Binary files /dev/null and b/assets/img/app-symbole/analyse-192.png differ diff --git a/assets/img/app-symbole/analyse-32.png b/assets/img/app-symbole/analyse-32.png new file mode 100644 index 00000000..ac88efcf Binary files /dev/null and b/assets/img/app-symbole/analyse-32.png differ diff --git a/assets/img/app-symbole/analyse-512-maskable.png b/assets/img/app-symbole/analyse-512-maskable.png new file mode 100644 index 00000000..9920dd5d Binary files /dev/null and b/assets/img/app-symbole/analyse-512-maskable.png differ diff --git a/assets/img/app-symbole/analyse-512.png b/assets/img/app-symbole/analyse-512.png new file mode 100644 index 00000000..364265d7 Binary files /dev/null and b/assets/img/app-symbole/analyse-512.png differ diff --git a/assets/img/app-symbole/cloud-180.png b/assets/img/app-symbole/cloud-180.png new file mode 100644 index 00000000..e22e4643 Binary files /dev/null and b/assets/img/app-symbole/cloud-180.png differ diff --git a/assets/img/app-symbole/cloud-192-maskable.png b/assets/img/app-symbole/cloud-192-maskable.png new file mode 100644 index 00000000..2e89a006 Binary files /dev/null and b/assets/img/app-symbole/cloud-192-maskable.png differ diff --git a/assets/img/app-symbole/cloud-192.png b/assets/img/app-symbole/cloud-192.png new file mode 100644 index 00000000..2d2b4d70 Binary files /dev/null and b/assets/img/app-symbole/cloud-192.png differ diff --git a/assets/img/app-symbole/cloud-32.png b/assets/img/app-symbole/cloud-32.png new file mode 100644 index 00000000..7611f789 Binary files /dev/null and b/assets/img/app-symbole/cloud-32.png differ diff --git a/assets/img/app-symbole/cloud-512-maskable.png b/assets/img/app-symbole/cloud-512-maskable.png new file mode 100644 index 00000000..908a984e Binary files /dev/null and b/assets/img/app-symbole/cloud-512-maskable.png differ diff --git a/assets/img/app-symbole/cloud-512.png b/assets/img/app-symbole/cloud-512.png new file mode 100644 index 00000000..5b6a2e07 Binary files /dev/null and b/assets/img/app-symbole/cloud-512.png differ diff --git a/assets/img/app-symbole/git-180.png b/assets/img/app-symbole/git-180.png new file mode 100644 index 00000000..adbd8090 Binary files /dev/null and b/assets/img/app-symbole/git-180.png differ diff --git a/assets/img/app-symbole/git-192-maskable.png b/assets/img/app-symbole/git-192-maskable.png new file mode 100644 index 00000000..5ea4e18d Binary files /dev/null and b/assets/img/app-symbole/git-192-maskable.png differ diff --git a/assets/img/app-symbole/git-192.png b/assets/img/app-symbole/git-192.png new file mode 100644 index 00000000..d8a421c5 Binary files /dev/null and b/assets/img/app-symbole/git-192.png differ diff --git a/assets/img/app-symbole/git-32.png b/assets/img/app-symbole/git-32.png new file mode 100644 index 00000000..a58d1d06 Binary files /dev/null and b/assets/img/app-symbole/git-32.png differ diff --git a/assets/img/app-symbole/git-512-maskable.png b/assets/img/app-symbole/git-512-maskable.png new file mode 100644 index 00000000..77d5ae22 Binary files /dev/null and b/assets/img/app-symbole/git-512-maskable.png differ diff --git a/assets/img/app-symbole/git-512.png b/assets/img/app-symbole/git-512.png new file mode 100644 index 00000000..a6bb8b78 Binary files /dev/null and b/assets/img/app-symbole/git-512.png differ diff --git a/assets/img/app-symbole/portal-180.png b/assets/img/app-symbole/portal-180.png new file mode 100644 index 00000000..2b17a7a6 Binary files /dev/null and b/assets/img/app-symbole/portal-180.png differ diff --git a/assets/img/app-symbole/portal-192-maskable.png b/assets/img/app-symbole/portal-192-maskable.png new file mode 100644 index 00000000..4772e9fa Binary files /dev/null and b/assets/img/app-symbole/portal-192-maskable.png differ diff --git a/assets/img/app-symbole/portal-192.png b/assets/img/app-symbole/portal-192.png new file mode 100644 index 00000000..5da168f1 Binary files /dev/null and b/assets/img/app-symbole/portal-192.png differ diff --git a/assets/img/app-symbole/portal-32.png b/assets/img/app-symbole/portal-32.png new file mode 100644 index 00000000..38f0a0d6 Binary files /dev/null and b/assets/img/app-symbole/portal-32.png differ diff --git a/assets/img/app-symbole/portal-512-maskable.png b/assets/img/app-symbole/portal-512-maskable.png new file mode 100644 index 00000000..96a6c657 Binary files /dev/null and b/assets/img/app-symbole/portal-512-maskable.png differ diff --git a/assets/img/app-symbole/portal-512.png b/assets/img/app-symbole/portal-512.png new file mode 100644 index 00000000..37315ce4 Binary files /dev/null and b/assets/img/app-symbole/portal-512.png differ diff --git a/assets/img/app-symbole/universe-180.png b/assets/img/app-symbole/universe-180.png new file mode 100644 index 00000000..d4abeb66 Binary files /dev/null and b/assets/img/app-symbole/universe-180.png differ diff --git a/assets/img/app-symbole/universe-192-maskable.png b/assets/img/app-symbole/universe-192-maskable.png new file mode 100644 index 00000000..6529e343 Binary files /dev/null and b/assets/img/app-symbole/universe-192-maskable.png differ diff --git a/assets/img/app-symbole/universe-192.png b/assets/img/app-symbole/universe-192.png new file mode 100644 index 00000000..40367384 Binary files /dev/null and b/assets/img/app-symbole/universe-192.png differ diff --git a/assets/img/app-symbole/universe-32.png b/assets/img/app-symbole/universe-32.png new file mode 100644 index 00000000..30172ded Binary files /dev/null and b/assets/img/app-symbole/universe-32.png differ diff --git a/assets/img/app-symbole/universe-512-maskable.png b/assets/img/app-symbole/universe-512-maskable.png new file mode 100644 index 00000000..8cc3f7a7 Binary files /dev/null and b/assets/img/app-symbole/universe-512-maskable.png differ diff --git a/assets/img/app-symbole/universe-512.png b/assets/img/app-symbole/universe-512.png new file mode 100644 index 00000000..91faca49 Binary files /dev/null and b/assets/img/app-symbole/universe-512.png differ diff --git a/assets/img/app-symbole/verwaltung-180.png b/assets/img/app-symbole/verwaltung-180.png new file mode 100644 index 00000000..ee086c8b Binary files /dev/null and b/assets/img/app-symbole/verwaltung-180.png differ diff --git a/assets/img/app-symbole/verwaltung-192-maskable.png b/assets/img/app-symbole/verwaltung-192-maskable.png new file mode 100644 index 00000000..264fc5a0 Binary files /dev/null and b/assets/img/app-symbole/verwaltung-192-maskable.png differ diff --git a/assets/img/app-symbole/verwaltung-192.png b/assets/img/app-symbole/verwaltung-192.png new file mode 100644 index 00000000..b7de21e5 Binary files /dev/null and b/assets/img/app-symbole/verwaltung-192.png differ diff --git a/assets/img/app-symbole/verwaltung-32.png b/assets/img/app-symbole/verwaltung-32.png new file mode 100644 index 00000000..3eaa23cf Binary files /dev/null and b/assets/img/app-symbole/verwaltung-32.png differ diff --git a/assets/img/app-symbole/verwaltung-512-maskable.png b/assets/img/app-symbole/verwaltung-512-maskable.png new file mode 100644 index 00000000..06c59ece Binary files /dev/null and b/assets/img/app-symbole/verwaltung-512-maskable.png differ diff --git a/assets/img/app-symbole/verwaltung-512.png b/assets/img/app-symbole/verwaltung-512.png new file mode 100644 index 00000000..997da3c8 Binary files /dev/null and b/assets/img/app-symbole/verwaltung-512.png differ diff --git a/assets/img/app-symbole/wdverwalt-180.png b/assets/img/app-symbole/wdverwalt-180.png new file mode 100644 index 00000000..03a7eb23 Binary files /dev/null and b/assets/img/app-symbole/wdverwalt-180.png differ diff --git a/assets/img/app-symbole/wdverwalt-192-maskable.png b/assets/img/app-symbole/wdverwalt-192-maskable.png new file mode 100644 index 00000000..79ba5617 Binary files /dev/null and b/assets/img/app-symbole/wdverwalt-192-maskable.png differ diff --git a/assets/img/app-symbole/wdverwalt-192.png b/assets/img/app-symbole/wdverwalt-192.png new file mode 100644 index 00000000..64bb33b8 Binary files /dev/null and b/assets/img/app-symbole/wdverwalt-192.png differ diff --git a/assets/img/app-symbole/wdverwalt-32.png b/assets/img/app-symbole/wdverwalt-32.png new file mode 100644 index 00000000..26f8edc0 Binary files /dev/null and b/assets/img/app-symbole/wdverwalt-32.png differ diff --git a/assets/img/app-symbole/wdverwalt-512-maskable.png b/assets/img/app-symbole/wdverwalt-512-maskable.png new file mode 100644 index 00000000..9fb4f75a Binary files /dev/null and b/assets/img/app-symbole/wdverwalt-512-maskable.png differ diff --git a/assets/img/app-symbole/wdverwalt-512.png b/assets/img/app-symbole/wdverwalt-512.png new file mode 100644 index 00000000..6fa760cd Binary files /dev/null and b/assets/img/app-symbole/wdverwalt-512.png differ diff --git a/assets/img/app-symbole/webdesign-180.png b/assets/img/app-symbole/webdesign-180.png new file mode 100644 index 00000000..47725094 Binary files /dev/null and b/assets/img/app-symbole/webdesign-180.png differ diff --git a/assets/img/app-symbole/webdesign-192-maskable.png b/assets/img/app-symbole/webdesign-192-maskable.png new file mode 100644 index 00000000..fefc49f1 Binary files /dev/null and b/assets/img/app-symbole/webdesign-192-maskable.png differ diff --git a/assets/img/app-symbole/webdesign-192.png b/assets/img/app-symbole/webdesign-192.png new file mode 100644 index 00000000..1cac861f Binary files /dev/null and b/assets/img/app-symbole/webdesign-192.png differ diff --git a/assets/img/app-symbole/webdesign-32.png b/assets/img/app-symbole/webdesign-32.png new file mode 100644 index 00000000..db758278 Binary files /dev/null and b/assets/img/app-symbole/webdesign-32.png differ diff --git a/assets/img/app-symbole/webdesign-512-maskable.png b/assets/img/app-symbole/webdesign-512-maskable.png new file mode 100644 index 00000000..9d5f4980 Binary files /dev/null and b/assets/img/app-symbole/webdesign-512-maskable.png differ diff --git a/assets/img/app-symbole/webdesign-512.png b/assets/img/app-symbole/webdesign-512.png new file mode 100644 index 00000000..307b2ba1 Binary files /dev/null and b/assets/img/app-symbole/webdesign-512.png differ diff --git a/assets/img/app-symbole/workspace-180.png b/assets/img/app-symbole/workspace-180.png new file mode 100644 index 00000000..0151c311 Binary files /dev/null and b/assets/img/app-symbole/workspace-180.png differ diff --git a/assets/img/app-symbole/workspace-192-maskable.png b/assets/img/app-symbole/workspace-192-maskable.png new file mode 100644 index 00000000..d89ccbdc Binary files /dev/null and b/assets/img/app-symbole/workspace-192-maskable.png differ diff --git a/assets/img/app-symbole/workspace-192.png b/assets/img/app-symbole/workspace-192.png new file mode 100644 index 00000000..3536d645 Binary files /dev/null and b/assets/img/app-symbole/workspace-192.png differ diff --git a/assets/img/app-symbole/workspace-32.png b/assets/img/app-symbole/workspace-32.png new file mode 100644 index 00000000..c245765e Binary files /dev/null and b/assets/img/app-symbole/workspace-32.png differ diff --git a/assets/img/app-symbole/workspace-512-maskable.png b/assets/img/app-symbole/workspace-512-maskable.png new file mode 100644 index 00000000..b98e2c35 Binary files /dev/null and b/assets/img/app-symbole/workspace-512-maskable.png differ diff --git a/assets/img/app-symbole/workspace-512.png b/assets/img/app-symbole/workspace-512.png new file mode 100644 index 00000000..c7feb930 Binary files /dev/null and b/assets/img/app-symbole/workspace-512.png differ diff --git a/assets/img/app-symbole/zocker-180.png b/assets/img/app-symbole/zocker-180.png new file mode 100644 index 00000000..98c931ee Binary files /dev/null and b/assets/img/app-symbole/zocker-180.png differ diff --git a/assets/img/app-symbole/zocker-192-maskable.png b/assets/img/app-symbole/zocker-192-maskable.png new file mode 100644 index 00000000..08fad1d1 Binary files /dev/null and b/assets/img/app-symbole/zocker-192-maskable.png differ diff --git a/assets/img/app-symbole/zocker-192.png b/assets/img/app-symbole/zocker-192.png new file mode 100644 index 00000000..3eff40b1 Binary files /dev/null and b/assets/img/app-symbole/zocker-192.png differ diff --git a/assets/img/app-symbole/zocker-32.png b/assets/img/app-symbole/zocker-32.png new file mode 100644 index 00000000..75ef9eb9 Binary files /dev/null and b/assets/img/app-symbole/zocker-32.png differ diff --git a/assets/img/app-symbole/zocker-512-maskable.png b/assets/img/app-symbole/zocker-512-maskable.png new file mode 100644 index 00000000..9f783d70 Binary files /dev/null and b/assets/img/app-symbole/zocker-512-maskable.png differ diff --git a/assets/img/app-symbole/zocker-512.png b/assets/img/app-symbole/zocker-512.png new file mode 100644 index 00000000..c22a6e4a Binary files /dev/null and b/assets/img/app-symbole/zocker-512.png differ diff --git a/assets/img/marke/logo-kopf-frei.png b/assets/img/marke/logo-kopf-frei.png new file mode 100644 index 00000000..da0ea20b Binary files /dev/null and b/assets/img/marke/logo-kopf-frei.png differ diff --git a/bewerben-creator.html b/bewerben-creator.html index f3f6fb40..77f449d9 100644 --- a/bewerben-creator.html +++ b/bewerben-creator.html @@ -5,7 +5,8 @@ Streamer gesucht — DOGFATHER UNIVERSE - + + diff --git a/bewerben-kooperation.html b/bewerben-kooperation.html index ac685ea0..3f8e6af7 100644 --- a/bewerben-kooperation.html +++ b/bewerben-kooperation.html @@ -8,7 +8,8 @@ Kooperation anfragen — DOGFATHER UNIVERSE - + + diff --git a/bewerben-modi.html b/bewerben-modi.html index 8dfd0db9..e25deb93 100644 --- a/bewerben-modi.html +++ b/bewerben-modi.html @@ -5,7 +5,8 @@ Modi werden — DOGFATHER UNIVERSE - + + diff --git a/bewerben-scout.html b/bewerben-scout.html index 2fd6eb00..039cc32e 100644 --- a/bewerben-scout.html +++ b/bewerben-scout.html @@ -5,7 +5,8 @@ Scout werden — DOGFATHER UNIVERSE - + + diff --git a/bewerben.html b/bewerben.html index b5652197..2a069d3c 100644 --- a/bewerben.html +++ b/bewerben.html @@ -5,7 +5,8 @@ Bewerben — DOGFATHER UNIVERSE - + + diff --git a/blickfang-portfolio-handy.png b/blickfang-portfolio-handy.png index b29210b4..c2efb706 100644 Binary files a/blickfang-portfolio-handy.png and b/blickfang-portfolio-handy.png differ diff --git a/blickfang-portfolio-pc.png b/blickfang-portfolio-pc.png index f1192cb3..6021a4f9 100644 Binary files a/blickfang-portfolio-pc.png and b/blickfang-portfolio-pc.png differ diff --git a/blickfang-ueber-handy.png b/blickfang-ueber-handy.png index c301e1b4..2335e15b 100644 Binary files a/blickfang-ueber-handy.png and b/blickfang-ueber-handy.png differ diff --git a/blickfang-ueber-pc.png b/blickfang-ueber-pc.png index 8b3853c1..4c7bd6d0 100644 Binary files a/blickfang-ueber-pc.png and b/blickfang-ueber-pc.png differ diff --git a/casper.html b/casper.html index 74c19826..0876d70f 100644 --- a/casper.html +++ b/casper.html @@ -5,7 +5,8 @@ Casper — DOGFATHER UNIVERSE - + + diff --git a/community.html b/community.html index 1294e3ea..b9db42d0 100644 --- a/community.html +++ b/community.html @@ -5,7 +5,8 @@ Die Community — DOGFATHER UNIVERSE - + + diff --git a/creator.html b/creator.html index 0dcb5579..e82a67ab 100644 --- a/creator.html +++ b/creator.html @@ -5,7 +5,8 @@ Creator im Team — DOGFATHER UNIVERSE - + + diff --git a/dogfather.html b/dogfather.html index 20e8ce00..3dde4364 100644 --- a/dogfather.html +++ b/dogfather.html @@ -5,7 +5,8 @@ Die Person, die DogFather erschuf — DOGFATHER UNIVERSE - + + diff --git a/entwurf-app-symbole.png b/entwurf-app-symbole.png new file mode 100644 index 00000000..3bb24441 Binary files /dev/null and b/entwurf-app-symbole.png differ diff --git a/entwurf-startbildschirm.png b/entwurf-startbildschirm.png new file mode 100644 index 00000000..26261a2e Binary files /dev/null and b/entwurf-startbildschirm.png differ diff --git a/gate.html b/gate.html index 1bb39255..db5bc6cb 100644 --- a/gate.html +++ b/gate.html @@ -5,7 +5,7 @@ Zugang — DOGFATHER UNIVERSE - + - + diff --git a/hasidog.html b/hasidog.html index 6ac2be43..f1d21b4f 100644 --- a/hasidog.html +++ b/hasidog.html @@ -5,7 +5,8 @@ HasiDog-Welt — DOGFATHER UNIVERSE - + + diff --git a/index.html b/index.html index 4ec0fa1b..659866b6 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,8 @@ DOGFATHER UNIVERSE — Streamer. Community. HasiDog. Manager. - + + diff --git a/kontakt.html b/kontakt.html index 71f49a6d..32e241bf 100644 --- a/kontakt.html +++ b/kontakt.html @@ -5,7 +5,8 @@ Kontakt & Rechtliches — DOGFATHER UNIVERSE - + + diff --git a/links.html b/links.html index 929eef94..758566cb 100644 --- a/links.html +++ b/links.html @@ -5,7 +5,8 @@ Alle Links — DOGFATHER UNIVERSE - + + diff --git a/manager.html b/manager.html index 078dc9bf..1d140ca7 100644 --- a/manager.html +++ b/manager.html @@ -5,7 +5,8 @@ Manager DogFather — Spicy Media — DOGFATHER UNIVERSE - + + diff --git a/manifest-verwaltung.json b/manifest-verwaltung.json index 40d94ea3..03890e96 100644 --- a/manifest-verwaltung.json +++ b/manifest-verwaltung.json @@ -13,25 +13,25 @@ "dir": "ltr", "icons": [ { - "src": "/assets/img/icon-192-admin.png", + "src": "/assets/img/app-symbole/verwaltung-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/icon-512-admin.png", + "src": "/assets/img/app-symbole/verwaltung-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/icon-192-maskable-admin.png", + "src": "/assets/img/app-symbole/verwaltung-192-maskable.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/icon-512-maskable-admin.png", + "src": "/assets/img/app-symbole/verwaltung-512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/manifest.json b/manifest.json index edeb6a0c..42a1686f 100644 --- a/manifest.json +++ b/manifest.json @@ -13,25 +13,25 @@ "dir": "ltr", "icons": [ { - "src": "/assets/img/icon-192.png", + "src": "/assets/img/app-symbole/universe-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/icon-512.png", + "src": "/assets/img/app-symbole/universe-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/icon-192-maskable.png", + "src": "/assets/img/app-symbole/universe-192-maskable.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/icon-512-maskable.png", + "src": "/assets/img/app-symbole/universe-512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/medien-casper.html b/medien-casper.html index 62e27087..8de85354 100644 --- a/medien-casper.html +++ b/medien-casper.html @@ -5,7 +5,8 @@ Medien — Casper — DOGFATHER UNIVERSE - + + diff --git a/medien-dogfather.html b/medien-dogfather.html index bb194bb3..f82aeecb 100644 --- a/medien-dogfather.html +++ b/medien-dogfather.html @@ -5,7 +5,8 @@ Medien — DogFather — DOGFATHER UNIVERSE - + + diff --git a/medien-hasidog.html b/medien-hasidog.html index 59ee626c..102c75a2 100644 --- a/medien-hasidog.html +++ b/medien-hasidog.html @@ -5,7 +5,8 @@ Medien — HasiDog — DOGFATHER UNIVERSE - + + diff --git a/profil.html b/profil.html index 5af34b56..204592d8 100644 --- a/profil.html +++ b/profil.html @@ -5,7 +5,8 @@ Profil — DOGFATHER UNIVERSE - + + diff --git a/pruef-ampel.png b/pruef-ampel.png index 1b45d24d..a6327ec9 100644 Binary files a/pruef-ampel.png and b/pruef-ampel.png differ diff --git a/pruef-aufgaben-computer.png b/pruef-aufgaben-computer.png index 656c5ee9..32e585aa 100644 Binary files a/pruef-aufgaben-computer.png and b/pruef-aufgaben-computer.png differ diff --git a/pruef-aufgaben-handy.png b/pruef-aufgaben-handy.png index e3ed659a..bfbdf9d1 100644 Binary files a/pruef-aufgaben-handy.png and b/pruef-aufgaben-handy.png differ diff --git a/pruef-aufgabenbrett.png b/pruef-aufgabenbrett.png index 642a0130..f1e02a8b 100644 Binary files a/pruef-aufgabenbrett.png and b/pruef-aufgabenbrett.png differ diff --git a/pruef-browser-lauf.txt b/pruef-browser-lauf.txt new file mode 100644 index 00000000..960a21c7 --- /dev/null +++ b/pruef-browser-lauf.txt @@ -0,0 +1,17 @@ + ok Chromium (Chrome, Edge) 16 Seiten + ok Firefox (Gecko) 16 Seiten + ok WebKit (Safari) 16 Seiten + ok WebKit auf dem Handy (iPhone) 16 Seiten + ok 4 von 4 Maschinen wirklich gestartet + ok jede Maschine konnte gestartet werden + ok 64 Seitenaufrufe ueber alle Maschinen + ok 14477 Elemente vermessen + ok Chromium: die Startseite ist unauffaellig (0px, 1613 Zeichen) + ok Chromium: zu breites Element wird gefunden (1720px) + ok Chromium: ein Skriptfehler wird bemerkt (Absichtlicher Testfehler) + ok Firefox: die Startseite ist unauffaellig (0px, 1565 Zeichen) + ok Firefox: zu breites Element wird gefunden (1720px) + ok Firefox: ein Skriptfehler wird bemerkt (Absichtlicher Testfehler) + ok WebKit: die Startseite ist unauffaellig (0px, 1565 Zeichen) + ok WebKit: zu breites Element wird gefunden (1720px) + ok WebKit: ein Skriptfehler wird bemerkt (Absichtlicher Testfehler) diff --git a/pruef-buehne.png b/pruef-buehne.png index 0d1d4e5d..f65fcf07 100644 Binary files a/pruef-buehne.png and b/pruef-buehne.png differ diff --git a/pruef-checkliste.png b/pruef-checkliste.png index 6e13a0a5..ed05eb24 100644 Binary files a/pruef-checkliste.png and b/pruef-checkliste.png differ diff --git a/pruef-code.png b/pruef-code.png new file mode 100644 index 00000000..a4940c63 Binary files /dev/null and b/pruef-code.png differ diff --git a/pruef-content-computer.png b/pruef-content-computer.png index be38fe95..9f6c0956 100644 Binary files a/pruef-content-computer.png and b/pruef-content-computer.png differ diff --git a/pruef-content-creator.png b/pruef-content-creator.png index 5cdcc1c0..41b134e3 100644 Binary files a/pruef-content-creator.png and b/pruef-content-creator.png differ diff --git a/pruef-content-handy.png b/pruef-content-handy.png index c3e95276..80a74911 100644 Binary files a/pruef-content-handy.png and b/pruef-content-handy.png differ diff --git a/pruef-detail-computer.png b/pruef-detail-computer.png index 0bacaf69..ab9d70ca 100644 Binary files a/pruef-detail-computer.png and b/pruef-detail-computer.png differ diff --git a/pruef-detail-handy.png b/pruef-detail-handy.png index 56a99370..caed9a08 100644 Binary files a/pruef-detail-handy.png and b/pruef-detail-handy.png differ diff --git a/pruef-felder.png b/pruef-felder.png index b0f49429..51869388 100644 Binary files a/pruef-felder.png and b/pruef-felder.png differ diff --git a/pruef-freie-namen.png b/pruef-freie-namen.png new file mode 100644 index 00000000..9cdcf10b Binary files /dev/null and b/pruef-freie-namen.png differ diff --git a/pruef-handy.png b/pruef-handy.png index 451b13d3..92fbb27b 100644 Binary files a/pruef-handy.png and b/pruef-handy.png differ diff --git a/pruef-kalender-computer.png b/pruef-kalender-computer.png index a69a3f18..2471e844 100644 Binary files a/pruef-kalender-computer.png and b/pruef-kalender-computer.png differ diff --git a/pruef-kalender-handy.png b/pruef-kalender-handy.png index b065742f..30c67baf 100644 Binary files a/pruef-kalender-handy.png and b/pruef-kalender-handy.png differ diff --git a/pruef-lesbarkeit-scouting.png b/pruef-lesbarkeit-scouting.png index ab4ab271..824d9a66 100644 Binary files a/pruef-lesbarkeit-scouting.png and b/pruef-lesbarkeit-scouting.png differ diff --git a/pruef-lesbarkeit-wissen.png b/pruef-lesbarkeit-wissen.png index 90077bbe..dc47b4e5 100644 Binary files a/pruef-lesbarkeit-wissen.png and b/pruef-lesbarkeit-wissen.png differ diff --git a/pruef-merkzeichen.png b/pruef-merkzeichen.png index 172548f4..456af9f2 100644 Binary files a/pruef-merkzeichen.png and b/pruef-merkzeichen.png differ diff --git a/pruef-personen-formular-handy.png b/pruef-personen-formular-handy.png new file mode 100644 index 00000000..bbf94d8e Binary files /dev/null and b/pruef-personen-formular-handy.png differ diff --git a/pruef-personen-formular.png b/pruef-personen-formular.png new file mode 100644 index 00000000..2b2f625b Binary files /dev/null and b/pruef-personen-formular.png differ diff --git a/pruef-personen.png b/pruef-personen.png index a0ec129d..42306b50 100644 Binary files a/pruef-personen.png and b/pruef-personen.png differ diff --git a/pruef-portal-aufgaben-computer.png b/pruef-portal-aufgaben-computer.png index cb875258..3b5dc5ed 100644 Binary files a/pruef-portal-aufgaben-computer.png and b/pruef-portal-aufgaben-computer.png differ diff --git a/pruef-portal-aufgaben-handy.png b/pruef-portal-aufgaben-handy.png index 539f1cd6..d97befab 100644 Binary files a/pruef-portal-aufgaben-handy.png and b/pruef-portal-aufgaben-handy.png differ diff --git a/pruef-portal-postfach-computer.png b/pruef-portal-postfach-computer.png index c26f8b2e..9df35724 100644 Binary files a/pruef-portal-postfach-computer.png and b/pruef-portal-postfach-computer.png differ diff --git a/pruef-portal-postfach-handy.png b/pruef-portal-postfach-handy.png index 81454829..86c45b05 100644 Binary files a/pruef-portal-postfach-handy.png and b/pruef-portal-postfach-handy.png differ diff --git a/pruef-portfolio-computer.png b/pruef-portfolio-computer.png index 24e4a4dc..8597d2d4 100644 Binary files a/pruef-portfolio-computer.png and b/pruef-portfolio-computer.png differ diff --git a/pruef-portfolio-handy.png b/pruef-portfolio-handy.png index f7715e5f..5319eb51 100644 Binary files a/pruef-portfolio-handy.png and b/pruef-portfolio-handy.png differ diff --git a/pruef-postfach-computer.png b/pruef-postfach-computer.png index e92b8071..dfa79f0b 100644 Binary files a/pruef-postfach-computer.png and b/pruef-postfach-computer.png differ diff --git a/pruef-postfach-handy.png b/pruef-postfach-handy.png index a16c853e..aa8ec588 100644 Binary files a/pruef-postfach-handy.png and b/pruef-postfach-handy.png differ diff --git a/pruef-protokoll-auf.png b/pruef-protokoll-auf.png index dbb453fb..64bb2c86 100644 Binary files a/pruef-protokoll-auf.png and b/pruef-protokoll-auf.png differ diff --git a/pruef-protokoll-loeschen.png b/pruef-protokoll-loeschen.png new file mode 100644 index 00000000..97da3aed Binary files /dev/null and b/pruef-protokoll-loeschen.png differ diff --git a/pruef-protokoll-zu.png b/pruef-protokoll-zu.png index 643c8af1..4236c91a 100644 Binary files a/pruef-protokoll-zu.png and b/pruef-protokoll-zu.png differ diff --git a/pruef-rechtliches.png b/pruef-rechtliches.png index 033e586a..b0895012 100644 Binary files a/pruef-rechtliches.png and b/pruef-rechtliches.png differ diff --git a/pruef-rollen-admin.png b/pruef-rollen-admin.png new file mode 100644 index 00000000..0f7b9b98 Binary files /dev/null and b/pruef-rollen-admin.png differ diff --git a/pruef-rollen-creator.png b/pruef-rollen-creator.png new file mode 100644 index 00000000..3a06ea78 Binary files /dev/null and b/pruef-rollen-creator.png differ diff --git a/pruef-rollen-manager.png b/pruef-rollen-manager.png new file mode 100644 index 00000000..90d1ed18 Binary files /dev/null and b/pruef-rollen-manager.png differ diff --git a/pruef-rollen-scout.png b/pruef-rollen-scout.png new file mode 100644 index 00000000..2131d85a Binary files /dev/null and b/pruef-rollen-scout.png differ diff --git a/pruef-schulung.png b/pruef-schulung.png index 6cf87062..59ccab12 100644 Binary files a/pruef-schulung.png and b/pruef-schulung.png differ diff --git a/pruef-scout-zuteilung.png b/pruef-scout-zuteilung.png index 56cfc88a..9699644e 100644 Binary files a/pruef-scout-zuteilung.png and b/pruef-scout-zuteilung.png differ diff --git a/pruef-serien-computer.png b/pruef-serien-computer.png new file mode 100644 index 00000000..62db1f5a Binary files /dev/null and b/pruef-serien-computer.png differ diff --git a/pruef-serien-handy.png b/pruef-serien-handy.png new file mode 100644 index 00000000..07a0cc8b Binary files /dev/null and b/pruef-serien-handy.png differ diff --git a/pruef-serien-liste.png b/pruef-serien-liste.png new file mode 100644 index 00000000..37a92161 Binary files /dev/null and b/pruef-serien-liste.png differ diff --git a/pruef-sprung.png b/pruef-sprung.png index 542c4e0a..9affd553 100644 Binary files a/pruef-sprung.png and b/pruef-sprung.png differ diff --git a/pruef-start-computer.png b/pruef-start-computer.png index 0e468ca8..0ae43571 100644 Binary files a/pruef-start-computer.png and b/pruef-start-computer.png differ diff --git a/pruef-start-creator.png b/pruef-start-creator.png index 7f24cb48..f009d19e 100644 Binary files a/pruef-start-creator.png and b/pruef-start-creator.png differ diff --git a/pruef-start-handy.png b/pruef-start-handy.png index 5e733b24..dd721b58 100644 Binary files a/pruef-start-handy.png and b/pruef-start-handy.png differ diff --git a/pruef-steckbrief.png b/pruef-steckbrief.png index 7f99264a..39021d70 100644 Binary files a/pruef-steckbrief.png and b/pruef-steckbrief.png differ diff --git a/pruef-team.png b/pruef-team.png index 2b4556d6..d49e2ab8 100644 Binary files a/pruef-team.png and b/pruef-team.png differ diff --git a/pruef-uebersicht-computer.png b/pruef-uebersicht-computer.png index f8a3608d..e5de787a 100644 Binary files a/pruef-uebersicht-computer.png and b/pruef-uebersicht-computer.png differ diff --git a/pruef-uebersicht-creator.png b/pruef-uebersicht-creator.png index 12cc459b..403319c4 100644 Binary files a/pruef-uebersicht-creator.png and b/pruef-uebersicht-creator.png differ diff --git a/pruef-uebersicht-handy.png b/pruef-uebersicht-handy.png index ac5a9d12..477414b1 100644 Binary files a/pruef-uebersicht-handy.png and b/pruef-uebersicht-handy.png differ diff --git a/pruef-vw-schmuck-computer.png b/pruef-vw-schmuck-computer.png index 5f3e1546..76ddf5e1 100644 Binary files a/pruef-vw-schmuck-computer.png and b/pruef-vw-schmuck-computer.png differ diff --git a/pruef-vw-schmuck-handy.png b/pruef-vw-schmuck-handy.png index 9832b4ac..3eb555aa 100644 Binary files a/pruef-vw-schmuck-handy.png and b/pruef-vw-schmuck-handy.png differ diff --git a/pruef-widerruf-computer.png b/pruef-widerruf-computer.png index 03580a3a..ab02ae38 100644 Binary files a/pruef-widerruf-computer.png and b/pruef-widerruf-computer.png differ diff --git a/pruef-widerruf-handy.png b/pruef-widerruf-handy.png index b3da693b..fd327394 100644 Binary files a/pruef-widerruf-handy.png and b/pruef-widerruf-handy.png differ diff --git a/pruef-zustand.png b/pruef-zustand.png index 0bec3e15..4892da17 100644 Binary files a/pruef-zustand.png and b/pruef-zustand.png differ diff --git a/rabattcodes.html b/rabattcodes.html index e36f08e9..fdaa146b 100644 --- a/rabattcodes.html +++ b/rabattcodes.html @@ -5,7 +5,8 @@ Rabattcodes — DOGFATHER UNIVERSE - + + diff --git a/server/abnahme-installiert.png b/server/abnahme-installiert.png index adcea9fb..df5f0ec8 100644 Binary files a/server/abnahme-installiert.png and b/server/abnahme-installiert.png differ diff --git a/server/helfer-port.mjs b/server/helfer-port.mjs new file mode 100644 index 00000000..6a346214 --- /dev/null +++ b/server/helfer-port.mjs @@ -0,0 +1,58 @@ +/* IST DER PORT WIRKLICH MEINER? + + Gemeinsamer Baustein für alle Prüfungen, die einen eigenen Server + starten. + + DER FEHLER, DEN DAS VERHINDERT — und der in diesem Haus schon zweimal + zugeschlagen hat: + + Läuft auf dem gewünschten Port schon ein anderer Prozess, dann startet + der eigene Server nicht. `app.listen()` meldet das an einen + Fehlerbehandler, den niemand gesetzt hat -- die Prüfung läuft + weiter und misst ab da gegen den FREMDEN Server. Sie bekommt Antworten, + sie bekommt sogar plausible Antworten, und sie meldet Fehler, die es + nicht gibt (oder schlimmer: sie meldet "in Ordnung" über einen Stand, + den sie nie angesehen hat). + + Das erste Mal stand es schon in der Projektnotiz vom 27.08.2026: + *"Ein alter Testserver hielt den Port besetzt (EADDRINUSE), der neue + startete still nicht — ich habe zwischenzeitlich gegen den alten Stand + gemessen."* + + Das zweite Mal am 05.09.2026: Ein Prüflauf lief von Hand, während die + Gesamtprüfung denselben Lauf ebenfalls aufrief. Beide wollten Port + 4289. Ergebnis war ein WebKit-Fehler, den es gar nicht gab -- die + Anmeldung scheiterte, weil der Code aus der einen Datenbank gegen den + Server der anderen geprüft wurde. Zwei Fehlersuchen an der falschen + Stelle (HSTS, dann CSP) waren die Folge. + + DESHALB: Vor dem Start wird nachgesehen, ob der Port frei ist. Ist er + belegt, bricht die Prüfung SOFORT ab und sagt, warum. Ein belegter + Port ist kein Messergebnis -- er ist der dritte Ausgang: nicht in + Ordnung ist etwas anderes als nicht nachgesehen. + =================================================================== */ + +import { createServer } from "node:net"; + +/** Wirft, wenn der Port belegt ist. Sonst gibt er ihn frei zurück. */ +export async function portMussFreiSein(port, wer = "diese Pruefung") { + const belegt = await new Promise((fertig) => { + const s = createServer(); + s.once("error", (f) => fertig(f.code === "EADDRINUSE")); + s.once("listening", () => s.close(() => fertig(false))); + s.listen(port, "127.0.0.1"); + }); + + if (belegt) { + console.error(`\nABBRUCH: Port ${port} ist belegt.\n`); + console.error(` ${wer} startet einen eigenen Server auf diesem Port. Laeuft dort`); + console.error(` schon etwas, wuerde gegen einen FREMDEN Stand gemessen -- mit`); + console.error(` Ergebnissen, die nichts ueber den eigenen Code aussagen.\n`); + console.error(` Meist laeuft parallel noch ein zweiter Prueflauf (etwa`); + console.error(` tools/alles-pruefen.mjs). Erst den beenden, dann hier neu starten.\n`); + console.error(` Nachsehen, wer den Port haelt:`); + console.error(` netstat -ano | findstr :${port}\n`); + process.exit(3); + } + return port; +} diff --git a/server/index.js b/server/index.js index edddefe2..cf33ed60 100644 --- a/server/index.js +++ b/server/index.js @@ -54,7 +54,42 @@ app.use((req, res, next) => { res.setHeader("X-Frame-Options", "SAMEORIGIN"); res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin"); res.setHeader("Permissions-Policy", "geolocation=(), microphone=(), camera=(), payment=()"); - res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); + + /* HSTS NUR ÜBER EINE GESICHERTE VERBINDUNG (Befund 04.09.2026, Audit). + + RFC 6797, Abschnitt 7.2, ist an dieser Stelle unmissverständlich: + "An HSTS Host MUST NOT include the STS header field in HTTP + responses conveyed over non-secure transport." + + Vorher ging der Header bei JEDER Antwort mit, auch über http. Live + fällt das nicht auf -- hinter Cloudflare und Caddy kommt ohnehin + alles über https an, und `trust proxy` macht `req.secure` dort zu + `true`. Aufgefallen ist es beim Browsertest: WebKit (Safari) nimmt + den Header auch von einer http-Adresse an, merkt sich 127.0.0.1 als + "nur noch https" -- und ab da scheitert jede Verbindung zum lokalen + Testserver mit "SSL connect error". Chromium und Firefox machen für + localhost eine Ausnahme, WebKit nicht. + + Der Fehler war also nie in der Website zu sehen, hätte aber jede + künftige Safari-Prüfung unmöglich gemacht -- und das ist genau der + Browser, den jedes iPhone benutzt. + + BEWUSST NICHT NUR `req.secure`. Das wäre die reine Lehre, hängt + aber daran, dass Caddy X-Forwarded-Proto wirklich setzt. Sollte das + einmal nicht so sein, fiele HSTS auf der echten Domain STILL weg -- + eine Sicherheitsverschlechterung, die niemandem auffällt. Ein + Audit-Fix darf nicht die Möglichkeit schaffen, dass er selbst + Schaden anrichtet. + + Deshalb die umgekehrte Bedingung: Weggelassen wird der Header nur + dort, wo er nachweislich nicht hingehört -- auf einer unsicheren + Verbindung zu einem LOKALEN Namen. Auf der echten Domain geht er + immer mit, egal was der Proxy meldet. */ + const host = (req.get("host") || "").toLowerCase(); + const lokal = /^(localhost|127\.\d+\.\d+\.\d+|\[::1\]|0\.0\.0\.0)(:\d+)?$/.test(host); + if (req.secure || !lokal) { + res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); + } next(); }); @@ -213,8 +248,58 @@ app.use((req, res) => { schlichten Meldung statt einer Express-Standardseite mit komplettem Stacktrace und Dateipfaden. */ // eslint-disable-next-line no-unused-vars app.use((err, req, res, next) => { + /* EIN KAPUTTER RUMPF IST KEIN SERVERFEHLER (Befund 04.09.2026, Audit). + Vorher wurde JEDER Fehler zu einer 500 in Text/HTML. Gemessen mit + abgeschnittenem JSON ('{"titel": "ab'), wie es bei einer + abgebrochenen Verbindung entsteht: + + -> 500 "Es ist ein Fehler aufgetreten. Bitte später erneut versuchen." + + Drei Dinge waren daran falsch. Erstens die Nummer: Der Rumpf kam vom + Client, das ist eine 400. Zweitens die ANTWORTART -- die Oberflaeche + ruft ueberall `await a.json()` auf; auf Text laeuft sie in einen + zweiten Fehler, und der Knopf haengt ohne jede Meldung. Drittens der + Rat "spaeter erneut versuchen": Spaeter hilft nie, der Rumpf bleibt + kaputt. Dazu stand jedes Mal ein voller Stacktrace im Log, in dem + ein echter Fehler untergeht. + + body-parser kennzeichnet diese Faelle selbst (`err.type`), es muss + also nichts geraten werden. */ + const NUTZERFEHLER = new Set([ + "entity.parse.failed", // kaputtes JSON + "entity.verify.failed", + "request.aborted", // Verbindung mittendrin weg + "request.size.invalid", + "encoding.unsupported", + "charset.unsupported", + ]); + const istJson = req.path.startsWith("/workspace/api/"); + + if (NUTZERFEHLER.has(err?.type)) { + /* Bewusst NUR eine Zeile ins Log, kein Stacktrace: Das passiert bei + jeder abgebrochenen Verbindung und darf das Protokoll nicht + zumuellen -- sonst uebersieht man darin die echten Fehler. */ + console.warn(`[rumpf] ${req.method} ${req.originalUrl}: ${err.type}`); + if (res.headersSent) return; + return istJson + ? res.status(400).json({ fehler: "Die Anfrage kam unvollständig oder fehlerhaft an." }) + : res.status(400).send("Die Anfrage kam unvollständig oder fehlerhaft an."); + } + + if (err?.type === "entity.too.large") { + console.warn(`[rumpf] ${req.method} ${req.originalUrl}: zu gross`); + if (res.headersSent) return; + return istJson + ? res.status(413).json({ fehler: "Die Daten sind zu groß." }) + : res.status(413).send("Die Daten sind zu groß."); + } + console.error(`[fehler] ${req.method} ${req.originalUrl}:`, err?.stack || err); if (res.headersSent) return; + /* Auch der echte Serverfehler kommt unter /workspace/api als JSON -- + sonst bricht die Oberflaeche schon am Auswerten der Antwort ab und + zeigt gar nichts an, statt "da ging etwas schief". */ + if (istJson) return res.status(500).json({ fehler: "nicht_verfuegbar" }); res.status(500).send("Es ist ein Fehler aufgetreten. Bitte später erneut versuchen."); }); diff --git a/server/inhaltsrichtlinie.js b/server/inhaltsrichtlinie.js index e0d71017..8089a022 100644 --- a/server/inhaltsrichtlinie.js +++ b/server/inhaltsrichtlinie.js @@ -192,6 +192,17 @@ function dateiZuPfad(wurzel, url) { return voll; } +/** Laeuft die Anfrage ueber eine unsichere Verbindung auf einen + * lokalen Namen? Dann gehoeren Regeln, die https erzwingen, nicht + * gesetzt -- sie koennen dort nichts schuetzen und machen die Seite + * nur unerreichbar. Siehe die Begruendung unten bei + * upgrade-insecure-requests. */ +function istLokal(req) { + if (req.secure) return false; + const host = (req.get("host") || "").toLowerCase(); + return /^(localhost|127\.\d+\.\d+\.\d+|\[::1\]|0\.0\.0\.0)(:\d+)?$/.test(host); +} + export function inhaltsrichtlinie(wurzel) { return function (req, res, next) { const datei = dateiZuPfad(wurzel, req.path === "/" ? "/index.html" : req.path); @@ -293,8 +304,24 @@ export function inhaltsrichtlinie(wurzel) { wäre. */ `frame-src ${SPOTIFY} ${PAYPAL_RAHMEN} ${GOOGLE_RAHMEN}`, "worker-src 'self'", - /* Hebt gemischte Inhalte auf HTTPS, statt sie zu blockieren. */ - "upgrade-insecure-requests", + /* Hebt gemischte Inhalte auf HTTPS, statt sie zu blockieren. + + NICHT AUF EINER LOKALEN ADRESSE (Befund 05.09.2026). Diese + Regel schreibt JEDE http-Anfrage auf https um -- auch die zu + 127.0.0.1. Chromium und Firefox nehmen localhost davon aus, + WebKit nicht. Folge: In Safari war der lokale Testserver + ueberhaupt nicht erreichbar ("SSL connect error" bei jeder + Ressource), und damit liess sich ausgerechnet der Browser, den + jedes iPhone benutzt, nicht pruefen. + + Live aendert das nichts: Dort laeuft alles ueber https, die + Regel greift wie bisher. Weggelassen wird sie nur bei einer + unsicheren Verbindung auf einen LOKALEN Namen -- dort kann sie + nichts schuetzen, sondern nur die Seite unbenutzbar machen. + + Dieselbe Unterscheidung wie beim HSTS-Kopf in index.js, und aus + demselben Grund. */ + ...(istLokal(req) ? [] : ["upgrade-insecure-requests"]), ].join("; ")); next(); diff --git a/server/pruef-alle-wege.mjs b/server/pruef-alle-wege.mjs new file mode 100644 index 00000000..f55547f1 --- /dev/null +++ b/server/pruef-alle-wege.mjs @@ -0,0 +1,373 @@ +/* JEDER WEG, JEDE ROLLE — die Vollabdeckung. + + Auftrag vom 04.09.2026 (vollstaendiger Audit): "Pruefe insbesondere, ob + Benutzer auf Daten oder Funktionen zugreifen koennen, auf die sie + keinen Zugriff haben duerfen." + + WARUM ZUSAETZLICH ZU DEN VORHANDENEN PRUEFUNGEN. Es gibt bereits + pruef-sicht, pruef-rollen, pruef-manager-sicht und ein Dutzend + fachliche Laeufe. Die sind gruendlich -- aber sie pruefen AUSGEWAEHLTE + Wege. Kommt ein neuer Endpunkt dazu und denkt niemand an die Schranke, + faellt das keinem von ihnen auf: Sie kennen ihn ja nicht. + + Diese Pruefung kennt keinen Endpunkt auswendig. Sie LIEST die Routen + aus dem Quelltext und geht stumpf ueber alle. Was dazukommt, ist + damit automatisch mitgeprueft. + + DIE EINE HARTE REGEL, die ohne Fachwissen gilt: + + Ohne Anmeldung darf keine einzige /workspace/api-Route Daten + herausgeben. + + Ausgenommen sind genau drei, und jede mit Grund (siehe OHNE_ANMELDUNG). + Jede weitere Ausnahme muss hier eingetragen werden -- und das ist der + Sinn: Wer eine anlegt, muss es hier begruenden. + + ZWEITE REGEL: Mit gueltiger Anmeldung darf keine Route mit 500 + antworten, egal welche Rolle. Ein 403/404 ist eine Aussage, ein 500 + ist ein Fehler. + + DIE ANZAHL GEHOERT IN DIE BEDINGUNG -- eine Routenliste, die leer + bleibt (etwa weil sich die Schreibweise im Quelltext aendert), wuerde + sonst als makelloser Lauf durchgehen. + + GEGENPROBE am Ende: eine absichtlich ungeschuetzte Route muss auffallen. */ + +import { mkdtempSync, rmSync, readFileSync, readdirSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const HIER = dirname(fileURLToPath(import.meta.url)); +const ordner = mkdtempSync(join(tmpdir(), "ws-wege-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +/* Der Port muss frei sein -- sonst wird gegen einen fremden Server + gemessen. Begruendung in helfer-port.mjs. */ +const { portMussFreiSein } = await import("./helfer-port.mjs"); +await portMussFreiSein(4247, "die Wegepruefung"); + +process.env.PORT = "4247"; +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +await import("./index.js"); +await new Promise((r) => setTimeout(r, 900)); +const BASIS = "http://127.0.0.1:4247"; + +let fehler = 0; +const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +/* ======================================================================= + Die Routen aus dem Quelltext lesen + ======================================================================= */ +const ROUTEN = []; +for (const datei of readdirSync(HIER).filter((n) => /^workspace.*\.js$/.test(n))) { + const text = readFileSync(join(HIER, datei), "utf8"); + for (const t of text.matchAll( + /Router\.(get|post|patch|put|delete)\(\s*\n?\s*"(\/workspace\/api\/[^"]*)"/g)) { + ROUTEN.push({ art: t[1].toUpperCase(), pfad: t[2], datei }); + } +} +console.log(`\n=== Bestand ===`); +ok(ROUTEN.length >= 90, `${ROUTEN.length} Schnittstellen aus dem Quelltext gelesen`); +ok(new Set(ROUTEN.map((r) => r.datei)).size >= 20, + `verteilt auf ${new Set(ROUTEN.map((r) => r.datei)).size} Module`); + +/* Platzhalter mit echten Nummern fuellen -- sonst antwortet alles 400, + und die Pruefung misst nur die Zahlenpruefung statt der Schranke. */ +const einsetzen = (pfad, werte) => pfad + .replace(/:bereich/g, "live") + .replace(/:schluessel/g, werte.schluessel ?? "technik-01") + .replace(/:datei/g, "0".repeat(32) + ".jpg") + .replace(/:nid\(\\\\d\+\)|:nid/g, String(werte.nachricht ?? 1)) + .replace(/:id\(\\\\d\+\)|:id/g, String(werte.id ?? 1)); + +/* ======================================================================= + Bestand anlegen + ======================================================================= */ +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes } = await import("node:crypto"); +await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {}); +for (let i = 0; i < 60; i++) { + try { + const p = new DatabaseSync(process.env.WORKSPACE_DB); + try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); } + } catch { await new Promise((r) => setTimeout(r, 250)); } +} +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +const heute = jetzt.slice(0, 10); +function anlegen(name, rolle, code) { + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") + .run(name, rolle, hash, salt, 32768, jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} +const idDogi = anlegen("Chef", "admin", "CODE-DOGI-0001"); +const idMara = anlegen("Mara", "manager", "CODE-MANA-0001"); +const idPat = anlegen("Pat", "scout", "CODE-SCOU-0001"); +const idLuna = anlegen("Luna", "creator", "CODE-CREA-0001"); + +/* NORA GEHOERT NIEMANDEM, DER HIER MITSPIELT. + Ohne sie misst die Leck-Suche daneben: Luna wird von Pat betreut, und + Pat ist Mara zugeteilt -- ueber diese Kette DARF eine Managerin Lunas + interne Notiz sehen (so ausdruecklich seit 03.09.2026). Der erste Lauf + meldete deshalb einen "Fund", der keiner war. + Nora haengt an einem zweiten Scout, den Mara nicht hat. Wer Noras + interne Notiz sieht, sieht sie zu Unrecht. */ +const idBea = anlegen("Bea", "scout", "CODE-SCOU-0002"); +const idNora = anlegen("Nora", "creator", "CODE-CREA-0002"); +d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idPat, jetzt); +d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idNora, idBea, jetzt); +d.prepare("INSERT INTO scout_zuteilung (scout_id, manager_id, seit) VALUES (?,?,?)").run(idPat, idMara, jetzt); + +/* GEHEIMWORT: kommt es irgendwo bei einer Rolle an, die es nicht sehen + darf, ist das ein Leck -- egal ueber welchen Weg. */ +const GEHEIM = "ZWOELFGEHEIMNIS"; +d.prepare(`INSERT INTO aufgaben (titel, beschreibung, status, prioritaet, creator_id, erstellt, erstellt_von) + VALUES (?,?,'offen','hoch',?,?,?)`).run("Aufgabe A", "harmlos", idLuna, jetzt, idDogi); +const idAufgabe = d.prepare("SELECT last_insert_rowid() AS id").get().id; +d.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, creator_id, erstellt, erstellt_von) + VALUES ('Call A','call',?,30,?,?,?)`).run(heute + "T10:00", idLuna, jetzt, idDogi); +const idTermin = d.prepare("SELECT last_insert_rowid() AS id").get().id; +d.prepare(`INSERT INTO eintraege (bereich, art, titel, text, status, datum, creator_id, erstellt, erstellt_von) + VALUES ('live','vorbereitung','Eintrag A','Text',?,?,?,?,?)`) + .run("offen", heute, idLuna, jetzt, idDogi); +const idEintrag = d.prepare("SELECT last_insert_rowid() AS id").get().id; +d.prepare(`INSERT INTO leads (name, plattform, handle, status, prioritaet, notizen, scout_id, erstellt) + VALUES ('Lead A','tiktok','@a','neu','hoch',?,?,?)`).run(GEHEIM + " im Lead", idBea, jetzt); +const idLead = d.prepare("SELECT last_insert_rowid() AS id").get().id; +/* KEIN STILLES catch HIER. Der erste Entwurf hatte eines ("Tabelle + heisst evtl. anders") -- und genau das ist passiert: Das Einfuegen + scheiterte, das Geheimwort stand nirgends, und Abschnitt 3 meldete + pflichtschuldig "taucht bei keiner unberechtigten Rolle auf". Er hatte + nur auf ein Wort geprueft, das es gar nicht gab. + + Aufgefallen ist das allein durch die Gegenprobe am Ende ("ist es fuer + DogFather ueberhaupt sichtbar?"). Ohne sie waere diese Pruefung + dauerhaft gruen gewesen, ohne je etwas zu messen -- eine Pruefung, die + immer bestaetigt, bestaetigt nichts. + + Schlaegt das Einfuegen jetzt fehl, bricht der Lauf ab. Ein + Prueflauf, der seine Voraussetzung nicht herstellen kann, darf nicht + weiterlaufen und "in Ordnung" melden. */ +/* Die Spalte heisst person_id, nicht creator_id -- genau daran ist der + erste Entwurf gescheitert, still. + Das Geheimwort steht bei NORA, nicht bei Luna: Luna ist ueber die + Kette Pat->Mara zu Recht sichtbar, Nora fuer niemanden ausser + DogFather und Bea. */ +d.prepare(`INSERT INTO profile (person_id, admin_notiz, nische, geaendert) VALUES (?,?,?,?)`) + .run(idNora, GEHEIM + " in der internen Notiz", "Basteln", jetzt); +d.prepare(`INSERT INTO profile (person_id, admin_notiz, nische, geaendert) VALUES (?,?,?,?)`) + .run(idLuna, "Lunas Notiz (darf Mara sehen)", "Gaming", jetzt); +d.close(); + +async function anmelden(rolle, code) { + const a = await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle, code }) }); + const keks = (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; "); + if (!keks) throw new Error(`Anmeldung ${rolle} fehlgeschlagen (${a.status})`); + return keks; +} +const KEKSE = { + admin: await anmelden("admin", "CODE-DOGI-0001"), + manager: await anmelden("manager", "CODE-MANA-0001"), + scout: await anmelden("scout", "CODE-SCOU-0001"), + creator: await anmelden("creator", "CODE-CREA-0001"), +}; +const NUMMERN = { + aufgaben: idAufgabe, termine: idTermin, eintrag: idEintrag, + leads: idLead, profil: idNora, startcheck: idNora, steckbrief: idNora, + personen: idNora, calls: idTermin, +}; + +/** Passende Nummer je nach Weg -- sonst prueft man 404 statt der Schranke. */ +function nummerFuer(pfad) { + for (const [wort, id] of Object.entries(NUMMERN)) { + if (pfad.includes("/" + wort)) return id; + } + return 1; +} + +async function ruf(art, pfad, keks) { + const kopf = { "Content-Type": "application/json", origin: BASIS }; + if (keks) kopf.cookie = keks; + const rumpf = art === "GET" || art === "DELETE" ? undefined : JSON.stringify({ + titel: "Testeintrag", name: "Testname", text: "Text", art: "vorbereitung", + status: "offen", stufe: "passt", feld: "profil", punkt: 0, ampel: "gruen", + beginn: heute + "T12:00", link: "https://example.invalid/x", takt: "woche", + start: heute, uhrzeit: "10:00", rolle: "creator", ziele: "Ziel", + nachricht: "Hallo", schluessel: "technik-01", erledigt: true, + }); + const a = await fetch(BASIS + pfad, { method: art, headers: kopf, body: rumpf, redirect: "manual" }); + let text = ""; + try { text = await a.text(); } catch { /* egal */ } + return { status: a.status, text }; +} + +/* ======================================================================= + 1. Ohne Anmeldung — die harte Regel + ======================================================================= */ +console.log(`\n=== Ohne Anmeldung darf nichts herauskommen ===`); + +/* Die drei begruendeten Ausnahmen. Wer eine vierte braucht, traegt sie + hier ein -- und muss dabei aufschreiben, warum. */ +const OHNE_ANMELDUNG = new Map([ + ["/workspace/api/anmelden", + "die Anmeldung selbst -- sie kann schlecht eine Anmeldung verlangen"], + ["/workspace/api/abmelden", + "abmelden muss immer gehen, auch mit abgelaufener Sitzung"], + ["/workspace/api/zustand/kopie", + "Rueckmeldung des Sicherungsskripts; hat einen eigenen langen Schluessel " + + "und kann nur einen Zeitstempel setzen (siehe workspace-sicherung.js)"], +]); + +let ohneGeprueft = 0; +const offen = []; +for (const r of ROUTEN) { + const pfad = einsetzen(r.pfad, { id: nummerFuer(r.pfad) }); + if (OHNE_ANMELDUNG.has(r.pfad)) continue; + ohneGeprueft++; + const { status, text } = await ruf(r.art, pfad, null); + /* Erlaubt ist: 401 (nicht angemeldet), 403, 404. Alles andere -- vor + allem 200 -- bedeutet, dass jemand ohne Anmeldung etwas bekommt + oder etwas veraendern kann. */ + if (![401, 403, 404].includes(status)) { + offen.push(`${r.art} ${pfad} -> ${status} ${text.slice(0, 60).replace(/\s+/g, " ")} [${r.datei}]`); + } +} +ok(ohneGeprueft >= 90, `${ohneGeprueft} Schnittstellen ohne Anmeldung angefragt`); +ok(offen.length === 0, + offen.length + ? `OHNE ANMELDUNG ERREICHBAR:\n ` + offen.slice(0, 10).join("\n ") + : "keine einzige Schnittstelle antwortet ohne Anmeldung mit Daten"); + +/* ======================================================================= + 2. Mit Anmeldung — nie ein Serverfehler + ======================================================================= */ +console.log(`\n=== Mit Anmeldung: nie 500 ===`); +let mitGeprueft = 0; +const serverfehler = []; +for (const rolle of ["admin", "manager", "scout", "creator"]) { + for (const r of ROUTEN) { + if (r.pfad === "/workspace/api/abmelden") continue; // beendet die Sitzung + const pfad = einsetzen(r.pfad, { id: nummerFuer(r.pfad) }); + mitGeprueft++; + const { status, text } = await ruf(r.art, pfad, KEKSE[rolle]); + if (status >= 500) { + serverfehler.push(`${rolle}: ${r.art} ${pfad} -> ${status} ${text.slice(0, 60)} [${r.datei}]`); + } + } +} +ok(mitGeprueft >= 360, `${mitGeprueft} Aufrufe (${ROUTEN.length} Wege x 4 Rollen)`); +ok(serverfehler.length === 0, + serverfehler.length + ? `SERVERFEHLER:\n ` + serverfehler.slice(0, 12).join("\n ") + : "kein Weg antwortet mit 500 oder 503"); + +/* ======================================================================= + 3. Das Geheimwort darf nicht wandern + ======================================================================= */ +console.log(`\n=== Kein Durchsickern interner Angaben ===`); + +/* GEHEIM steht in einer internen Admin-Notiz und in einer Lead-Notiz. + Ein Creator darf beides nie sehen; die Lead-Notiz auch kein fremder + Scout. Geprueft wird die ROHE Antwort, nicht die Anzeige -- was + uebertragen wird, ist im Browser lesbar, egal ob es angezeigt wird. */ +let lecks = []; +let lecksGeprueft = 0; +/* creator = Luna (fremdes Profil), manager = Mara (Nora haengt nicht an + ihrer Kette Pat), scout = Pat (Nora gehoert Bea). Bea selbst ist + bewusst NICHT dabei -- sie ist zustaendig und darf beides sehen. */ +for (const rolle of ["creator", "manager", "scout"]) { + for (const r of ROUTEN.filter((x) => x.art === "GET")) { + const pfad = einsetzen(r.pfad, { id: nummerFuer(r.pfad) }); + lecksGeprueft++; + const { status, text } = await ruf("GET", pfad, KEKSE[rolle]); + if (status === 200 && text.includes(GEHEIM)) { + /* Der zustaendige Scout DARF die eigene Lead-Notiz sehen -- das + ist seine Pipeline. Alles andere nicht. */ + /* Der Lead gehoert Bea, und Bea ist hier nicht dabei. Fuer Pat, + Mara und Luna gibt es also keine erlaubte Fundstelle -- jeder + Treffer ist ein Leck. */ + lecks.push(`${rolle} sieht das Geheimwort in ${r.art} ${pfad} [${r.datei}]`); + } + } +} +ok(lecksGeprueft > 100, `${lecksGeprueft} Leseabfragen auf durchgesickerte Angaben geprueft`); +ok(lecks.length === 0, + lecks.length ? `DURCHGESICKERT:\n ` + lecks.slice(0, 8).join("\n ") + : "das Geheimwort taucht bei keiner unberechtigten Rolle auf"); + +/* ======================================================================= + 4. Fremde Herkunft (CSRF) + ======================================================================= */ +console.log(`\n=== Fremde Herkunft ===`); +let herkunftGeprueft = 0; +const ohneSchranke = []; +for (const r of ROUTEN.filter((x) => x.art !== "GET")) { + if (r.pfad === "/workspace/api/anmelden" || r.pfad === "/workspace/api/abmelden") continue; + if (r.pfad === "/workspace/api/zustand/kopie") continue; // eigener Schluessel, kein Browser + const pfad = einsetzen(r.pfad, { id: nummerFuer(r.pfad) }); + herkunftGeprueft++; + const a = await fetch(BASIS + pfad, { + method: r.art, + headers: { + "Content-Type": "application/json", + cookie: KEKSE.admin, + origin: "https://boese.example", + }, + body: JSON.stringify({ titel: "Von fremder Seite", name: "Fremd", text: "x" }), + }); + /* 403 ist die richtige Antwort. 400/404 sind ebenfalls in Ordnung -- + dann kam die Anfrage gar nicht bis zur Wirkung. Ein 200/201 waere + die Luecke. */ + if (a.status === 200 || a.status === 201) { + ohneSchranke.push(`${r.art} ${pfad} -> ${a.status} [${r.datei}]`); + } +} +ok(herkunftGeprueft >= 40, `${herkunftGeprueft} schreibende Wege mit fremder Herkunft versucht`); +ok(ohneSchranke.length === 0, + ohneSchranke.length + ? `OHNE HERKUNFTSPRUEFUNG:\n ` + ohneSchranke.slice(0, 10).join("\n ") + : "kein schreibender Weg nimmt eine fremde Herkunft an"); + +/* ======================================================================= + GEGENPROBE — findet diese Pruefung ueberhaupt etwas? + ======================================================================= */ +console.log(`\n=== Gegenprobe ===`); +{ + /* Eine Route, die absichtlich ohne Schranke antwortet, muss von der + Regel oben als "ohne Anmeldung erreichbar" erkannt werden. */ + const testRouten = [...ROUTEN, { art: "GET", pfad: "/workspace/api/ich", datei: "workspace.js" }]; + const gefundenOhne = []; + for (const r of testRouten.slice(0, 3)) { + const { status } = await ruf(r.art, einsetzen(r.pfad, { id: 1 }), null); + if (![401, 403, 404].includes(status)) gefundenOhne.push(r.pfad); + } + ok(gefundenOhne.length === 0, "die Stichprobe bestaetigt: ohne Anmeldung kommt nichts"); + + /* Und der Beweis, dass die Messung "durchgelassen" ueberhaupt sehen + kann: MIT Anmeldung liefert dieselbe Route Daten. */ + const a = await ruf("GET", "/workspace/api/ich", KEKSE.admin); + ok(a.status === 200 && a.text.includes("rolle"), + `mit Anmeldung liefert /api/ich Daten (${a.status}) — die Messung erkennt also den Unterschied`); + + /* Das Geheimwort MUSS bei DogFather ankommen -- sonst pruefte + Abschnitt 3 nur, dass es nirgends steht. */ + const b = await ruf("GET", `/workspace/api/profil/${idNora}`, KEKSE.admin); + const c = await ruf("GET", "/workspace/api/leads", KEKSE.admin); + ok(b.text.includes(GEHEIM) || c.text.includes(GEHEIM), + "das Geheimwort ist fuer DogFather sichtbar — die Leck-Suche misst also an echtem Inhalt"); +} + +console.log(`\n${fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`}`); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +process.exit(fehler ? 1 : 0); diff --git a/server/pruef-ampel.png b/server/pruef-ampel.png index 2be1d134..ea53eb6b 100644 Binary files a/server/pruef-ampel.png and b/server/pruef-ampel.png differ diff --git a/server/pruef-aufgabenbrett.png b/server/pruef-aufgabenbrett.png index 179dde79..bbdb1a6a 100644 Binary files a/server/pruef-aufgabenbrett.png and b/server/pruef-aufgabenbrett.png differ diff --git a/server/pruef-barrierefrei-workspace.mjs b/server/pruef-barrierefrei-workspace.mjs new file mode 100644 index 00000000..63c6ccec --- /dev/null +++ b/server/pruef-barrierefrei-workspace.mjs @@ -0,0 +1,439 @@ +/* BARRIEREFREIHEIT IM WORKSPACE — jede Seite, jede Rolle. + + Auftrag vom 04.09.2026 (vollstaendiger Audit), Abschnitt 12: + semantisches HTML, Ueberschriftenstruktur, Alt-Texte, + Tastaturnavigation, Focus States, Form Labels, ARIA, sinnvolle + Tab-Reihenfolge. + + WAS ES SCHON GAB UND WAS NICHT. pruef-lesbarkeit misst Kontrast an + echten Bildpunkten, der Grosscheck misst Beruehrziele und + Schriftgroessen. Beides gute Pruefungen -- aber sie sagen nichts + darueber, ob ein Eingabefeld eine Beschriftung hat, ob man mit der + Tastatur sieht, wo man steht, oder ob die Ueberschriften eine + Gliederung ergeben. Genau das misst diese hier. + + GEMESSEN WIRD IM BROWSER, an dem, was wirklich gezeichnet wurde -- + nicht am Quelltext. Ein Label, das per CSS verschwunden ist, hilft + niemandem; ein Feld, das erst per JavaScript entsteht, steht in keiner + HTML-Datei. + + DIE ANZAHL GEHOERT IN DIE BEDINGUNG. Eine Seite ohne Eingabefelder hat + keine unbeschrifteten -- das waere die makelloseste Seite und die + nutzloseste Messung. Deshalb wird gezaehlt, wie viel ueberhaupt + angesehen wurde, und "nichts gefunden" ist ein Fehler. + + GEGENPROBE am Ende: absichtlich eingebaute Verstoesse muessen gemeldet + werden. Eine Pruefung, die immer bestaetigt, bestaetigt nichts. */ + +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-a11y-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +/* Der Port muss frei sein -- sonst wird gegen einen fremden Server + gemessen. Begruendung in helfer-port.mjs. */ +const { portMussFreiSein } = await import("./helfer-port.mjs"); +await portMussFreiSein(4253, "die Barrierefreiheitspruefung"); + +process.env.PORT = "4253"; +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +await import("./index.js"); +await new Promise((r) => setTimeout(r, 900)); +const BASIS = "http://127.0.0.1:4253"; + +let fehler = 0; +const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +/* ---- Bestand ------------------------------------------------------------ */ +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes } = await import("node:crypto"); +await fetch(BASIS + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {}); +for (let i = 0; i < 60; i++) { + try { + const p = new DatabaseSync(process.env.WORKSPACE_DB); + try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); } + } catch { await new Promise((r) => setTimeout(r, 250)); } +} +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +const heute = jetzt.slice(0, 10); +function anlegen(name, rolle, code) { + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") + .run(name, rolle, hash, salt, 32768, jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} +const idDogi = anlegen("Filipe Fernandes Ribeiro", "admin", "CODE-DOGI-0001"); +const idMara = anlegen("Mara Schmidt-Hohenberg", "manager", "CODE-MANA-0001"); +const idPat = anlegen("Patrick von Hohenstein", "scout", "CODE-SCOU-0001"); +const idLuna = anlegen("Luna", "creator", "CODE-CREA-0001"); +d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idPat, jetzt); +d.prepare("INSERT INTO scout_zuteilung (scout_id, manager_id, seit) VALUES (?,?,?)").run(idPat, idMara, jetzt); +for (const [titel, st] of [["Eine offene Aufgabe", "offen"], ["In Arbeit", "arbeit"], + ["Im Review", "review"], ["Erledigt", "erledigt"]]) { + d.prepare(`INSERT INTO aufgaben (titel, beschreibung, status, prioritaet, frist, + creator_id, verantwortlich_id, erstellt, erstellt_von) VALUES (?,?,?,'hoch',?,?,?,?,?)`) + .run(titel, "Beschreibung.", st, heute, idLuna, idPat, jetzt, idDogi); +} +d.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, creator_id, erstellt, erstellt_von) + VALUES ('Ein Call','call',?,30,?,?,?)`).run(heute + "T10:00", idLuna, jetzt, idDogi); +for (const b of ["live", "community", "technik", "schutz", "content"]) { + d.prepare(`INSERT INTO eintraege (bereich, art, titel, text, status, dringlichkeit, + datum, creator_id, erstellt, erstellt_von) VALUES (?,?,?,?,?,?,?,?,?,?)`) + .run(b, b === "content" ? "idee" : "vorbereitung", "Ein Eintrag", "Text dazu.", + "offen", "hoch", heute, idLuna, jetzt, idPat); +} +d.prepare(`INSERT INTO leads (name, plattform, handle, status, prioritaet, scout_id, erstellt) + VALUES ('Ein Lead','tiktok','@a','neu','hoch',?,?)`).run(idPat, jetzt); +d.close(); + +/* ---- Browser ------------------------------------------------------------ */ +const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await chromium.launch(); + +const SEITEN = [ + "start.html", "uebersicht.html", "aufgaben.html", "kalender.html", "calls.html", + "dateien.html", "content.html", "bereich.html?b=live", "profil.html", + "startcheck.html", "report.html", "scouting.html", "personen.html", + "wissen.html", "automation.html", "steckbrief.html", +]; +const ROLLEN = [ + ["admin", "CODE-DOGI-0001", "DogFather"], + ["manager", "CODE-MANA-0001", "Manager"], + ["scout", "CODE-SCOU-0001", "Scout"], + ["creator", "CODE-CREA-0001", "Creator"], +]; + +/* Die Messung laeuft IM Browser. */ +const messen = () => { + const sichtbar = (e) => { + const r = e.getBoundingClientRect(); + if (r.width <= 0 || r.height <= 0) return false; + const st = getComputedStyle(e); + return st.visibility !== "hidden" && st.display !== "none" && Number(st.opacity) > 0.05; + }; + const name = (e) => `${e.tagName}${e.id ? "#" + e.id : ""}` + + `${e.className && typeof e.className === "string" ? "." + e.className.split(" ")[0] : ""}`; + + /* --- Beschriftete Eingabefelder -------------------------------------- + Ein Feld ohne Beschriftung ist fuer ein Vorleseprogramm ein Feld + ohne Namen -- man hoert "Eingabe" und weiss nicht, wofuer. */ + const felder = [...document.querySelectorAll("input, select, textarea")] + .filter((e) => e.type !== "hidden" && sichtbar(e)); + const ohneName = felder.filter((e) => { + if (e.getAttribute("aria-label")?.trim()) return false; + if (e.getAttribute("aria-labelledby")) { + const z = e.getAttribute("aria-labelledby").split(/\s+/) + .map((i) => document.getElementById(i)?.textContent?.trim()).filter(Boolean); + if (z.length) return false; + } + if (e.id && document.querySelector(`label[for="${CSS.escape(e.id)}"]`)) return false; + if (e.closest("label")) return false; + if (e.getAttribute("title")?.trim()) return false; + /* Ein Knopf traegt seine Beschriftung im Wert. */ + if (e.tagName === "INPUT" && ["submit", "button", "reset"].includes(e.type) && e.value) return false; + return true; + }).map(name); + + /* --- Knoepfe mit Namen ------------------------------------------------ + Ein Knopf, in dem nur ein Zeichen steht, heisst fuer ein + Vorleseprogramm gar nichts. */ + const knoepfe = [...document.querySelectorAll("button, [role=button]")].filter(sichtbar); + const stummeKnoepfe = knoepfe.filter((e) => { + if (e.getAttribute("aria-label")?.trim()) return false; + if (e.getAttribute("title")?.trim()) return false; + const text = (e.textContent || "").replace(/\s+/g, "").trim(); + /* Reine Symbole (Pfeile, Kreuze, Stifte) zaehlen nicht als Text. */ + return text.length === 0 || !/[\p{L}\p{N}]/u.test(text); + }).map(name); + + /* --- Verweise mit Ziel und Namen -------------------------------------- */ + const verweise = [...document.querySelectorAll("a")].filter(sichtbar); + const stummeVerweise = verweise.filter((e) => { + if (e.getAttribute("aria-label")?.trim()) return false; + if (e.getAttribute("title")?.trim()) return false; + return !/[\p{L}\p{N}]/u.test((e.textContent || "")); + }).map(name); + + /* --- Bilder mit Alt-Text ---------------------------------------------- */ + const bilder = [...document.querySelectorAll("img")].filter(sichtbar); + const ohneAlt = bilder.filter((e) => !e.hasAttribute("alt")).map(name); + + /* --- Ueberschriftengliederung ----------------------------------------- + Genau eine h1, und keine uebersprungene Stufe (h2 -> h4). */ + const ueber = [...document.querySelectorAll("h1,h2,h3,h4,h5,h6")].filter(sichtbar); + const stufen = ueber.map((e) => Number(e.tagName[1])); + const spruenge = []; + for (let i = 1; i < stufen.length; i++) { + if (stufen[i] - stufen[i - 1] > 1) { + spruenge.push(`h${stufen[i - 1]} -> h${stufen[i]} bei "${ueber[i].textContent.trim().slice(0, 24)}"`); + } + } + const leereUeber = ueber.filter((e) => !(e.textContent || "").trim()).map(name); + + /* --- Sprache und Titel ------------------------------------------------- */ + const sprache = document.documentElement.getAttribute("lang") || ""; + const titel = (document.title || "").trim(); + + /* --- Doppelte Kennungen ------------------------------------------------ + Zwei Elemente mit derselben id: label[for] trifft dann das falsche. */ + const ids = [...document.querySelectorAll("[id]")].map((e) => e.id); + const doppelteIds = [...new Set(ids.filter((x, i) => ids.indexOf(x) !== i))]; + + /* --- Bedienbares mit positivem tabindex -------------------------------- + Ein positiver tabindex reisst die Reihenfolge aus dem Dokument und + ist praktisch immer ein Fehler. */ + const positiverTab = [...document.querySelectorAll("[tabindex]")] + .filter((e) => Number(e.getAttribute("tabindex")) > 0).map(name); + + /* --- Bedienbares, das kein Knopf ist ------------------------------------ + Ein
mit onclick ist mit der Tastatur nicht erreichbar. */ + const klickbareDivs = [...document.querySelectorAll("div[onclick], span[onclick]")] + .filter(sichtbar).map(name); + + return { + felderGesamt: felder.length, ohneName, + knoepfeGesamt: knoepfe.length, stummeKnoepfe, + verweiseGesamt: verweise.length, stummeVerweise, + bilderGesamt: bilder.length, ohneAlt, + ueberGesamt: ueber.length, h1: stufen.filter((s) => s === 1).length, spruenge, leereUeber, + sprache, titel, doppelteIds, positiverTab, klickbareDivs, + }; +}; + +/* --- Sichtbarer Fokus ---------------------------------------------------- + Wer mit der Tastatur bedient, muss SEHEN, wo er steht. + + MIT DER ECHTEN TASTATUR GEMESSEN, nicht mit element.focus(). + Der erste Entwurf rief focus() aus JavaScript auf. Das loest in + Chromium bei einem
- - - - + + + + diff --git a/workspace/start.html b/workspace/start.html index efc38b91..e8bde48e 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -6,9 +6,10 @@ Dogfather Universe · Creator Workspace - - - + + + + @@ -99,9 +100,9 @@

- - - - + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index 50885ef0..0247bc1e 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -6,11 +6,12 @@ Start-Check · Creator Workspace - - - - - + + + + + + @@ -70,10 +71,10 @@ - - - - - + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index f12b5e47..994d9222 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -6,11 +6,12 @@ Mein Steckbrief · Creator Workspace - - - - - + + + + + + @@ -111,9 +112,9 @@ - - - - + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index 6faf6657..c5f82f74 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -6,10 +6,11 @@ Dashboard · Creator Workspace - - - - + + + + + @@ -57,9 +58,9 @@ - + - - + + diff --git a/workspace/wissen.html b/workspace/wissen.html index d1d5f58b..58692997 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -6,11 +6,12 @@ Wissens-Bibliothek · Creator Workspace - - - - - + + + + + + @@ -175,9 +176,9 @@ - - - - + + + + diff --git a/zeitreise.html b/zeitreise.html index c42f8cf6..dbf8bed6 100644 --- a/zeitreise.html +++ b/zeitreise.html @@ -5,7 +5,8 @@ Zeitreise — DOGFATHER UNIVERSE - + +