From 2b1841e17533543d1cb15220ca86a2a37c853daf Mon Sep 17 00:00:00 2001
From: Matthias Andreas Benkard
Date: Sat, 15 Aug 2026 10:51:11 +0200
Subject: Die Web-App rechnet im Browser statt auf dem Server
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bisher lief die Weboberfläche auf einem JDK-HttpServer: Uploads landeten als
temporäre Dateien auf dem Server, die Synopse entstand dort. Das kostete
Betrieb (systemd, Reverse Proxy, Rate-Limiting, Timeouts) und verlangte ein
Datenschutzversprechen, das nur zusicherbar, nicht nachprüfbar war — gerade
Entwurfstexte verließen den Rechner der Nutzer:innen.
Neu übersetzt `./mvnw -Pwasm package` dieselbe Pipeline mit GraalVM Web Image
(`native-image --tool:svm-wasm`) nach WebAssembly, PDFBox eingeschlossen.
Ausgeliefert werden nur noch statische Dateien; gerechnet wird im Browser.
Die erzeugte Synopse ist byteweise identisch mit der der Befehlszeile
(SHA-256 verglichen für IfSG 48/27/21 und BayJG 151/3/54).
Die Befehlszeile bleibt unberührt: `./mvnw package` erzeugt unverändert das
JAR, alle Optionen und Meldungstexte sind gleich, das Wasm-Profil ist rein
additiv und verlangt Oracle GraalVM 25.1+ (die CE hat kein Web Image).
Portabilitätsschnitt (nützt beiden Fassungen):
* `Quelle` (Name + Bytes) ersetzt `Path` in der Pipeline; nur die
Befehlszeile kennt noch ein Dateisystem. Der Name trägt genau den
bisherigen `getFileName()`-Text, damit Warnungen und Quellenzeile
wortgleich bleiben.
* `DateiTyp` erkennt PDF/XML/Klartext an den Signaturbytes. Tika entfällt —
eine schwergewichtige Abhängigkeit samt ServiceLoader- und
XML-Konfiguration weniger, was der Wasm-Übersetzung unmittelbar zugutekommt.
Vier Eigenheiten von Web Image, die der Quelltext jeweils an Ort und Stelle
vermerkt:
* `java.util.zip.Inflater` ist nicht angebunden (GR-65205), ohne Inflate ist
kein PDF lesbar. `InflaterErsatz` substituiert ihn durch jzlib.
* Typisierte JS-Felder lassen sich nicht nach `byte[]` umsetzen
(„byteArrayHub is not defined“); der Dateiinhalt wandert als Base64.
* JULs Standardformatter ruft `StackWalker`, den es dort nicht gibt.
* Im Worker fehlt `document.currentScript`, worauf die Laufzeit das
Wasm-Modul neben `worker.js` sucht; die VM wird deshalb mit ausdrücklichem
Pfad ein zweites Mal gestartet.
Die Reachability-Metadaten stammen aus einem Lauf des Tracing-Agents über die
Pipeline; die PDFBox- und FontBox-Ressourcen sind als Globs ergänzt, sonst
scheitern PDFs an „Could not find referenced cmap stream Identity-H“.
Entfallen: WebMain, UploadHandler, StaticHandler, Multipart und die
systemd-Unit. Die nginx-Vorlage liefert jetzt statische Dateien aus, und die
Datenschutzseite sagt, was nun stimmt: Die Dateien verlassen den Rechner
nicht.
Co-Authored-By: Claude Opus 5
Change-Id: I38faf2ac0f764d601f080d4276babe4747773683
---
src/main/resources/eu/mulk/aendggner/web/app.js | 90 ++++++++++++++++++++++
.../eu/mulk/aendggner/web/datenschutz.html | 23 +++---
.../resources/eu/mulk/aendggner/web/impressum.html | 4 +-
.../resources/eu/mulk/aendggner/web/index.html | 19 +++--
src/main/resources/eu/mulk/aendggner/web/style.css | 26 +++++++
src/main/resources/eu/mulk/aendggner/web/worker.js | 42 ++++++++++
6 files changed, 184 insertions(+), 20 deletions(-)
create mode 100644 src/main/resources/eu/mulk/aendggner/web/app.js
create mode 100644 src/main/resources/eu/mulk/aendggner/web/worker.js
(limited to 'src/main/resources/eu')
diff --git a/src/main/resources/eu/mulk/aendggner/web/app.js b/src/main/resources/eu/mulk/aendggner/web/app.js
new file mode 100644
index 0000000..c59f6e3
--- /dev/null
+++ b/src/main/resources/eu/mulk/aendggner/web/app.js
@@ -0,0 +1,90 @@
+// Nimmt die Dateien aus dem Formular entgegen, reicht sie an den Worker und zeigt die Synopse.
+// Es gibt keinen Server: Alles läuft im Browser.
+
+const formular = document.querySelector("#synopse-formular");
+const knopf = formular.querySelector("button");
+const meldung = document.querySelector("#meldung");
+
+let worker = null;
+
+function zeige(text, art) {
+ meldung.textContent = text;
+ meldung.className = art ? "meldung " + art : "meldung";
+ meldung.hidden = !text;
+}
+
+/**
+ * Der Dateiinhalt geht als Base64 an das Wasm-Modul: Die Umsetzung typisierter Felder nach
+ * byte[] ist in Web Image derzeit defekt, Zeichenketten überqueren die Grenze zuverlässig.
+ */
+async function alsBase64(datei) {
+ const bytes = new Uint8Array(await datei.arrayBuffer());
+ let roh = "";
+ const block = 0x8000; // btoa verträgt keine beliebig langen Argumentlisten.
+ for (let i = 0; i < bytes.length; i += block) {
+ roh += String.fromCharCode.apply(null, bytes.subarray(i, i + block));
+ }
+ return { name: datei.name, base64: btoa(roh) };
+}
+
+formular.addEventListener("submit", async (ereignis) => {
+ ereignis.preventDefault();
+
+ const stammDatei = document.querySelector("#stamm").files[0];
+ const patchDateien = Array.from(document.querySelector("#aenderung").files);
+ if (!stammDatei || patchDateien.length === 0) {
+ zeige("Bitte ein Stammgesetz und mindestens ein Änderungsdokument wählen.", "fehler");
+ return;
+ }
+
+ knopf.disabled = true;
+ zeige("Lade das Rechenwerk (einmalig einige Megabyte) und werte aus …", "arbeit");
+
+ try {
+ const [stamm, patches] = await Promise.all([
+ alsBase64(stammDatei),
+ Promise.all(patchDateien.map(alsBase64)),
+ ]);
+
+ if (!worker) {
+ worker = new Worker("worker.js");
+ }
+
+ const ergebnis = await new Promise((aufloesen, ablehnen) => {
+ worker.onmessage = (nachricht) => aufloesen(nachricht.data);
+ worker.onerror = (fehler) => ablehnen(new Error(fehler.message || "Worker-Fehler"));
+ worker.postMessage({
+ stamm,
+ patches,
+ vollstaendig: document.querySelector("#vollstaendig").checked,
+ artikel: null,
+ });
+ });
+
+ if (ergebnis.fehler) {
+ zeige("Verarbeitung fehlgeschlagen: " + ergebnis.fehler, "fehler");
+ return;
+ }
+
+ const blob = new Blob([ergebnis.html], { type: "text/html;charset=utf-8" });
+ const adresse = URL.createObjectURL(blob);
+
+ // Kein window.open: Der Aufruf käme nach dem Warten auf den Worker und gälte dem Browser
+ // nicht mehr als Nutzerhandlung — er würde als Popup blockiert. Stattdessen ein Link, der
+ // sich öffnen und ebenso gut speichern lässt.
+ zeige(
+ `${ergebnis.angewandt} Befehle angewandt, ${ergebnis.manuell} manuell zu prüfen, ` +
+ `${ergebnis.normen} geänderte Normen. `,
+ "fertig",
+ );
+ const verweis = document.createElement("a");
+ verweis.href = adresse;
+ verweis.target = "_blank";
+ verweis.textContent = "Synopse öffnen";
+ meldung.append(verweis);
+ } catch (e) {
+ zeige("Verarbeitung fehlgeschlagen: " + (e && e.message ? e.message : e), "fehler");
+ } finally {
+ knopf.disabled = false;
+ }
+});
diff --git a/src/main/resources/eu/mulk/aendggner/web/datenschutz.html b/src/main/resources/eu/mulk/aendggner/web/datenschutz.html
index 65ec0c9..5b9bbac 100644
--- a/src/main/resources/eu/mulk/aendggner/web/datenschutz.html
+++ b/src/main/resources/eu/mulk/aendggner/web/datenschutz.html
@@ -4,7 +4,7 @@
Datenschutz — ÄndGgner
-
+
@@ -15,19 +15,22 @@
Zugriffslogs, Hosting-Anbieter).
-
Hochgeladene Dateien
+
Ihre Dateien
- Stammgesetz und Änderungsgesetz(e) werden ausschließlich im Arbeitsspeicher bzw. in
- temporären Dateien verarbeitet, um die Synopse zu erzeugen. Nach Auslieferung der Antwort
- werden diese temporären Dateien sofort gelöscht. Es findet keine dauerhafte Speicherung des
- Inhalts statt.
+ Die gewählten Dateien werden nicht übertragen. Die gesamte Auswertung —
+ PDF-Textgewinnung, Anwendung der Änderungsbefehle, Erzeugung der Synopse — läuft als
+ WebAssembly-Programm in Ihrem Browser. Der Server sieht die Dateien nicht, kann sie nicht
+ sehen und speichert folglich auch nichts davon. Wer das prüfen möchte, öffne die
+ Netzwerkanzeige der Entwicklerwerkzeuge: Nach dem Laden der Seite und des Rechenwerks
+ entsteht kein weiterer Netzverkehr.
Zugriffsprotokolle
- Wie bei jedem Webserver werden beim Aufruf technische Zugriffsdaten (u. a. IP-Adresse,
- Zeitpunkt, aufgerufene Adresse) für einen begrenzten Zeitraum zur Absicherung des Betriebs
- protokolliert.
+ Beim Abruf der Seite selbst und der zugehörigen Dateien werden wie bei jedem Webserver
+ technische Zugriffsdaten (u. a. IP-Adresse, Zeitpunkt, aufgerufene Adresse) für einen
+ begrenzten Zeitraum zur Absicherung des Betriebs protokolliert. Diese Protokolle enthalten
+ keine Angaben über die von Ihnen ausgewerteten Dokumente.
Keine Cookies, keine Konten, kein Tracking
@@ -36,6 +39,6 @@
Analyse- oder Tracking-Dienste Dritter ein.
-
+