Ein DGS-Video liess sich bisher gar nicht einbinden: Der Markdown-
Prozessor laeuft mit Sanitizing, und remark reicht rohes HTML dann von
vornherein nicht durch - <video> kam als leeres <p> heraus. Eine
erweiterte Freigabeliste allein half deshalb nicht.
Statt rohes HTML fuer alle Textfelder zu oeffnen - auch fuer die 121
Erklaerungen - bekommt die Gebaerdensprach-Seite zwei eigene Felder fuer
Video- und Untertiteladresse. Ausgegeben wird ein natives <video
controls> mit <track kind="captions"> und Bildunterschrift.
Adressen werden zweimal geprueft, beim Speichern und vor der Ausgabe:
zugelassen sind nur der eigene Server und https. javascript:, data:,
protokollrelative und einfache http-Adressen werden abgewiesen. iframe
bleibt bewusst gesperrt - eine YouTube-Einbindung wuerde Besucherdaten
an Dritte uebertragen.
Erklaerung zur Barrierefreiheit entsprechend gekuerzt: Die beiden
Luecken nach § 4 BITV 2.0 entfallen, es bleibt die ungepruefte
Druckansicht.
Geprueft: Video wird mit Untertitelspur ausgeliefert und ist per Tastatur
erreichbar, axe-core meldet auf beiden neuen Seiten in hell und dunkel
keine Verstoesse, Adresspruefung weist alle acht Testfaelle korrekt zu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rechtsrahmen: § 13 Abs. 3 LBGG SH i. V. m. §§ 3 und 4 BITV 2.0. Eine
Erklaerung "nicht barrierefrei" ersetzt die Herstellungspflicht nicht,
sie dokumentiert den Stand nur voruebergehend.
Farben, alle Werte gegen die WCAG-Formel nachgerechnet:
- Sekundaertext 4.40:1 auf muted-Flaechen -> 4.55:1
- Rahmen von Eingabefeldern 1.24:1 -> 3:1 (WCAG 1.4.11); dekorative
Trenner bleiben unveraendert
- Badge-Farben scheiterten auf der eigenen 10-Prozent-Flaeche
(brand 4.20, success 4.18, destructive 3.88) -> jeweils ueber 4.5
- Warnfarbe im Admin 3.11:1 -> amber-700 mit 4.90:1
Fokus war faktisch unsichtbar: Der Ring wurde durchgaengig mit halber
Deckkraft gezeichnet (ring-ring/50), effektiv rund 1.5:1. Jetzt volle
Deckkraft in Markenfarbe, 12 Dateien betroffen.
Weiter: Sprungmarke zum Inhalt auf allen Seiten, Suchfelder beschriftet,
Druck-Schaltflaechen hatten als reine Icon-Buttons gar keinen
zugaenglichen Namen, Liste als echte Liste ausgezeichnet, doppelter Link
je Zeile entfernt, Trefferzahl als Live-Region.
Responsiv: Aussenabstaende und Kopfzeilen ab 320 Pixel, Listenzeilen
brechen um.
Neu nach § 4 BITV 2.0: Seiten fuer Leichte Sprache und Gebaerdensprache,
im Admin pflegbar. Leer gelassen und damit noch nicht online - die
Inhalte kommen vom Amt. In der Erklaerung als bekannte Luecke benannt.
Geprueft mit axe-core (WCAG 2.1 A und AA) ueber alle oeffentlichen Seiten
und den Adminbereich, hell und dunkel, dazu Tastaturbedienung, 200
Prozent Vergroesserung und 320 Pixel Breite: keine Verstoesse, kein
waagerechter Ueberlauf.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sortierung: Erklaerungen erscheinen im Admin wie im oeffentlichen
Bereich alphabetisch nach Titel. Bewusst in JavaScript statt ueber die
Datenbank - SQLite vergleicht binaer, dort landeten "Aenderungs-
mitteilung" und "Uebernahme" hinter "Zaehlerstandsablesung". Intl mit
deutscher Kollation sortiert Ae bei A. Gemeinsame Regel in
lib/policy-sort.ts, benutzt von der Server-Komponente, der API und der
Admin-Tabelle; die Spaltenkoepfe bleiben umschaltbar.
Overlay: @radix-ui/react-select verlangte react-dismissable-layer
1.1.11, alle anderen Radix-Pakete 1.1.19. Dieses Paket setzt und
entfernt pointer-events auf dem body und fuehrt dazu eine Liste offener
Layer im Modulzustand - bei zwei Kopien zwei getrennte Listen. Ein
Select im Dialog liess deshalb beim Schliessen das Overlay des Dialogs
verschwinden und pointer-events: none stehen. Die Seite war danach bis
zum Neuladen tot.
Belegt im Browser mit einer Wegwerfseite (Select im Dialog):
zwei Versionen -> Dialog nach Select-Schliessen nicht mehr bedienbar,
body pointer-events bleibt "none"
eine Version -> body wieder leer, Seite klickbar
react-select, -label, -slot und -tabs auf aktuelle Versionen gehoben;
damit liegt jedes Radix-Kernpaket nur noch einmal im Baum.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Workflow bricht weiter mit ENOSPC ab, jetzt in beiden npm-Stufen
gleichzeitig nach 8,2 Sekunden - der Datentraeger ist praktisch voll.
- prisma-cli baut auf deps statt auf base auf. Damit laufen die beiden
npm-Installationen nacheinander statt parallel, was den gleichzeitigen
Platzbedarf halbiert. Die deps-Schicht liegt ohnehin vor.
- Der Workflow protokolliert vor dem Bauen Belegung, Inodes und
docker system df. ENOSPC heisst auch "keine Inodes frei", was df -h
nicht zeigt - beim naechsten Fehlschlag stehen die Zahlen im Log.
- Ungenutzter Build-Cache aelter als 24h wird vor dem Bauen freigegeben.
Jeder abgebrochene Versuch hinterlaesst sonst Schichten und macht es
beim naechsten Mal enger. Betrifft nur Cache, keine Images, Volumes
oder Container.
Geprueft: Image baut mit serialisierten Stufen, Container startet,
Migrationen laufen, Healthcheck gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Workflow scheiterte mit ENOSPC auf dem Runner. Ursache ist der
volle Datentraeger dort, nicht der Build - was wir beitragen koennen:
- 46 der 54 Inter-Schriftdateien waren unbenutzt. Nur die acht in
app/layout.tsx referenzierten Schnitte bleiben: 18 MB -> 2 MB.
- npm-Cache wird in derselben Schicht entfernt, in der er entsteht.
- data/policies aus dem Docker-Kontext genommen; die Datensaetze
stehen in der Datenbank, die JSON-Ablage ist reiner Nachweis.
Ausserdem im Dockerfile: Eine frühere Bearbeitung hatte ein echtes
Steuerzeichen CR in das sed-Muster geschrieben statt der Escape-Folge.
Es funktionierte, war aber unlesbar - jetzt wieder 's/\r$//'.
Geprueft: Image baut, Container startet, alle acht Schriftschnitte
werden ausgeliefert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gegen den geltenden Normbestand geprüft und berichtigt:
- Landesbauordnung SH, neu gefasst zum 01.09.2022 und dabei umnummeriert.
Bauvoranfrage § 61 -> § 75 (Vorbescheid), Befreiungs-/Abweichungsantrag
§ 61 -> § 67 LBO und § 31 BauGB, gemeindliches Einvernehmen § 61 ->
§ 36 BauGB und § 71 LBO. Beim Bauantrag § 68 ergänzt, § 64 bleibt.
- Wohnberechtigungsschein: benannte das Dokument statt der Norm,
jetzt § 27 WoFG i. V. m. § 5 WoBindG.
- Bildung und Teilhabe: "BKKG" berichtigt zu § 6b BKGG, § 34 SGB XII
präzisiert (drei Erklärungen).
- Ausschreibungsunterlagen: überholte VOL/A entfernt.
- Beihilfe, Mutterschutz und Arbeitsmedizin verarbeiten Gesundheitsdaten
und haben zusätzlich Art. 9 Abs. 2 lit. b DSGVO erhalten.
Nicht angefasst, weil ohne belegte Fassung: nicht zitierfähige Satzungen,
die Anschlussbescheinigung, die Angelerlaubnis sowie alle fehlenden
Fristen, Zwecke und Datenkategorien.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Art. 13 Abs. 1 lit. c DSGVO verlangt die Rechtsgrundlage der Verarbeitung.
Die Liste nannte durchgehend nur die Fachnorm ("§ 64 LBO"), also die
Aufgabe, nicht die datenschutzrechtliche Erlaubnis. Ergänzt wird je nach
Art der Verarbeitung:
91x hoheitlich Art. 6 Abs. 1 lit. e DSGVO i. V. m. § 3 LDSG SH
9x Beschaeftigung Art. 6 Abs. 1 lit. b DSGVO i. V. m. § 15 LDSG SH
6x Vertrag Art. 6 Abs. 1 lit. b DSGVO
2x Einwilligung Art. 6 Abs. 1 lit. a DSGVO, mit Hinweis auf Art. 7 Abs. 3
Der vom Amt angegebene Text bleibt unveraendert darunter stehen. Die 13
Erklaerungen ganz ohne Angabe wurden nicht angefasst: Eine fehlende
Grundlage zu erfinden ist etwas anderes, als eine vorhandene zu
vervollstaendigen.
Wiederholbar, und mit `npm run import-csv` wieder auf die Rohwerte der
Liste zurueckzusetzen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Im Container lief der Import bis zum Ende der Auswertung und brach dann
mit EACCES beim Schreiben der ersten JSON-Datei ab. Weil die Ablage vor
dem Datenbankschreiben stand, wurde keine einzige Erklärung importiert.
Reihenfolge umgedreht: Erst die Datenbank, danach die JSON-Dateien. Ein
Fehler dabei wird einmal gemeldet und beendet nur die Ablage, nicht den
Lauf. Neu ist --no-json, das sie ganz abschaltet.
Getestet: Normalfall schreibt 120 Dateien, simulierter Schreibfehler
meldet ihn und importiert vollständig weiter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das fertige Image kopiert data/ nicht, die Build-Schicht enthielt die
Gesamtliste mit den Namen der Beschaeftigten aber trotzdem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Führende Quelle ist jetzt der Excel-Export des Amtes statt der einzelnen
Merkblatt-PDFs: 120 Zeilen, davon 4 bisher nicht erfasste Sozialamts-
Leistungen. Zuordnung über den Bezeichner, nicht den Titel – die aus PDF
gelesenen Titel unterscheiden sich in neun Fällen nur in Leerzeichen um
Schrägstriche und hätten Dubletten erzeugt.
Zwei Angaben stehen nicht in der Liste und werden ergänzt: der in allen
Merkblättern enthaltene Empfängersatz zur Finanzbuchhaltung (ohne ihn
hätten 27 Erklärungen gar keine Empfängerangabe) und die Bezeichnung der
Verarbeitungstätigkeit aus den Organisationsspalten.
Die zehn Bausteine folgen durchgehend den zentralen Standardtexten;
gespeichert wird nur die eine echte Abweichung: die gemeinsame
Verantwortlichkeit mit dem ZIT SH beim Online-Wohngeldantrag.
Alle 121 Erklärungen bleiben unveröffentlicht – drei Standardtexte
(Widerspruchsrecht, Drittlandübermittlung, automatisierte Entscheidungen)
sind noch nicht gepflegt und ohne sie ist keine vollständig.
Die Quellliste selbst bleibt unversioniert: Sie führt Namen von
Beschäftigten, die nicht in die Erklärungen übernommen werden.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die öffentliche API gab bei ?admin=true alle Datensätze heraus, auch die
nicht veröffentlichten: von 117 Erklärungen waren 112 Entwürfe, die jeder
Besucher abrufen konnte. Der Parameter setzt jetzt eine Admin-Sitzung
voraus, sonst kommt die öffentliche Liste. Der Einzelabruf behandelt
Entwürfe wie nicht vorhanden, damit sich über die fortlaufende ID nicht
abklopfen lässt, welche Datensätze existieren.
Markdown wurde mit sanitize: false gerendert und per
dangerouslySetInnerHTML eingesetzt. Das Standard-Schema lässt jetzt
Überschriften, Listen, Tabellen und http(s)/mailto-Links durch und
entfernt Skripte, Event-Attribute, eingebettete Rahmen und
javascript:-Ziele. Der Titel in der Druckfassung war an drei Stellen
unmaskiert.
Standardtexte
- Zehn Bausteine, die für alle Erklärungen gelten, werden einmal zentral
gepflegt. In der Erklärung steht nur eine Abweichung; ein leeres Feld
bedeutet "folgt dem Standard". Ein Wechsel in der Amtsleitung wirkt
damit sofort auf alle Erklärungen, die nicht bewusst abweichen.
- Die Migration übernimmt den häufigsten Bestandswert als Standard und
setzt übereinstimmende Felder auf NULL. Von 117 Erklärungen folgen
danach 116 dem Standard, eine weicht ab (Online-Verfahren mit ZIT-SH
als zusätzlichem Verantwortlichen).
Pflichtangaben nach Art. 13/14 DSGVO
- Neue Felder für Drittlandübermittlung (Art. 13 Abs. 1 lit. f),
automatisierte Entscheidungsfindung (Art. 13 Abs. 2 lit. f),
Datenkategorien (Art. 14 Abs. 1 lit. d) und berechtigte Interessen
(Art. 13 Abs. 1 lit. d). Auf die ersten beiden muss auch dann
hingewiesen werden, wenn es sie nicht gibt, deshalb sind sie
Standardtexte mit ausdrücklichem "findet nicht statt".
- Der Hinweis auf das Widerspruchsrecht steht nach Art. 21 Abs. 4 in
einem eigenen, abgesetzten Kasten vor allen Abschnitten, nicht als
Zeile im Fließtext der Betroffenenrechte.
- Zweck, Rechtsgrundlage und Speicherdauer sind Pflicht. Geprüft wird
beim Veröffentlichen, nicht beim Speichern: Ein Entwurf darf
unvollständig sein, eine öffentlich sichtbare Erklärung nicht. So
bleiben Bestandsdaten bearbeitbar, ohne dass die Anwendung Angaben
erfindet, die nur die Fachabteilung kennt. Die Admin-Übersicht
markiert unvollständige Erklärungen und nennt die fehlenden Angaben.
Öffentliche Seiten
- Jede Erklärung hat eine dauerhafte Adresse /erklaerung/<bezeichner>,
serverseitig gerendert, zum Verlinken aus Antragsformularen. Der
Bezeichner wird aus dem Titel abgeleitet und bleibt beim Bearbeiten
erhalten, solange der Titel gleich bleibt. Entwürfe liefern 404.
- Impressum, Datenschutzerklärung des Portals und Erklärung zur
Barrierefreiheit werden im Admin als Markdown gepflegt und in der
Fußzeile verlinkt. Die Fußzeile rendert serverseitig, weil das
Impressum ohne JavaScript erreichbar sein muss. Ein leerer Text nimmt
die Seite vom Netz, statt eine leere Seite auszuliefern.
- Die Übersicht nutzt echte Links statt Klick-Handler und wird
serverseitig vorbefüllt.
APP_COMPANY_NAME ersetzt den Namen der Anwendung in Kopf- und Fußzeile,
Seitentiteln, Anmeldeseite, Druckfassung und Startmeldung des
Containers. Der Wert wird zur Laufzeit gelesen, ein Neubau des Images
ist zum Umbenennen nicht nötig.
Getestet gegen die 117 Bestandsdatensätze: Migrationen angewendet, der
angezeigte Text blieb bei allen 117 x 7 Baustein-Feldern identisch. Alle
117 Bezeichner sind eindeutig und stimmen mit lib/slug.ts überein
(SQLites LOWER() kennt nur ASCII, Großumlaute werden deshalb vorher
ersetzt). ?admin=true liefert ohne Anmeldung 5 statt 117 Datensätze,
Entwürfe 404. Über die API mit Anmeldung geprüft: unvollständig als
Entwurf wird angelegt, dieselben Angaben mit isPublic abgelehnt, ebenso
fehlende Datenkategorien bei angegebener Herkunft. Ohne
APP_COMPANY_NAME erscheint "Syncova Policies", mit gesetztem Wert kein
einziges Vorkommen mehr.
Die Änderungen liegen in einem Commit, weil Schema, API-Routen und
Formulare von allen Teilen berührt werden und eine thematische Aufteilung
nicht lauffähige Zwischenstände ergäbe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Weiterleitungen hingen am containerinternen Port 3000: Die Middleware
baute sie aus req.nextUrl.origin. Sie wertet jetzt X-Forwarded-Host und
-Proto aus, ersatzweise den Host-Header. NEXTAUTH_URL ist damit optional
und wird nicht mehr auf localhost:3000 vorbelegt.
Der Container legt beim ersten Start selbst einen Admin an. Ohne
ADMIN_PASSWORD wird ein Zufallspasswort erzeugt und einmalig ins Log
geschrieben; bisher musste seed-admin von Hand nachgeholt werden.
Ein mit anderem NEXTAUTH_SECRET verschlüsseltes Cookie erzeugte bei jedem
Request einen JWTSessionError samt Stacktrace. Die Middleware löscht das
Cookie jetzt beim Umleiten, der Logger meldet den Fall in einer Zeile.
Die Prüfung nutzt das type-Feld, da Klassennamen im Produktions-Build
minifiziert sind.
Getestet auf Port 8080: Weiterleitung nach localhost:8080/auth/signin,
Passwort im Erststart-Log, veraltetes Cookie mit Max-Age=0 entfernt,
keine Stacktraces mehr.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- trustHost für Auth.js: selbst gehostet wird der Host sonst mit
UntrustedHost abgelehnt und die Anmeldung schlägt fehl
- Prisma-CLI in eigener Stage installieren; im Standalone-Bundle fehlen
seine transitiven Abhängigkeiten, `migrate deploy` brach beim Start ab
- Zeilenenden des Entrypoints im Image normalisieren
Getestet: Container startet, wendet Migrationen an, Anmeldung und
Admin-Bereich antworten mit 200.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Next.js 15 App Router mit öffentlicher Übersicht und geschütztem
Admin-Bereich zur Pflege von Datenschutzerklärungen nach Art. 12ff. DSGVO.
- Oberfläche auf Basis von shadcn/ui, Inter lokal eingebunden
- Prisma/SQLite, Auth.js mit Credentials-Provider und Rollen
- Massenimport der Merkblätter des Amtes Leezen aus PDF
- Docker-Image (standalone) und Gitea-Workflow zur Veröffentlichung
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>