Excel-Dateien werden gelesen -- und drei Fallen dahinter entschaerft

Filipe: "mach doch bitte so dass man alle dateien hoch laden könne auch
excel dateien. gib gas und krieg das hin."

server/workspace-xlsx.js liest .xlsx mit Bordmitteln: Eine .xlsx ist ein
ZIP mit XML darin, und Node kann beides (`zlib.inflateRawSync`). Kein
zusaetzliches Paket fuer eine Datei mit neun Zeilen.

GEMESSEN AN ZWEI ECHTEN BACKSTAGE-AUSGABEN vom 11. und 12.09.2026, die
auf dem Rechner lagen -- nicht an der Dokumentation. Sie haben mir an
drei Stellen widersprochen, und JEDE davon waere sonst ein stiller
Fehler geworden:

1. DER ZEITRAUM. In "Datenzeitraum" steht `2026-09-01 ~ 2026-09-11`.
   `datumLesen` griff sich davon den ersten Tag -- elf Tage Diamanten
   waeren auf den 1. September gebucht worden. Die Zahl steht da, sie
   ist gross, sie sieht richtig aus, und niemand kann spaeter sagen,
   dass elf Tage darin stecken. Neu: `zeitraumLesen`; eine Zeile mit
   einem Zeitraum ueber mehrere Tage wird abgelehnt UND begruendet
   ("Stell in Backstage den Zeitraum auf EINEN Tag"). Ein Zeitraum von
   einem Tag geht durch.

2. DIE EINHEIT. "LIVE-Dauer" enthaelt `86Std. 10Min. 52Sek.`.
   `zahlLesen` ergab daraus `null` -- die Dauer fiel weg. Bei einem
   anderen Trennzeichen waere es schlimmer gewesen: 86 statt 5171,
   Faktor 60 daneben und plausibel. Neu: `dauerLesen`, versteht die
   deutsche und englische Schreibweise, die Uhrzeitform und weiterhin
   die blosse Zahl.

3. DIE DATUMSSPALTE. `/datum|date|tag|day/i` erklaerte "Tage seit dem
   Beitritt" zur Datumsspalte (Wert "65") und traf in der
   Leistungstabelle "Gueltige LIVE-Gehen-Tage" genauso. Jetzt nur noch
   als ganzes Wort, dafuer mit "zeitraum" -- Backstages Spalte wurde
   bisher nur zufaellig gefunden, weil in "Daten" die Silbe "date"
   steckt.

Gefunden hat das keine Ueberlegung, sondern der ganze Weg einmal mit
der echten Datei durchlaufen.

WEITER GEBAUT:
- Titelzeilen werden uebersprungen: "Creator:innen verwalten" hat in
  Zeile 1 nur "Exportiert am :…", die Ueberschriften stehen darunter.
  Die Regel misst (drei gefuellte Felder UND halb so breit wie die
  breiteste Zeile), statt eine feste Zahl zu nehmen.
- Fehlende Zellen verschieben nichts: Eine leere Zelle steht in der
  Datei gar nicht; wer der Reihe nach liest, verrutscht ab dort jede
  Spalte, und die Zeile sieht voll aus.
- Datums-Seriennummern werden nur umgerechnet, wenn das FORMAT es sagt
  (sonst stuende 46271 in der Vorschau). Der Nullpunkt ist an zwei
  nachschlagbaren Werten festgenagelt.
- Der Backstage-Dialog nimmt die Datei jetzt AUCH -- dort gehoert sie
  hin, denn Filipes Ausgabe enthaelt alle Creator auf einmal. Sie fuellt
  das Einfuegefeld; ab da laeuft derselbe Weg wie beim Einfuegen. Keine
  zweite Fassung derselben Regeln.
- Die alte .xls (BIFF, kein ZIP) wird erkannt und bekommt einen Weg
  gezeigt, statt "ging nicht" zu sagen.

NEU: pruef-xlsx.mjs (60) -- baut seine Dateien selbst (ZIP-Schreiber in
helfer-xlsx-bauen.mjs), damit keine Creator-Daten ins Repo wandern und
auch Faelle pruefbar sind, die es als Datei nicht gibt: kaputtes
Verzeichnis, fehlendes Blatt, abgeschnittene Datei. Jeder davon mit
Gegenprobe, dass die heile Datei durchgeht.

pruef-backstage-import 77 -> 82, dabei zwei Pruefungen GEDREHT: Die
.xlsx bekommt keine Absage mehr, sondern eine Vorschau.

DREI EIGENE FEHLER DABEI, alle von einer Messung gefunden:
- Ich hielt Seriennummer 46264 fuer den 06.09.; es ist der 30.08. Der
  Code hatte recht. Deshalb stehen jetzt zwei nachschlagbare Anker drin.
- Eine Zeile war gruen, weil mein Muster den SPALTENNAMEN
  "Datenzeitraum" traf statt der Begruendung. Jetzt wird auf den Text
  der Ablehnung geprueft.
- Beim Umbau habe ich pruef-backstage-import beschaedigt (ein
  Suchtreffer weiter oben als gemeint) und aus Git zurueckgeholt.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-14 11:35:21 +02:00
co-authored by Claude Opus 5
parent 6973eda30b
commit 58932ece8c
36 changed files with 1594 additions and 401 deletions
+8
View File
@@ -417,6 +417,14 @@
.feld-hinweis {
margin: 5px 0 0; font-size: .76rem; color: var(--text-still);
}
/* "… oder direkt die Datei": steht UNTER dem Einfuegefeld und gehoert
sichtbar dazu, ohne ihm den Rang abzulaufen. Das Dateifeld selbst
bleibt klein -- es ist der zweite Weg, nicht der erste. */
.feld-hinweis--oder {
display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
margin-top: 8px;
}
.feld-datei { flex: 1 1 220px; min-width: 0; font-size: .76rem; }
/* Ein Fenster, das laenger ist als der Bildschirm, versteckt seine
eigenen Knoepfe. Gerollt wird INNEN, damit "Uebernehmen" immer
+104 -13
View File
@@ -476,26 +476,66 @@
gelesen ergibt Zeichensalat -- und der schlimmste Ausgang waere
nicht "geht nicht", sondern eine Vorschau mit Zahlen, die
irgendwo aus dem Dateikopf stammen. */
/* WAS IST DAS UEBERHAUPT FUER EINE DATEI? (11.09.2026, erweitert 14.09.2026)
Filipe zuerst: "meine excel datei kann ich nicht auswaehlen
wieso??" -- dann: "mach doch bitte so dass man alle dateien hoch
laden könne auch excel dateien."
Erkannt wird am INHALT, nicht an der Endung: Eine umbenannte
Datei ist keine andere Datei. Eine .xlsx ist ein ZIP-Archiv und
beginnt mit "PK", die alte .xls mit dem OLE-Kennzeichen
D0 CF 11 E0.
Seit heute wird die .xlsx nicht mehr abgewiesen, sondern
GELESEN: Der Server macht daraus eine TAB-getrennte Tabelle --
genau die Form, die auch beim Einfuegen aus dem Browser
entsteht. Danach laeuft alles durch dieselbe Vorschau wie
bisher. */
const anfang = new Uint8Array(await datei.slice(0, 8).arrayBuffer());
const beginnt = (...b) => b.every((x, i) => anfang[i] === x);
if (beginnt(0x50, 0x4b) || beginnt(0xd0, 0xcf, 0x11, 0xe0)) {
let ausExcel = null;
if (beginnt(0x50, 0x4b)) {
$('import-fehler').textContent = 'Excel-Datei wird gelesen …';
try {
const a = await hole('/workspace/api/leistung/tabelle-lesen', {
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream' },
body: await datei.arrayBuffer(),
});
const d = await a.json().catch(() => ({}));
if (!a.ok) {
$('import-fehler').textContent = d.fehler || 'Die Excel-Datei ließ sich nicht lesen.';
$('datei').value = '';
return;
}
importText = d.text;
ausExcel = d;
$('import-fehler').textContent = '';
} catch {
$('import-fehler').textContent = 'Die Excel-Datei konnte nicht gelesen werden.';
$('datei').value = '';
return;
}
} else if (beginnt(0xd0, 0xcf, 0x11, 0xe0)) {
/* Die ALTE .xls ist kein ZIP, sondern ein ganz anderes Format
(BIFF, vor 2007). Sie zu lesen waere ein zweiter Leser fuer
einen Fall, den Backstage nicht erzeugt. */
$('import-fehler').innerHTML =
'Das ist eine <strong>Excel-Datei</strong>. Hier passt eine Text-Tabelle '
+ '(CSV) hinein. Zwei Wege, beide dauern keine Minute:<br>'
+ '<strong>1.</strong> In Excel alles markieren (Strg+A), kopieren (Strg+C) – '
+ 'und statt hier den Knopf <strong>„Backstage-Tabelle einfügen"</strong> nehmen. '
+ 'Dort einfügen, fertig.<br>'
+ '<strong>2.</strong> Oder in Excel: <em>Datei → Speichern unter</em> und als '
+ 'Dateityp <em>CSV UTF-8 (durch Trennzeichen getrennt)</em> wählen.';
'Das ist eine <strong>alte Excel-Datei (.xls)</strong>. Speichere sie in Excel '
+ 'einmal als <em>.xlsx</em> oder als <em>CSV UTF-8</em> – beides geht dann durch.';
$('datei').value = '';
return;
}
try {
importText = await datei.text();
} catch {
$('import-fehler').textContent = 'Die Datei ließ sich nicht lesen.';
return;
if (!ausExcel) {
try {
importText = await datei.text();
} catch {
$('import-fehler').textContent = 'Die Datei ließ sich nicht lesen.';
return;
}
}
/* Eine leere Datei ist kein Lesefehler, sieht aber wie einer aus. */
@@ -736,6 +776,57 @@
$('netz-text').focus();
});
$('netz-zu').addEventListener('click', () => $('netz-dialog').close());
/* DIE BACKSTAGE-AUSGABE ALS DATEI (14.09.2026).
Sie enthaelt ALLE Creator auf einmal -- deshalb hier und nicht im
Einzel-Import daneben. Der Weg ist bewusst derselbe wie beim
Einfuegen: Die Datei wird zu Text, der Text landet im Feld, und ab
da laeuft die vorhandene Vorschau. Ein eigener Ablauf fuer Dateien
waere eine zweite Fassung derselben Regeln. */
$('netz-datei').addEventListener('change', async (e) => {
const datei = e.target.files?.[0];
if (!datei) return;
$('netz-fehler').textContent = 'Datei wird gelesen …';
try {
const roh = new Uint8Array(await datei.slice(0, 8).arrayBuffer());
let text = null;
let ausExcel = null;
if (roh[0] === 0x50 && roh[1] === 0x4b) {
const a = await hole('/workspace/api/leistung/tabelle-lesen', {
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream' },
body: await datei.arrayBuffer(),
});
const d = await a.json().catch(() => ({}));
if (!a.ok) { $('netz-fehler').textContent = d.fehler || 'Ging nicht.'; return; }
text = d.text;
ausExcel = d;
} else if (roh[0] === 0xd0 && roh[1] === 0xcf) {
$('netz-fehler').textContent = 'Das ist eine alte Excel-Datei (.xls). '
+ 'Speichere sie einmal als .xlsx oder als CSV.';
return;
} else {
text = await datei.text();
}
if (!text || !text.trim()) { $('netz-fehler').textContent = 'Die Datei ist leer.'; return; }
$('netz-text').value = text;
$('netz-fehler').textContent = '';
/* WAS ICH MIT DER DATEI GEMACHT HABE, STEHT DA. Eine Tabelle, die
stillschweigend anders anfaengt als die Datei (Backstage setzt
bei einer Ausgabe eine Titelzeile darueber), ist die Sorte
Hilfsbereitschaft, die spaeter niemand nachvollzieht. */
if (ausExcel) {
melde(`Excel gelesen: Blatt „${ausExcel.blatt}", ${ausExcel.zeilen} Zeilen, `
+ `${ausExcel.spalten} Spalten`
+ (ausExcel.uebersprungen ? ` (${ausExcel.uebersprungen} Titelzeile übersprungen)` : ''));
setTimeout(() => melde(''), 7000);
}
await netzVorschau();
} catch {
$('netz-fehler').textContent = 'Die Datei konnte nicht gelesen werden.';
}
});
/* Beim EINFÜGEN sofort nachsehen -- nicht erst auf einen Knopf warten.
Wer eine Tabelle einfügt, will wissen, ob sie erkannt wurde; ein
zweiter Klick dazwischen ist nur eine Gelegenheit, es nicht zu tun. */