Files
dogfather-universe/workspace/assets/js/hochladen.js
T
DogFatherGitandClaude Opus 5 95c4599baa Anfuehrungszeichen: 199 falsche Schlusszeichen im sichtbaren Text
Deutsch oeffnet mit „ und schliesst mit “. An 199 Stellen, die ein
Mensch liest, stand als Schlusszeichen ein GERADES " -- „Nächstes"
statt „Nächstes“. Auf sechzehn oeffentlichen Seiten, in vierzig
Dateien des Workspace und in zwoelf Servermodulen, die Texte
verschicken.

Auf dem Bildschirm sieht man den Unterschied sofort. Beim Schreiben
nicht: Das gerade " liegt auf der Tastatur, die anderen nicht.

WARUM EIN ERSTER ANLAUF ZURUECKGENOMMEN WURDE

Ein gerades " ist an vielen Stellen SYNTAX und kein Schriftzeichen --
Grenze einer Zeichenkette, Grenze eines HTML-Attributs, Zeichen in
einem regulaeren Ausdruck. Wer stumpf ersetzt, macht aus

    „<a href="https://…                  ein kaputtes Attribut
    /^["'„»\s]+|["'“«.\s]+$/              einen kaputten Ausdruck
    "… nichts „mal " + "eben …"          eine kaputte Zeichenkette

DIE UNTERSCHEIDUNG LAEUFT AN MERKMALEN, NICHT AN EINER LISTE

  < > = dazwischen        -> HTML-Marke oder Attribut
  endet auf Leerzeichen   -> die Zeichenkette hoert hier auf, der
                             Satz geht in der naechsten Zeile weiter.
                             Ein deutsches Schlusszeichen steht NIE
                             hinter einem Leerzeichen.
  Rueckstrich mittendrin  -> regulaerer Ausdruck
  ${ ohne }               -> mitten in einem Ausdruck

Eine Liste erlaubter Ausnahmen waere die naechste, die niemand
pflegt. Zwoelf Stellen bleiben dadurch stehen, alle zwoelf einzeln
angesehen und alle zwoelf zu Recht -- dort steht das richtige
Schlusszeichen ohnehin weiter unten im Satz.

Fuenf davon waren allerdings ECHTE Fehler HINTER dem Link
(`…>HasiDog</a>".`) -- die erste Regel hatte nur das Attribut
gesehen, nicht den Satz danach. Gezielt nachgezogen.

Ein maskiertes `\"` in workspace-vorlagen.js (28 Hooks) wird zu “ --
ohne Rueckstrich, denn “ begrenzt nichts.

KOMMENTARE BLEIBEN, WIE SIE SIND. Dort liest es niemand ausser mir;
eine Wache, die auch Kosmetik anmahnt, wird weggeklickt. Beim ersten
Messen fielen ausserdem acht Stellen aus buehne.html faelschlich an,
weil `/* */` in HTML (in <style> und <script>) nicht ausgeblendet
war -- jetzt schon.

DIE WACHE DAZU

pruef-struktur prueft es ab sofort mit derselben Regel: 322 Dateien
mit sichtbarem Text, 0 Funde, und die zwoelf bewussten Ausnahmen
werden GEZAEHLT und genannt (erlaubt: 12). Eine Ausnahme, die niemand
sieht, waechst -- und irgendwann steht der echte Fall darin.

GEPRUEFT

  pruef-struktur   59 -> 68 Pruefungen, 0 Fehler
  node --check auf allen geaenderten JS-Dateien
  nachgemessen: 199 geaendert, 12 mit Grund stehen geblieben

  gruen geblieben: bewerbung-aufgaben 163, nachwuchs 262,
  reaktion 421, support 63, content 45, terminregel 35, treff 85

  Und nachgesehen, ob eine Pruefung noch die alte Schreibweise
  ERWARTET: 13 Fundstellen, alle dreizehn nur Text in ihrer eigenen
  Ausgabe, keine einzige ein Vergleich mit dem Seitentext.

GEGENPROBE: In reaktion.html ein Schlusszeichen zurueckgedreht ->
„workspace/reaktion.html:546 „Nächstes"", mit Datei und Zeile. Und
die Erkennung einzeln gegen HTML-Attribut, fortgesetzte
Zeichenkette, regulaeren Ausdruck und eingesetzten Wert geprueft.

Stempel gesetzt: workspace 670 Verweise in 45 Dateien, oeffentlich
554 in 48 Seiten.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-01 01:56:15 +02:00

241 lines
9.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
HOCHLADEN, DAS MAN SEHEN KANN — window.hochladen()
Filipe, 19.09.2026: „Ladezustände … Man soll auf die Seite kommen,
sofort verstehen, was man machen kann."
---------------------------------------------------------------------
WARUM `fetch` HIER NICHT REICHT
Alle fünf Hochlade-Wege im Haus (Dateien, Chat-Anhang, Wissen-PDF,
Aufgaben-Anhang, Profilbild) benutzten `fetch`. Das funktioniert --
aber `fetch` kann beim SENDEN nicht sagen, wie weit es ist. Es gibt
`response.body` zum Lesen, nichts zum Schreiben.
Sichtbar war deshalb nur „wird hochgeladen …", und zwar von der
ersten bis zur letzten Sekunde gleich. Bei einer 40-MB-Datei im
Mobilfunknetz sind das zwei Minuten, in denen nichts darauf
hindeutet, dass überhaupt etwas passiert. Wer das sieht, drückt ein
zweites Mal -- und lädt dieselbe Datei doppelt hoch.
`XMLHttpRequest` ist älter und kann genau das eine, was hier fehlt:
`upload.onprogress`. Deshalb steht es hier, und nur hier.
---------------------------------------------------------------------
UND ABBRECHEN GEHÖRT DAZU
Ein Fortschrittsbalken ohne Abbruch ist eine Zumutung: Wer merkt,
dass er die falsche Datei erwischt hat, kann bis heute nur die Seite
neu laden -- und weiß dann nicht, ob die Datei trotzdem angekommen
ist. `xhr.abort()` bricht die Verbindung wirklich ab; was schon
gesendet wurde, verwirft der Server, weil er den Körper nie
vollständig bekommt.
---------------------------------------------------------------------
DREI AUSGÄNGE
fertig -> { ok: true, antwort, daten }
abgebrochen -> { ok: false, abgebrochen: true }
schiefgegangen -> { ok: false, status, daten } (mit Grund)
Der mittlere ist wichtig: Ein Abbruch ist KEIN Fehler und darf keine
rote Meldung erzeugen. Wer selbst abbricht, weiß, was er getan hat.
401 wird behandelt wie überall im Haus -- zurück zur Anmeldung.
---------------------------------------------------------------------
AUFRUF
const lauf = hochladen('/workspace/api/dateien', datei, {
kopf: { 'X-Dateiname': encodeURIComponent(datei.name) },
beiFortschritt: (anteil, gesendet, gesamt) => { ... },
});
abbruchKnopf.onclick = () => lauf.abbrechen();
const ergebnis = await lauf.fertig; // NICHT `e` -- in einem
// Ereignis-Handler wuerde das den Parameter verdecken, und
// `e.target.value` zeigte danach auf das Hochlade-Ergebnis.
===================================================================== */
(function () {
'use strict';
/** Bytes als Text, den ein Mensch liest. Dieselbe Rechnung wie in
* `dateien.js` -- damit nicht an zwei Stellen zwei Zahlen stehen. */
function groesse(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB';
return (bytes / 1024 / 1024).toFixed(1).replace('.', ',') + ' MB';
}
window.groesseInWorten = groesse;
window.hochladen = function hochladen(pfad, daten, o) {
o = o || {};
const xhr = new XMLHttpRequest();
let abgebrochen = false;
const fertig = new Promise((liefere) => {
xhr.open(o.art || 'POST', pfad, true);
xhr.withCredentials = true;
for (const [k, v] of Object.entries(o.kopf || {})) xhr.setRequestHeader(k, v);
/* DER FORTSCHRITT. `lengthComputable` ist nicht selbstverständlich:
Bei einem Strom ohne bekannte Länge kann der Browser nichts
ausrechnen. Dann bleibt der Anteil `null` -- und der Aufrufer
zeigt einen laufenden Balken statt einer falschen Zahl. Eine
erfundene Prozentzahl wäre schlimmer als gar keine. */
xhr.upload.addEventListener('progress', (e) => {
if (!o.beiFortschritt) return;
const anteil = e.lengthComputable && e.total > 0 ? e.loaded / e.total : null;
o.beiFortschritt(anteil, e.loaded, e.total);
});
/* Wenn alles gesendet ist, der Server aber noch rechnet (bei einem
PDF wird geprüft, bei einem Bild verkleinert), steht der Balken
auf 100 % und es passiert scheinbar nichts mehr. Deshalb sagt
der Aufrufer ab hier "wird verarbeitet". */
xhr.upload.addEventListener('load', () => {
if (o.beiVerarbeitung) o.beiVerarbeitung();
});
xhr.addEventListener('load', () => {
if (xhr.status === 401) { location.assign('/workspace/'); return; }
let inhalt = null;
try { inhalt = JSON.parse(xhr.responseText); } catch { /* kein JSON */ }
liefere(xhr.status >= 200 && xhr.status < 300
? { ok: true, status: xhr.status, daten: inhalt }
: { ok: false, status: xhr.status, daten: inhalt });
});
/* ABBRUCH IST KEIN FEHLER. Ohne diese Unterscheidung bekäme der
Nutzer nach seinem eigenen Klick eine rote Meldung. */
xhr.addEventListener('abort', () => {
liefere({ ok: false, abgebrochen: true });
});
xhr.addEventListener('error', () => {
liefere({ ok: false, status: 0, netz: true });
});
xhr.addEventListener('timeout', () => {
liefere({ ok: false, status: 0, zeit: true });
});
/* Keine Frist von Hand: Eine große Datei in einem langsamen Netz
braucht ihre Zeit, und eine feste Zahl wäre wieder "eine
Rechnung von gestern". Der Browser bricht von selbst ab, wenn
die Verbindung wirklich weg ist -- das meldet `error`. */
xhr.send(daten);
});
return {
fertig,
abbrechen() {
if (abgebrochen) return;
abgebrochen = true;
try { xhr.abort(); } catch { /* war schon fertig */ }
},
get laeuft() { return xhr.readyState > 0 && xhr.readyState < 4; },
};
};
})();
/* =====================================================================
UND DIE ANZEIGE DAZU — window.hochladeAnzeige()
Die Mechanik oben nützt nichts, solange jede der fünf Stellen ihren
eigenen Balken baut. Dann sieht Hochladen im Chat anders aus als in
den Dateien, und die Abbruchtaste sitzt woanders -- genau das, was
Filipe mit „weniger Chaos, mehr Struktur" meint.
Deshalb baut diese Funktion die Anzeige EINMAL, in ein vorhandenes
Element hinein, und gibt drei Handgriffe zurück.
DREI ZUSTÄNDE, weil es drei gibt:
senden -> Balken mit Prozent, Abbrechen möglich
verarbeiten -> Balken voll, kein Prozent mehr, kein Abbruch
(der Server prüft/verkleinert -- abbrechen würde
jetzt nur eine halbe Datei hinterlassen)
weg -> alles verschwindet
Der mittlere Zustand ist der, den man am leichtesten vergisst: Der
Balken steht auf 100 %, und für zwei, drei Sekunden passiert nichts.
Ohne ein Wort dafür sieht das aus wie ein Hänger.
===================================================================== */
(function () {
'use strict';
window.hochladeAnzeige = function hochladeAnzeige(ziel, dateiname, beiAbbruch) {
if (!ziel) return { fortschritt() {}, verarbeiten() {}, weg() {} };
ziel.hidden = false;
ziel.textContent = '';
ziel.classList.add('hochlade');
const kopf = document.createElement('div');
kopf.className = 'hochlade__kopf';
const text = document.createElement('span');
text.className = 'hochlade__text';
text.textContent = `„${dateiname}“ wird hochgeladen …`;
kopf.append(text);
let abbruch = null;
if (beiAbbruch) {
abbruch = document.createElement('button');
abbruch.type = 'button';
abbruch.className = 'hochlade__abbruch';
abbruch.textContent = 'Abbrechen';
abbruch.addEventListener('click', () => {
abbruch.disabled = true;
text.textContent = 'wird abgebrochen …';
beiAbbruch();
});
kopf.append(abbruch);
}
/* Ein <progress> und kein eigenes <div>: Vorleseprogramme kennen
es, es sagt von selbst „Fortschritt 40 Prozent", und ohne
`value` zeigt es von allein den unbestimmten Lauf -- genau der
Fall, in dem der Browser die Gesamtgröße nicht kennt. */
const balken = document.createElement('progress');
balken.className = 'hochlade__balken';
balken.max = 1;
balken.removeAttribute('value');
ziel.append(kopf, balken);
return {
fortschritt(anteil, gesendet, gesamt) {
if (anteil === null || anteil === undefined) {
balken.removeAttribute('value');
text.textContent = `„${dateiname}“ wird hochgeladen …`;
return;
}
balken.value = anteil;
const g = window.groesseInWorten;
text.textContent = `„${dateiname}“ – ${Math.round(anteil * 100)} %`
+ (gesamt ? ` von ${g(gesamt)}` : '');
},
verarbeiten() {
balken.value = 1;
text.textContent = 'ist da – wird noch geprüft …';
if (abbruch) abbruch.remove();
},
weg() {
ziel.hidden = true;
ziel.textContent = '';
ziel.classList.remove('hochlade', 'hochlade--schluss');
},
/** Ein letzter, NEUTRALER Satz -- fuer den selbst ausgeloesten
* Abbruch. Er gehoert nicht in die rote Fehlerzeile: Wer selbst
* abbricht, hat keinen Fehler gemacht, und eine rote Meldung
* dafuer lehrt nur, rote Meldungen zu ignorieren.
* Verschwindet von allein -- ein Hinweis, den man wegklicken
* muss, ist ein Hindernis. */
abschluss(satz) {
balken.remove();
if (abbruch) abbruch.remove();
text.textContent = satz;
ziel.classList.add('hochlade--schluss');
setTimeout(() => { if (text.textContent === satz) this.weg(); }, 4000);
},
};
};
})();