DREI BLOECKE AUS DEM PERFEKTIONSLAUF.
1. HOCHLADEN MIT FORTSCHRITT UND ABBRUCH
Alle fuenf Wege (Dateien, Chat-Anhang, Wissen-PDF, Aufgaben-Anhang,
Profilbild) benutzten `fetch`. Das kann beim SENDEN nicht sagen, wie
weit es ist -- sichtbar war "wird hochgeladen …", von der ersten bis
zur letzten Sekunde gleich. Bei 40 MB im Mobilfunknetz zwei Minuten.
Wer das sieht, drueckt noch einmal und laedt dieselbe Datei doppelt.
Neu: workspace/assets/js/hochladen.js (XMLHttpRequest, das Einzige,
was `upload.onprogress` kann) samt gemeinsamer Anzeige.
Drei Ausgaenge: fertig / abgebrochen / schiefgegangen -- und ein
Abbruch ist KEIN Fehler und bekommt keine rote Meldung.
DABEI AUFGEFALLEN: KEINE EINZIGE PRUEFUNG im Haus laedt eine Datei
ueber die Oberflaeche hoch. Der ganze Umbau waere gruen gewesen,
ohne dass ein Byte je den Weg der Nutzer gegangen waere.
Neu: server/pruef-hochladen.mjs -- 18/0, mit echter Datei.
Zwei Irrtuemer beim Bauen, beide gemessen statt vermutet:
Ohne Drosselung gibt es auf localhost EINEN Fortschritt-Stand.
Das sah nach Befund aus und war keiner. Jetzt 2 MBit/s ueber
CDP -- derselbe Verlauf wie bei den Modis im Mobilfunk, 57
gemessene Zwischenstaende.
Gewartet wurde auf den Dateinamen "irgendwo im Dokument" -- der
stand auch im Fortschrittsbalken. Die Bedingung war erfuellt,
bevor etwas angekommen war.
2. LEERZUSTAENDE
Elf von 18 Brettern fielen auf "Noch kein Eintrag in diesem
Bereich" zurueck. Am ersten Tag ist ALLES leer -- wer da achtzehn
Bretter oeffnet und achtzehnmal denselben Satz liest, lernt nichts
ueber die Bretter, sondern dass das System kaputt ist. Jeder Satz
sagt jetzt, was hier hingehoert UND was der naechste Schritt ist.
Neu: server/pruef-leerzustand.mjs -- 13/0, leitet die Bretter aus
BEREICHE ab; ein neunzehntes ohne Satz macht sie rot.
3. ABMELDEN UND KONTRASTMODUS
Abmelden war am Handy ein 44-Pixel-Zeichen neben Glocke und Suche,
sofort wirksam. Teurer als es aussieht: Zum Wiederanmelden braucht
man den Zugangscode, und den gibt es EINMAL. Jetzt mit Rueckfrage,
die genau das sagt -- und dazu, dass Zumachen reicht (12 Stunden).
Kontrastmodus: 68 Regeln zeigen einen Zustand NUR ueber Farbe
(35x aria-pressed, 33x data-an). Der Modus ersetzt alle Farben und
entfernt box-shadow -- gedrueckt sah aus wie nicht gedrueckt.
14 CSS-Dateien hatten gar keinen Block. Statt 14 Bloecke zu pflegen
eine Regel in gate.css, die den ZUSTAND trifft statt die Datei.
Gemessen mit forcedColors: active -- vorher ununterscheidbar,
jetzt `solid 2px Highlight`.
Was seinen Zustand als WORT traegt (.marke-status, .t-stufe,
.spalte), braucht nichts -- nachgesehen, nicht vermutet.
PRUEFUNGEN, DIE AUF confirm() WARTETEN: Fuenf Dateien benutzten
`seite.once("dialog", d => d.accept())`. Playwright faengt confirm()
selbst ab, einen <dialog> nicht -- pruef-chat-anhaenge meldete acht
Fehler, keiner davon im Code. Neu: server/helfer-nachfrage.mjs, der
beide Wege kennt (auch den Notnagel fuer Safari vor 15.4).
pruef-chat-anhaenge, -ausbau, -optik und pruef-code wieder gruen.
hilfeAufraeumen bleibt ausgeschaltet -- das loescht echte Daten und
ist Filipes Entscheidung.
241 lines
9.6 KiB
JavaScript
241 lines
9.6 KiB
JavaScript
/* =====================================================================
|
||
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);
|
||
},
|
||
};
|
||
};
|
||
})();
|