diff options
Diffstat (limited to 'src/main/resources/eu')
6 files changed, 184 insertions, 20 deletions
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 @@ <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Datenschutz — ÄndGgner</title> - <link rel="stylesheet" href="/style.css"> + <link rel="stylesheet" href="style.css"> </head> <body> <main> @@ -15,19 +15,22 @@ Zugriffslogs, Hosting-Anbieter). </p> - <h2>Hochgeladene Dateien</h2> + <h2>Ihre Dateien</h2> <p> - 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 <strong>nicht übertragen</strong>. 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. </p> <h2>Zugriffsprotokolle</h2> <p> - 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. </p> <h2>Keine Cookies, keine Konten, kein Tracking</h2> @@ -36,6 +39,6 @@ Analyse- oder Tracking-Dienste Dritter ein. </p> </main> - <footer><a href="/">Zurück zum Formular</a></footer> + <footer><a href="index.html">Zurück zum Formular</a></footer> </body> </html> diff --git a/src/main/resources/eu/mulk/aendggner/web/impressum.html b/src/main/resources/eu/mulk/aendggner/web/impressum.html index 303db8e..525ef77 100644 --- a/src/main/resources/eu/mulk/aendggner/web/impressum.html +++ b/src/main/resources/eu/mulk/aendggner/web/impressum.html @@ -4,7 +4,7 @@ <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Impressum — ÄndGgner</title> - <link rel="stylesheet" href="/style.css"> + <link rel="stylesheet" href="style.css"> </head> <body> <main> @@ -21,6 +21,6 @@ [E-Mail-Adresse] </address> </main> - <footer><a href="/">Zurück zum Formular</a></footer> + <footer><a href="index.html">Zurück zum Formular</a></footer> </body> </html> diff --git a/src/main/resources/eu/mulk/aendggner/web/index.html b/src/main/resources/eu/mulk/aendggner/web/index.html index 02f8f75..9f26889 100644 --- a/src/main/resources/eu/mulk/aendggner/web/index.html +++ b/src/main/resources/eu/mulk/aendggner/web/index.html @@ -4,7 +4,8 @@ <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>ÄndGgner — Änderungsgesetze konsolidieren</title> - <link rel="stylesheet" href="/style.css"> + <link rel="stylesheet" href="style.css"> + <script src="app.js" defer></script> </head> <body> <header> @@ -15,7 +16,7 @@ </header> <main> - <form method="post" action="/synopse" enctype="multipart/form-data"> + <form id="synopse-formular"> <div class="field"> <label for="stamm">Stammgesetz</label> <input type="file" id="stamm" name="stamm" required> @@ -39,7 +40,7 @@ <div class="field checkbox"> <label> - <input type="checkbox" name="vollstaendig"> + <input type="checkbox" id="vollstaendig" name="vollstaendig"> Unveränderte Vorschriften mit in die Synopse aufnehmen </label> </div> @@ -47,18 +48,20 @@ <button type="submit">Synopse erzeugen</button> </form> + <p id="meldung" class="meldung" hidden></p> + <p class="disclaimer"> Die Synopse wird automatisch erstellt und ist <strong>ohne Gewähr</strong> für Vollständigkeit und Richtigkeit; sie ersetzt keine Rechtsberatung. Uneindeutige Änderungsbefehle werden zur - manuellen Prüfung markiert statt stillschweigend verworfen. Hochgeladene Dateien werden - ausschließlich zur Erzeugung der Synopse verarbeitet und danach sofort wieder gelöscht — es - findet keine dauerhafte Speicherung statt. Bitte nur Rechtstexte hochladen. + manuellen Prüfung markiert statt stillschweigend verworfen. Die gewählten Dateien + <strong>verlassen Ihren Rechner nicht</strong>: Die Auswertung läuft vollständig im Browser, + es wird nichts hochgeladen und nichts gespeichert. </p> </main> <footer> - <a href="/impressum">Impressum</a> · - <a href="/datenschutz">Datenschutz</a> · + <a href="impressum.html">Impressum</a> · + <a href="datenschutz.html">Datenschutz</a> · <a href="https://TODO-QUELLCODE-URL-EINTRAGEN">Quellcode (AGPLv3)</a> </footer> </body> diff --git a/src/main/resources/eu/mulk/aendggner/web/style.css b/src/main/resources/eu/mulk/aendggner/web/style.css index 6e2ec70..cdea20e 100644 --- a/src/main/resources/eu/mulk/aendggner/web/style.css +++ b/src/main/resources/eu/mulk/aendggner/web/style.css @@ -112,3 +112,29 @@ footer { footer a { color: var(--accent); } + +.meldung { + margin-top: 1.25rem; + padding: 0.75rem 1rem; + border: 1px solid var(--border); + border-radius: 6px; + font-size: 0.9375rem; +} + +.meldung.arbeit { + color: var(--muted); +} + +.meldung.fertig { + border-color: var(--accent); +} + +.meldung.fehler { + border-color: #a33; + color: #a33; +} + +button[disabled] { + filter: grayscale(0.6); + cursor: progress; +} diff --git a/src/main/resources/eu/mulk/aendggner/web/worker.js b/src/main/resources/eu/mulk/aendggner/web/worker.js new file mode 100644 index 0000000..12ba400 --- /dev/null +++ b/src/main/resources/eu/mulk/aendggner/web/worker.js @@ -0,0 +1,42 @@ +// Führt das Wasm-Modul in einem eigenen Thread aus: Web Image ist einthreadig, und ein Lauf über +// mehrere Sekunden würde die Oberfläche sonst einfrieren. + +const bereit = new Promise((aufloesen) => { + self.aendggnerBereit = aufloesen; +}); + +// Die Laufzeit von aendggner.js sucht das Wasm-Modul neben ihrer eigenen Datei und findet die +// über `document.currentScript` — den es im Worker nicht gibt, wo sie stattdessen auf +// `location.href` (also diese Datei) zurückfällt und ins Leere greift. Der Selbststart beim +// Import scheitert deshalb; danach starten wir die VM mit ausdrücklichem Pfad noch einmal. +self.importScripts("aendggner.js"); + +const gestartet = GraalVM.run([], Object.assign(new GraalVM.Config(), { + wasm_path: new URL("aendggner.js.wasm", self.location.href).href, +})); + +self.onmessage = async (nachricht) => { + const { stamm, patches, vollstaendig, artikel } = nachricht.data; + try { + await gestartet; + await bereit; + const ergebnis = self.aendggnerSynopse({ + stamm, + patches, + artikel: artikel ?? null, + vollstaendig: Boolean(vollstaendig), + }); + if (ergebnis.fehler) { + self.postMessage({ fehler: String(ergebnis.fehler) }); + } else { + self.postMessage({ + html: String(ergebnis.html), + angewandt: Number(ergebnis.angewandt), + manuell: Number(ergebnis.manuell), + normen: Number(ergebnis.normen), + }); + } + } catch (e) { + self.postMessage({ fehler: e && e.message ? e.message : String(e) }); + } +}; |
