Files
dogfather-universe/workspace/assets/js/hochladen.js
T
DogFatherGit 1207b79a33 Hochladen sieht man jetzt, Leerzustaende sagen was hingehoert
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.
2026-09-19 20:16:23 +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);
},
};
};
})();